UNPKG

@neo4j-ndl/react

Version:

React implementation of Neo4j Design System

967 lines (872 loc) 33.4 kB
# TreeView Import: `import { TreeView } from '@neo4j-ndl/react/next'` ## Props ### TreeView | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `ariaLabel` | `string \| null` | ✅ | | The aria-label for the tree. Required for accessibility, unless using ariaLabelledby. Pass null to omit. | | `ariaLabelledby` | `string` | | | The aria-labelledby for the tree. Pass a string of space-separated IDs of elements that label the tree. | | `children` | `ReactNode` | | | The children of the tree. Should be TreeView.Item components or TreeView.SkeletonItem components only. | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | | `selectionMode` | `'multiple' \| 'single'` | | `single` | Selection mode for the tree. Defaults to "single". | ### TreeView.Item | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `actionMenuItems` | `ReactNode` | | | ReactNode containing Menu.Item elements to render in an action menu. When provided, an action button is rendered. | | `actionMenuProps` | `MenuProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>` | | | Props forwarded to the Menu sub-components (anchorRef, onClose). | | `children` | `ReactNode` | | | Nested TreeView.Item or TreeView.SkeletonItem elements rendered as sub-items in a collapsible group. | | `defaultExpanded` | `boolean` | | `false` | Default expansion for uncontrolled items | | `hasChildren` | `boolean` | | | Whether the item has children. Decides if the item should render a chevron for expansion. Needed for lazy loading. | | `isDisabled` | `boolean` | | | Whether the item is disabled | | `isExpanded` | `boolean` | | | Whether the item is expanded. Makes the item controlled. | | `isIndeterminate` | `boolean` | | | Whether the item is in an indeterminate state. Only meaningful in multi-select mode for parent nodes. | | `isLoading` | `boolean` | | | Whether the item is loading. Applies aria-busy="true" to the item. | | `isSelected` | `boolean` | | | Whether the item is selected | | `leadingVisual` | `ReactNode` | | | Leading visual for the item. | | `onExpandedChange` | `(isExpanded: boolean) => void` | | | Callback called when the item is expanded/collapsed. | | `onSelectedChange` | `(isSelected: boolean) => void` | | | Callback called when the item is selected/deselected. | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | | `title` | `ReactNode` | | | The label content displayed in the item row. | | `tooltipContent` | `ReactNode` | | | Content rendered inside the tooltip. When provided, the item is wrapped in a Tooltip. | | `tooltipProps` | `TooltipObjectProps` | | | Props forwarded to the Tooltip sub-components (root, trigger, content). | | `trailingContent` | `ReactNode` | | | Trailing content for the item. Shown after the children but before the action menu. | ### TreeView.SkeletonItem | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | | `rows` | `number` | | `1` | The number of rows to render. | ## Accessibility ## Keyboard interactions Implements the keyboard interactions defined in the [WAI-ARIA TreeView pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/). | Key | Description | |-----|-------------| | `ArrowDown` | Moves focus to the next visible tree item | | `ArrowUp` | Moves focus to the previous visible tree item | | `ArrowRight` | Expands a collapsed parent item. If already expanded, moves focus to the first child item | | `ArrowLeft` | Collapses an expanded parent item. If already collapsed (or a leaf), moves focus to the parent item | | `Home` | Moves focus to the first tree item | | `End` | Moves focus to the last visible tree item | | `Enter` | Toggles selection on the focused item, or toggles expansion if the item is not selectable | | `Space` | Toggles selection on the focused item, or toggles expansion if the item is not selectable | | `Shift + F10` | Opens the action menu on the focused item (when `actionMenu` is provided) | 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. ## WAI-ARIA roles and attributes The TreeView component follows the [WAI-ARIA TreeView pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/). - The root container has `role="tree"` and is labeled via `aria-label` or `aria-labelledby` - When `selectionMode` is `"multiple"`, the root sets `aria-multiselectable="true"` - Each item has `role="treeitem"` with `aria-level`, `aria-posinset`, and `aria-setsize` set automatically - Parent items (items with children) set `aria-expanded` and `aria-owns` linking to their child `role="group"` container - 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 - Disabled items set `aria-disabled="true"` - Loading items set `aria-busy="true"` - 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"` - `TreeView.SkeletonItem` is `aria-hidden="true"` since it is a visual placeholder with no interactive content - Child groups use `role="group"` and are `hidden` when the parent is collapsed ## Implementation guidelines - Always provide either `ariaLabel` or `ariaLabelledby` on the root so the tree has an accessible name - `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 - 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 - 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 - 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 - The checkbox rendered in multiple selection mode has `tabIndex={-1}` — selection is driven from the treeitem via `Enter`/`Space`, not from the checkbox directly - The action menu button has `tabIndex={-1}` and is only reachable via `Shift+F10`, keeping arrow-key navigation within the tree clean ### Related WCAG criteria - [2.1.1 Keyboard](https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html) (A): All tree items are fully navigable and operable via keyboard - [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 - [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 - [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 ## Examples ### Actions ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { Menu } from '@neo4j-ndl/react'; import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons'; import { TreeView } from '@neo4j-ndl/react/next'; import { useState } from 'react'; const Component = () => { const [expanded, setExpanded] = useState<Record<string, boolean>>({}); const [selected, setSelected] = useState<string | null>(null); const toggle = (key: string) => setExpanded((prev) => ({ ...prev, [key]: !prev[key] })); return ( <TreeView ariaLabel="Project files"> <TreeView.Item title="src" hasChildren isExpanded={expanded['src']} onExpandedChange={() => toggle('src')} isSelected={selected === 'src'} onSelectedChange={() => setSelected('src')} leadingVisual={<FolderIconSolid />} actionMenuItems={ <> <Menu.Item title="New File" onClick={() => alert('New file in src')} /> <Menu.Item title="Rename" onClick={() => alert('Rename src')} /> <Menu.Item title="Delete" onClick={() => alert('Delete src')} /> </> } > <TreeView.Item title="index.ts" isSelected={selected === 'index'} onSelectedChange={() => setSelected('index')} leadingVisual={<DocumentIconOutline />} actionMenuItems={ <> <Menu.Item title="Open" onClick={() => alert('Open index.ts')} /> <Menu.Item title="Rename" onClick={() => alert('Rename index.ts')} /> <Menu.Item title="Delete" onClick={() => alert('Delete index.ts')} /> </> } /> <TreeView.Item title="utils.ts" isSelected={selected === 'utils'} onSelectedChange={() => setSelected('utils')} leadingVisual={<DocumentIconOutline />} actionMenuItems={ <> <Menu.Item title="Open" onClick={() => alert('Open utils.ts')} /> <Menu.Item title="Rename" onClick={() => alert('Rename utils.ts')} /> <Menu.Item title="Delete" onClick={() => alert('Delete utils.ts')} /> </> } /> </TreeView.Item> <TreeView.Item title="tests" hasChildren isExpanded={expanded['tests']} onExpandedChange={() => toggle('tests')} isSelected={selected === 'tests'} onSelectedChange={() => setSelected('tests')} leadingVisual={<FolderIconSolid />} actionMenuItems={ <> <Menu.Item title="Run Tests" onClick={() => alert('Run all tests')} /> <Menu.Item title="New Test" onClick={() => alert('New test in tests')} /> <Menu.Item title="Delete" onClick={() => alert('Delete tests')} /> </> } > <TreeView.Item title="index.test.ts" isSelected={selected === 'index-test'} onSelectedChange={() => setSelected('index-test')} leadingVisual={<DocumentIconOutline />} actionMenuItems={ <> <Menu.Item title="Run" onClick={() => alert('Run index.test.ts')} /> <Menu.Item title="Rename" onClick={() => alert('Rename index.test.ts')} /> <Menu.Item title="Delete" onClick={() => alert('Delete index.test.ts')} /> </> } /> <TreeView.Item title="utils.test.ts" isSelected={selected === 'utils-test'} onSelectedChange={() => setSelected('utils-test')} leadingVisual={<DocumentIconOutline />} actionMenuItems={ <> <Menu.Item title="Run" onClick={() => alert('Run utils.test.ts')} /> <Menu.Item title="Rename" onClick={() => alert('Rename utils.test.ts')} /> <Menu.Item title="Delete" onClick={() => alert('Delete utils.test.ts')} /> </> } /> </TreeView.Item> <TreeView.Item title="README.md" isSelected={selected === 'readme'} onSelectedChange={() => setSelected('readme')} leadingVisual={<DocumentIconOutline />} actionMenuItems={ <> <Menu.Item title="Open" onClick={() => alert('Open README.md')} /> <Menu.Item title="Rename" onClick={() => alert('Rename README.md')} /> <Menu.Item title="Delete" onClick={() => alert('Delete README.md')} /> </> } /> </TreeView> ); }; export default Component; ``` ### Lazy ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DocumentIconOutline, FolderIconSolid, PhotoIconOutline, } from '@neo4j-ndl/react/icons'; import { TreeView } from '@neo4j-ndl/react/next'; import { useCallback, useMemo, useRef, useState } from 'react'; type TreeNode = { id: string; label: string; children?: TreeNode[]; }; const TREE_DATA: TreeNode[] = [ { children: [ { children: [ { id: 'app-tsx', label: 'App.tsx' }, { id: 'index-tsx', label: 'index.tsx' }, { id: 'styles-css', label: 'styles.css' }, ], id: 'frontend', label: 'Frontend', }, { children: [ { id: 'server-ts', label: 'server.ts' }, { id: 'routes-ts', label: 'routes.ts' }, ], id: 'backend', label: 'Backend', }, { id: 'readme-md', label: 'README.md' }, ], id: 'projects', label: 'Projects', }, { children: [ { children: [ { id: 'beach-jpg', label: 'beach.jpg' }, { id: 'sunset-jpg', label: 'sunset.jpg' }, ], id: 'vacation', label: 'Vacation', }, { id: 'profile-png', label: 'profile.png' }, ], id: 'photos', label: 'Photos', }, { children: [ { id: 'resume-pdf', label: 'resume.pdf' }, { id: 'cover-letter-pdf', label: 'cover-letter.pdf' }, { id: 'notes-txt', label: 'notes.txt' }, ], id: 'documents', label: 'Documents', }, { children: [ { children: [ { id: 'chill-m3u', label: 'chill.m3u' }, { id: 'workout-m3u', label: 'workout.m3u' }, ], id: 'playlists', label: 'Playlists', }, { id: 'favorites-m3u', label: 'favorites.m3u' }, ], id: 'music', label: 'Music', }, { id: 'todo-txt', label: 'TODO.txt' }, ]; const LOAD_DELAY_MS = 600; function iconForNode(node: TreeNode) { if (node.children) { return <FolderIconSolid />; } if (/\.(jpg|jpeg|png|gif|svg)$/i.test(node.label)) { return <PhotoIconOutline />; } return <DocumentIconOutline />; } type NodeState = { isExpanded: boolean; isLoading: boolean; hasLoaded: boolean; }; const INITIAL_NODE_STATE: NodeState = { hasLoaded: false, isExpanded: false, isLoading: false, }; function useTreeState() { const [nodeStates, setNodeStates] = useState<Record<string, NodeState>>({}); const [selectedId, setSelectedId] = useState<string | null>(null); const timers = useRef<Record<string, ReturnType<typeof setTimeout>>>({}); const getNodeState = useCallback( (id: string): NodeState => nodeStates[id] ?? INITIAL_NODE_STATE, [nodeStates], ); const handleExpand = useCallback((id: string, expanded: boolean) => { setNodeStates((prev) => { const current = prev[id] ?? INITIAL_NODE_STATE; if (expanded && !current.hasLoaded) { if (timers.current[id]) { clearTimeout(timers.current[id]); } timers.current[id] = setTimeout(() => { setNodeStates((p) => ({ ...p, [id]: { hasLoaded: true, isExpanded: true, isLoading: false }, })); delete timers.current[id]; }, LOAD_DELAY_MS); return { ...prev, [id]: { hasLoaded: false, isExpanded: true, isLoading: true }, }; } return { ...prev, [id]: { ...current, isExpanded: expanded } }; }); }, []); const handleSelect = useCallback( (id: string) => setSelectedId((prev) => (prev === id ? null : id)), [], ); return { getNodeState, handleExpand, handleSelect, selectedId }; } type RenderContext = { getNodeState: (id: string) => NodeState; selectedId: string | null; handleExpand: (id: string, expanded: boolean) => void; handleSelect: (id: string) => void; }; function renderNode(node: TreeNode, ctx: RenderContext) { const hasChildren = node.children !== undefined && node.children !== null && node.children.length > 0; const state = ctx.getNodeState(node.id); return ( <TreeView.Item key={node.id} title={node.label} hasChildren={hasChildren} isExpanded={state.isExpanded} isLoading={state.isLoading} isSelected={ctx.selectedId === node.id} leadingVisual={iconForNode(node)} onExpandedChange={(expanded: boolean) => ctx.handleExpand(node.id, expanded) } onSelectedChange={() => ctx.handleSelect(node.id)} > {hasChildren && state.isLoading && !state.hasLoaded && ( <TreeView.SkeletonItem rows={2} /> )} {hasChildren && state.hasLoaded && node.children!.map((child) => renderNode(child, ctx))} </TreeView.Item> ); } const Component = () => { const { getNodeState, selectedId, handleExpand, handleSelect } = useTreeState(); const ctx = useMemo<RenderContext>( () => ({ getNodeState, handleExpand, handleSelect, selectedId }), [getNodeState, selectedId, handleExpand, handleSelect], ); return ( <TreeView ariaLabel="Lazy-loaded file explorer"> {TREE_DATA.map((node) => renderNode(node, ctx))} </TreeView> ); }; export default Component; ``` ### Multi ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DocumentIconOutline, FolderIconSolid, PhotoIconOutline, } from '@neo4j-ndl/react/icons'; import { TreeView } from '@neo4j-ndl/react/next'; import { useCallback, useState } from 'react'; type CheckedState = Record<string, boolean>; const WORK_CHILDREN = ['report', 'presentation'] as const; const DOC_CHILDREN = ['work', 'resume'] as const; const IMAGE_CHILDREN = ['photo', 'screenshot'] as const; const DISABLED_KEYS = new Set(['presentation']); function getParentChecked( checked: CheckedState, childKeys: readonly string[], ): { isChecked: boolean; isIndeterminate: boolean } { const enabledKeys = childKeys.filter((k) => !DISABLED_KEYS.has(k)); if (enabledKeys.length === 0) { return { isChecked: false, isIndeterminate: false }; } const checkedCount = enabledKeys.filter((k) => checked[k]).length; if (checkedCount === 0) { return { isChecked: false, isIndeterminate: false }; } if (checkedCount === enabledKeys.length) { return { isChecked: true, isIndeterminate: false }; } return { isChecked: false, isIndeterminate: true }; } const Component = () => { const [expanded, setExpanded] = useState<Record<string, boolean>>({}); const [checked, setChecked] = useState<CheckedState>({}); const toggle = (key: string) => setExpanded((prev) => ({ ...prev, [key]: !prev[key] })); const toggleCheck = useCallback((key: string) => { setChecked((prev) => ({ ...prev, [key]: !prev[key] })); }, []); const toggleParent = useCallback( (childKeys: readonly string[]) => { const enabledKeys = childKeys.filter((k) => !DISABLED_KEYS.has(k)); const isAllChecked = enabledKeys.every((k) => checked[k]); setChecked((prev) => { const next = { ...prev }; for (const k of enabledKeys) { next[k] = !isAllChecked; } return next; }); }, [checked], ); const docState = getParentChecked(checked, [ ...DOC_CHILDREN, ...WORK_CHILDREN, ]); const workState = getParentChecked(checked, WORK_CHILDREN); const imageState = getParentChecked(checked, IMAGE_CHILDREN); return ( <TreeView ariaLabel="File explorer" selectionMode="multiple"> <TreeView.Item title="Documents" hasChildren isExpanded={expanded['documents']} onExpandedChange={() => toggle('documents')} isSelected={docState.isChecked} isIndeterminate={docState.isIndeterminate} onSelectedChange={() => toggleParent([...DOC_CHILDREN, ...WORK_CHILDREN]) } leadingVisual={<FolderIconSolid />} > <TreeView.Item title="Work" hasChildren isExpanded={expanded['work']} onExpandedChange={() => toggle('work')} isSelected={workState.isChecked} isIndeterminate={workState.isIndeterminate} onSelectedChange={() => toggleParent(WORK_CHILDREN)} leadingVisual={<FolderIconSolid />} > <TreeView.Item title="Report.pdf" isSelected={!!checked['report']} onSelectedChange={() => toggleCheck('report')} leadingVisual={<DocumentIconOutline />} /> <TreeView.Item title="Presentation.pptx" isDisabled onSelectedChange={() => null} leadingVisual={<DocumentIconOutline />} /> </TreeView.Item> <TreeView.Item title="Resume.pdf" isSelected={!!checked['resume']} onSelectedChange={() => toggleCheck('resume')} leadingVisual={<DocumentIconOutline />} /> </TreeView.Item> <TreeView.Item title="Images" hasChildren isExpanded={expanded['images']} onExpandedChange={() => toggle('images')} isSelected={imageState.isChecked} isIndeterminate={imageState.isIndeterminate} onSelectedChange={() => toggleParent(IMAGE_CHILDREN)} leadingVisual={<FolderIconSolid />} > <TreeView.Item title="Photo.jpg" isSelected={!!checked['photo']} onSelectedChange={() => toggleCheck('photo')} leadingVisual={<PhotoIconOutline />} /> <TreeView.Item title="Screenshot.png" isSelected={!!checked['screenshot']} onSelectedChange={() => toggleCheck('screenshot')} leadingVisual={<PhotoIconOutline />} /> </TreeView.Item> </TreeView> ); }; export default Component; ``` ### Non Selectable Folders ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons'; import { TreeView } from '@neo4j-ndl/react/next'; import { useState } from 'react'; const Component = () => { const [expanded, setExpanded] = useState<Record<string, boolean>>({}); const [selected, setSelected] = useState<string | null>(null); const toggle = (key: string) => setExpanded((prev) => ({ ...prev, [key]: !prev[key] })); return ( <TreeView ariaLabel="File explorer"> <TreeView.Item title="Documents" hasChildren isExpanded={expanded['documents']} onExpandedChange={() => toggle('documents')} leadingVisual={<FolderIconSolid />} > <TreeView.Item title="Work" hasChildren isExpanded={expanded['work']} onExpandedChange={() => toggle('work')} leadingVisual={<FolderIconSolid />} > <TreeView.Item title="Report.pdf" isSelected={selected === 'report'} onSelectedChange={() => setSelected('report')} leadingVisual={<DocumentIconOutline />} /> <TreeView.Item title="Presentation.pptx" isSelected={selected === 'presentation'} onSelectedChange={() => setSelected('presentation')} leadingVisual={<DocumentIconOutline />} /> </TreeView.Item> <TreeView.Item title="Resume.pdf" isSelected={selected === 'resume'} onSelectedChange={() => setSelected('resume')} leadingVisual={<DocumentIconOutline />} /> </TreeView.Item> <TreeView.Item title="Images" hasChildren isExpanded={expanded['images']} onExpandedChange={() => toggle('images')} leadingVisual={<FolderIconSolid />} > <TreeView.Item title="Photo.jpg" isSelected={selected === 'photo'} onSelectedChange={() => setSelected('photo')} leadingVisual={<DocumentIconOutline />} /> <TreeView.Item title="Screenshot.png" isSelected={selected === 'screenshot'} onSelectedChange={() => setSelected('screenshot')} leadingVisual={<DocumentIconOutline />} /> </TreeView.Item> </TreeView> ); }; export default Component; ``` ### Single ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons'; import { TreeView } from '@neo4j-ndl/react/next'; import { useState } from 'react'; const Component = () => { const [expanded, setExpanded] = useState<Record<string, boolean>>({}); const [selected, setSelected] = useState<string | null>(null); const toggle = (key: string) => setExpanded((prev) => ({ ...prev, [key]: !prev[key] })); return ( <TreeView ariaLabel="File explorer"> <TreeView.Item title="Documents" hasChildren isExpanded={expanded['documents']} onExpandedChange={() => toggle('documents')} isSelected={selected === 'documents'} onSelectedChange={() => setSelected('documents')} leadingVisual={<FolderIconSolid />} > <TreeView.Item title="Work" hasChildren isExpanded={expanded['work']} onExpandedChange={() => toggle('work')} isSelected={selected === 'work'} onSelectedChange={() => setSelected('work')} leadingVisual={<FolderIconSolid />} > <TreeView.Item title="Report.pdf" isSelected={selected === 'report'} onSelectedChange={() => setSelected('report')} leadingVisual={<DocumentIconOutline />} /> <TreeView.Item title="Presentation.pptx" isDisabled isSelected={selected === 'presentation'} onSelectedChange={() => setSelected('presentation')} leadingVisual={<DocumentIconOutline />} /> </TreeView.Item> <TreeView.Item title="Resume.pdf" isSelected={selected === 'resume'} onSelectedChange={() => setSelected('resume')} leadingVisual={<DocumentIconOutline />} /> </TreeView.Item> <TreeView.Item title="Images (disabled)" hasChildren isExpanded={expanded['images']} onExpandedChange={() => toggle('images')} isSelected={selected === 'images'} onSelectedChange={() => setSelected('images')} leadingVisual={<FolderIconSolid />} isDisabled /> <TreeView.Item title="README.md" isSelected={selected === 'readme'} onSelectedChange={() => setSelected('readme')} leadingVisual={<DocumentIconOutline />} /> </TreeView> ); }; export default Component; ``` ### Tooltip ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { Menu } from '@neo4j-ndl/react'; import { AppearanceIcon, CheckCircleIconSolid, DatabaseSignalIcon, } from '@neo4j-ndl/react/icons'; import { TreeView } from '@neo4j-ndl/react/next'; import { useState } from 'react'; const Component = () => { const [selected, setSelected] = useState<string | null>(null); return ( <TreeView ariaLabel="Databases" style={{ marginTop: '100px' }}> <TreeView.Item title="Production" isSelected={selected === 'production'} onSelectedChange={() => setSelected('production')} leadingVisual={<DatabaseSignalIcon />} trailingContent={ <CheckCircleIconSolid className="n-size-5 n-text-success-bg-status" /> } actionMenuItems={ <> <Menu.Item title="Edit" /> <Menu.Item title="Delete" /> <Menu.Item title="Rename" /> </> } tooltipContent={ <> <span className="n-font-semibold">Production</span> <span className="n-flex n-items-center n-gap-token-4 n-mt-token-8"> <CheckCircleIconSolid className="n-size-5 n-text-success-bg-status" /> : Endorsed </span> </> } /> <TreeView.Item title="Development" isSelected={selected === 'development'} onSelectedChange={() => setSelected('development')} leadingVisual={<DatabaseSignalIcon />} trailingContent={ <AppearanceIcon className="n-size-5 n-text-warning-bg-status" /> } tooltipContent={ <> <span className="n-font-semibold">Development</span> <span className="n-flex n-items-center n-gap-token-4 n-mt-token-8"> <AppearanceIcon className="n-size-5 n-text-warning-bg-status" />: In development </span> </> } /> </TreeView> ); }; export default Component; ``` ### Trailing ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { Menu } from '@neo4j-ndl/react'; import { AppearanceIcon, CheckCircleIconSolid, DatabaseSignalIcon, } from '@neo4j-ndl/react/icons'; import { TreeView } from '@neo4j-ndl/react/next'; import { useState } from 'react'; const Component = () => { const [selected, setSelected] = useState<string | null>(null); return ( <TreeView ariaLabel="Databases"> <TreeView.Item title="Production" isSelected={selected === 'production'} onSelectedChange={() => setSelected('production')} leadingVisual={<DatabaseSignalIcon />} trailingContent={ <CheckCircleIconSolid className="n-size-5 n-text-success-bg-status" /> } actionMenuItems={ <> <Menu.Item title="Edit" /> <Menu.Item title="Delete" /> <Menu.Item title="Rename" /> </> } /> <TreeView.Item title="Development" isSelected={selected === 'development'} onSelectedChange={() => setSelected('development')} leadingVisual={<DatabaseSignalIcon />} trailingContent={ <AppearanceIcon className="n-size-5 n-text-warning-bg-status" /> } /> </TreeView> ); }; export default Component; ``` ### Uncontrolled ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons'; import { TreeView } from '@neo4j-ndl/react/next'; import { useState } from 'react'; const Component = () => { const [selected, setSelected] = useState<string | null>(null); return ( <TreeView ariaLabel="File explorer"> <TreeView.Item title="Documents (expanded by default)" defaultExpanded={true} leadingVisual={<FolderIconSolid />} > <TreeView.Item title="Work" leadingVisual={<FolderIconSolid />}> <TreeView.Item title="Report.pdf" isSelected={selected === 'report'} onSelectedChange={() => setSelected('report')} leadingVisual={<DocumentIconOutline />} /> <TreeView.Item title="Presentation.pptx" isSelected={selected === 'presentation'} onSelectedChange={() => setSelected('presentation')} leadingVisual={<DocumentIconOutline />} /> </TreeView.Item> <TreeView.Item title="Resume.pdf" isSelected={selected === 'resume'} onSelectedChange={() => setSelected('resume')} leadingVisual={<DocumentIconOutline />} /> </TreeView.Item> <TreeView.Item title="Images (collapsed by default)" leadingVisual={<FolderIconSolid />} defaultExpanded={false} > <TreeView.Item title="Photo.jpg" isSelected={selected === 'photo'} onSelectedChange={() => setSelected('photo')} leadingVisual={<DocumentIconOutline />} /> <TreeView.Item title="Screenshot.png" isSelected={selected === 'screenshot'} onSelectedChange={() => setSelected('screenshot')} leadingVisual={<DocumentIconOutline />} /> </TreeView.Item> </TreeView> ); }; export default Component; ```