Skip to content

feat(web): add typeahead keyboard selection to Select (Alpha) - #870

Open
adrienzheng-cb wants to merge 6 commits into
masterfrom
adrienzheng/cds-2505-select-alpha-lacks-typeahead-keyboard-selection
Open

feat(web): add typeahead keyboard selection to Select (Alpha)#870
adrienzheng-cb wants to merge 6 commits into
masterfrom
adrienzheng/cds-2505-select-alpha-lacks-typeahead-keyboard-selection

Conversation

@adrienzheng-cb

Copy link
Copy Markdown
Contributor

What changed? Why?

Adds native <select>-style typeahead keyboard selection to the web Select (Alpha):

  • A short-lived, multi-character search buffer that resets ~500ms after the last keystroke.
  • Repeated-key cycling: pressing the same letter repeatedly cycles through every option that starts with it, matching native <select> semantics.
  • Works both when the listbox is closed (opens it, then focuses the match) and while it is open, via a window-level keydown listener so keystrokes are handled consistently even after focus moves into the portaled dropdown.
  • Buffer construction and match resolution are extracted into a pure typeahead.ts module (isPrintableTypeaheadKey, normalizeOptionText, getTypeaheadMatchIndex) with focused unit tests.

The previous implementation only matched the first letter of an option and only at open time, so users could not type multiple characters or cycle between same-prefixed options.

Testing

How has it been tested?

  • Unit tests
  • Interaction tests
  • Pseudo State tests
  • Manual - Web
  • Manual - Android (Emulator / Device)
  • Manual - iOS (Emulator / Device)

Testing instructions

Focus a web Select (Alpha) and type characters: with the listbox closed, typing opens it and focuses the first match; with it open, continue typing to refine the prefix match, or repeat a single letter to cycle through same-prefixed options. See typeahead.test.ts for the pure matching logic and the added Select.test.tsx cases for integration behavior.

Linear: https://linear.app/coinbase/issue/CDS-2505

Change management

type=routine
risk=low
impact=sev5

automerge=false

Made with Cursor

@linear

linear Bot commented Sep 1, 2026

Copy link
Copy Markdown

CDS-2505

@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 1
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1
CODEOWNERS 🟡 See below

🟡 CODEOWNERS

Code Owner Status Calculation
ui-systems-eng-team 🟡 0/1
Denominator calculation
Additional CODEOWNERS Requirement
Show calculation
Sum 0
0
From CODEOWNERS 1
Sum 1

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

@adrienzheng-cb
adrienzheng-cb force-pushed the adrienzheng/cds-2505-select-alpha-lacks-typeahead-keyboard-selection branch 2 times, most recently from 50ba4a4 to 64c2e69 Compare September 3, 2026 17:00
adrienzheng-cb and others added 2 commits September 3, 2026 13:43
The web Select (Alpha) previously only matched the first letter of an
option and only when opening the listbox. Add native <select>-style
typeahead: a multi-character search buffer that resets ~500ms after the
last keystroke, repeated-key cycling through options that share a first
letter, and matching that works both when the listbox is open and
closed. Buffer/matching logic is extracted into a pure typeahead module
with focused unit tests.

Refs CDS-2505

Co-authored-by: Cursor <cursoragent@cursor.com>
Unify the closed- and open-state key guards behind a shared
isTypeaheadKeyEvent helper and reduce comments to intent-only.

Co-authored-by: Cursor <cursoragent@cursor.com>
@adrienzheng-cb
adrienzheng-cb force-pushed the adrienzheng/cds-2505-select-alpha-lacks-typeahead-keyboard-selection branch from 64c2e69 to 2fa6852 Compare September 3, 2026 17:46

@cb-ekuersch cb-ekuersch left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot going on here - i think it may help to review together. We should potentailly consider the possibility of abstracting this entire typeahead behavior into a single hook that can be called in the component render instead of inlining all this complex react logc in an already compex/long component render body

type TypeaheadKeyEvent = Pick<KeyboardEvent, 'key' | 'ctrlKey' | 'metaKey' | 'altKey'>;

// Bare printable key, no modifier (shared by closed and open paths).
export function isTypeaheadKeyEvent(event: TypeaheadKeyEvent): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: why not make this KeyboardEvent?


useEffect(
() => () => {
if (typeaheadResetTimeoutRef.current) clearTimeout(typeaheadResetTimeoutRef.current);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should clear this in the cleanup of the effect that creates the timeout

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see that the timeout is crated in a user interaction so i dont think this effect just to do cleanup in even necessary

);

useEffect(() => {
if (!open || !pendingTypeAheadKeyRef.current) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we label what these different effects are for? Maybe there is value in giving them names and pulling them into custom hooks just for oganization's sake (e.g. useFocusInPortaledDropdown etc.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

3 participants