@ankhorage/zora 22.0.81 → 22.0.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/dist/features/tree-view/adapters/inbound/web-artifact/TreeView.d.ts.map +1 -1
- package/dist/features/tree-view/adapters/inbound/web-artifact/TreeView.js +14 -1
- package/dist/features/tree-view/adapters/inbound/web-artifact/TreeView.js.map +1 -1
- package/package.json +2 -2
- package/src/features/tree-view/adapters/inbound/web-artifact/TreeView.test.tsx +40 -0
- package/src/features/tree-view/adapters/inbound/web-artifact/TreeView.tsx +26 -0
- package/web-dist/components/tree-view/TreeView.js +11 -0
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# @ankhorage/zora
|
|
5
5
|
|
|
6
|
-
         
|
|
7
7
|
|
|
8
8
|
Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.
|
|
9
9
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TreeView.d.ts","sourceRoot":"","sources":["../../../../../../src/features/tree-view/adapters/inbound/web-artifact/TreeView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,MAAM,WAAW,YAAY,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IACvD,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC;IACjB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;IACjD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACpC;AAED,MAAM,WAAW,mBAAmB,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IAC9D,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IACxD,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;IAC7C,QAAQ,CAAC,UAAU,CAAC,EAAE,GAAG,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,GAAG,EAAE,CAAC;IACtC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,GAAG,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,GAAG,EAAE,KAAK,IAAI,CAAC;IAC1D,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC3E,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACnD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,EAAE,EACpD,KAAK,EACL,UAAU,EACV,WAAW,EAAE,qBAAqB,EAClC,kBAAkB,EAClB,QAAQ,EACR,gBAAgB,EAChB,UAAU,EACV,kBAA6B,EAC7B,SAAS,EACT,KAAK,EACL,SAAkB,GACnB,EAAE,aAAa,CAAC,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"TreeView.d.ts","sourceRoot":"","sources":["../../../../../../src/features/tree-view/adapters/inbound/web-artifact/TreeView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,MAAM,WAAW,YAAY,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IACvD,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC;IACjB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;IACjD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACpC;AAED,MAAM,WAAW,mBAAmB,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IAC9D,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IACxD,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;IAC7C,QAAQ,CAAC,UAAU,CAAC,EAAE,GAAG,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,GAAG,EAAE,CAAC;IACtC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,GAAG,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,GAAG,EAAE,KAAK,IAAI,CAAC;IAC1D,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC3E,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACnD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,EAAE,EACpD,KAAK,EACL,UAAU,EACV,WAAW,EAAE,qBAAqB,EAClC,kBAAkB,EAClB,QAAQ,EACR,gBAAgB,EAChB,UAAU,EACV,kBAA6B,EAC7B,SAAS,EACT,KAAK,EACL,SAAkB,GACnB,EAAE,aAAa,CAAC,GAAG,CAAC,qBAwCpB"}
|
|
@@ -8,6 +8,7 @@ import { TreeItemRow } from './TreeItemRow';
|
|
|
8
8
|
*/
|
|
9
9
|
export function TreeView({ nodes, selectedId, expandedIds: controlledExpandedIds, defaultExpandedIds, onSelect, onExpandedChange, renderItem, expansionIndicator = 'folder', className, style, ariaLabel = 'Tree', }) {
|
|
10
10
|
const [internalExpandedIds, setInternalExpandedIds] = React.useState(defaultExpandedIds ?? []);
|
|
11
|
+
const rootRef = React.useRef(null);
|
|
11
12
|
const isControlled = controlledExpandedIds !== undefined;
|
|
12
13
|
const expandedIds = isControlled ? controlledExpandedIds : internalExpandedIds;
|
|
13
14
|
const handleToggleExpand = React.useCallback((id) => {
|
|
@@ -16,10 +17,22 @@ export function TreeView({ nodes, selectedId, expandedIds: controlledExpandedIds
|
|
|
16
17
|
setInternalExpandedIds(nextExpandedIds);
|
|
17
18
|
onExpandedChange?.(nextExpandedIds);
|
|
18
19
|
}, [expandedIds, isControlled, onExpandedChange]);
|
|
19
|
-
|
|
20
|
+
useSelectedTreeItemReveal(rootRef, selectedId, expandedIds);
|
|
21
|
+
return (<div ref={rootRef} aria-label={ariaLabel} className={className} role="tree" style={{ ...TREE_STYLE, ...style }}>
|
|
20
22
|
{nodes.map((node) => (<TreeItemRow key={node.id} depth={0} expandedIds={expandedIds} expansionIndicator={expansionIndicator} node={node} onSelect={onSelect} onToggleExpand={handleToggleExpand} renderItem={renderItem} selectedId={selectedId}/>))}
|
|
21
23
|
</div>);
|
|
22
24
|
}
|
|
25
|
+
/*** Reveal a controlled selection after it is rendered without changing expansion ownership. */
|
|
26
|
+
function useSelectedTreeItemReveal(rootRef, selectedId, expandedIds) {
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
if (selectedId === undefined)
|
|
29
|
+
return;
|
|
30
|
+
const root = rootRef.current;
|
|
31
|
+
root
|
|
32
|
+
?.querySelector('[role="treeitem"][aria-selected="true"]')
|
|
33
|
+
?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
34
|
+
}, [expandedIds, rootRef, selectedId]);
|
|
35
|
+
}
|
|
23
36
|
const TREE_STYLE = {
|
|
24
37
|
display: 'flex',
|
|
25
38
|
flexDirection: 'column',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TreeView.js","sourceRoot":"","sources":["../../../../../../src/features/tree-view/adapters/inbound/web-artifact/TreeView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAkC5C;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAA8B,EACpD,KAAK,EACL,UAAU,EACV,WAAW,EAAE,qBAAqB,EAClC,kBAAkB,EAClB,QAAQ,EACR,gBAAgB,EAChB,UAAU,EACV,kBAAkB,GAAG,QAAQ,EAC7B,SAAS,EACT,KAAK,EACL,SAAS,GAAG,MAAM,GACC;IACnB,MAAM,CAAC,mBAAmB,EAAE,sBAAsB,CAAC,GAAG,KAAK,CAAC,QAAQ,CAClE,kBAAkB,IAAI,EAAE,CACzB,CAAC;IACF,MAAM,YAAY,GAAG,qBAAqB,KAAK,SAAS,CAAC;IACzD,MAAM,WAAW,GAAG,YAAY,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,mBAAmB,CAAC;IAC/E,MAAM,kBAAkB,GAAG,KAAK,CAAC,WAAW,CAC1C,CAAC,EAAO,EAAE,EAAE;QACV,MAAM,eAAe,GAAG,iBAAiB,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAC3D,IAAI,CAAC,YAAY;YAAE,sBAAsB,CAAC,eAAe,CAAC,CAAC;QAC3D,gBAAgB,EAAE,CAAC,eAAe,CAAC,CAAC;IACtC,CAAC,EACD,CAAC,WAAW,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAC9C,CAAC;
|
|
1
|
+
{"version":3,"file":"TreeView.js","sourceRoot":"","sources":["../../../../../../src/features/tree-view/adapters/inbound/web-artifact/TreeView.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\n\nimport { toggleExpandedIds } from '../../../utils/toggleExpandedIds';\nimport { TreeItemRow } from './TreeItemRow';\n\nexport interface TreeItemNode<TId extends string = string> {\n readonly id: TId;\n readonly label: React.ReactNode;\n readonly icon?: React.ReactNode;\n readonly children?: readonly TreeItemNode<TId>[];\n readonly disabled?: boolean;\n readonly meta?: React.ReactNode;\n readonly actions?: React.ReactNode;\n}\n\nexport interface TreeItemRenderProps<TId extends string = string> {\n readonly node: TreeItemNode<TId>;\n readonly depth: number;\n readonly selected: boolean;\n readonly expanded: boolean;\n readonly hasChildren: boolean;\n}\n\nexport interface TreeViewProps<TId extends string = string> {\n readonly nodes: readonly TreeItemNode<TId>[];\n readonly selectedId?: TId;\n readonly expandedIds?: readonly TId[];\n readonly defaultExpandedIds?: readonly TId[];\n readonly onSelect?: (id: TId) => void;\n readonly onExpandedChange?: (ids: readonly TId[]) => void;\n readonly renderItem?: (props: TreeItemRenderProps<TId>) => React.ReactNode;\n readonly expansionIndicator?: 'chevron' | 'folder';\n readonly className?: string;\n readonly style?: React.CSSProperties;\n readonly ariaLabel?: string;\n}\n\n/***\n * Render the standalone browser TreeView artifact without React Native runtime dependencies.\n * expansionIndicator Use `chevron` beside custom node icons, or `folder` (default) for\n * built-in folder/file indicators. Expansion is leading and independent from row selection.\n */\nexport function TreeView<TId extends string = string>({\n nodes,\n selectedId,\n expandedIds: controlledExpandedIds,\n defaultExpandedIds,\n onSelect,\n onExpandedChange,\n renderItem,\n expansionIndicator = 'folder',\n className,\n style,\n ariaLabel = 'Tree',\n}: TreeViewProps<TId>) {\n const [internalExpandedIds, setInternalExpandedIds] = React.useState<readonly TId[]>(\n defaultExpandedIds ?? [],\n );\n const rootRef = React.useRef<HTMLDivElement | null>(null);\n const isControlled = controlledExpandedIds !== undefined;\n const expandedIds = isControlled ? controlledExpandedIds : internalExpandedIds;\n const handleToggleExpand = React.useCallback(\n (id: TId) => {\n const nextExpandedIds = toggleExpandedIds(expandedIds, id);\n if (!isControlled) setInternalExpandedIds(nextExpandedIds);\n onExpandedChange?.(nextExpandedIds);\n },\n [expandedIds, isControlled, onExpandedChange],\n );\n useSelectedTreeItemReveal(rootRef, selectedId, expandedIds);\n\n return (\n <div\n ref={rootRef}\n aria-label={ariaLabel}\n className={className}\n role=\"tree\"\n style={{ ...TREE_STYLE, ...style }}\n >\n {nodes.map((node) => (\n <TreeItemRow\n key={node.id}\n depth={0}\n expandedIds={expandedIds}\n expansionIndicator={expansionIndicator}\n node={node}\n onSelect={onSelect}\n onToggleExpand={handleToggleExpand}\n renderItem={renderItem}\n selectedId={selectedId}\n />\n ))}\n </div>\n );\n}\n\n/*** Reveal a controlled selection after it is rendered without changing expansion ownership. */\nfunction useSelectedTreeItemReveal<TId extends string>(\n rootRef: React.RefObject<HTMLDivElement | null>,\n selectedId: TId | undefined,\n expandedIds: readonly TId[],\n) {\n React.useEffect(() => {\n if (selectedId === undefined) return;\n const root = rootRef.current as unknown as TreeViewQueryableElement | null;\n root\n ?.querySelector('[role=\"treeitem\"][aria-selected=\"true\"]')\n ?.scrollIntoView({ block: 'nearest', inline: 'nearest' });\n }, [expandedIds, rootRef, selectedId]);\n}\n\nconst TREE_STYLE = {\n display: 'flex',\n flexDirection: 'column',\n minWidth: 0,\n} as const satisfies React.CSSProperties;\n\ninterface TreeViewQueryableElement {\n querySelector(selector: string): TreeViewRevealTarget | null;\n}\n\ninterface TreeViewRevealTarget {\n scrollIntoView(options: { readonly block: 'nearest'; readonly inline: 'nearest' }): void;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/zora",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "22.0.
|
|
4
|
+
"version": "22.0.82",
|
|
5
5
|
"description": "Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/zora#readme",
|
|
7
7
|
"bugs": {
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
"version-packages": "ankhorage-changeset version"
|
|
125
125
|
},
|
|
126
126
|
"devDependencies": {
|
|
127
|
-
"@ankhorage/devtools": "^2.1.
|
|
127
|
+
"@ankhorage/devtools": "^2.1.6",
|
|
128
128
|
"@ankhorage/paradox": "^0.2.27",
|
|
129
129
|
"@react-native-vector-icons/fontawesome": "^13.1.4",
|
|
130
130
|
"@react-native-vector-icons/fontawesome5": "^13.1.4",
|
|
@@ -66,6 +66,46 @@ test('expands from the leading control without selecting the row, including keyb
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
test('reveals a controlled selection when expansion makes the row renderable', async () => {
|
|
70
|
+
const browserWindow = new Window();
|
|
71
|
+
const keys = ['window', 'document', 'Node', 'navigator', 'IS_REACT_ACT_ENVIRONMENT'] as const;
|
|
72
|
+
const previous = keys.map(
|
|
73
|
+
(key) => [key, Object.getOwnPropertyDescriptor(globalThis, key)] as const,
|
|
74
|
+
);
|
|
75
|
+
Object.assign(globalThis, {
|
|
76
|
+
IS_REACT_ACT_ENVIRONMENT: true,
|
|
77
|
+
window: browserWindow,
|
|
78
|
+
document: browserWindow.document,
|
|
79
|
+
Node: browserWindow.Node,
|
|
80
|
+
navigator: browserWindow.navigator,
|
|
81
|
+
});
|
|
82
|
+
const reveals: unknown[] = [];
|
|
83
|
+
browserWindow.HTMLElement.prototype.scrollIntoView = (options) => reveals.push(options);
|
|
84
|
+
const host = document.createElement('div');
|
|
85
|
+
document.body.appendChild(host);
|
|
86
|
+
const root = createRoot(host);
|
|
87
|
+
const interactAsync = (action: () => void) => act(() => Promise.resolve().then(action));
|
|
88
|
+
const nodes = [{ id: 'src', label: 'source', children: [{ id: 'leaf', label: 'leaf' }] }];
|
|
89
|
+
try {
|
|
90
|
+
await interactAsync(() =>
|
|
91
|
+
root.render(<TreeView expandedIds={[]} nodes={nodes} selectedId="leaf" />),
|
|
92
|
+
);
|
|
93
|
+
expect(reveals).toEqual([]);
|
|
94
|
+
|
|
95
|
+
await interactAsync(() =>
|
|
96
|
+
root.render(<TreeView expandedIds={['src']} nodes={nodes} selectedId="leaf" />),
|
|
97
|
+
);
|
|
98
|
+
expect(reveals).toEqual([{ block: 'nearest', inline: 'nearest' }]);
|
|
99
|
+
} finally {
|
|
100
|
+
await interactAsync(() => root.unmount());
|
|
101
|
+
browserWindow.close();
|
|
102
|
+
for (const [key, descriptor] of previous) {
|
|
103
|
+
if (descriptor) Object.defineProperty(globalThis, key, descriptor);
|
|
104
|
+
else Reflect.deleteProperty(globalThis, key);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
69
109
|
test('renders a standalone web tree without a ZORA or React Native provider', () => {
|
|
70
110
|
const markup = renderToStaticMarkup(
|
|
71
111
|
<TreeView
|
|
@@ -56,6 +56,7 @@ export function TreeView<TId extends string = string>({
|
|
|
56
56
|
const [internalExpandedIds, setInternalExpandedIds] = React.useState<readonly TId[]>(
|
|
57
57
|
defaultExpandedIds ?? [],
|
|
58
58
|
);
|
|
59
|
+
const rootRef = React.useRef<HTMLDivElement | null>(null);
|
|
59
60
|
const isControlled = controlledExpandedIds !== undefined;
|
|
60
61
|
const expandedIds = isControlled ? controlledExpandedIds : internalExpandedIds;
|
|
61
62
|
const handleToggleExpand = React.useCallback(
|
|
@@ -66,9 +67,11 @@ export function TreeView<TId extends string = string>({
|
|
|
66
67
|
},
|
|
67
68
|
[expandedIds, isControlled, onExpandedChange],
|
|
68
69
|
);
|
|
70
|
+
useSelectedTreeItemReveal(rootRef, selectedId, expandedIds);
|
|
69
71
|
|
|
70
72
|
return (
|
|
71
73
|
<div
|
|
74
|
+
ref={rootRef}
|
|
72
75
|
aria-label={ariaLabel}
|
|
73
76
|
className={className}
|
|
74
77
|
role="tree"
|
|
@@ -91,8 +94,31 @@ export function TreeView<TId extends string = string>({
|
|
|
91
94
|
);
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
/*** Reveal a controlled selection after it is rendered without changing expansion ownership. */
|
|
98
|
+
function useSelectedTreeItemReveal<TId extends string>(
|
|
99
|
+
rootRef: React.RefObject<HTMLDivElement | null>,
|
|
100
|
+
selectedId: TId | undefined,
|
|
101
|
+
expandedIds: readonly TId[],
|
|
102
|
+
) {
|
|
103
|
+
React.useEffect(() => {
|
|
104
|
+
if (selectedId === undefined) return;
|
|
105
|
+
const root = rootRef.current as unknown as TreeViewQueryableElement | null;
|
|
106
|
+
root
|
|
107
|
+
?.querySelector('[role="treeitem"][aria-selected="true"]')
|
|
108
|
+
?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
109
|
+
}, [expandedIds, rootRef, selectedId]);
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
const TREE_STYLE = {
|
|
95
113
|
display: 'flex',
|
|
96
114
|
flexDirection: 'column',
|
|
97
115
|
minWidth: 0,
|
|
98
116
|
} as const satisfies React.CSSProperties;
|
|
117
|
+
|
|
118
|
+
interface TreeViewQueryableElement {
|
|
119
|
+
querySelector(selector: string): TreeViewRevealTarget | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface TreeViewRevealTarget {
|
|
123
|
+
scrollIntoView(options: { readonly block: 'nearest'; readonly inline: 'nearest' }): void;
|
|
124
|
+
}
|
|
@@ -229,6 +229,7 @@ function TreeView({
|
|
|
229
229
|
ariaLabel = "Tree"
|
|
230
230
|
}) {
|
|
231
231
|
const [internalExpandedIds, setInternalExpandedIds] = React2.useState(defaultExpandedIds ?? []);
|
|
232
|
+
const rootRef = React2.useRef(null);
|
|
232
233
|
const isControlled = controlledExpandedIds !== undefined;
|
|
233
234
|
const expandedIds = isControlled ? controlledExpandedIds : internalExpandedIds;
|
|
234
235
|
const handleToggleExpand = React2.useCallback((id) => {
|
|
@@ -237,7 +238,9 @@ function TreeView({
|
|
|
237
238
|
setInternalExpandedIds(nextExpandedIds);
|
|
238
239
|
onExpandedChange?.(nextExpandedIds);
|
|
239
240
|
}, [expandedIds, isControlled, onExpandedChange]);
|
|
241
|
+
useSelectedTreeItemReveal(rootRef, selectedId, expandedIds);
|
|
240
242
|
return /* @__PURE__ */ jsx3("div", {
|
|
243
|
+
ref: rootRef,
|
|
241
244
|
"aria-label": ariaLabel,
|
|
242
245
|
className,
|
|
243
246
|
role: "tree",
|
|
@@ -254,6 +257,14 @@ function TreeView({
|
|
|
254
257
|
}, node.id))
|
|
255
258
|
});
|
|
256
259
|
}
|
|
260
|
+
function useSelectedTreeItemReveal(rootRef, selectedId, expandedIds) {
|
|
261
|
+
React2.useEffect(() => {
|
|
262
|
+
if (selectedId === undefined)
|
|
263
|
+
return;
|
|
264
|
+
const root = rootRef.current;
|
|
265
|
+
root?.querySelector('[role="treeitem"][aria-selected="true"]')?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
266
|
+
}, [expandedIds, rootRef, selectedId]);
|
|
267
|
+
}
|
|
257
268
|
var TREE_STYLE = {
|
|
258
269
|
display: "flex",
|
|
259
270
|
flexDirection: "column",
|