refactor(TreeList): let useTreeFocus own roving tabindex#3488
Conversation
Add hasRovingTabIndex + handleFocus to useTreeFocus so it stamps and repairs the treeitem tab stop internally (matching useListFocus). TreeList drops its inline activeId state and resolvedActiveId derivation, seeds only the initial tabbable item in render, and wires handleFocus on the tree container.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR Analysis Report📚 Storybook PreviewView Storybook for this PR 🧪 Sandbox PreviewView Sandbox for this PR No new or modified components detected. Bundle Size Summary
Accessibility AuditStatus: No accessibility violations detected. Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
|
Heads up: the |
Summary
Make
useTreeFocusown roving-tabindex management internally soTreeListno longer has to hand-roll it. This mirrors theuseListFocuspattern (hasRovingTabIndex+handleFocus), where the hook stampstabindex0/-1 on the items directly, repairs the tab stop on mount/layout, and moves the stop alongside keyboard navigation.Previously
useTreeFocusonly providedhandleKeyDown;TreeListstampedtabindexon treeitem<li>elements itself via anactiveIdstate, aresolvedActiveIdderivation, and anisTabbableprop threaded throughTreeListItem.Changes
packages/core/src/hooks/useTreeFocus.tshasRovingTabIndex?: booleanoption (defaultfalse).focusItemstampstabindex=0on the focused treeitem and-1on all others viasetAttribute.syncTabStopsutility +useIsomorphicLayoutEffectthat repairs the stop on mount/commit: it preserves an existingtabindex="0"seed, otherwise promotes the first enabled treeitem.handleFocusreturn (always returned; a no-op unlesshasRovingTabIndexis on) that keeps the stop in sync after clicks/programmatic focus.false, behavior is unchanged — the hook only moves focus and never touchestabindex.packages/core/src/TreeList/TreeList.tsxhasRovingTabIndex: trueand wireshandleFocuson therole="tree"onFocus.activeIdstate, theresolvedActiveIduseMemoderivation, theonActiveChangewiring, and renames the seed helper tofindInitialTabbableId.packages/core/src/TreeList/TreeListItem.tsxisTabbableis now documented as the initial mount seed; the hook takes over dynamically.Verification
typecheck(core + docs): passeslint: cleanvitest run packages/core/src/TreeList packages/core/src/hooks/useTreeFocus: 57/57 pass (arrow nav, expand/collapse, Home/End, typeahead, aria-level/posinset/setsize, roving-tabindex seeding + movement all preserved)