Refs #14619
DRAFT — FM cockpit focus-order: keyboard-operable cards (Tier-1) + roving-tabindex (Tier-2). The cockpit was mouse-only (zero tabindex/keydown across the fleet components); it is now keyboard-navigable. Draft + Refs (not Resolves) because #14619's a11y pass spans multiple slices — the aria-live slice #15081 carries the close-target; this is the focus-order slice (the ticket-model may warrant an epic split — flagging for the operator, not deciding here).
Evidence: L2 (hermetic unit — the card activation contract + the grid roving state machine) achieved. The actual DOM focus-movement on arrow-nav is the whitebox-e2e layer (mounted-guarded off in units), listed under Remaining.
What is wired
- Tier-1 — keyboard-operable
AgentCard: root tabIndex + role: 'button' + a record-derived aria-label (the resident's displayName, set in applyRecord so it updates on re-seat). Enter activation shares the existing onCardSelect handler (keydown filtered to Enter; a click has no data.key and passes; the control-cluster path carve-out works for keydown too — DomEvents.getKeyboardEventData carries path).
- Tier-2 — roving-tabindex on
FleetGrid: the card set is a single tab stop (focusIndex card is tabIndex 0, every other card -1), and arrows rove it. The keys config auto-wires KeyNavigation (Up/Left step back, Down/Right advance); moveFocus clamps to the card count and moves DOM focus (mounted-guarded); refreshGrid re-establishes the tab stop on a roster rebuild WITHOUT stealing focus (clamps a stale focusIndex into range). The neo-selection class opts the card region into the main-thread arrow-key preventDefault rule, so navigation never scrolls the viewport.
Deltas
- Card activation is Enter-primary. Space's scroll default cannot be cleanly
preventDefault'd on a raw keydown across the worker boundary; arrow-scroll (the roving case) IS handled — via the neo-selection main-thread rule. Card Space-activation is the one deferred nicety (design + source-verified path on #14619 comments 4951014271 / 4951044887 / 4951108209).
- Linear roving over the ranked card list (the visual grid is one focus ring) — Up/Left ≡ prev, Down/Right ≡ next. 2D row/column nav is a possible future refinement, not required for keyboard operability.
Test Evidence
agentCard.spec.mjs (14, +1): construct-probe (tabIndex/role/aria-label), aria-label re-seat update, Enter drills, non-Enter + control-cluster keydowns do not. All green.
fleetGrid.spec.mjs (12, +2): roving — one tab stop, arrows move it clamped at both ends, and a roster rebuild clamps a stale focusIndex into range preserving exactly one tab stop. All green, no regression in the 10 existing tests.
node --check · block-alignment · parse — green (pre-commit).
Post-Merge Validation
- Additive a11y: Tier-1 adds card vdom attrs + a keydown path (the click drill + B4/C2 control seam are unchanged); Tier-2 adds grid roving state (the ranking/fold/re-rank behavior is unchanged — 10/10 existing fleetGrid tests hold). Re-run trigger: changes to
AgentCard activation / onCardSelect, or FleetGrid roving (keys/focusIndex/applyRovingTabIndex/refreshGrid).
Remaining Before Ready (blocks ready-for-review)
Authored by Vega (Claude Opus 4.8, Claude Code). Session d99146da-0478-4f23-bc16-dff04f5d650c.
Refs #14619
DRAFT — FM cockpit focus-order: keyboard-operable cards (Tier-1) + roving-tabindex (Tier-2). The cockpit was mouse-only (zero tabindex/keydown across the fleet components); it is now keyboard-navigable. Draft +
Refs(notResolves) because #14619's a11y pass spans multiple slices — the aria-live slice #15081 carries the close-target; this is the focus-order slice (the ticket-model may warrant an epic split — flagging for the operator, not deciding here).Evidence: L2 (hermetic unit — the card activation contract + the grid roving state machine) achieved. The actual DOM focus-movement on arrow-nav is the whitebox-e2e layer (mounted-guarded off in units), listed under Remaining.
What is wired
AgentCard: roottabIndex+role: 'button'+ a record-derivedaria-label(the resident'sdisplayName, set inapplyRecordso it updates on re-seat). Enter activation shares the existingonCardSelecthandler (keydown filtered to Enter; a click has nodata.keyand passes; the control-clusterpathcarve-out works for keydown too —DomEvents.getKeyboardEventDatacarriespath).FleetGrid: the card set is a single tab stop (focusIndexcard istabIndex 0, every other card-1), and arrows rove it. Thekeysconfig auto-wiresKeyNavigation(Up/Left step back, Down/Right advance);moveFocusclamps to the card count and moves DOM focus (mounted-guarded);refreshGridre-establishes the tab stop on a roster rebuild WITHOUT stealing focus (clamps a stalefocusIndexinto range). Theneo-selectionclass opts the card region into the main-thread arrow-keypreventDefaultrule, so navigation never scrolls the viewport.Deltas
preventDefault'd on a raw keydown across the worker boundary; arrow-scroll (the roving case) IS handled — via theneo-selectionmain-thread rule. Card Space-activation is the one deferred nicety (design + source-verified path on #14619 comments 4951014271 / 4951044887 / 4951108209).Test Evidence
agentCard.spec.mjs(14, +1): construct-probe (tabIndex/role/aria-label), aria-label re-seat update, Enter drills, non-Enter + control-cluster keydowns do not. All green.fleetGrid.spec.mjs(12, +2): roving — one tab stop, arrows move it clamped at both ends, and a roster rebuild clamps a stalefocusIndexinto range preserving exactly one tab stop. All green, no regression in the 10 existing tests.node --check· block-alignment · parse — green (pre-commit).Post-Merge Validation
AgentCardactivation /onCardSelect, orFleetGridroving (keys/focusIndex/applyRovingTabIndex/refreshGrid).Remaining Before Ready (blocks ready-for-review)
Resolvesa sub) — operator call; this staysRefs+ draft until then.Authored by Vega (Claude Opus 4.8, Claude Code). Session d99146da-0478-4f23-bc16-dff04f5d650c.