Skip to content

TreeView

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/tree-view.json
See it liveA file tree with a selection, and a folder with nothing in it.→

A tree is one control with a cursor in it, not a nest of lists with click handlers. That difference is the whole component, and it shows in two places that are invisible until someone uses a keyboard.

One tab stop, not one per node. A tree of four hundred files with a tabIndex on each is four hundred stops between the sidebar and the editor. The container is what the keyboard reaches, and the arrows move a cursor inside it — the arrangement RadioGroup has, for the same reason.

Right and Left do different things depending on where you are:

Key On a closed folder On an open folder On a leaf
→ opens it steps into the first child nothing
← jumps to the parent closes it jumps to the parent

The parent jump is the part hand-rolled trees leave out, and it is the one that matters most: without it the only way back out of a deep folder is through every sibling below it.

<TreeView
nodes={nodes}
selected={openFile}
onSelect={setOpenFile}
label="Files"
/>

The cursor and the selection are different things. The cursor is where the keyboard is; the selection is the file being edited. A reader walks the tree with arrows without opening anything until Enter — so the cursor starts on the selected row rather than at the top, which is halfway up a long sidebar.

An empty folder is a folder. children: [] is not enough to say so — absence of children is what a leaf looks like — so an empty folder is marked with empty, opens, and shows nothing.

The nodes are given, not discovered. A tree that loads its children when a folder opens is a different component with a different problem: a spinner per node, a request per open, an error state inside a row.

The sums live next door, in tree-rows, with no React in them — the same split table-sort is. visibleRows is also the answer to “how many rows is this tree”, which is what a product needs to put a tree inside a VirtualList.

Props

Read from the types in ui/tree-view.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

PropTypeDefault
nodesrequiredreadonly TreeNode[]The tree, as nested nodes. Only the folders in open show their children.
openReadonlySet<string>Which folders are open. Uncontrolled if omitted.
onOpenChange(open: Set<string>) => voidTold the new set of open folders whenever one opens or shuts, whether the tree is controlled or not.
selectedstringWhich row is chosen - the file being edited, not the one the cursor is on. The two move apart: a reader walks the tree with arrows without opening anything until Enter.
onSelect(id: string) => voidCalled with the id of a leaf when it is chosen by click, Enter or Space. A folder toggles instead.
labelrequiredstringWhat a screen reader calls the tree. Required: a bare tree announces "tree" and nothing else, and the word belongs to the product.
classNamestring

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 20 rules applied and passed, and none failed. The accessibility report has every item.

Left to a person, where axe could not decide: color-contrast, duplicate-id-aria.

History

Each release that changed ui/tree-view.tsx - the file a product copies - newest first.

  1. v0.41.0
    • docs read every props table from the component's types ee3b159
  2. v0.22.0
    • feat a long list, a deep one, and the facts beside them 4cbd42e