@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.
- package/package.json +5 -2
- package/skills/ndl-react/SKILL.md +152 -0
- package/skills/ndl-react/components/accordion.md +386 -0
- package/skills/ndl-react/components/ai/code-preview.md +128 -0
- package/skills/ndl-react/components/ai/file-tag.md +90 -0
- package/skills/ndl-react/components/ai/image-tag.md +91 -0
- package/skills/ndl-react/components/ai/more-files.md +73 -0
- package/skills/ndl-react/components/ai/presence.md +54 -0
- package/skills/ndl-react/components/ai/preview.md +498 -0
- package/skills/ndl-react/components/ai/prompt.md +362 -0
- package/skills/ndl-react/components/ai/reasoning.md +95 -0
- package/skills/ndl-react/components/ai/response.md +1109 -0
- package/skills/ndl-react/components/ai/suggestion.md +84 -0
- package/skills/ndl-react/components/ai/thinking.md +43 -0
- package/skills/ndl-react/components/ai/tool-call.md +43 -0
- package/skills/ndl-react/components/ai/user-bubble.md +187 -0
- package/skills/ndl-react/components/avatar.md +169 -0
- package/skills/ndl-react/components/banner.md +225 -0
- package/skills/ndl-react/components/box.md +39 -0
- package/skills/ndl-react/components/breadcrumbs.md +245 -0
- package/skills/ndl-react/components/checkbox.md +269 -0
- package/skills/ndl-react/components/clean-icon-button.md +294 -0
- package/skills/ndl-react/components/clipboard-button.md +154 -0
- package/skills/ndl-react/components/code-block.md +276 -0
- package/skills/ndl-react/components/code.md +68 -0
- package/skills/ndl-react/components/color-picker.md +131 -0
- package/skills/ndl-react/components/conditional-wrap.md +46 -0
- package/skills/ndl-react/components/data-grid.md +3121 -0
- package/skills/ndl-react/components/date-picker.md +652 -0
- package/skills/ndl-react/components/dialog.md +485 -0
- package/skills/ndl-react/components/dismissible-tag.md +136 -0
- package/skills/ndl-react/components/divider.md +96 -0
- package/skills/ndl-react/components/drawer.md +543 -0
- package/skills/ndl-react/components/dropdown-button.md +161 -0
- package/skills/ndl-react/components/dropzone.md +371 -0
- package/skills/ndl-react/components/filled-button.md +26 -0
- package/skills/ndl-react/components/flex.md +82 -0
- package/skills/ndl-react/components/graph-label.md +167 -0
- package/skills/ndl-react/components/icon-button-array.md +187 -0
- package/skills/ndl-react/components/icon-button.md +193 -0
- package/skills/ndl-react/components/inline-edit.md +209 -0
- package/skills/ndl-react/components/kbd.md +63 -0
- package/skills/ndl-react/components/loading-bar.md +43 -0
- package/skills/ndl-react/components/loading-spinner.md +57 -0
- package/skills/ndl-react/components/logo.md +57 -0
- package/skills/ndl-react/components/menu.md +993 -0
- package/skills/ndl-react/components/modal.md +27 -0
- package/skills/ndl-react/components/next/tree-view.md +966 -0
- package/skills/ndl-react/components/outlined-button.md +26 -0
- package/skills/ndl-react/components/popover.md +283 -0
- package/skills/ndl-react/components/progress-bar.md +70 -0
- package/skills/ndl-react/components/radio.md +150 -0
- package/skills/ndl-react/components/read-only-tag.md +96 -0
- package/skills/ndl-react/components/segmented-control.md +200 -0
- package/skills/ndl-react/components/select-icon-button.md +183 -0
- package/skills/ndl-react/components/select.md +530 -0
- package/skills/ndl-react/components/selectable-tag.md +203 -0
- package/skills/ndl-react/components/side-navigation.md +652 -0
- package/skills/ndl-react/components/skeleton.md +274 -0
- package/skills/ndl-react/components/slider.md +308 -0
- package/skills/ndl-react/components/spotlight.md +796 -0
- package/skills/ndl-react/components/status-indicator.md +57 -0
- package/skills/ndl-react/components/status-label.md +132 -0
- package/skills/ndl-react/components/switch.md +167 -0
- package/skills/ndl-react/components/tabs.md +433 -0
- package/skills/ndl-react/components/tag.md +91 -0
- package/skills/ndl-react/components/text-area.md +178 -0
- package/skills/ndl-react/components/text-button.md +25 -0
- package/skills/ndl-react/components/text-input.md +475 -0
- package/skills/ndl-react/components/text-link.md +119 -0
- package/skills/ndl-react/components/time-picker.md +137 -0
- package/skills/ndl-react/components/timezone-picker.md +496 -0
- package/skills/ndl-react/components/tooltip.md +478 -0
- package/skills/ndl-react/components/tree-view.md +545 -0
- package/skills/ndl-react/components/typography.md +72 -0
- 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
|
+
```
|