Skip to content

Use a native <dialog> for the search modal - #2031

Merged
benbalter merged 1 commit into
mainfrom
audit-search-dialog
Sep 27, 2026
Merged

benbalter merged 1 commit into
mainfrom
audit-search-dialog

Conversation

@benbalter

Copy link
Copy Markdown
Owner

What

The search modal was a div role="dialog" with a hand-rolled Tab trap. It's now a native <dialog> opened with showModal(), following the pattern ShortcutsModal already uses.

  • Focus: the browser handles containment, inertness, and ::backdrop. The manual trapFocus handler and its focusable-selector list are gone.
  • Closing: a single close event handler resets the input, results, and scroll lock, and returns focus to the trigger. It runs however the dialog closes: Esc, ⌘K, the close button, a backdrop click, or a result click.
  • Esc: the explicit Esc handler stays, because a search input holding a query can use up the first Esc to clear itself.
  • Backdrop clicks: detected with the same bounding-box check the shortcuts dialog uses.
  • Styling: the dialog is now the panel itself. The fade and scale animation uses @starting-style plus allow-discrete, and falls back to no animation where unsupported. display: flex is scoped to [open] so it can't override the UA's hidden state. Light and dark tokens are unchanged.
  • Live region: aria-live is off #search-results, which re-announced the whole list on every keystroke. A visually hidden role="status" element now announces only the count ("8 results", "No results").
  • shortcuts.ts checks dialog.open instead of hidden.
  • Rendering and navigation helpers are exported and covered by a new src/scripts/search-modal.test.ts, which tests escaping, title-as-text, arrow and j/k navigation, and count labels.

Verification

  • npm run check: 0 errors
  • npx vitest run src/scripts/search-modal.test.ts: 13 passed
  • npx eslint on changed TS files: clean
  • SKIP_PDF=1 npm run build: OK
  • Playwright (chromium) search-shortcuts, csp, accessibility-astro against astro preview: 71 passed
  • Checked by hand in Chrome: the trigger opens the dialog and focuses the input, the status reads "8 results", a backdrop click closes it and returns focus to the trigger, and Esc closes it and clears state. Light and dark screenshots look right.

🤖 Generated with Claude Code

Swap the div role="dialog" and hand-rolled Tab trap for a <dialog> opened
with showModal(), matching ShortcutsModal. The browser now handles focus
containment, inertness, and the ::backdrop; a close-event handler resets
state and returns focus to the trigger however the dialog closes.

Move the live region off #search-results so screen readers stop re-reading
the whole list on each keystroke; a visually hidden role="status" element
announces only the count ("5 results", "No results").

Export the rendering and navigation helpers and cover them with Vitest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@benbalter
benbalter merged commit e475e06 into main Sep 27, 2026
22 checks passed
@benbalter
benbalter deleted the audit-search-dialog branch September 27, 2026 14:13
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