feat(react,ui): PretableSelect — the four pickers on the kit Listbox (components SP2) - #582
Merged
Conversation
A select-only combobox on the SP1 contract, built on a Listbox primitive extracted from the enum cell editor so the grid has one list implementation; the four native selects migrate, and their test drivers with them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The kit's one list — the portalled role=listbox markup in one place — and the keyboard both triggers need: arrows with wrap and disabled-skip, Home/End, 500ms typeahead, Enter/Space, Escape/Tab, and open-on-key for a closed trigger. Mutation-checked: ignoring disabled in the step fails the skip test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…fect The set-state-in-effect rule was right: an effect commits a frame of stale highlight. Adjust during render instead, and pin the re-seed with a test. Mutation-checked: deleting the re-seed fails it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…lose, ids ArrowUp from no highlight lands on the last enabled option, as the native control does; an empty list still closes on an outside press; option ids come from one helper; the reveal effect watches the options too. Escape's stopPropagation and the toggling-anchor contract are now stated, not implied. An all-disabled list is pinned: no highlight, nothing commits. One hook test now drives a real DOM keydown instead of a hand-rolled event, and the scrollIntoView patch is restored. Placement assertion pins menuPopoverStyle (mutation-checked by swapping in popoverStyle); ArrowUp fix mutation-checked by reverting the start normalisation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Its list and its arrow keys come from the primitive; its filtering input, its direction-aware commit and its strict blur stay its own; its list keeps the dialog width through Listbox's new `width` prop, and aria-selected keeps following the highlight, the editable-combobox pattern. The enum editor suite passes unchanged, which is the proof. The list rules move to [data-pretable-listbox] / [data-pretable-option] in the kit section; the editor passes its own listbox attribute through. Mutation-checked: an ignored width prop fails its test; a deleted line-height fails the trio guard. References found outside EnumCellEditor.tsx and grid.css, all in packages/ui/src/__tests__/css-cascade.test.ts: - the "styles the enum combobox listbox" guard: renamed to the kit attributes ([data-pretable-listbox], [data-pretable-option]). - the "portaled popovers declare the whole inherited trio" guard: its block regex is now [data-pretable-listbox], and the listbox joins the blocks that must declare line-height. No e2e locator, docs page or other test named either attribute. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ows only The listbox's enum attribute vouched for a consumer that did not exist; it and the listProps passthrough go. Home/End stay on the text caret, the editable-combobox pattern — only the arrows reach the kit keyboard (test-pinned; mutation-checked by re-forwarding Home). The seed and the render share one "what is shown" rule; the index clamp guards both ends; the unreachable :empty rule and its guard go together; the editor's dialog width is pinned (mutation-checked by dropping the prop). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A button[role=combobox] trigger carrying data-pretable-select / -site / -value and the kit list beneath it; opens on click and on the navigation keys, commits on option click or Enter/Space, closes on Escape (focus returned), Tab and outside press; honours the toggling-anchor contract (mutation-checked: dropping the pointerdown stopPropagation fails the close-not-reopen test). A value absent from the options renders as its own label — the pruned-operator case (mutation-checked). jsdom helpers replace the native-select drivers every later test used. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…closes, named list A press inside the portalled list no longer reaches a host popover's outside-press listener (the mirror of the trigger's own stopPropagation; mutation-checked). Disabling an open select closes it — a disabled button gets no keydown, so Escape would have died with it (mutation-checked). The list takes the trigger's name. A merged callback ref replaces the imperative handle; children are a compile error; an absent value seeds the highlight on the first enabled option (mutation-checked); re-selecting the current value fires no onChange (mutation-checked); the read-only helper no longer moves focus. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Name required, string value in and out, no type/children leak, the four select sites in the built-in vocabulary (mutation-checked by removing one), ref and native attributes through. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comparison, defaults, literal and deps all gain it — the four-part edit — and the changed-slot test now covers it; mutation-checked by dropping the dep. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Trigger box, label ellipsis, caret, active option, ring and disabled in the state section, Highlight/HighlightText under forced colours. The site guard is red by design until the pickers' rules collapse in the migration. Kit guard mutation-checked by deleting cursor: pointer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Filter dialog operator (ref kept — the dialog focuses it on open), the builder's column and operator, the aggregate picker (custom entry now a disabled option, never written). Every site keeps its attribute; the aggregate picker gains one. Site rules that selected by element type are rewritten onto the attributes and keep only size and flex participation. Test drivers move to chooseOption / readOptions / selectValue; no claim changed. Site guard mutation-checked (a re-added border fails it); the kit guard now covers the label and caret (a deleted label rule fails it); the bracket guard covers the select sites. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he caret The anchor was a prefix match that the caret rule also satisfied, so the ordering guard could not fail (mutation-checked by moving the base rule below the builder pickers). Comments that still described native-select substitution now describe what the kit picker does; the pane-width floor's rationale says what it was measured against; the label helper lives in one place; the override test checks the option count it records. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t a select Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`select.tsx` returns a `<>…</>` fragment, but the package builds JSX with the classic pragma (`pragma: createElement`, `pragmaFrag: Fragment`), so the missing import made every rendered PretableSelect throw "Fragment is not defined" and take the whole grid down with it. The vitest suite could not see this: it transforms JSX with the automatic runtime, which needs no import. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the classic-JSX imports The five undocumented members of PretableSelectOption/Props now carry their docs in the API report. PretableSelectOption is pinned to the internal ListboxOption by TYPE IDENTITY, not two-way assignability — an optional field added to either side satisfies both assignments and would have slipped through (mutation-checked with `readonly extra?: string` on ListboxOption: typecheck fails). A new test walks every shipped .tsx and demands the createElement / Fragment imports the classic JSX build needs — the trap that shipped an undefined Fragment in PretableSelect while 1844 unit tests stayed green (mutation-checked by removing the import; the test fails naming components/select.tsx). No other offenders: every other shipped .tsx already imports what it calls. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…select Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…oseOption Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…xteen Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The guard's reactImportedNames read the unstripped source (so a mention of createElement/Fragment inside a comment counted as a real import) and matched `X as Y` by X (so `createElement as h` satisfied the guard even though the classic pragma needs the bare name in scope). Both were false negatives: a shipped file could pass the guard and still throw "Fragment is not defined" at runtime. Fixed by scanning a comments-only-stripped source (stripping strings too would blank the "react" specifier itself) and binding each import by its post-alias name. Added unit tests for the helper covering a commented-out import, an aliased import, and a multi-line import; updated the header comment to describe the two-stripper split and alias-binding rule. Also corrected an over-claiming comment on the option-shape pin in select.tsx: a `false` there is a compile error at that line, not something that names the drifted field. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…shared; search combobox scoped Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t never opens
The four pickers this component replaced were native <select>s — a stop in
every browser's sequential focus order. A plain <button> is not one in
WebKit unless macOS's "Tab moves between all controls" is on, so the
trigger carries an explicit `tabIndex={0}`, placed before the prop spread
so a consumer running its own roving tabindex still wins. The tool panel's
rail tab carries one for the same reason.
And `openList` returns early on an empty option set: opening there left the
trigger claiming `aria-expanded="true"` with `aria-controls` pointing at a
list that renders nothing. The keyboard path opens through the same
`onOpen`, so one guard covers both.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…se stops describing native selects The list is portalled to <body>, so the option a pointer picks is outside the funnel dialog's subtree and its outside-press listener sees that press. A browser test on `/fixtures/grouping` now picks an operator and asserts the dialog is still standing with the new value, then that Escape unwinds one layer per press: the list first, the dialog second. The tool panel's two keyboard walks move the pickers back out of the conditional sets. They carry an explicit tabindex again, so their exact position is asserted in every browser — verified against WebKit, which offered all of them. Prose only, no declarations: the two skins' `color-scheme` comments named an operator select and its popup list that are no longer native, and the token reference said `--pretable-bg-selected` is read in two cell editors when it is the kit's shared selected-option rule plus the date picker's day. The components page's styling recipe named `--pretable-surface-hover`, which does not exist; it is `--pretable-bg-hover`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
blove
enabled auto-merge (squash)
September 6, 2026 07:01
Contributor
Vercel preview readyPreview: https://pretable-92dfx6hm7-cacheplane.vercel.app Updated automatically by the |
Merged
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.
Components SP2. The kit gains a picker, and the four
<select>elements left in the grid are rebuilt on it.What ships
Listbox+useListboxKeys, extracted from the enum cell editor. The editor's own suite passed unchanged across the extraction — the primitive is the behaviour that was already there, given a name and a second consumer.PretableSelect, a select-only combobox: abutton[role="combobox"]carryingdata-pretable-value, opening a portalled[data-pretable-listbox]indocument.body.Selectslot onPretableComponents(components={{ Select }}), alongside theButtonandIconButtonslots from SP1, withPretableSelectProps,PretableSelectOptionandPretableSelectComponentexported.PretableBuiltInButtonSitenames:filter-operator,filter-row-column,filter-row-operator,aggregate.Measurement, stated honestly
Three of the four pickers hold their box; the two builder pickers deliberately shrink.
filter-operatoraggregatefilter-row-columnfilter-row-operatorThe two tool-panel pickers now size to their label, not to their longest option — that is the intended change, and it is why the filter row wraps less. Heights, padding, border, colours and fonts are unchanged on all four.
The focus ring was verified under real keyboard focus as
rgb(37, 84, 207) solid 2pxat offset-2pxon the operator and aggregate pickers. The raw after-JSON showsnonebecause a programmaticel.focus()does not match:focus-visibleon a button — the probe, not the ring, is what was missing.Guards, mutation-checked
Equalpin on thePretableSelectOptionshapejsx-runtime-importsguard (below)Found in the browser, not by 1844 unit tests
PretableSelectused<>…</>without importingFragment. The classic-JSX build emitted an undefined identifier, and the home-page grid rendered nothing. Every unit test passed. Fixed in119d5b80, and guarded byjsx-runtime-imports, which is itself tested against commented and aliased imports.Consumer notes
The four pickers are buttons, not
<select>elements: a test keyed onselect,.value,selectOption()ortoHaveValue()must readdata-pretable-valueand click the[data-pretable-option][data-value]. The enum editor'sdata-pretable-enum-listbox/data-pretable-enum-optionare replaced bydata-pretable-listbox/data-pretable-option. The trigger is an explicit tab stop in every browser — the native control was not one in WebKit. Full detail in the changeset.Review provenance
Every task ran spec review and quality review with fix rounds; the fix rounds are visible as their own commits (
93c8371flistbox,987c72ffenum editor,9f272e05select,c313a023guard). The final whole-branch review's blocking items are addressed: the Tab-stop gap in WebKit and the empty-list open (5e906a17), the JSX-import guard's false positives on commented and aliased imports (c313a023), the add-group menu's own keyboard and the site count (06699b23), and the funnel dialog surviving a picked operator (1b8183c2).Verification
format,lint,typecheck,build,api,api:check,typecheck:public— all clean,pnpm apileft the tree unchanged.pnpm test: every workspace green first run (react 1851/1851, 134 files).pnpm bench:e2e: 29 passed. Website e2e against a local production build (smoke components tool-panel grid-header, chromium + webkit,--workers=1): 170 passed.🤖 Generated with Claude Code