@neo4j-ndl/react 4.16.8 → 4.16.9

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.
Files changed (76) hide show
  1. package/package.json +5 -2
  2. package/skills/ndl-react/SKILL.md +152 -0
  3. package/skills/ndl-react/components/accordion.md +386 -0
  4. package/skills/ndl-react/components/ai/code-preview.md +128 -0
  5. package/skills/ndl-react/components/ai/file-tag.md +90 -0
  6. package/skills/ndl-react/components/ai/image-tag.md +91 -0
  7. package/skills/ndl-react/components/ai/more-files.md +73 -0
  8. package/skills/ndl-react/components/ai/presence.md +54 -0
  9. package/skills/ndl-react/components/ai/preview.md +498 -0
  10. package/skills/ndl-react/components/ai/prompt.md +362 -0
  11. package/skills/ndl-react/components/ai/reasoning.md +95 -0
  12. package/skills/ndl-react/components/ai/response.md +1109 -0
  13. package/skills/ndl-react/components/ai/suggestion.md +84 -0
  14. package/skills/ndl-react/components/ai/thinking.md +43 -0
  15. package/skills/ndl-react/components/ai/tool-call.md +43 -0
  16. package/skills/ndl-react/components/ai/user-bubble.md +187 -0
  17. package/skills/ndl-react/components/avatar.md +169 -0
  18. package/skills/ndl-react/components/banner.md +225 -0
  19. package/skills/ndl-react/components/box.md +39 -0
  20. package/skills/ndl-react/components/breadcrumbs.md +245 -0
  21. package/skills/ndl-react/components/checkbox.md +269 -0
  22. package/skills/ndl-react/components/clean-icon-button.md +294 -0
  23. package/skills/ndl-react/components/clipboard-button.md +154 -0
  24. package/skills/ndl-react/components/code-block.md +276 -0
  25. package/skills/ndl-react/components/code.md +68 -0
  26. package/skills/ndl-react/components/color-picker.md +131 -0
  27. package/skills/ndl-react/components/conditional-wrap.md +46 -0
  28. package/skills/ndl-react/components/data-grid.md +3121 -0
  29. package/skills/ndl-react/components/date-picker.md +652 -0
  30. package/skills/ndl-react/components/dialog.md +485 -0
  31. package/skills/ndl-react/components/dismissible-tag.md +136 -0
  32. package/skills/ndl-react/components/divider.md +96 -0
  33. package/skills/ndl-react/components/drawer.md +543 -0
  34. package/skills/ndl-react/components/dropdown-button.md +161 -0
  35. package/skills/ndl-react/components/dropzone.md +371 -0
  36. package/skills/ndl-react/components/filled-button.md +26 -0
  37. package/skills/ndl-react/components/flex.md +82 -0
  38. package/skills/ndl-react/components/graph-label.md +167 -0
  39. package/skills/ndl-react/components/icon-button-array.md +187 -0
  40. package/skills/ndl-react/components/icon-button.md +193 -0
  41. package/skills/ndl-react/components/inline-edit.md +209 -0
  42. package/skills/ndl-react/components/kbd.md +63 -0
  43. package/skills/ndl-react/components/loading-bar.md +43 -0
  44. package/skills/ndl-react/components/loading-spinner.md +57 -0
  45. package/skills/ndl-react/components/logo.md +57 -0
  46. package/skills/ndl-react/components/menu.md +993 -0
  47. package/skills/ndl-react/components/modal.md +27 -0
  48. package/skills/ndl-react/components/next/tree-view.md +966 -0
  49. package/skills/ndl-react/components/outlined-button.md +26 -0
  50. package/skills/ndl-react/components/popover.md +283 -0
  51. package/skills/ndl-react/components/progress-bar.md +70 -0
  52. package/skills/ndl-react/components/radio.md +150 -0
  53. package/skills/ndl-react/components/read-only-tag.md +96 -0
  54. package/skills/ndl-react/components/segmented-control.md +200 -0
  55. package/skills/ndl-react/components/select-icon-button.md +183 -0
  56. package/skills/ndl-react/components/select.md +530 -0
  57. package/skills/ndl-react/components/selectable-tag.md +203 -0
  58. package/skills/ndl-react/components/side-navigation.md +652 -0
  59. package/skills/ndl-react/components/skeleton.md +274 -0
  60. package/skills/ndl-react/components/slider.md +308 -0
  61. package/skills/ndl-react/components/spotlight.md +796 -0
  62. package/skills/ndl-react/components/status-indicator.md +57 -0
  63. package/skills/ndl-react/components/status-label.md +132 -0
  64. package/skills/ndl-react/components/switch.md +167 -0
  65. package/skills/ndl-react/components/tabs.md +433 -0
  66. package/skills/ndl-react/components/tag.md +91 -0
  67. package/skills/ndl-react/components/text-area.md +178 -0
  68. package/skills/ndl-react/components/text-button.md +25 -0
  69. package/skills/ndl-react/components/text-input.md +475 -0
  70. package/skills/ndl-react/components/text-link.md +119 -0
  71. package/skills/ndl-react/components/time-picker.md +137 -0
  72. package/skills/ndl-react/components/timezone-picker.md +496 -0
  73. package/skills/ndl-react/components/tooltip.md +478 -0
  74. package/skills/ndl-react/components/tree-view.md +545 -0
  75. package/skills/ndl-react/components/typography.md +72 -0
  76. package/skills/ndl-react/components/wizard.md +152 -0
@@ -0,0 +1,966 @@
1
+ # TreeView
2
+
3
+ Import: `import { TreeView } from '@neo4j-ndl/react/next'`
4
+
5
+ ## Props
6
+
7
+ ### TreeView
8
+
9
+ | Prop | Type | Required | Default | Description |
10
+ |------|------|----------|---------|-------------|
11
+ | `ariaLabel` | `string \| null` | ✅ | | The aria-label for the tree. Required for accessibility, unless using ariaLabelledby. Pass null to omit. |
12
+ | `ariaLabelledby` | `string` | | | The aria-labelledby for the tree. Pass a string of space-separated IDs of elements that label the tree. |
13
+ | `children` | `ReactNode` | | | The children of the tree. Should be TreeView.Item components or TreeView.SkeletonItem components only. |
14
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
15
+ | `selectionMode` | `'multiple' \| 'single'` | | `single` | Selection mode for the tree. Defaults to "single". |
16
+
17
+ ### TreeView.Item
18
+
19
+ | Prop | Type | Required | Default | Description |
20
+ |------|------|----------|---------|-------------|
21
+ | `actionMenuItems` | `ReactNode` | | | ReactNode containing Menu.Item elements to render in an action menu. When provided, an action button is rendered. |
22
+ | `actionMenuProps` | `MenuProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>` | | | Props forwarded to the Menu sub-components (anchorRef, onClose). |
23
+ | `children` | `ReactNode` | | | Nested TreeView.Item or TreeView.SkeletonItem elements rendered as sub-items in a collapsible group. |
24
+ | `defaultExpanded` | `boolean` | | `false` | Default expansion for uncontrolled items |
25
+ | `hasChildren` | `boolean` | | | Whether the item has children. Decides if the item should render a chevron for expansion. Needed for lazy loading. |
26
+ | `isDisabled` | `boolean` | | | Whether the item is disabled |
27
+ | `isExpanded` | `boolean` | | | Whether the item is expanded. Makes the item controlled. |
28
+ | `isIndeterminate` | `boolean` | | | Whether the item is in an indeterminate state. Only meaningful in multi-select mode for parent nodes. |
29
+ | `isLoading` | `boolean` | | | Whether the item is loading. Applies aria-busy="true" to the item. |
30
+ | `isSelected` | `boolean` | | | Whether the item is selected |
31
+ | `leadingVisual` | `ReactNode` | | | Leading visual for the item. |
32
+ | `onExpandedChange` | `(isExpanded: boolean) => void` | | | Callback called when the item is expanded/collapsed. |
33
+ | `onSelectedChange` | `(isSelected: boolean) => void` | | | Callback called when the item is selected/deselected. |
34
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
35
+ | `title` | `ReactNode` | | | The label content displayed in the item row. |
36
+ | `tooltipContent` | `ReactNode` | | | Content rendered inside the tooltip. When provided, the item is wrapped in a Tooltip. |
37
+ | `tooltipProps` | `TooltipObjectProps` | | | Props forwarded to the Tooltip sub-components (root, trigger, content). |
38
+ | `trailingContent` | `ReactNode` | | | Trailing content for the item. Shown after the children but before the action menu. |
39
+
40
+ ### TreeView.SkeletonItem
41
+
42
+ | Prop | Type | Required | Default | Description |
43
+ |------|------|----------|---------|-------------|
44
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
45
+ | `rows` | `number` | | `1` | The number of rows to render. |
46
+
47
+ ## Accessibility
48
+
49
+ ## Keyboard interactions
50
+
51
+ Implements the keyboard interactions defined in the [WAI-ARIA TreeView pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).
52
+
53
+ | Key | Description |
54
+ |-----|-------------|
55
+ | `ArrowDown` | Moves focus to the next visible tree item |
56
+ | `ArrowUp` | Moves focus to the previous visible tree item |
57
+ | `ArrowRight` | Expands a collapsed parent item. If already expanded, moves focus to the first child item |
58
+ | `ArrowLeft` | Collapses an expanded parent item. If already collapsed (or a leaf), moves focus to the parent item |
59
+ | `Home` | Moves focus to the first tree item |
60
+ | `End` | Moves focus to the last visible tree item |
61
+ | `Enter` | Toggles selection on the focused item, or toggles expansion if the item is not selectable |
62
+ | `Space` | Toggles selection on the focused item, or toggles expansion if the item is not selectable |
63
+ | `Shift + F10` | Opens the action menu on the focused item (when `actionMenu` is provided) |
64
+
65
+ The tree uses a roving tabindex strategy: only the currently focused item has `tabIndex={0}`, all other items have `tabIndex={-1}`. When the tree root receives focus it delegates to the previously focused item, or the first item if none was focused.
66
+
67
+ ## WAI-ARIA roles and attributes
68
+
69
+ The TreeView component follows the [WAI-ARIA TreeView pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).
70
+
71
+ - The root container has `role="tree"` and is labeled via `aria-label` or `aria-labelledby`
72
+ - When `selectionMode` is `"multiple"`, the root sets `aria-multiselectable="true"`
73
+ - Each item has `role="treeitem"` with `aria-level`, `aria-posinset`, and `aria-setsize` set automatically
74
+ - Parent items (items with children) set `aria-expanded` and `aria-owns` linking to their child `role="group"` container
75
+ - In single selection mode, selected items set `aria-selected`. In multiple selection mode, selected items set `aria-checked`, with `aria-checked="mixed"` for indeterminate parent nodes
76
+ - Disabled items set `aria-disabled="true"`
77
+ - Loading items set `aria-busy="true"`
78
+ - When `actionMenu` is provided, the treeitem sets `aria-keyshortcuts="Shift+F10"` and the action button sets `aria-haspopup="menu"`, `aria-expanded`, and `aria-label="Actions"`
79
+ - `TreeView.SkeletonItem` is `aria-hidden="true"` since it is a visual placeholder with no interactive content
80
+ - Child groups use `role="group"` and are `hidden` when the parent is collapsed
81
+
82
+ ## Implementation guidelines
83
+
84
+ - Always provide either `ariaLabel` or `ariaLabelledby` on the root so the tree has an accessible name
85
+ - `role="tree"` only allows children with roles `treeitem` or `group`. The built-in sub-components handle this automatically, but wrapping items in custom elements without a valid role will break the tree semantics for assistive technologies
86
+ - In single selection mode (`selectionMode="single"`, the default), only one item should have `isSelected` set to `true` at a time. The component does not enforce this — it is the consumer's responsibility to manage the selection state
87
+ - In multiple selection mode, manage `isIndeterminate` on parent nodes to communicate partial selection via `aria-checked="mixed"`. Screen reader users rely on this to understand that some but not all children are selected
88
+ - When using `hasChildren` for lazy loading, set `isLoading` to apply `aria-busy="true"` and render `TreeView.SkeletonItem` inside the expanded item so users know content is being fetched
89
+ - The checkbox rendered in multiple selection mode has `tabIndex={-1}` — selection is driven from the treeitem via `Enter`/`Space`, not from the checkbox directly
90
+ - The action menu button has `tabIndex={-1}` and is only reachable via `Shift+F10`, keeping arrow-key navigation within the tree clean
91
+
92
+ ### Related WCAG criteria
93
+
94
+ - [2.1.1 Keyboard](https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html) (A): All tree items are fully navigable and operable via keyboard
95
+ - [2.4.3 Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html) (A): Roving tabindex ensures a logical focus order; `ArrowLeft` returns focus to the parent item
96
+ - [1.3.1 Info and Relationships](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html) (A): Tree hierarchy is expressed via `aria-level`, `aria-posinset`, `aria-setsize`, and `role="group"` nesting
97
+ - [4.1.2 Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) (A): Roles (`tree`, `treeitem`, `group`), states (`aria-expanded`, `aria-selected`/`aria-checked`, `aria-disabled`, `aria-busy`), and accessible names (`aria-label`/`aria-labelledby`) are set automatically by the component
98
+
99
+ ## Examples
100
+
101
+ ### Actions
102
+
103
+ ```tsx
104
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
105
+
106
+ import { Menu } from '@neo4j-ndl/react';
107
+ import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons';
108
+ import { TreeView } from '@neo4j-ndl/react/next';
109
+ import { useState } from 'react';
110
+
111
+ const Component = () => {
112
+ const [expanded, setExpanded] = useState<Record<string, boolean>>({});
113
+ const [selected, setSelected] = useState<string | null>(null);
114
+
115
+ const toggle = (key: string) =>
116
+ setExpanded((prev) => ({ ...prev, [key]: !prev[key] }));
117
+
118
+ return (
119
+ <TreeView ariaLabel="Project files">
120
+ <TreeView.Item
121
+ title="src"
122
+ hasChildren
123
+ isExpanded={expanded['src']}
124
+ onExpandedChange={() => toggle('src')}
125
+ isSelected={selected === 'src'}
126
+ onSelectedChange={() => setSelected('src')}
127
+ leadingVisual={<FolderIconSolid />}
128
+ actionMenuItems={
129
+ <>
130
+ <Menu.Item
131
+ title="New File"
132
+ onClick={() => alert('New file in src')}
133
+ />
134
+ <Menu.Item title="Rename" onClick={() => alert('Rename src')} />
135
+ <Menu.Item title="Delete" onClick={() => alert('Delete src')} />
136
+ </>
137
+ }
138
+ >
139
+ <TreeView.Item
140
+ title="index.ts"
141
+ isSelected={selected === 'index'}
142
+ onSelectedChange={() => setSelected('index')}
143
+ leadingVisual={<DocumentIconOutline />}
144
+ actionMenuItems={
145
+ <>
146
+ <Menu.Item title="Open" onClick={() => alert('Open index.ts')} />
147
+ <Menu.Item
148
+ title="Rename"
149
+ onClick={() => alert('Rename index.ts')}
150
+ />
151
+ <Menu.Item
152
+ title="Delete"
153
+ onClick={() => alert('Delete index.ts')}
154
+ />
155
+ </>
156
+ }
157
+ />
158
+ <TreeView.Item
159
+ title="utils.ts"
160
+ isSelected={selected === 'utils'}
161
+ onSelectedChange={() => setSelected('utils')}
162
+ leadingVisual={<DocumentIconOutline />}
163
+ actionMenuItems={
164
+ <>
165
+ <Menu.Item title="Open" onClick={() => alert('Open utils.ts')} />
166
+ <Menu.Item
167
+ title="Rename"
168
+ onClick={() => alert('Rename utils.ts')}
169
+ />
170
+ <Menu.Item
171
+ title="Delete"
172
+ onClick={() => alert('Delete utils.ts')}
173
+ />
174
+ </>
175
+ }
176
+ />
177
+ </TreeView.Item>
178
+ <TreeView.Item
179
+ title="tests"
180
+ hasChildren
181
+ isExpanded={expanded['tests']}
182
+ onExpandedChange={() => toggle('tests')}
183
+ isSelected={selected === 'tests'}
184
+ onSelectedChange={() => setSelected('tests')}
185
+ leadingVisual={<FolderIconSolid />}
186
+ actionMenuItems={
187
+ <>
188
+ <Menu.Item
189
+ title="Run Tests"
190
+ onClick={() => alert('Run all tests')}
191
+ />
192
+ <Menu.Item
193
+ title="New Test"
194
+ onClick={() => alert('New test in tests')}
195
+ />
196
+ <Menu.Item title="Delete" onClick={() => alert('Delete tests')} />
197
+ </>
198
+ }
199
+ >
200
+ <TreeView.Item
201
+ title="index.test.ts"
202
+ isSelected={selected === 'index-test'}
203
+ onSelectedChange={() => setSelected('index-test')}
204
+ leadingVisual={<DocumentIconOutline />}
205
+ actionMenuItems={
206
+ <>
207
+ <Menu.Item
208
+ title="Run"
209
+ onClick={() => alert('Run index.test.ts')}
210
+ />
211
+ <Menu.Item
212
+ title="Rename"
213
+ onClick={() => alert('Rename index.test.ts')}
214
+ />
215
+ <Menu.Item
216
+ title="Delete"
217
+ onClick={() => alert('Delete index.test.ts')}
218
+ />
219
+ </>
220
+ }
221
+ />
222
+ <TreeView.Item
223
+ title="utils.test.ts"
224
+ isSelected={selected === 'utils-test'}
225
+ onSelectedChange={() => setSelected('utils-test')}
226
+ leadingVisual={<DocumentIconOutline />}
227
+ actionMenuItems={
228
+ <>
229
+ <Menu.Item
230
+ title="Run"
231
+ onClick={() => alert('Run utils.test.ts')}
232
+ />
233
+ <Menu.Item
234
+ title="Rename"
235
+ onClick={() => alert('Rename utils.test.ts')}
236
+ />
237
+ <Menu.Item
238
+ title="Delete"
239
+ onClick={() => alert('Delete utils.test.ts')}
240
+ />
241
+ </>
242
+ }
243
+ />
244
+ </TreeView.Item>
245
+ <TreeView.Item
246
+ title="README.md"
247
+ isSelected={selected === 'readme'}
248
+ onSelectedChange={() => setSelected('readme')}
249
+ leadingVisual={<DocumentIconOutline />}
250
+ actionMenuItems={
251
+ <>
252
+ <Menu.Item title="Open" onClick={() => alert('Open README.md')} />
253
+ <Menu.Item
254
+ title="Rename"
255
+ onClick={() => alert('Rename README.md')}
256
+ />
257
+ <Menu.Item
258
+ title="Delete"
259
+ onClick={() => alert('Delete README.md')}
260
+ />
261
+ </>
262
+ }
263
+ />
264
+ </TreeView>
265
+ );
266
+ };
267
+
268
+ export default Component;
269
+ ```
270
+
271
+ ### Lazy
272
+
273
+ ```tsx
274
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
275
+
276
+ import {
277
+ DocumentIconOutline,
278
+ FolderIconSolid,
279
+ PhotoIconOutline,
280
+ } from '@neo4j-ndl/react/icons';
281
+ import { TreeView } from '@neo4j-ndl/react/next';
282
+ import { useCallback, useMemo, useRef, useState } from 'react';
283
+
284
+ type TreeNode = {
285
+ id: string;
286
+ label: string;
287
+ children?: TreeNode[];
288
+ };
289
+
290
+ const TREE_DATA: TreeNode[] = [
291
+ {
292
+ children: [
293
+ {
294
+ children: [
295
+ { id: 'app-tsx', label: 'App.tsx' },
296
+ { id: 'index-tsx', label: 'index.tsx' },
297
+ { id: 'styles-css', label: 'styles.css' },
298
+ ],
299
+ id: 'frontend',
300
+ label: 'Frontend',
301
+ },
302
+ {
303
+ children: [
304
+ { id: 'server-ts', label: 'server.ts' },
305
+ { id: 'routes-ts', label: 'routes.ts' },
306
+ ],
307
+ id: 'backend',
308
+ label: 'Backend',
309
+ },
310
+ { id: 'readme-md', label: 'README.md' },
311
+ ],
312
+ id: 'projects',
313
+ label: 'Projects',
314
+ },
315
+ {
316
+ children: [
317
+ {
318
+ children: [
319
+ { id: 'beach-jpg', label: 'beach.jpg' },
320
+ { id: 'sunset-jpg', label: 'sunset.jpg' },
321
+ ],
322
+ id: 'vacation',
323
+ label: 'Vacation',
324
+ },
325
+ { id: 'profile-png', label: 'profile.png' },
326
+ ],
327
+ id: 'photos',
328
+ label: 'Photos',
329
+ },
330
+ {
331
+ children: [
332
+ { id: 'resume-pdf', label: 'resume.pdf' },
333
+ { id: 'cover-letter-pdf', label: 'cover-letter.pdf' },
334
+ { id: 'notes-txt', label: 'notes.txt' },
335
+ ],
336
+ id: 'documents',
337
+ label: 'Documents',
338
+ },
339
+ {
340
+ children: [
341
+ {
342
+ children: [
343
+ { id: 'chill-m3u', label: 'chill.m3u' },
344
+ { id: 'workout-m3u', label: 'workout.m3u' },
345
+ ],
346
+ id: 'playlists',
347
+ label: 'Playlists',
348
+ },
349
+ { id: 'favorites-m3u', label: 'favorites.m3u' },
350
+ ],
351
+ id: 'music',
352
+ label: 'Music',
353
+ },
354
+ { id: 'todo-txt', label: 'TODO.txt' },
355
+ ];
356
+
357
+ const LOAD_DELAY_MS = 600;
358
+
359
+ function iconForNode(node: TreeNode) {
360
+ if (node.children) {
361
+ return <FolderIconSolid />;
362
+ }
363
+ if (/\.(jpg|jpeg|png|gif|svg)$/i.test(node.label)) {
364
+ return <PhotoIconOutline />;
365
+ }
366
+ return <DocumentIconOutline />;
367
+ }
368
+
369
+ type NodeState = {
370
+ isExpanded: boolean;
371
+ isLoading: boolean;
372
+ hasLoaded: boolean;
373
+ };
374
+
375
+ const INITIAL_NODE_STATE: NodeState = {
376
+ hasLoaded: false,
377
+ isExpanded: false,
378
+ isLoading: false,
379
+ };
380
+
381
+ function useTreeState() {
382
+ const [nodeStates, setNodeStates] = useState<Record<string, NodeState>>({});
383
+ const [selectedId, setSelectedId] = useState<string | null>(null);
384
+ const timers = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
385
+
386
+ const getNodeState = useCallback(
387
+ (id: string): NodeState => nodeStates[id] ?? INITIAL_NODE_STATE,
388
+ [nodeStates],
389
+ );
390
+
391
+ const handleExpand = useCallback((id: string, expanded: boolean) => {
392
+ setNodeStates((prev) => {
393
+ const current = prev[id] ?? INITIAL_NODE_STATE;
394
+ if (expanded && !current.hasLoaded) {
395
+ if (timers.current[id]) {
396
+ clearTimeout(timers.current[id]);
397
+ }
398
+ timers.current[id] = setTimeout(() => {
399
+ setNodeStates((p) => ({
400
+ ...p,
401
+ [id]: { hasLoaded: true, isExpanded: true, isLoading: false },
402
+ }));
403
+ delete timers.current[id];
404
+ }, LOAD_DELAY_MS);
405
+ return {
406
+ ...prev,
407
+ [id]: { hasLoaded: false, isExpanded: true, isLoading: true },
408
+ };
409
+ }
410
+ return { ...prev, [id]: { ...current, isExpanded: expanded } };
411
+ });
412
+ }, []);
413
+
414
+ const handleSelect = useCallback(
415
+ (id: string) => setSelectedId((prev) => (prev === id ? null : id)),
416
+ [],
417
+ );
418
+
419
+ return { getNodeState, handleExpand, handleSelect, selectedId };
420
+ }
421
+
422
+ type RenderContext = {
423
+ getNodeState: (id: string) => NodeState;
424
+ selectedId: string | null;
425
+ handleExpand: (id: string, expanded: boolean) => void;
426
+ handleSelect: (id: string) => void;
427
+ };
428
+
429
+ function renderNode(node: TreeNode, ctx: RenderContext) {
430
+ const hasChildren =
431
+ node.children !== undefined &&
432
+ node.children !== null &&
433
+ node.children.length > 0;
434
+ const state = ctx.getNodeState(node.id);
435
+
436
+ return (
437
+ <TreeView.Item
438
+ key={node.id}
439
+ title={node.label}
440
+ hasChildren={hasChildren}
441
+ isExpanded={state.isExpanded}
442
+ isLoading={state.isLoading}
443
+ isSelected={ctx.selectedId === node.id}
444
+ leadingVisual={iconForNode(node)}
445
+ onExpandedChange={(expanded: boolean) =>
446
+ ctx.handleExpand(node.id, expanded)
447
+ }
448
+ onSelectedChange={() => ctx.handleSelect(node.id)}
449
+ >
450
+ {hasChildren && state.isLoading && !state.hasLoaded && (
451
+ <TreeView.SkeletonItem rows={2} />
452
+ )}
453
+ {hasChildren &&
454
+ state.hasLoaded &&
455
+ node.children!.map((child) => renderNode(child, ctx))}
456
+ </TreeView.Item>
457
+ );
458
+ }
459
+
460
+ const Component = () => {
461
+ const { getNodeState, selectedId, handleExpand, handleSelect } =
462
+ useTreeState();
463
+
464
+ const ctx = useMemo<RenderContext>(
465
+ () => ({ getNodeState, handleExpand, handleSelect, selectedId }),
466
+ [getNodeState, selectedId, handleExpand, handleSelect],
467
+ );
468
+
469
+ return (
470
+ <TreeView ariaLabel="Lazy-loaded file explorer">
471
+ {TREE_DATA.map((node) => renderNode(node, ctx))}
472
+ </TreeView>
473
+ );
474
+ };
475
+
476
+ export default Component;
477
+ ```
478
+
479
+ ### Multi
480
+
481
+ ```tsx
482
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
483
+
484
+ import {
485
+ DocumentIconOutline,
486
+ FolderIconSolid,
487
+ PhotoIconOutline,
488
+ } from '@neo4j-ndl/react/icons';
489
+ import { TreeView } from '@neo4j-ndl/react/next';
490
+ import { useCallback, useState } from 'react';
491
+
492
+ type CheckedState = Record<string, boolean>;
493
+
494
+ const WORK_CHILDREN = ['report', 'presentation'] as const;
495
+ const DOC_CHILDREN = ['work', 'resume'] as const;
496
+ const IMAGE_CHILDREN = ['photo', 'screenshot'] as const;
497
+ const DISABLED_KEYS = new Set(['presentation']);
498
+
499
+ function getParentChecked(
500
+ checked: CheckedState,
501
+ childKeys: readonly string[],
502
+ ): { isChecked: boolean; isIndeterminate: boolean } {
503
+ const enabledKeys = childKeys.filter((k) => !DISABLED_KEYS.has(k));
504
+ if (enabledKeys.length === 0) {
505
+ return { isChecked: false, isIndeterminate: false };
506
+ }
507
+ const checkedCount = enabledKeys.filter((k) => checked[k]).length;
508
+ if (checkedCount === 0) {
509
+ return { isChecked: false, isIndeterminate: false };
510
+ }
511
+ if (checkedCount === enabledKeys.length) {
512
+ return { isChecked: true, isIndeterminate: false };
513
+ }
514
+ return { isChecked: false, isIndeterminate: true };
515
+ }
516
+
517
+ const Component = () => {
518
+ const [expanded, setExpanded] = useState<Record<string, boolean>>({});
519
+ const [checked, setChecked] = useState<CheckedState>({});
520
+
521
+ const toggle = (key: string) =>
522
+ setExpanded((prev) => ({ ...prev, [key]: !prev[key] }));
523
+
524
+ const toggleCheck = useCallback((key: string) => {
525
+ setChecked((prev) => ({ ...prev, [key]: !prev[key] }));
526
+ }, []);
527
+
528
+ const toggleParent = useCallback(
529
+ (childKeys: readonly string[]) => {
530
+ const enabledKeys = childKeys.filter((k) => !DISABLED_KEYS.has(k));
531
+ const isAllChecked = enabledKeys.every((k) => checked[k]);
532
+ setChecked((prev) => {
533
+ const next = { ...prev };
534
+ for (const k of enabledKeys) {
535
+ next[k] = !isAllChecked;
536
+ }
537
+ return next;
538
+ });
539
+ },
540
+ [checked],
541
+ );
542
+
543
+ const docState = getParentChecked(checked, [
544
+ ...DOC_CHILDREN,
545
+ ...WORK_CHILDREN,
546
+ ]);
547
+ const workState = getParentChecked(checked, WORK_CHILDREN);
548
+ const imageState = getParentChecked(checked, IMAGE_CHILDREN);
549
+
550
+ return (
551
+ <TreeView ariaLabel="File explorer" selectionMode="multiple">
552
+ <TreeView.Item
553
+ title="Documents"
554
+ hasChildren
555
+ isExpanded={expanded['documents']}
556
+ onExpandedChange={() => toggle('documents')}
557
+ isSelected={docState.isChecked}
558
+ isIndeterminate={docState.isIndeterminate}
559
+ onSelectedChange={() =>
560
+ toggleParent([...DOC_CHILDREN, ...WORK_CHILDREN])
561
+ }
562
+ leadingVisual={<FolderIconSolid />}
563
+ >
564
+ <TreeView.Item
565
+ title="Work"
566
+ hasChildren
567
+ isExpanded={expanded['work']}
568
+ onExpandedChange={() => toggle('work')}
569
+ isSelected={workState.isChecked}
570
+ isIndeterminate={workState.isIndeterminate}
571
+ onSelectedChange={() => toggleParent(WORK_CHILDREN)}
572
+ leadingVisual={<FolderIconSolid />}
573
+ >
574
+ <TreeView.Item
575
+ title="Report.pdf"
576
+ isSelected={!!checked['report']}
577
+ onSelectedChange={() => toggleCheck('report')}
578
+ leadingVisual={<DocumentIconOutline />}
579
+ />
580
+ <TreeView.Item
581
+ title="Presentation.pptx"
582
+ isDisabled
583
+ onSelectedChange={() => null}
584
+ leadingVisual={<DocumentIconOutline />}
585
+ />
586
+ </TreeView.Item>
587
+ <TreeView.Item
588
+ title="Resume.pdf"
589
+ isSelected={!!checked['resume']}
590
+ onSelectedChange={() => toggleCheck('resume')}
591
+ leadingVisual={<DocumentIconOutline />}
592
+ />
593
+ </TreeView.Item>
594
+ <TreeView.Item
595
+ title="Images"
596
+ hasChildren
597
+ isExpanded={expanded['images']}
598
+ onExpandedChange={() => toggle('images')}
599
+ isSelected={imageState.isChecked}
600
+ isIndeterminate={imageState.isIndeterminate}
601
+ onSelectedChange={() => toggleParent(IMAGE_CHILDREN)}
602
+ leadingVisual={<FolderIconSolid />}
603
+ >
604
+ <TreeView.Item
605
+ title="Photo.jpg"
606
+ isSelected={!!checked['photo']}
607
+ onSelectedChange={() => toggleCheck('photo')}
608
+ leadingVisual={<PhotoIconOutline />}
609
+ />
610
+ <TreeView.Item
611
+ title="Screenshot.png"
612
+ isSelected={!!checked['screenshot']}
613
+ onSelectedChange={() => toggleCheck('screenshot')}
614
+ leadingVisual={<PhotoIconOutline />}
615
+ />
616
+ </TreeView.Item>
617
+ </TreeView>
618
+ );
619
+ };
620
+
621
+ export default Component;
622
+ ```
623
+
624
+ ### Non Selectable Folders
625
+
626
+ ```tsx
627
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
628
+
629
+ import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons';
630
+ import { TreeView } from '@neo4j-ndl/react/next';
631
+ import { useState } from 'react';
632
+
633
+ const Component = () => {
634
+ const [expanded, setExpanded] = useState<Record<string, boolean>>({});
635
+ const [selected, setSelected] = useState<string | null>(null);
636
+
637
+ const toggle = (key: string) =>
638
+ setExpanded((prev) => ({ ...prev, [key]: !prev[key] }));
639
+
640
+ return (
641
+ <TreeView ariaLabel="File explorer">
642
+ <TreeView.Item
643
+ title="Documents"
644
+ hasChildren
645
+ isExpanded={expanded['documents']}
646
+ onExpandedChange={() => toggle('documents')}
647
+ leadingVisual={<FolderIconSolid />}
648
+ >
649
+ <TreeView.Item
650
+ title="Work"
651
+ hasChildren
652
+ isExpanded={expanded['work']}
653
+ onExpandedChange={() => toggle('work')}
654
+ leadingVisual={<FolderIconSolid />}
655
+ >
656
+ <TreeView.Item
657
+ title="Report.pdf"
658
+ isSelected={selected === 'report'}
659
+ onSelectedChange={() => setSelected('report')}
660
+ leadingVisual={<DocumentIconOutline />}
661
+ />
662
+ <TreeView.Item
663
+ title="Presentation.pptx"
664
+ isSelected={selected === 'presentation'}
665
+ onSelectedChange={() => setSelected('presentation')}
666
+ leadingVisual={<DocumentIconOutline />}
667
+ />
668
+ </TreeView.Item>
669
+ <TreeView.Item
670
+ title="Resume.pdf"
671
+ isSelected={selected === 'resume'}
672
+ onSelectedChange={() => setSelected('resume')}
673
+ leadingVisual={<DocumentIconOutline />}
674
+ />
675
+ </TreeView.Item>
676
+ <TreeView.Item
677
+ title="Images"
678
+ hasChildren
679
+ isExpanded={expanded['images']}
680
+ onExpandedChange={() => toggle('images')}
681
+ leadingVisual={<FolderIconSolid />}
682
+ >
683
+ <TreeView.Item
684
+ title="Photo.jpg"
685
+ isSelected={selected === 'photo'}
686
+ onSelectedChange={() => setSelected('photo')}
687
+ leadingVisual={<DocumentIconOutline />}
688
+ />
689
+ <TreeView.Item
690
+ title="Screenshot.png"
691
+ isSelected={selected === 'screenshot'}
692
+ onSelectedChange={() => setSelected('screenshot')}
693
+ leadingVisual={<DocumentIconOutline />}
694
+ />
695
+ </TreeView.Item>
696
+ </TreeView>
697
+ );
698
+ };
699
+
700
+ export default Component;
701
+ ```
702
+
703
+ ### Single
704
+
705
+ ```tsx
706
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
707
+
708
+ import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons';
709
+ import { TreeView } from '@neo4j-ndl/react/next';
710
+ import { useState } from 'react';
711
+
712
+ const Component = () => {
713
+ const [expanded, setExpanded] = useState<Record<string, boolean>>({});
714
+ const [selected, setSelected] = useState<string | null>(null);
715
+
716
+ const toggle = (key: string) =>
717
+ setExpanded((prev) => ({ ...prev, [key]: !prev[key] }));
718
+
719
+ return (
720
+ <TreeView ariaLabel="File explorer">
721
+ <TreeView.Item
722
+ title="Documents"
723
+ hasChildren
724
+ isExpanded={expanded['documents']}
725
+ onExpandedChange={() => toggle('documents')}
726
+ isSelected={selected === 'documents'}
727
+ onSelectedChange={() => setSelected('documents')}
728
+ leadingVisual={<FolderIconSolid />}
729
+ >
730
+ <TreeView.Item
731
+ title="Work"
732
+ hasChildren
733
+ isExpanded={expanded['work']}
734
+ onExpandedChange={() => toggle('work')}
735
+ isSelected={selected === 'work'}
736
+ onSelectedChange={() => setSelected('work')}
737
+ leadingVisual={<FolderIconSolid />}
738
+ >
739
+ <TreeView.Item
740
+ title="Report.pdf"
741
+ isSelected={selected === 'report'}
742
+ onSelectedChange={() => setSelected('report')}
743
+ leadingVisual={<DocumentIconOutline />}
744
+ />
745
+ <TreeView.Item
746
+ title="Presentation.pptx"
747
+ isDisabled
748
+ isSelected={selected === 'presentation'}
749
+ onSelectedChange={() => setSelected('presentation')}
750
+ leadingVisual={<DocumentIconOutline />}
751
+ />
752
+ </TreeView.Item>
753
+ <TreeView.Item
754
+ title="Resume.pdf"
755
+ isSelected={selected === 'resume'}
756
+ onSelectedChange={() => setSelected('resume')}
757
+ leadingVisual={<DocumentIconOutline />}
758
+ />
759
+ </TreeView.Item>
760
+ <TreeView.Item
761
+ title="Images (disabled)"
762
+ hasChildren
763
+ isExpanded={expanded['images']}
764
+ onExpandedChange={() => toggle('images')}
765
+ isSelected={selected === 'images'}
766
+ onSelectedChange={() => setSelected('images')}
767
+ leadingVisual={<FolderIconSolid />}
768
+ isDisabled
769
+ />
770
+ <TreeView.Item
771
+ title="README.md"
772
+ isSelected={selected === 'readme'}
773
+ onSelectedChange={() => setSelected('readme')}
774
+ leadingVisual={<DocumentIconOutline />}
775
+ />
776
+ </TreeView>
777
+ );
778
+ };
779
+
780
+ export default Component;
781
+ ```
782
+
783
+ ### Tooltip
784
+
785
+ ```tsx
786
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
787
+
788
+ import { Menu } from '@neo4j-ndl/react';
789
+ import {
790
+ AppearanceIcon,
791
+ CheckCircleIconSolid,
792
+ DatabaseSignalIcon,
793
+ } from '@neo4j-ndl/react/icons';
794
+ import { TreeView } from '@neo4j-ndl/react/next';
795
+ import { useState } from 'react';
796
+
797
+ const Component = () => {
798
+ const [selected, setSelected] = useState<string | null>(null);
799
+
800
+ return (
801
+ <TreeView ariaLabel="Databases" style={{ marginTop: '100px' }}>
802
+ <TreeView.Item
803
+ title="Production"
804
+ isSelected={selected === 'production'}
805
+ onSelectedChange={() => setSelected('production')}
806
+ leadingVisual={<DatabaseSignalIcon />}
807
+ trailingContent={
808
+ <CheckCircleIconSolid className="n-size-5 n-text-success-bg-status" />
809
+ }
810
+ actionMenuItems={
811
+ <>
812
+ <Menu.Item title="Edit" />
813
+ <Menu.Item title="Delete" />
814
+ <Menu.Item title="Rename" />
815
+ </>
816
+ }
817
+ tooltipContent={
818
+ <>
819
+ <span className="n-font-semibold">Production</span>
820
+ <span className="n-flex n-items-center n-gap-token-4 n-mt-token-8">
821
+ <CheckCircleIconSolid className="n-size-5 n-text-success-bg-status" />
822
+ : Endorsed
823
+ </span>
824
+ </>
825
+ }
826
+ />
827
+ <TreeView.Item
828
+ title="Development"
829
+ isSelected={selected === 'development'}
830
+ onSelectedChange={() => setSelected('development')}
831
+ leadingVisual={<DatabaseSignalIcon />}
832
+ trailingContent={
833
+ <AppearanceIcon className="n-size-5 n-text-warning-bg-status" />
834
+ }
835
+ tooltipContent={
836
+ <>
837
+ <span className="n-font-semibold">Development</span>
838
+ <span className="n-flex n-items-center n-gap-token-4 n-mt-token-8">
839
+ <AppearanceIcon className="n-size-5 n-text-warning-bg-status" />:
840
+ In development
841
+ </span>
842
+ </>
843
+ }
844
+ />
845
+ </TreeView>
846
+ );
847
+ };
848
+
849
+ export default Component;
850
+ ```
851
+
852
+ ### Trailing
853
+
854
+ ```tsx
855
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
856
+
857
+ import { Menu } from '@neo4j-ndl/react';
858
+ import {
859
+ AppearanceIcon,
860
+ CheckCircleIconSolid,
861
+ DatabaseSignalIcon,
862
+ } from '@neo4j-ndl/react/icons';
863
+ import { TreeView } from '@neo4j-ndl/react/next';
864
+ import { useState } from 'react';
865
+
866
+ const Component = () => {
867
+ const [selected, setSelected] = useState<string | null>(null);
868
+
869
+ return (
870
+ <TreeView ariaLabel="Databases">
871
+ <TreeView.Item
872
+ title="Production"
873
+ isSelected={selected === 'production'}
874
+ onSelectedChange={() => setSelected('production')}
875
+ leadingVisual={<DatabaseSignalIcon />}
876
+ trailingContent={
877
+ <CheckCircleIconSolid className="n-size-5 n-text-success-bg-status" />
878
+ }
879
+ actionMenuItems={
880
+ <>
881
+ <Menu.Item title="Edit" />
882
+ <Menu.Item title="Delete" />
883
+ <Menu.Item title="Rename" />
884
+ </>
885
+ }
886
+ />
887
+ <TreeView.Item
888
+ title="Development"
889
+ isSelected={selected === 'development'}
890
+ onSelectedChange={() => setSelected('development')}
891
+ leadingVisual={<DatabaseSignalIcon />}
892
+ trailingContent={
893
+ <AppearanceIcon className="n-size-5 n-text-warning-bg-status" />
894
+ }
895
+ />
896
+ </TreeView>
897
+ );
898
+ };
899
+
900
+ export default Component;
901
+ ```
902
+
903
+ ### Uncontrolled
904
+
905
+ ```tsx
906
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
907
+
908
+ import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons';
909
+ import { TreeView } from '@neo4j-ndl/react/next';
910
+ import { useState } from 'react';
911
+
912
+ const Component = () => {
913
+ const [selected, setSelected] = useState<string | null>(null);
914
+
915
+ return (
916
+ <TreeView ariaLabel="File explorer">
917
+ <TreeView.Item
918
+ title="Documents (expanded by default)"
919
+ defaultExpanded={true}
920
+ leadingVisual={<FolderIconSolid />}
921
+ >
922
+ <TreeView.Item title="Work" leadingVisual={<FolderIconSolid />}>
923
+ <TreeView.Item
924
+ title="Report.pdf"
925
+ isSelected={selected === 'report'}
926
+ onSelectedChange={() => setSelected('report')}
927
+ leadingVisual={<DocumentIconOutline />}
928
+ />
929
+ <TreeView.Item
930
+ title="Presentation.pptx"
931
+ isSelected={selected === 'presentation'}
932
+ onSelectedChange={() => setSelected('presentation')}
933
+ leadingVisual={<DocumentIconOutline />}
934
+ />
935
+ </TreeView.Item>
936
+ <TreeView.Item
937
+ title="Resume.pdf"
938
+ isSelected={selected === 'resume'}
939
+ onSelectedChange={() => setSelected('resume')}
940
+ leadingVisual={<DocumentIconOutline />}
941
+ />
942
+ </TreeView.Item>
943
+ <TreeView.Item
944
+ title="Images (collapsed by default)"
945
+ leadingVisual={<FolderIconSolid />}
946
+ defaultExpanded={false}
947
+ >
948
+ <TreeView.Item
949
+ title="Photo.jpg"
950
+ isSelected={selected === 'photo'}
951
+ onSelectedChange={() => setSelected('photo')}
952
+ leadingVisual={<DocumentIconOutline />}
953
+ />
954
+ <TreeView.Item
955
+ title="Screenshot.png"
956
+ isSelected={selected === 'screenshot'}
957
+ onSelectedChange={() => setSelected('screenshot')}
958
+ leadingVisual={<DocumentIconOutline />}
959
+ />
960
+ </TreeView.Item>
961
+ </TreeView>
962
+ );
963
+ };
964
+
965
+ export default Component;
966
+ ```