feat(styles,react): add combobox, option list, input search and checkbox - #52
Draft
Gnuk wants to merge 1 commit into
Draft
feat(styles,react): add combobox, option list, input search and checkbox#52Gnuk wants to merge 1 commit into
Gnuk wants to merge 1 commit into
Conversation
Gnuk
force-pushed
the
51-combobox
branch
9 times, most recently
from
August 18, 2026 14:15
c55807a to
d390433
Compare
The library had a floating panel of actions but no floating panel of options, so every consumer needing a picker rebuilt one on top of input-text and dropdown, inventing its own keyboard handling and its own ARIA wiring. dropdown could not be reused as is: it is opened by an invoker button carrying command/commandfor, which anchors the panel implicitly, while a combobox is anchored to a text field that is not an invoker and has to open programmatically. The panel is therefore a manual popover — with auto, the click landing in the field would light-dismiss the panel that click just opened — anchored explicitly through anchor-name and position-anchor, with anchor-scope confining the name to each instance. Two quarks keep the new atoms from drifting: one holds the field container input-text and input-search now share, the other the check box the checkbox atom and the option row share. The input-text refactor leaves the generated CSS unchanged byte for byte. Closes #51
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.
Closes #51
What this brings
@ippon-ui/styleshad a floating panel of actions (ippon-dropdown) but no floating panel of options. Every consumer needing a picker rebuilt one on top ofinput-textanddropdown, inventing its own keyboard handling and its own ARIA wiring — the two parts hardest to get right and the two the design system should own.Five new components, bottom up:
control-box,checkbox,visually-hiddeninput-searchcheckboxoptionoption-listlistbox, its footer, and its loading, empty and failed contentcomboboxPlus
IpponInputSearch,IpponCheckbox,IpponOption,IpponOptionListandIpponComboboxin@ippon-ui/react.The anchoring point
dropdowncould not be reused as is. It is opened by an invoker button carryingcommand/commandfor, and the invoker anchors the panel implicitly. A combobox is anchored to a text field, which is not an invoker and has to open and close programmatically.popover="manual", notauto: withauto, the click landing in the field would light-dismiss the panel that click just opened;anchor-nameon the field,position-anchorandanchor-size(width)on the panel — withanchor-scopeon the wrapper so two combobox on a page do not both anchor to the first field.IpponDropdowngainsalternativeandpopoverprops, both defaulting to the current behaviour.Design fidelity
The measurements in the issue were read off the rendered frames rather than the auto-layout (the Figma seat hit its tool-call limit). Everything is snapped to the existing token scale instead, per @Gnuk's arbitration: the
52field height becomes--ippon-size-48—input-text's own height,52being it plus the 2×2 focus ring caught in the measurement — and the10panel gap becomes the--ippon-size-8dropdownalready uses.One open design question. The panel reuses
dropdown's floating surface rather than introducing a second one, as the issue asked, so it keeps--ippon-radius-land its shadow-without-border where the Figma binds radius M and a border. The field and the panel therefore have two different corner radii while the design draws them identical, and the two shapes nearly touch, so it is visible. Library coherence against design fidelity — worth a call before merge; it is one line either way.States
The nominal multi-select is what the Figma draws. The rest was undrawn and is derived from conventions already in the library, on @Gnuk's call to settle them here rather than leave each consumer to improvise:
input-text's-error/-success;ReactNodeslots onoption-list, filled in the stories byIpponProgress,IpponTextandIpponErrorAreawith its retry action. The state renders beside the rows, never in their place, so a search still in flight keeps the previous results on screen instead of making the panel flicker at every keystroke;calc(5 * var(--ippon-size-48) + 2 * var(--ippon-size-4))), and joined labels truncate with an ellipsis;-singlealternative swaps the box for a bare check glyph, shows no counter and closes on pick.Pagination
option-listtakes afooter, rendered after the rows, inside the scrolling area and outside thelistbox. A "load more" button, a "20 of 137" counter or the sentinel of an infinite scroll goes there. Being inside the scroll is what makes a sentinel work: pinned under the list it would always be in view and would never stop asking for more.IpponComboboxpasses it straight through — no data enters the component.Accessibility
role="combobox"witharia-expanded,aria-controlsandaria-autocomplete="list";role="listbox"witharia-multiselectableandaria-busy;role="option"witharia-selectedandaria-disabled. DOM focus never leaves the input — the active option is tracked witharia-activedescendant, which is why the option row draws a check box rather than containing one.Keyboard: arrows,
Home/End,Enter,Escape, disabled options skipped, active option scrolled into view.Dismissal took three passes to get right, and the browser corrected me twice. It now rests on two mechanisms: a pointer landing outside the component, and focus moving to a named element outside it — so
Tabcloses the panel without selecting, whileTabonto something the panel itself holds, such as a footer button, keeps it open, which is the only way that button is reachable without a pointer.The third case is the one a footer makes possible: a "load more" button that hides itself once the last page is in removes the focused element from the document. Chromium fires no focus event at all there, so nothing closed the panel — but nothing could close it afterwards either, since no part of the component held focus any more. It stayed open through a click on a button entirely outside it. The outside-pointer listener is what fixes that, and
Escapeis listened for at the document level for the same reason, so an orphaned focus never leaves a panel only a pointer could dismiss.That document-level
Escapethen needed its own default action cancelled, so it dismisses the panel alone instead of also reaching whatever else on the page answers that key — a nativedialogaround the field, for instance, which would otherwise close and take the form with it. The path through the field already cancelled it and hid the gap. A closed combobox claims nothing. Engines that do report such a removal are told apart from a genuine focus exit byrelatedTarget === null && !target.isConnected; that branch is defensive and untestable from jsdom, which is stated plainly rather than implied by a green suite.The selection count is also stated in words for assistive technology, so the counter badge is not its only carrier.
IpponIongains alabelprop (and the Pug mixin alabeloption) settingaria-label: a clickable icon carrying no text — the counter's clear cross — had no accessible name.Out of scope
No data layer: the component never fetches, never debounces, never caches and never filters. It renders the options it is handed; the consumer owns the query. The stories wire the filtering and the pagination to show what that looks like.
Known gap, deliberately not invented here: the
checkboxatom has no indeterminate state. It is in neither the design nor the list of states to settle, and a "select all" will want it one day.Checks
mise format-ci,mise lint-ci,mise test-unit-ci(369 React tests, 32 files) andmise buildall pass. Theinput-textSCSS refactor onto the shared quark was verified to produce byte-identical CSS.The organism was driven in a real browser through Storybook, not only in jsdom: focus opens the panel, the anchoring positions it, arrows move the active option,
Enterselects, typing filters,Escapecloses and leaves focus in the field,Tabreaches the footer button with the panel open and the nextTabcloses it, "load more" grows the list into its scroll, a footer that removes itself leaves the panel open on its full list, and an outside click — orEscape— then closes it.🤖 Generated with Claude Code