[spike] Modular SelectPanel with tabs (layered)#7966
Conversation
Decomposes SelectPanel into the modular architecture (hooks -> foundations -> parts -> ready-made) so consumers can compose a tabbed picker (e.g. Branches / Tags) with one shared search input — not possible with today's prop-based API. - L0 hooks: useFilter, useSelectionState, useAsyncList (consumer-owned, reusable) - L1 foundation: useSelectPanel + unstyled components (dialog/combobox/listbox/ tablist ARIA; outer role=dialog, listbox scoped to the active tab panel) - L2 parts: Primer-styled SelectPanel.* incl. TabList/Tab(count)/Panel - L3 ready-made: thin no-tabs wrapper (tabbed use is L2-only by design) - Per-layer stories, spec.md, and tests Branched off main; contains only the SelectPanel work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
|
|
🤖 Lint issues have been automatically fixed and committed to this PR. |
SelectPanel no longer depends on the Tabs primitive. Remove the TabList/Tab/Panel parts (L2), the Panel foundation component (L1), and getPanelProps from useSelectPanel, along with their now-unused type exports. A tabbed picker is now a consumer composition of SelectPanel parts + the generic Tabs primitive. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rewrite the headline Parts story to build the Branches/Tags tabbed picker by composing SelectPanel parts with the generic Tabs primitive via local RefTabList/RefTab/RefTabPanel wrappers (Tabs exports only hooks). The tabpanel is non-focusable, the list is input-driven via aria-activedescendant, tabs are a roving-tabindex zone, and switching a tab returns focus to the input — no keyboard dead-end. Update the SelectPanel tests to compose with Tabs and drop the removed Panel assertions, and rewrite the spec around the compose-don't-depend model. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ed layer model - SelectPanel.Input forwards its ref, so the tabbed recipe returns focus via a real inputRef instead of a DOM querySelector hack - spec.md: relabel to the refined model (useSelectPanel = L0, unstyled = L1, the generic state hooks are Utilities outside the model) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🤖 Lint issues have been automatically fixed and committed to this PR. |
Draft — not for merge. Part of a spike comparing how a tabbed SelectPanel (Branches / Tags, one shared search) gets built across Primer's APIs. Same task, same model (Opus 4.8); the API is the only variable:
SelectPanel, pragmatic (tabs work visually but aren't ARIA-associated)SelectPanel, ARIA correctness forced (the component gets abandoned)SelectPanel(composes correctly, ~35 lines of glue)SelectPanel(composes correctly, zero glue)This branch decomposes
SelectPanelinto the 4-layer modular architecture so a consumer can compose a tabbed picker — and it is the hand-refined canonical example of the intended implementation (it may differ from a one-shot agent output; that's deliberate).Design: compose, don't depend. SelectPanel does not own tabs and does not import
Tabs. It provides the listbox-in-a-dialog machinery; the consumer brings the genericTabsprimitive and composes it. A tabbed picker is a consumer composition, not a component.Layers (refined model):
@primer/react/hooks/experimental) —useFilter,useSelectionState,useAsyncList: generic, consumer-owned state, shareable across tabs.useSelectPanel(/foundations/experimental): prop-getters, all behaviour + ARIA, no markup. Outer popup isrole="dialog", withrole="listbox"scoped inside the active tab panel./foundations/experimental).SelectPanel.*(no tab parts):Root/Anchor/Overlay/Header/Title/Input/List/Option/Empty/Footer.Accessibility model for the tabbed composition (see
SelectPanelParts.stories.tsx): the tabpanel is non-focusable, the list is driven by the input'saria-activedescendant, the tabs are a roving-tabindex zone, and switching a tab returns focus to the input — so there is no keyboard dead-end. A shared composite-focus utility (Ariakitcombobox-tabsstyle) is noted as a future enhancement, not required for correctness.Start with
SelectPanel.spec.mdand the "Branches / Tags tabbed picker (styled)" story. This is the zero-glue end of the comparison.Note surfaced by this work: the experimental
Tabsprimitive exports only hooks, notTab/TabList/TabPanelcomponents — the recipe wraps them locally; ideally Tabs would export them.