Skip to content

feat(tree-picker): TreePicker, a tree-select dialog for picking many items - #597

Open
itsprade wants to merge 2 commits into
mainfrom
feat/tree-picker
Open

itsprade wants to merge 2 commits into
mainfrom
feat/tree-picker

Conversation

@itsprade

Copy link
Copy Markdown
Contributor

Summary

Adds TreePicker: a dialog for picking many items out of a hierarchical catalog (products with variants, category trees, warehouses → bins) and handing them back in one batch.

It is generic and standalone. data is opaque to the picker, and onCommit returns the picked leaves for the app to use. It was split out of #595 so it can be reviewed on its own; #595 (LineItems) builds on it, and its demo uses the picker for "Bulk add".

<TreePicker<CatalogItem>
  open={open}
  onOpenChange={setOpen}
  title="Add products"
  items={catalogTree}
  rowLabel="Product name"
  metricLabel="Unit price"
  renderRow={({ node }) => node.data.name}
  renderMetric={({ node }) => (node.children ? null : money(node.data.price))}
  matchesSearch={({ node, query }) => node.data.name.toLowerCase().includes(query.toLowerCase())}
  onCommit={(picked) => addToOrder(picked.map((n) => n.data))}
/>

Behaviour

  • Selection is leaf-only. A parent's checkbox is derived from its children: checked, or indeterminate when partly selected. Ticking it selects every leaf beneath.
  • Mouse:
    • Clicking a group row opens or closes it; the checkbox still selects everything inside.
    • Clicking an item row ticks it.
    • The dialog height eases as groups open and close, the caret rotates, and revealed rows fade in. All of this is skipped under prefers-reduced-motion.
  • Search keeps any node that matches or has a matching descendant, and auto-expands to the matches.
  • Keyboard: role="tree" with roving focus.
    • Arrow keys move and expand/collapse.
    • Space toggles.
    • Enter commits (but not while typing in the search box).
    • The row's checkbox and caret are hidden from assistive tech, because the treeitem already exposes checked and expanded state.
  • Labels are in English and Japanese via defineI18nLabels; any label prop overrides them.
  • Built on app-shell's Dialog, so open/close uses the shared dialog motion. A separate draft PR proposes tuning that motion for every dialog.

Notes for review

  • Callbacks take one object (renderRow({ node, depth }), matchesSearch({ node, query })), so fields can be added later without breaking callers.
  • The height easing uses the native Web Animations API, not a library. CSS can't transition a height change driven by content.
  • Import-order workaround: i18n.ts imports @/contexts/appshell-context first, as a side effect. This works around a pre-existing module cycle in @/hooks/i18n, and will be removed when that cycle is fixed separately.

Test plan

  • vitest: 53 TreePicker tests (selection, search, keyboard, row clicks, snapshots), plus the full suite
  • tsc, oxlint, pnpm docs:check, check-dts
  • Checked in the vite example at /showcase/tree-picker: open, expand, pick, commit

🤖 Generated with Claude Code

…any items

Generic multi-select from a hierarchical catalog (products with variants,
category trees): leaf-only selection with tri-state parents, search that
auto-expands to matches, a role=tree with roving focus, and one onCommit
with the picked leaves. Group rows open/close on click with an eased height
and a rotating caret; item rows tick on click. Labels in English and
Japanese. Includes docs, a changeset, and a /showcase/tree-picker page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Code Metrics Report

main (95bc8f2) #597 (1e1a88c) +/-
Coverage 88.7% 88.8% +0.1%
Test Execution Time 2m21s 1m32s -49s
Details
  |                     | main (95bc8f2) | #597 (1e1a88c) |  +/-  |
  |---------------------|----------------|----------------|-------|
+ | Coverage            |          88.7% |          88.8% | +0.1% |
  |   Files             |            208 |            216 |    +8 |
  |   Lines             |           6285 |           6488 |  +203 |
+ |   Covered           |           5575 |           5766 |  +191 |
+ | Test Execution Time |          2m21s |          1m32s |  -49s |

Code coverage of files in pull request scope (0.0% → 94.0%, patch 94.0%)

Files Coverage +/- Patch Coverage Status
packages/core/src/components/tree-picker/TreePicker.tsx 100.0% +100.0% 100.0% added
packages/core/src/components/tree-picker/TreePickerRow.tsx 100.0% +100.0% 100.0% added
packages/core/src/components/tree-picker/i18n.ts 75.0% +75.0% 75.0% added
packages/core/src/components/tree-picker/index.ts 0.0% 0.0% - added
packages/core/src/components/tree-picker/tree-utils.ts 100.0% +100.0% 100.0% added
packages/core/src/components/tree-picker/use-animated-height.ts 56.5% +56.5% 56.5% added
packages/core/src/components/tree-picker/use-tree-focus.ts 98.3% +98.3% 98.3% added
packages/core/src/components/tree-picker/use-tree-selection.ts 100.0% +100.0% 100.0% added
packages/core/src/index.ts 0.0% 0.0% - modified

Reported by octocov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant