fix(dev-fleet): arrow-key navigation in row-actions menu (#5851) - #6226
Conversation
role="menu" promises ArrowDown/ArrowUp item navigation per the WAI-ARIA menu pattern, but the row-actions dropdown only handled Tab containment. Handle ArrowDown/ArrowUp over the same focusableItems() list (wrap at both ends) plus Home/End, with preventDefault to stop page scroll, modified arrows (Cmd/Ctrl/Alt) passed through as OS shortcuts, and the shared IME latch claim so composition-owned arrows never move menu focus. Tab containment and focus return are unchanged. Closes #5851
UX Review (Fable 5) — ✅ PASSUX-level review of UX-Verdict: PASS Arrow/Home/End navigation delivers the behavior [UX-REVIEWED] ec4db96 |
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Small, correctly-scoped fix that makes the announced Suggestions
[DESIGN-REVIEWED] ec4db96 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All evidence gathered. The fix itself is derived (WAI-ARIA First-Principles-Verdict: CONCERNS The arrow fix is derived and honest, but it is the codebase's fourth keyboard-nav spelling and five counted What this change shipsIntent: make the Dev Fleet row-actions menu answer the arrow keys its
Watch
[FIRST-PRINCIPLES-REVIEWED] ec4db96 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
|
Dispositions for the First Principles CONCERNS (advisory, head ec4db96): 1. Five sibling 2. |
dwu96
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: role="menu" already promised WAI-ARIA arrow-key item navigation but the keydown handler only bound Escape/Tab, so screen-reader users had no arrow path through the row-actions menu; the fix adds ArrowUp/ArrowDown/Home/End with wrap, IME-composition and modified-key claims, plus five regression tests.
bolichen97
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: fix — arrow-key item navigation in the row-actions menu (WAI-ARIA menu pattern) for #5851, test-covered, no runtime/behavior change outside the menu keyboard handler.
bolichen97
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean (Semgrep success/0, CodeQL success + 0 PR-scoped alerts), security checklist all-NO, AI reviewers green. Category: fix(dev-fleet) adds WAI-ARIA arrow-key/Home/End navigation to the existing row-actions menu keydown handler with tests, clear root cause (#5851: role=menu promised arrow navigation it lacked).
…nu surfaces Fixes #6231 Five containers declare role="menu" — which promises WAI-ARIA arrow-key item navigation — and none implemented it. Extract MenuBtn's proven roving-focus keydown logic (DevFleetPage, #6226) into a shared useMenuKeyboard hook (arrows wrap at both ends, Home/End, Tab containment per #2533, document IME latch), refactor MenuBtn onto it (its tests pass unchanged — the extraction's behaviour-preservation proof), and wire the five surfaces: AgentPillBar (menu semantics on the actual controls, with host-driven focus repair), MicSourceMenu, mochi ContextMenu (guarded useLayoutEffect unmount focus restore), mochi PackEditor SlotPopover (closeToOpener for Escape and every command), and PierreWorkspaceTreeImpl (single-item semantic no-op, wired for contract consistency). Zero i18n changes; no visible label, ordering, or click behaviour changed.
…nu surfaces (#6267) * fix: honour the role="menu" arrow-key contract on all five sibling menu surfaces Fixes #6231 Five containers declare role="menu" — which promises WAI-ARIA arrow-key item navigation — and none implemented it. Extract MenuBtn's proven roving-focus keydown logic (DevFleetPage, #6226) into a shared useMenuKeyboard hook (arrows wrap at both ends, Home/End, Tab containment per #2533, document IME latch), refactor MenuBtn onto it (its tests pass unchanged — the extraction's behaviour-preservation proof), and wire the five surfaces: AgentPillBar (menu semantics on the actual controls, with host-driven focus repair), MicSourceMenu, mochi ContextMenu (guarded useLayoutEffect unmount focus restore), mochi PackEditor SlotPopover (closeToOpener for Escape and every command), and PierreWorkspaceTreeImpl (single-item semantic no-op, wired for contract consistency). Zero i18n changes; no visible label, ordering, or click behaviour changed. * chore: inclusive-language gate — black-box -> closed-box in a test comment --------- Co-authored-by: Di Wu <diwm@dev-dsk-diwm-2b-abc02530.us-west-2.amazon.com>
What is the problem?
The Dev Fleet row-actions dropdown (
MenuBtninwebsite/src/pages/DevFleetPage.tsx) announcesrole="menu", and that role promises ArrowDown/ArrowUp item navigation. PR #3525 added focus entry, Tab/Shift-Tab containment, and focus return, so the menu is usable via Tab -- but arrow keys did nothing.Why this issue matters to the user
Screen-reader users hear "menu" and reach for arrow keys first; when arrows do nothing the menu appears broken even though Tab works. Keyboard users get the same dead keys, and an unhandled arrow scrolls the page behind the open menu.
How our fix solves it
The announced role and the handled keys now agree. In
MenuBtn's open-effect key handler, ArrowDown/ArrowUp move focus through the samefocusableItems()list the existing Tab containment uses (disabled items are already filtered out), wrapping at both ends; Home/End jump to the boundary items;preventDefaultstops the page scrolling behind the menu. Three guards keep the new branch honest:useDocumentImeLatch.claimKey) runs beforepreventDefault()and any focus move, so an arrow that belongs to a composition (candidate navigation) never moves menu focus -- the same claim contract as the existing Tab/Escape branches. The menu takes the stricter side thanuseListKeyboardNav(which lets composition arrows through) because the menu holds no editable target; the code comment records the divergence.Tab containment, Escape, focus return, and outside-click behavior are unchanged. Scope note: this delivers arrow-key navigation; full menu-pattern semantics (menuitem child roles, roving tabindex) are a pre-existing gap in the shared
Clickablecomponent and are out of scope here.What tests we did
website/src/test/DevFleetPage.test.tsx: arrow cycling with wrap at both ends, Home/End jumps, IME-composition decline (focus stays put), outside-the-list entry (Down to first, Up to last), and modified-arrow passthrough.claimKeyline reds the decline test.npx tsc -bclean; full frontend suite 24646 tests passed; eslint warning count identical to base for both touched files.Any other suggestions on the work
The IME ratchet (
scanUnguardedTabFocusTraps) only recognizes Tab-shaped traps, so an unguarded arrow branch that moves focus is invisible to it -- widening the predicate to the choose-class key set is the cause-level fix and belongs to the existing ratchet-widening issue #5475.Closes #5851