Skip to content

feat(a11y): modal focus-trap + restore + role=dialog, with @a11y tests#94

Merged
mvalancy merged 2 commits into
devfrom
test/a11y-focus
Jun 17, 2026
Merged

feat(a11y): modal focus-trap + restore + role=dialog, with @a11y tests#94
mvalancy merged 2 commits into
devfrom
test/a11y-focus

Conversation

@mvalancy

Copy link
Copy Markdown
Member

What

Implements the keyboard-accessibility behavior modals were missing, plus the @a11y test category that locks it in.

hooks/useModalA11y (new shared hook): marks a modal role="dialog" aria-modal="true" with an accessible name, moves focus into it on open, traps Tab/Shift+Tab within it, and restores focus to the trigger on close. Pairs with useDialog (Escape / click-outside).

Applied to CreateWorkItemModal, CreateGraphModal, and WorkItemDetailsModal (the details modal keeps its own container focus; fixed its aria-labelledbyaria-label so the accessible name isn't the title input's value).

Why

Discovery (a parallel audit of every modal) found nearly all of them had no role=dialog, no focus trap, and no focus restore — you could Tab straight out of an open modal into the graph behind it, and keyboard focus never came back to where you were. Real keyboard friction.

@a11y suite — tests/e2e/a11y-focus.spec.ts

  • details modal: role=dialog + accessible name + Tab trapped
  • create-graph modal: role=dialog + Tab trapped
  • create-work-item modal: focus enters on open, Tab trapped, focus restored to the FAB on close

Verification

  • @a11y 3/3 pass; typecheck clean.
  • No regressions: smoke 5/5, @dismissal + @zorder 25/25 (same modals).

🤖 Generated with Claude Code

…11y)

Adds hooks/useModalA11y: marks a modal as role="dialog" aria-modal with an
accessible name, moves focus into it on open, traps Tab/Shift+Tab within it, and
restores focus to the trigger on close. Applied to CreateWorkItemModal,
CreateGraphModal, and WorkItemDetailsModal (the details modal keeps its own
initial container focus; fixed its aria-labelledby→aria-label so the name isn't
the title input's value). Pairs with useDialog (Escape/click-outside).

New @a11y suite (tests/e2e/a11y-focus.spec.ts) asserts role=dialog, focus
enters the modal, Tab is trapped, and focus returns to the trigger on close —
behavior that was previously untested and mostly absent (you could Tab out of a
modal into the graph behind it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🧪 Comprehensive Test Suite

  • Unit suites (Node 18.x & 20.x) — core, web, server, mcp-server: ✅ passed
  • Installer & deploy config: ✅ passed

Full-stack smoke gate runs in the CI workflow.

Adversarial review fixes:
- useModalA11y: prefer the first form field for initial focus (not the icon-only
  Close button); restore focus only when it dropped to <body> so a deliberate
  post-close focus move isn't overridden; sharper isVisible (getClientRects +
  computed visibility); honest docstring (Tab-trap + aria-modal hint, not a
  background-inert); include [contenteditable] in the focusable set.
- CreateWorkItemModal / CreateGraphModal: aria-label="Close" on the X buttons.
- WorkItemDetailsModal: drop the bespoke capture-phase Escape handler so Escape
  is owned by the dialog-manager (defers while typing, keeps the stack coherent);
  keep only Ctrl/Cmd+S.
- a11y-focus.spec: test the trap AT THE BOUNDARY (Tab from the last focusable
  must wrap inside; Shift+Tab from the first) — the previous fixed 14 Tabs never
  reached the edge on modals with many focusables, so it passed even without the
  trap. Restore test now focuses the trigger first and asserts focus left it
  while open.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🧪 Comprehensive Test Suite

  • Unit suites (Node 18.x & 20.x) — core, web, server, mcp-server: ✅ passed
  • Installer & deploy config: ✅ passed

Full-stack smoke gate runs in the CI workflow.

@mvalancy mvalancy merged commit e74b99c into dev Jun 17, 2026
16 checks passed
@mvalancy mvalancy deleted the test/a11y-focus branch June 17, 2026 11:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant