Skip to content

feat(ui): custom accessible listbox Select#5

Merged
DCCA merged 2 commits into
mainfrom
feat/custom-select-listbox
Jun 20, 2026
Merged

feat(ui): custom accessible listbox Select#5
DCCA merged 2 commits into
mainfrom
feat/custom-select-listbox

Conversation

@DCCA

@DCCA DCCA commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

The native <select> open option list is OS-rendered and unstylable, so it broke the design system when expanded (system font, blue highlight, square corners). This replaces it with a custom WAI-ARIA select-only combobox we fully control.

  • New API: value + onValueChange + options (no more <option> children).
  • A11y: role="combobox" trigger (aria-haspopup/expanded/controls/activedescendant) + role="listbox"/"option" popup. Focus stays on the trigger; aria-activedescendant tracks the highlight. Labelled via aria-labelledby from the visible field labels.
  • Keyboard: ↑/↓, Home/End, Enter/Space to open & select, Esc to close, Tab to dismiss, printable-char typeahead. Outside-pointer-down closes.
  • Visual: token surface, emerald highlight, selected checkmark, chevron rotates on open.
  • Testable core: navigation rules extracted to a pure helper (src/lib/selectNavigation.ts) with tests. Suite 26 → 34.
  • Editor Interaction/Tool selects migrated to the new API.

Docs

CLAUDE.md updated: documents the design-token system and the custom Select contract (use the options API, not native <option>), and adds selectNavigation.ts to the pure-helpers list.

Verification

  • npm run check — typecheck + lint + 34 tests + build all green
  • Built and manually tested in Chrome: open-list styling, keyboard nav, typeahead, outside-click, selected checkmark

Known limitation

The popup is positioned inside the editor sidebar card (overflow-auto). For the two short, top-of-card selects it opens cleanly in view; a Select placed low in a scrolling panel could clip and would need a portal. Not an issue for current placement.

🤖 Generated with Claude Code

DCCA and others added 2 commits June 20, 2026 19:07
The native <select> open option list is rendered by the OS and cannot be
styled, so it broke the design system when expanded (system font, blue
highlight, square corners). Replace it with a custom WAI-ARIA select-only
combobox that we fully control.

- New Select API: `value` + `onValueChange` + `options` (no <option> children).
- WAI-ARIA select-only combobox pattern: role="combobox" trigger with
  aria-haspopup/expanded/controls/activedescendant; role="listbox"/"option"
  popup; focus stays on the trigger and aria-activedescendant tracks the
  highlight.
- Keyboard: Up/Down, Home/End, Enter/Space to open/select, Escape to close,
  Tab to dismiss, printable-char typeahead. Outside-pointer-down closes.
- Styled open list: token surface, emerald highlight, selected checkmark,
  chevron rotates on open.
- Navigation rules extracted to a pure, tested helper
  (src/lib/selectNavigation.ts) + tests; total suite 26 -> 34.
- Editor's Interaction/Tool selects migrated to the new API and labelled via
  aria-labelledby.

Docs: CLAUDE.md documents the design-token system and the custom Select
contract so future work uses the options API, not native <option> children.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback: the typeahead reset setTimeout was cleared on
the next keystroke but never on unmount. Add an unmount-cleanup effect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@DCCA DCCA merged commit 7cb4c25 into main Jun 20, 2026
0 of 2 checks passed
@DCCA DCCA deleted the feat/custom-select-listbox branch June 20, 2026 22:09
DCCA pushed a commit that referenced this pull request Jun 22, 2026
PRs #3-#5 (UI refactor) and a new CLAUDE.md landed on main without
Prettier formatting, so the format:check gate added in the hardening
sweep failed on main. Format the 4 offending files (CLAUDE.md,
badge.tsx, editor/main.tsx, viewer/main.tsx). No logic changes.

All gates pass locally: typecheck, lint, format:check, test (34), build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJ5c2e5VcKVhugy7TAzi4W
DCCA added a commit that referenced this pull request Jun 22, 2026
Apply Prettier to CLAUDE.md and the UI files (badge, editor, viewer) that landed unformatted via PRs #3-#5, restoring the format:check gate on main. No logic changes.
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