Skip to content

feat(react): add the Select primitive#13

Open
ivanbanov wants to merge 2 commits into
mainfrom
feat/select
Open

feat(react): add the Select primitive#13
ivanbanov wants to merge 2 commits into
mainfrom
feat/select

Conversation

@ivanbanov

Copy link
Copy Markdown
Member

Summary

Adds the Select primitive as two packages:

  • @dunky.dev/select — the framework-free core: a state machine (built on
    @dunky.dev/state-machine) that owns value + open state, option
    registration, highlight navigation, typeahead, and selection.
  • @dunky.dev/react-select — the thin React binding that renders the
    compound parts and wires DOM events into the machine.

A single-choice dropdown listbox following the WAI-ARIA select-only combobox
pattern. Value and open state are each controlled or uncontrolled; every
value, open, and highlight change is reported through onValueChange /
onOpenChange / onHighlightChange.

Anatomy

<Select>                   - root; owns value + open state, renders nothing of its own
  |_ <Trigger>             - the button that shows the choice and opens the list;
  |  |                       keyboard focus lives here the whole time
  |  |_ <Value>            - the selected option's label, or a placeholder
  |_ <Listbox>             - the popup list of options
     |_ <Item>             - one option; registers itself with the machine
        |_ <ItemIndicator> - the "this one is chosen" mark, rendered only
                             inside the selected item

Behavior

  • Focus model: DOM focus stays on the trigger the whole time; while open,
    aria-activedescendant names the highlighted option — the APG select-only
    combobox model (one tab stop, no focus jumps on open/close).
  • Roles: trigger is a combobox with aria-haspopup="listbox",
    aria-expanded, and aria-controls; the popup is a listbox of options
    carrying aria-selected / aria-disabled.
  • Keyboard: Enter/Space/ArrowUp/ArrowDown open (highlighting the selected
    option, else the first enabled); Home/End open pinned to the first/last.
    While open, arrows move the highlight across enabled options (wrapping when
    loop), Enter/Space select and close, Escape/Tab/outside interaction close
    without selecting.
  • Typeahead: printable characters accumulate into a machine-owned buffer
    (1s reset) with case-insensitive prefix matching; a repeated character
    cycles through options starting with it.
  • Items register themselves from the render tree (value, label, disabled),
    so the option list always mirrors what is rendered; disabled items are
    skipped by navigation and typeahead.

Test plan

  • pnpm test:ci — 162 tests passing
  • pnpm typecheck
  • pnpm lint
  • pnpm format:check
  • pnpm build

🤖 Generated with Claude Code

ivanbanov and others added 2 commits July 18, 2026 11:53
Adds @dunky.dev/select (core state machine) and @dunky.dev/react-select
(React binding). Anatomy: Select root with Trigger, Value, Listbox,
Item, and ItemIndicator parts.

A single-choice dropdown listbox following the WAI-ARIA select-only
combobox pattern: focus stays on the trigger while
aria-activedescendant tracks the highlighted option, and the machine
owns navigation, typeahead, and selection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ivanbanov ivanbanov changed the title feat: add the Select primitive feat(react): add the Select primitive Jul 18, 2026
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