Tree views
Expandable rows of a hierarchy, with arrow keys
11 rows · 3 open · @gpuixui/tokens
Usage
import { TreeView, useExpandedRows } from "@gpuixui/data";
const tree = useExpandedRows({
roots: files,
keyOf: (node) => node.name,
childrenOf: (node) => node.children ?? [],
labelOf: (node) => node.name,
initialExpanded: ["src"],
});
<TreeView
aria-label="Workspace files"
height={260}
rows={tree.rows}
cursor={tree.cursor}
onCursorChange={tree.setCursor}
labelOf={(node) => node.name}
onActivate={(row) => open(row.node)}
/>;API
useExpandedRows
The controller holds the expansion set and the cursor. A saved workspace hands
its open folders back, so initialExpanded is data: only an iterable
carrying text keys opens anything (expandedSet), and a value a store wrote as
something else — an object where JSON has no Set, a number, null — is no
expansion, the state a fresh tree starts in, rather than a TypeError on the
first render. An entry that is not text is not a key.
The hierarchy is data as well, so roots and every node's childrenOf are read
as lists: JSON writes null for a list a record never carried, and a
hand-built node often omits it, so a leaf with children: null is the ordinary
shape of a saved tree. An absent roots paints no rows and an absent children
list is a leaf — flattenTree, branchKeys and expandedPath all read the slot
through listOf, so a stored hierarchy cannot take the render down
(behaviour/tree.test.tsx's stored-children case).
TreeView
| Prop | Type | Default | Consequence |
|---|---|---|---|
rows | TreeRow<T>[] | — | the visible rows, in paint order; a value that is not a list paints an empty tree (see below) |
labelOf | (node: T) => string | — | the row's accessible name and type-ahead match; a blank label reads as the row's key |
cursor | TreeCursor | — | the caller's cursor; omit for an uncontrolled tree |
onCursorChange | (cursor: TreeCursor) => void | — | reports every cursor |
defaultCursor | TreeCursor | nothing open | the uncontrolled tree's start |
onActivate | (row: TreeRow<T>) => void | — | a press or Enter on a row |
renderRow | (row: TreeRow<T>) => ReactNode | labelOf as text | paints a row's content |
height | number | — | a definite height; the rows scroll inside it |
className | string | — | joined after the recipe |
testId | string | — | the tree's locator |
A blank labelOf reads as the row's key everywhere the row's words are
read: the paint, the accessible name and the type-ahead match. A tree whose
names never arrived therefore still paints a word and a typed prefix reaches it
(behaviour/tree.test.tsx's blank-label cases).
A row is built at the density's rowHeight, read through the primitives'
density seam (rowHeightOf(theme)): a theme a store restored without the
density group paints the library's row instead of throwing from the first row
(tree/row.test.ts), and VirtualTree resolves its own rowHeight through the
same seam.
The rows are stored data as often as a literal: a workspace that keeps its
flattened list hands the tree whatever the store gave back, and snapshot?.rows
is undefined until the snapshot arrives. rows is read through listOf, so a
value that is not a list paints the tree's frame and no rows — before the read,
the type-ahead's mapping threw Cannot read properties of null (reading 'map')
in the first render and nothing painted at all
(behaviour/stored-rows.test.tsx).
Edit this page on GitHubpackages/data/docs/tree-view.mdx