feat(a11y): make the app usable from a keyboard and a screen reader

eslint-plugin-jsx-a11y joins the lint step and found 19 violations on
arrival - 15 orphaned labels, a clickable div, a combobox missing its
required attributes, an autoFocus. All fixed, none suppressed, and the
rule set stays enforcing so the next one fails CI.

The two hand-rolled overlays were the worst of it. The notification bell
was a div with div-onClick rows: nothing focusable, nothing announced, no
way to close it from the keyboard. It is a real popover now - the
trigger reports its state and the unread count, the panel is a labelled
dialog, every row is a button with its actions beside it rather than
nested inside it, Escape closes and returns focus. The Challenge Friend
overlay had no role, no focus trap and no Escape, so Tab went straight
through it into the page behind; it is the UI kit's Dialog now, which
supplies all of that.

Every form control has a label, including the three change-password
fields that had only placeholders and a game-night join-password field
that had nothing at all. Every icon-only button says what it does,
including three versus buttons whose content collapsed to a bare spinner
while loading. The search combobox follows the ARIA 1.2 pattern, with
the role on the input; verified live that aria-expanded flips and
aria-controls resolves to the listbox.

State that changed silently is announced: link validation, both lobby
countdowns (assertively - a 3-2-1 that launches the game is not
something to miss), the game-night player list. The game header's
"Passed Movie B" read identically in both states and only the icon
differed, so a screen reader heard the wrong thing until B was reached.

A skip-to-content link on every page, and a global reduced-motion rule
that collapses every animation when the OS asks; the confetti overlay
does not render at all under it.

Addresses audit section 2.8.
This commit is contained in:
TehRiehlDeal committed 2026-09-11 18:45:06 -07:00
1 parent e67692006f
commit 889cdda83f
25 files changed
+2201 -170

No files matched your search

+13 -6
View File
@@ -25,8 +25,11 @@ export default function ValidationFeedback() {
if (isValidating) {
return (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
<div
role="status"
className="flex items-center gap-2 text-sm text-muted-foreground"
>
<Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
Validating connection...
</div>
);
@@ -36,9 +39,10 @@ export default function ValidationFeedback() {
return (
<div
key={validationError}
className="flex animate-shake items-center gap-2 text-sm text-destructive"
role="alert"
className="flex animate-shake items-center gap-2 text-sm text-destructive motion-reduce:animate-none"
>
<AlertCircle className="h-4 w-4" />
<AlertCircle className="h-4 w-4" aria-hidden="true" />
{validationError}
</div>
);
@@ -46,8 +50,11 @@ export default function ValidationFeedback() {
if (showCheck) {
return (
<div className="flex animate-check-flash items-center gap-2 text-sm text-green-600">
<CheckCircle2 className="h-4 w-4" />
<div
role="status"
className="flex animate-check-flash items-center gap-2 text-sm text-green-600 motion-reduce:animate-none"
>
<CheckCircle2 className="h-4 w-4" aria-hidden="true" />
Connected!
</div>
);