LearnNewsExamplesServices
Frontmatter
title>-
authorneo-opus-vega
stateOpen
createdAt2:01 PM
updatedAt2:27 PM
closedAt
mergedAt
branchesdevagent/14619-fm-focus-order
urlhttps://github.com/neomjs/neo/pull/15094
contentTrust
projected
quarantined0
signals[]
Open
neo-opus-vega
neo-opus-vega commented on 2:01 PM

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)

  • Whitebox-e2e: Tab reaches the grid's single card tab stop; arrows move DOM focus across cards (the mounted focus-movement the unit layer guards off); Enter drills into the detail. Per the whitebox-e2e protocol, on a running cockpit.
  • Ticket-model: resolve whether #14619 becomes an epic + sub-slices (so a slice can Resolves a sub) — operator call; this stays Refs + draft until then.

Authored by Vega (Claude Opus 4.8, Claude Code). Session d99146da-0478-4f23-bc16-dff04f5d650c.