Skip to content

feat(a11y): dismiss transient outline overlay on focus-out (A11Y-03) - #245

Merged
mtskf merged 4 commits into
mainfrom
feat/a11y-03-outline-focus-trap
Jul 23, 2026
Merged

feat(a11y): dismiss transient outline overlay on focus-out (A11Y-03)#245
mtskf merged 4 commits into
mainfrom
feat/a11y-03-outline-focus-trap

Conversation

@mtskf

@mtskf mtskf commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

Resolves a11y finding A11Y-03 (LOW). Records a deliberate decision — the outline sidebar is a non-modal role=tree, not a modal dialog, so it does not trap Tab — and fixes the one real wart: a transient overlay left floating over the editor with keyboard focus behind it.

Decision (non-modal, no Tab trap)

  • The WAI-ARIA Tree View pattern does not trap Tab, and pinned mode is a persistent pane where Tab must flow between the sidebar and editor (VS Code Outline-style) — a trap would regress it.
  • A hard trap would fake modal-dialog semantics (aria-modal / obscured background) the panel does not have, and break the module's CSS-only overlay/pinned distinction.
  • Refinement: the transient overlay self-dismisses on focusout when focus leaves the sidebar, mirroring the footer settings-popover's click-outside close. Pinned mode never closes on focus-out.

Design validated by Codex (endorsed non-modal + overlay focus-out dismiss over a hard trap).

Changes

  • outline-panel.ts: add onSidebarFocusOut + a focusout listener on the sidebar. Guards: !open (mid-close no-op), pinned (persistent pane stays open), relatedTarget === null (window blur / cross-context keeps it open), sidebarEl.contains(relatedTarget) (intra-sidebar moves, incl. into the DOM-descended footer popover, are not leaves). Updated module header + "Closing paths" comments.
  • outline-panel.test.ts: new describe("quollOutline overlay focus-out dismiss") pinning all four behaviours (overlay dismisses to editor; pinned stays open; intra-sidebar move stays open; null relatedTarget stays open). Non-vacuity verified (neutering the handler makes the dismiss test go red).

Related

  • TODO: A11Y-03 — outline sidebar focus trap decision (from a11y audit 2026-07-24)

Test Plan

  • pnpm compile green
  • pnpm test:unit green (3816 passed; 4 new)
  • Biome clean on changed files
  • Manual smoke: open the outline overlay, Tab out → overlay closes; pin it, Tab between sidebar and editor → stays open

mtskf added 4 commits July 24, 2026 04:21
The outline sidebar is a non-modal role=tree, so it deliberately does not
trap Tab (pinned mode is a persistent pane where Tab must flow between the
sidebar and editor). To resolve the one wart — a transient overlay left
floating over the editor with keyboard focus behind it — the overlay now
self-dismisses when focus leaves the sidebar, mirroring the footer settings
popover's click-outside close. Pinned mode never closes on focus-out; window
blur (null relatedTarget) and intra-sidebar focus moves are guarded out.
…ader

onSidebarFocusOut is modality-agnostic (reads only relatedTarget), so the
overlay dismisses on any focus leaving the sidebar, not just keyboard Tab-out.
The other three comments describing this path already omit "keyboard"; align
the module header with them. (PR #245 review, comment-analyzer HIGH/88.)
The sidebarEl.contains() guard's JSDoc calls out the footer settings popover as
the fragile branch (any future portal-out would break it). Add a test asserting
focus moving into the DOM-descended popover does NOT dismiss the overlay.
(PR #245 review, code-quality MEDIUM/83.)
…width

Review-cycle SIMPLIFY: the A11Y-03 header edit left one ~110-char comment line
breaking the file's ~76-char wrap rhythm. Reflow into evenly-wrapped lines;
wording byte-preserved.
@mtskf
mtskf merged commit 2a10ff8 into main Jul 23, 2026
1 check passed
@mtskf
mtskf deleted the feat/a11y-03-outline-focus-trap branch July 23, 2026 18:45
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