Tree lens on @headless-tree: ARIA tree pattern, keyboard nav, typeahead search#21
Merged
Conversation
Swaps the web Tree's hand-rolled internals for @headless-tree 1.7 (zero-dep DOM-free core + react hook, ~14 kB) — chosen over react-arborist / react-aria Tree / MUI X after a landscape survey: it is architecturally identical to the lens (flat visible-list render, bring-your-own markup, external state) and fully happy-dom-testable. - W3C ARIA tree pattern out of the box: role=tree/treeitem, aria-level/setsize/posinset/expanded/selected, roving tabindex; arrow keys / Home / End navigate, Enter activates, type-to-search opens a search box and marks matches (styled ring). - Author surface, buildForest, TUI renderer, registries: UNCHANGED. - Entity-level selection preserved: activation dispatches the node's KEY to select_state; controlled selectedItems derive from the bound value, so every occurrence of the selected entity highlights. - Re-read-safe disclosure preserved via CONTROLLED expandedItems: expand_depth default overlaid with a user-toggle map (diffed from the lib's setter) — the existing regression scenario passes. - UX change (standard tree pattern): row click both selects and toggles folders; the chevron is now a visual indicator. Tests 8 (was 6): + ARIA attributes/roving tabindex, ArrowDown/ ArrowRight keyboard nav, typeahead search (matches marked), duplicate- entity highlight. Test gotchas encoded: hotkey matching tracks HELD keys (keyup on document required) and typeahead matches event CODE (KeyB), not key. Verified live on the concepts graph: 152 nodes, arrow-key walk, "a" → search with 106 matches, click → /selected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011TrhtF1SiJghJASMWECzBh
Greptile x3: (1) click fired dispatchSelect twice (selectionFeature's setSelectedItems AND onPrimaryAction) — two state patches per click meant double invalidation cycles for dependent cells; onPrimaryAction (which both click and Enter route through) is now the only dispatch site and the controlled-selection setter is an intentional no-op. (2) The expansion setter's functional-updater path computed against a render-time snapshot; "current" is now derived inside the state updater from the accumulated toggle map, so batched sequential updates see the true latest state. (3) itemByLabel throws a labeled error listing the tree's contents instead of tests dying on undefined.click. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011TrhtF1SiJghJASMWECzBh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 1 of the Tree-lens upgrade researched earlier: swap the web renderer's hand-rolled internals for @headless-tree 1.7 (zero-dep, DOM-free core + React hook, ~14 kB modular) — picked over react-arborist (opinionated react-window layout, react-dnd@14 underneath), react-aria Tree (a second heavyweight headless kit beside Base UI), and MUI X (paid tier + emotion) because it is architecturally identical to the existing lens: flat visible-list render, bring-your-own markup/Tailwind, external state, and a fully happy-dom-testable core.
What changes
role=tree/treeitem,aria-level/setsize/posinset/expanded/selected, roving tabindex; arrow keys / Home / End navigate, Enter activates, type-to-search opens a search box and marks matches.buildForest, TUI renderer, registries untouched.select_state; controlledselectedItemsderive from the bound value (every occurrence of the entity highlights — the documented semantics).expandedItems: theexpand_depthdefault overlaid with a user-toggle map diffed from the library's setter. The existing regression scenario (re-read grows the forest → new branch gets the default, user's collapse survives) passes.Verification
documentmust follow every keydown) and typeahead matches the event code (KeyB), not the key.aopened search and marked 106 matches, click wrote/selectedand highlighted the entity.children_queryloading, drag-to-reparent mutations, virtualization.🤖 Generated with Claude Code
https://claude.ai/code/session_011TrhtF1SiJghJASMWECzBh
Greptile Summary
This PR replaces the hand-rolled Tree lens internals with @headless-tree 1.7, bringing a W3C ARIA tree pattern (roving tabindex, arrow-key navigation, Home/End, type-to-search) while preserving the author-facing YAML API, the TUI renderer, and the re-read-safe disclosure semantics.
useTree+ controlledexpandedItems/selectedItems; entity-level selection dispatches viaonPrimaryActiononly, with a no-opsetSelectedItemsto avoid double-dispatch;setExpandedItemsderives its current snapshot inside the state-updater rather than from the render closure, making batched expansion safe.@headless-tree/core@1.7.0and@headless-tree/react@1.7.0; the previous review's double-dispatch and stale functional-updater issues are both resolved.Confidence Score: 5/5
Safe to merge — the rewrite is self-contained to the web Tree component; the author-facing YAML API, TUI renderer, and core catalog are untouched, and the test suite green-lights all eight paths including the re-read regression.
The implementation is well-contained: controlled state is threaded correctly through useTree, onPrimaryAction is the single dispatch site, and setExpandedItems derives its diff from inside the state updater rather than from a render-time snapshot. All three previously flagged issues are resolved. The new test cases cover ARIA structure, keyboard navigation, typeahead, entity-level selection, and re-read survival in a happy-dom environment.
No files require special attention.
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant User participant TreeDOM as Tree DOM participant HT as headless-tree participant Notebook as Notebook State User->>TreeDOM: click or Enter on item TreeDOM->>HT: item onClick via getProps HT->>Notebook: onPrimaryAction calls dispatchSelect with node key Notebook-->>TreeDOM: useStateValue returns updated selected TreeDOM->>HT: useTree receives new selectedItems derived from selected HT-->>TreeDOM: all matching entity paths rendered with bg-accent User->>TreeDOM: click folder row TreeDOM->>HT: setExpandedItems updater called HT->>HT: setUserToggled derives current inside updater HT-->>TreeDOM: expandedItems recomputed and tree re-renders User->>TreeDOM: type a letter for typeahead TreeDOM->>HT: searchFeature opens search input HT-->>TreeDOM: matching treeitems rendered with ring-primary User->>TreeDOM: query re-read changes p rows TreeDOM->>TreeDOM: byPath useMemo rebuilds TreeDOM->>HT: useEffect calls rebuildTree HT-->>TreeDOM: new branches appear with default policy, user toggles preserved%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant User participant TreeDOM as Tree DOM participant HT as headless-tree participant Notebook as Notebook State User->>TreeDOM: click or Enter on item TreeDOM->>HT: item onClick via getProps HT->>Notebook: onPrimaryAction calls dispatchSelect with node key Notebook-->>TreeDOM: useStateValue returns updated selected TreeDOM->>HT: useTree receives new selectedItems derived from selected HT-->>TreeDOM: all matching entity paths rendered with bg-accent User->>TreeDOM: click folder row TreeDOM->>HT: setExpandedItems updater called HT->>HT: setUserToggled derives current inside updater HT-->>TreeDOM: expandedItems recomputed and tree re-renders User->>TreeDOM: type a letter for typeahead TreeDOM->>HT: searchFeature opens search input HT-->>TreeDOM: matching treeitems rendered with ring-primary User->>TreeDOM: query re-read changes p rows TreeDOM->>TreeDOM: byPath useMemo rebuilds TreeDOM->>HT: useEffect calls rebuildTree HT-->>TreeDOM: new branches appear with default policy, user toggles preservedReviews (2): Last reviewed commit: "Single dispatch site, updater-safe expan..." | Re-trigger Greptile