Problem
Five components independently implement the same roving-highlight arrow-key navigation over a list. The logic is duplicated, and the behavior has already drifted between them.
| Component |
Wrap arithmetic |
Home / End |
Notes |
Autocomplete |
yes |
no |
uses a -1 sentinel for "nothing active" |
Messaging/MessageComposer |
yes |
no |
mention suggestions |
CodeLookup |
yes |
yes |
switch-based |
Select |
yes |
yes |
switch-based |
AI/ComposerModelSelector |
yes |
yes |
plus back-navigation out of a drill-down view |
Each one hand-rolls the same modular arithmetic:
setHighlight((current) => (current + 1) % length);
setHighlight((current) => (current - 1 + length) % length);
The inconsistency is the real cost: only three of the five support Home/End, so keyboard users get different capabilities depending on which list they happen to be in.
Proposal
Design a useListNavigation hook in src/hooks/ and migrate all five components to it.
This is not a mechanical extraction — the hook needs deliberate API decisions before it is worth doing:
- Wrap vs. clamp at the list boundaries.
- Sentinel support.
Autocomplete treats -1 as a valid "no active row" state, which changes what ArrowUp from index 0 should do. The others have no such state.
- Opt-in keys. Home/End should be available but not forced on every consumer.
- Optional back-navigation, for drill-down menus like
ComposerModelSelector.
Getting these wrong produces an abstraction shaped by whichever component was migrated first, so the design should be settled up front rather than discovered during migration.
Out of scope
Slider, Tabs, and MediaEditor also handle ArrowDown/ArrowUp, but for value adjustment and tab movement rather than list highlighting. They should not be folded into this hook.
Context
Surfaced during review of the reasoning-effort work on ComposerModelSelector, which added a second listbox to that component. A local helper to deduplicate the two handlers within that one file was written and then deliberately reverted — since this refactor supersedes it, carrying it would have meant reviewing code destined to be replaced.
Problem
Five components independently implement the same roving-highlight arrow-key navigation over a list. The logic is duplicated, and the behavior has already drifted between them.
Autocomplete-1sentinel for "nothing active"Messaging/MessageComposerCodeLookupSelectAI/ComposerModelSelectorEach one hand-rolls the same modular arithmetic:
The inconsistency is the real cost: only three of the five support Home/End, so keyboard users get different capabilities depending on which list they happen to be in.
Proposal
Design a
useListNavigationhook insrc/hooks/and migrate all five components to it.This is not a mechanical extraction — the hook needs deliberate API decisions before it is worth doing:
Autocompletetreats-1as a valid "no active row" state, which changes what ArrowUp from index 0 should do. The others have no such state.ComposerModelSelector.Getting these wrong produces an abstraction shaped by whichever component was migrated first, so the design should be settled up front rather than discovered during migration.
Out of scope
Slider,Tabs, andMediaEditoralso handleArrowDown/ArrowUp, but for value adjustment and tab movement rather than list highlighting. They should not be folded into this hook.Context
Surfaced during review of the reasoning-effort work on
ComposerModelSelector, which added a second listbox to that component. A local helper to deduplicate the two handlers within that one file was written and then deliberately reverted — since this refactor supersedes it, carrying it would have meant reviewing code destined to be replaced.