Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Allow to configure `SearchResults` utility component with `isItemDisabled` and `multiSelection` props:
* Remove `singleSelectOnClick` mode from `SearchResults` as it mostly superseded by `multiSelection`.
- Extend `ListElementView` utility component to accept any other additional HTML props.
- Export `AccessibleList` component as base for [accessible](https://www.w3.org/TR/wai-aria/) list-like container.
- Always display ungroup buttons on `StandardGroup` when the element is single-selected.

## [0.34.1] - 2026-03-30
Expand Down
13 changes: 9 additions & 4 deletions src/coreUtils/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,11 @@ export function useAsync<const I extends React.DependencyList, T>(params: {
load: (input: I, options: { signal: AbortSignal }) => Promise<T> | undefined;
}): UseAsyncResult<T> {
const {input, load} = params;
const latestLoad = React.useRef(load);
const latestLoad = useLatest(load);
const [result, setResult] = React.useState<UseAsyncResult<T>>({
data: undefined,
status: 'loading',
});
React.useEffect(() => {
latestLoad.current = load;
});
React.useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
Expand All @@ -201,3 +198,11 @@ export function useAsync<const I extends React.DependencyList, T>(params: {
}, input);
return result;
}

export function useLatest<T>(value: T): { readonly current: T } {
const ref = React.useRef<T>(value);
React.useEffect(() => {
ref.current = value;
});
return ref;
}
38 changes: 19 additions & 19 deletions src/widgets/classTree/classTreeResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { useTranslation } from '../../coreUtils/i18n';
import { ElementTypeIri, ElementTypeModel } from '../../data/model';
import { useWorkspace } from '../../workspace/workspaceContext';

import { highlightSubstring } from '../utility/listElementView';
import {
TreeList, type TreeListModel, type TreeListRenderItem, type TreeListFocusProps,
TreeListState, type TreeListUpPath, treeListPathToDown,
} from '../utility/treeList';
AccessibleTree, type TreeModel, type TreeRenderItem, TreeState, type TreeUpPath,
TreeFocusableProps, treePathToDown,
} from '../utility/accessibleTree';
import { highlightSubstring } from '../utility/listElementView';

export interface ClassTreeResultsProps extends ClassTreeProvidedContext {
nodes: ReadonlyArray<TreeNode>;
Expand All @@ -28,12 +28,12 @@ export interface ClassTreeProvidedContext {

export interface ClassTreeSelection {
readonly node: TreeNode;
readonly selection: TreeListState<TreeNode>;
readonly selection: TreeState<TreeNode>;
}

interface ClassTreeContext extends ClassTreeProvidedContext {
readonly onExpand: (path: TreeListUpPath) => void;
readonly onSelect: (node: TreeNode, path: TreeListUpPath) => void;
readonly onExpand: (path: TreeUpPath) => void;
readonly onSelect: (node: TreeNode, path: TreeUpPath) => void;
}

const ClassTreeContext = React.createContext<ClassTreeContext | null>(null);
Expand All @@ -46,7 +46,7 @@ export function ClassTreeResults(props: ClassTreeResultsProps) {
onClickCreate, onDragCreate, draggableItems,
} = props;

const renderItem = React.useCallback<TreeListRenderItem<TreeNode, TreeNode>>(
const renderItem = React.useCallback<TreeRenderItem<TreeNode, TreeNode>>(
({item, path, focusProps, expanded, selected}) => (
<Leaf node={item}
path={path}
Expand All @@ -71,11 +71,11 @@ export function ClassTreeResults(props: ClassTreeResultsProps) {
}), []);

const defaultExpanded = Boolean(searchText);
const [expanded, setExpanded] = React.useState<TreeListState<boolean>>();
const onExpand = React.useCallback((path: TreeListUpPath) => {
const [expanded, setExpanded] = React.useState<TreeState<boolean>>();
const onExpand = React.useCallback((path: TreeUpPath) => {
setExpanded(previous =>
(previous ?? new TreeListState())
.setAt(treeListPathToDown(path), itemExpanded => !(itemExpanded ?? defaultExpanded))
(previous ?? new TreeState())
.setAt(treePathToDown(path), itemExpanded => !(itemExpanded ?? defaultExpanded))
);
}, [defaultExpanded]);
React.useEffect(() => setExpanded(undefined), [searchText]);
Expand All @@ -90,8 +90,8 @@ export function ClassTreeResults(props: ClassTreeResultsProps) {
onExpand,
onSelect: (node, path) => onSelect({
node,
selection: new TreeListState<TreeNode>().setAt(
treeListPathToDown(path),
selection: new TreeState<TreeNode>().setAt(
treePathToDown(path),
() => node
),
}),
Expand All @@ -109,14 +109,14 @@ export function ClassTreeResults(props: ClassTreeResultsProps) {

return (
<ClassTreeContext.Provider value={classTreeContext}>
<TreeList
<AccessibleTree
model={ClassTreeModel}
items={nodes}
renderItem={renderItem}
expanded={expanded}
defaultExpanded={defaultExpanded}
onSetExpanded={(item, path, expand) => setExpanded(previous => (
(previous ?? new TreeListState()).setAt(path, () => expand)
(previous ?? new TreeState()).setAt(path, () => expand)
))}
selected={selection?.selection}
rootProps={rootProps}
Expand All @@ -138,7 +138,7 @@ export const TreeNode = {
setDerived: (node: TreeNode, derived: ReadonlyArray<TreeNode>): TreeNode => ({...node, derived}),
};

const ClassTreeModel: TreeListModel<TreeNode, TreeNode> = {
const ClassTreeModel: TreeModel<TreeNode, TreeNode> = {
getKey: item => item.iri,
getChildren: item => item.derived,
getDefaultSelected: (item, selected) => undefined,
Expand All @@ -147,8 +147,8 @@ const ClassTreeModel: TreeListModel<TreeNode, TreeNode> = {

function Leaf(props: {
node: TreeNode;
path: TreeListUpPath;
focusProps: TreeListFocusProps;
path: TreeUpPath;
focusProps: TreeFocusableProps;
expanded: boolean;
selected?: TreeNode;
}) {
Expand Down
35 changes: 17 additions & 18 deletions src/widgets/connectionsMenu/connectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import type { LinkTypeModel } from '../../data/model';
import { generate128BitID, makeCaseInsensitiveFilter } from '../../data/utils';
import { WithFetchStatus } from '../../editor/withFetchStatus';

import { highlightSubstring } from '../utility/listElementView';
import {
TreeList, type TreeListModel, type TreeListRenderItem, type TreeListFocusProps,
} from '../utility/treeList';
AccessibleList, type ListRenderItem, type ListFocusableProps,
} from '../utility/accessibleList';
import { highlightSubstring } from '../utility/listElementView';

import { useWorkspace } from '../../workspace/workspaceContext';

Expand Down Expand Up @@ -93,7 +93,7 @@ export function ConnectionsList(props: {
}
}

const renderItem = React.useCallback<TreeListRenderItem<ConnectionEntry, void>>(
const renderItem = React.useCallback<ListRenderItem<ConnectionEntry, void>>(
({item, focusProps}) => {
if (item.type === 'link') {
return (
Expand Down Expand Up @@ -128,7 +128,6 @@ export function ConnectionsList(props: {
className: `${CLASS_NAME}__links-root`,
role: 'list',
}), []);
const forestProps = React.useMemo((): React.HTMLProps<HTMLUListElement> => ({}), []);
const itemProps = React.useMemo((): React.HTMLProps<HTMLLIElement> => ({
className: `${CLASS_NAME}__links-item`,
role: 'listitem',
Expand All @@ -143,12 +142,12 @@ export function ConnectionsList(props: {
)}
tabIndex={-1}
>
<TreeList
model={ConnectionListModel}
<AccessibleList
items={entries}
getItemKey={getEntryKey}
isItemActive={isEntryActive}
renderItem={renderItem}
rootProps={rootProps}
forestProps={forestProps}
itemProps={itemProps}
/>
{entries.length === 0 ? (
Expand All @@ -160,13 +159,6 @@ export function ConnectionsList(props: {
);
}

const ConnectionListModel: TreeListModel<ConnectionEntry, void> = {
getKey: item => item.type === 'link' ? item.key : item.type,
getChildren: item => undefined,
getDefaultSelected: (item, selected) => undefined,
isActive: item => item.type === 'link',
};

type ConnectionEntry = ConnectionEntryLink | ConnectionEntrySeparator;

interface ConnectionEntrySeparator {
Expand All @@ -183,6 +175,14 @@ interface ConnectionEntryLink {
readonly probability?: number;
}

function getEntryKey(entry: ConnectionEntry): string {
return entry.type === 'link' ? entry.key : entry.type;
}

function isEntryActive(entry: ConnectionEntry): boolean {
return entry.type === 'link';
}

function getConnectionLinks(links: LinkTypeModel[], options: {
counts: ConnectionsData['counts'];
scores: ConnectionSuggestions['scores'];
Expand All @@ -201,11 +201,10 @@ function getConnectionLinks(links: LinkTypeModel[], options: {
return;
}

const postfix = probable ? '-probable' : '';
const score = scores.get(link.id);
entries.push({
type: 'link',
key: `${direction}-${link.id}-${postfix}`,
key: `${direction}-${probable ? 'probable' : ''}-${link.id}`,
linkType: link,
direction,
count: inexact && count > 0 ? 'some' : count,
Expand Down Expand Up @@ -253,7 +252,7 @@ function ConnectionLink(props: {
onExpandLink: (linkDataChunk: LinkDataChunk) => void;
onMoveToFilter: ((linkDataChunk: LinkDataChunk) => void) | undefined;
probability?: number;
focusProps?: TreeListFocusProps;
focusProps?: ListFocusableProps;
}) {
const {
link, filterKey, direction, count, onExpandLink, onMoveToFilter, probability = 0, focusProps,
Expand Down
Loading
Loading