Skip to content

fix(a11y): wrap matrix DialogTitle in Radix VisuallyHidden - #9

Merged
build-with-dhiraj merged 1 commit into
mainfrom
fix/dialog-title-a11y
Jun 23, 2026
Merged

build-with-dhiraj merged 1 commit into
mainfrom
fix/dialog-title-a11y

Conversation

@build-with-dhiraj

Copy link
Copy Markdown
Owner

What & why

A11y-only hardening of the shared matrix Dialog primitive (src/components/ui/Dialog.tsx) used by the three Skills-matrix modals (Gap / Capture / Export).

Radix's DialogContent warns when it cannot find its DialogTitle in the DOM (document.getElementById(titleId)), pointing you to wrap a hidden title with its VisuallyHidden component. This PR adopts that exact recommended pattern, replacing the project-sr-only-classed Title with Radix's own VisuallyHidden:

-<RadixDialog.Title className="sr-only">{label}</RadixDialog.Title>
+<VisuallyHidden asChild>
+  <RadixDialog.Title>{label}</RadixDialog.Title>
+</VisuallyHidden>

The Title stays visually hidden (renders 1x1, clipped) so there is zero layout or behavior change. This makes the accessible name robust regardless of how the project's sr-only utility is defined now or in the future, which is the canonical Radix guidance.

Changes

  • src/components/ui/Dialog.tsx — use VisuallyHidden asChild around RadixDialog.Title; updated the doc comment to match.
  • package.json / package-lock.json — add @radix-ui/react-visually-hidden as a direct dependency (it was already present transitively via @radix-ui/react-dialog).

No other files touched. src/lib/labs/mcclean.ts untouched. Zero em-dashes. joVE | LabOS design system preserved.

Verification (empirical, in-browser)

Ran the app locally and opened each of the three modals via the real triggers (coverage-gap cell, "Launch tacit capture", and Compliance-on -> "Form 483 audit export").

Modal Console (errors/warnings) Accessible name Focus in Escape closes + focus restores Layout
Gap (Create onboarding plan) 0 / 0 present (VisuallyHidden Title) yes (Close) yes -> restored to gap cell unchanged
Capture (Tacit knowledge capture) 0 / 0 present yes (Close capture) yes -> restored to launch button unchanged
Export (Form 483 audit export) 0 / 0 present yes (Close export) yes -> restored to export button unchanged (visible h2 still 316x24)

The VisuallyHidden Title renders at 1x1 (clipped) and the dialog's aria-labelledby resolves to it, so screen-reader users get the name while sighted layout is identical.

CI parity (local)

  • tsc --noEmit clean
  • lint: no lint script (CI skips it, matching main)
  • next build succeeds (all 9 static pages generated)
  • npm ci --dry-run confirms lockfile in sync

🤖 Generated with Claude Code

Harden the shared matrix Dialog primitive (Gap / Capture / Export) so the
accessible Title is guaranteed present in the accessibility tree regardless
of how the project's `sr-only` utility is defined. Replace the
`<RadixDialog.Title className="sr-only">` with Radix's own recommended
`<VisuallyHidden asChild><RadixDialog.Title>` pattern, which is the wrapper
Radix points to in its DialogContent-requires-a-Title warning.

The Title stays visually hidden (1x1, clipped) so there is zero layout or
behavior change: focus still moves in, Tab stays trapped, Escape closes, and
focus restores to the trigger. Verified empirically in-browser across all
three modals (0 console errors / warnings, names announced, behavior intact).

Adds @radix-ui/react-visually-hidden as a direct dependency (it was already
present transitively via react-dialog).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@build-with-dhiraj
build-with-dhiraj merged commit dd081f9 into main Jun 23, 2026
1 check passed
@build-with-dhiraj
build-with-dhiraj deleted the fix/dialog-title-a11y branch June 23, 2026 02:15
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