From f9090609801d52e6b47f7822e116acf0fec20e79 Mon Sep 17 00:00:00 2001 From: Sinduri Guntupalli Date: Sat, 6 Jun 2026 17:03:15 +0200 Subject: [PATCH 01/10] feat(a11y): add guardrails, tests, and accessibility infrastructure - Add `eslint-plugin-jsx-a11y` to catch ARIA violations at lint time - Add `tabIndex={-1}` to all `
` elements so the skip nav link correctly moves keyboard focus (not just scrolls) - Add weekly scheduled a11y scan workflow and skip-nav e2e test - Expand ACCESSIBILITY.md with six contributor personas, screen reader quick reference, SVG and Tooltip ARIA guidelines - Add device/OS and WCAG criterion fields to the accessibility issue template - Rework `ChallengeFilters` disclosure from `role="menu"` to `role="group"` with `aria-pressed` toggle buttons; use `useId()` for stable ARIA IDs - Add `loaded` field to `useDiscussionPosts` so consumers can distinguish loading from genuinely empty; settle immediately when IDs are missing - Replace `aria-live` wrapper in `DiscussionSection` with a focused sr-only status node that announces a concise count, not card content - Fix `CommunitySidebar` to gate the empty-state paragraph on `loaded`, removing the premature flash before discussion data arrives - Fix scroll hint on verification `
` to use `aria-label` directly
  instead of an unassociated sr-only span that screen readers skipped
- Add `aria-current="page"` comment to `NavLink` so future maintainers
  do not replace it with a plain `` and silently break navigation AT
- Add reduced-motion axe scan suite; fix flaky `waitForTimeout` to use
  `waitForLoadState`; add HTTP 200 guard before each axe scan
- Add regression tests covering all new hook and component behaviour

Signed-off-by: Sinduri Guntupalli 
---
 .github/ISSUE_TEMPLATE/accessibility.yml |   16 +-
 .github/workflows/a11y-scan.yml          |   32 +
 ACCESSIBILITY.md                         |  134 +-
 CLAUDE.md                                |   16 +
 e2e/smoke.spec.ts                        |   31 +
 eslint.config.js                         |   19 +-
 package-lock.json                        | 1807 +++++++++++++++++++++-
 package.json                             |    1 +
 public/sitemap.xml                       |   76 +-
 src/components/ChallengeFilters.tsx      |   22 +-
 src/components/CommunitySidebar.tsx      |    6 +-
 src/components/DiscussionSection.tsx     |   12 +-
 src/components/NavLink.tsx               |    3 +
 src/hooks/useDiscussionPosts.ts          |   14 +-
 src/pages/About.tsx                      |    2 +-
 src/pages/Accessibility.tsx              |    2 +-
 src/pages/AdventureDetail.tsx            |    2 +-
 src/pages/Adventures.tsx                 |    2 +-
 src/pages/BrandGuidelines.tsx            |    2 +-
 src/pages/ChallengeDetail.tsx            |    4 +-
 src/pages/Challenges.tsx                 |    2 +-
 src/pages/CommunityGuide.tsx             |    2 +-
 src/pages/Contribute.tsx                 |    2 +-
 src/pages/Index.tsx                      |    2 +-
 src/pages/NotFound.tsx                   |    2 +-
 src/pages/Privacy.tsx                    |    2 +-
 src/pages/Sponsors.tsx                   |    2 +-
 src/test/discussionSection.test.tsx      |   34 +-
 src/test/navbar.test.tsx                 |   14 +
 src/test/useClickTracking.test.tsx       |    1 +
 src/test/useDiscussionPosts.test.ts      |   26 +-
 31 files changed, 2199 insertions(+), 93 deletions(-)
 create mode 100644 .github/workflows/a11y-scan.yml

diff --git a/.github/ISSUE_TEMPLATE/accessibility.yml b/.github/ISSUE_TEMPLATE/accessibility.yml
index de3ac157..c704257f 100644
--- a/.github/ISSUE_TEMPLATE/accessibility.yml
+++ b/.github/ISSUE_TEMPLATE/accessibility.yml
@@ -68,11 +68,18 @@ body:
       description: For example, NVDA 2024.4, VoiceOver on macOS 15, JAWS 2024, Windows Magnifier, Dragon NaturallySpeaking. Leave blank if not applicable.
       placeholder: VoiceOver on macOS 15.1
 
+  - type: input
+    id: device-os
+    attributes:
+      label: Device and operating system
+      description: For example, Windows 11, macOS 15, iPhone iOS 18, Android 14.
+      placeholder: macOS 15.1
+
   - type: input
     id: browser
     attributes:
       label: Browser and version
-      placeholder: Firefox 132 on Windows 11
+      placeholder: Firefox 132
     validations:
       required: true
 
@@ -89,6 +96,13 @@ body:
     validations:
       required: true
 
+  - type: input
+    id: wcag-criterion
+    attributes:
+      label: WCAG success criterion (if known)
+      description: Optional. For example, "1.4.3 Contrast (Minimum)" or "2.1.1 Keyboard". Helps us triage faster but is not required.
+      placeholder: 2.1.1 Keyboard
+
   - type: textarea
     id: extra
     attributes:
diff --git a/.github/workflows/a11y-scan.yml b/.github/workflows/a11y-scan.yml
new file mode 100644
index 00000000..517b2f90
--- /dev/null
+++ b/.github/workflows/a11y-scan.yml
@@ -0,0 +1,32 @@
+name: Scheduled Accessibility Scan
+
+on:
+  schedule:
+    # Every Monday at 08:00 UTC
+    - cron: "0 8 * * 1"
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+env:
+  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+  scan:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: actions/setup-node@v4
+        with:
+          node-version-file: .nvmrc
+          cache: npm
+
+      - run: npm ci
+
+      - run: npm run build
+
+      - run: npx playwright install --with-deps chromium
+
+      - run: npm run test:e2e
diff --git a/ACCESSIBILITY.md b/ACCESSIBILITY.md
index bb919b11..1552ac1a 100644
--- a/ACCESSIBILITY.md
+++ b/ACCESSIBILITY.md
@@ -57,14 +57,106 @@ Automated axe passes are necessary but not sufficient. Automated tools catch rou
 
 ### Manual
 
-For every UI change, contributors must verify:
+For every UI change, step through each persona below and verify the listed checks. These are the failure modes axe-core cannot catch. A change is not done until all six personas pass.
 
-1. **Keyboard-only navigation:** tab through the entire changed flow without a mouse. Tab order matches the visual reading order. No focus traps. Every interactive element is reachable and operable.
-2. **Focus visibility:** focus ring is visible on every interactive element in both light and dark mode.
-3. **Screen reader:** spot-check the changed flow with VoiceOver (macOS/iOS) or NVDA (Windows). Dynamic content updates are announced correctly.
-4. **Zoom:** the page works at 200% browser zoom without content clipping or layout breakage.
-5. **Viewports:** verify at 375px, 768px, and 1280px widths against the production build.
-6. **Windows High Contrast Mode:** interactive states (hover, focus, disabled) are visible. Do not rely solely on background-color or semi-transparent borders to communicate state.
+#### 1. Screen Reader Navigator (non-visual)
+
+Simulates a user who navigates entirely by audio output.
+
+- Spot-check the changed flow with VoiceOver (macOS/iOS) or NVDA (Windows). See the screen reader quick reference below.
+- Every interactive element is announced with its role and accessible name.
+- Dynamic content updates (filters, route changes, consent state) are announced without forcibly moving focus.
+- Heading order matches the visual reading order with no skipped levels.
+- `aria-current="page"` is set on the active nav link.
+- No vague or duplicate link text ("view", "read more", "click here" alone).
+
+#### 2. Power Keyboard User (motor limit)
+
+Simulates a user who operates the page with Tab, Shift+Tab, Enter, Space, and arrow keys only.
+
+- Tab through the entire changed flow without a mouse. Every interactive element is reachable.
+- Tab order follows a logical visual reading order.
+- No focus traps. Every trap-like UI (modal, dropdown) has a working Escape exit.
+- Focus ring is visible on every interactive element in both light and dark mode.
+- After dynamic changes (filter apply, modal close, route change), focus lands on a logical element — not reset to the top of the page.
+- Skip link is the first focusable element and is visible on focus.
+
+#### 3. Magnification Expert (low vision)
+
+Simulates a user navigating with browser zoom at 200% and 400%.
+
+- At 400% zoom: no horizontal scrolling required. Content reflows into a single column (WCAG 1.4.10).
+- Text is not truncated or clipped at high zoom.
+- No sticky/fixed elements that consume more than half the viewport height at 400%.
+- Focus ring remains clearly visible at high zoom.
+- Verify at 375px, 768px, and 1280px widths against the production build.
+- Windows High Contrast Mode: interactive states (hover, focus, disabled) are visible. Do not rely solely on `background-color` or semi-transparent borders.
+
+#### 4. Cognitive Strategist (neurodivergent)
+
+Simulates a user sensitive to clutter, inconsistent UI, and unpredictable behaviour.
+
+- Page title clearly reflects where the user is in the site.
+- Multi-step flows (e.g. challenge instructions) have a visible indication of progress or position.
+- UI controls are consistent: the same action uses the same element and label across all pages.
+- No auto-advancing UI (carousels, auto-dismiss toasts) that interrupts reading without user intent.
+- CTA labels clearly state the outcome ("Start the Beginner level" not "Go").
+- Error states identify the problem in plain language and suggest a fix — no technical "fail" messages.
+
+#### 5. Vestibular User (motion sensitivity)
+
+Simulates a user for whom animations can trigger motion sickness.
+
+- Every animation and transition in the changed code is gated by `@media (prefers-reduced-motion: no-preference)` in CSS, or by `window.matchMedia('(prefers-reduced-motion: reduce)')` in JS.
+- No parallax or scroll-linked motion effects.
+- No auto-playing animations triggered without user intent.
+- No content that flashes more than three times per second (WCAG 2.3.1).
+
+#### 6. Distracted / Fatigued User (situational limit)
+
+Simulates a user navigating under high cognitive load.
+
+- System status is clear after every action: the user knows whether something succeeded, is loading, or failed.
+- Consent and theme state are visually obvious at a glance (the banner or the toggle reflect the current state).
+- No session timeouts on this static site — confirm no new timed behaviour has been introduced.
+- The purpose of every CTA is unambiguous without surrounding context.
+
+### Screen reader quick reference
+
+Use these commands to run through a changed flow without a full-session setup.
+
+#### VoiceOver on macOS (VO = Caps Lock or Ctrl+Option)
+
+| Task | Keys |
+|---|---|
+| Toggle VoiceOver on/off | Cmd+F5 |
+| Read next / previous item | VO+→ / VO+← |
+| Navigate to next heading | VO+Cmd+H (and Shift to go back) |
+| List all headings | VO+U, then select Headings |
+| List all landmarks | VO+U, then select Landmarks |
+| List all links | VO+U, then select Links |
+| Activate a link or button | VO+Space |
+| Enter / exit a web area | VO+Shift+↓ / VO+Shift+↑ |
+| Stop speaking | Ctrl |
+
+#### NVDA on Windows (NVDA key = Insert by default)
+
+| Task | Keys |
+|---|---|
+| Toggle NVDA on/off | Ctrl+Alt+N (installer default) |
+| Read next / previous item | ↓ / ↑ (browse mode) |
+| Navigate to next heading | H (and Shift+H to go back) |
+| Navigate to next landmark | D (and Shift+D to go back) |
+| List elements dialog | NVDA+F7 |
+| Activate a link or button | Enter |
+| Toggle browse/focus mode | NVDA+Space |
+| Stop speaking | Ctrl |
+
+**What to verify in every changed flow:**
+1. Every interactive element is announced with its role (button, link, etc.) and accessible name.
+2. Dynamic content updates (state changes, filter results) are announced without forcibly moving focus.
+3. No phantom tab stops appear (e.g. from SVGs missing `focusable="false"` or decorative elements missing `aria-hidden`).
+4. Heading order matches the visual reading order.
 
 ---
 
@@ -172,6 +264,21 @@ Apply this to every component you write or modify.
 - Never use raw Unicode characters (`→`, `♥`, `✓`) to convey meaning.
 - Decorative separators between pill segments: use an empty `