Files
atomic-design-poc/docs/backlog/WP-17-app-a11y.md
Edwin van den Houdt e272869f00 feat(fp): WP-17 — app-level a11y: route focus, template lint, WCAG checklist
Adds route-change focus management (new page's h1, afterNextRender) plus
scroll-position restoration wired once in app.config.ts; angular-eslint's
templateAccessibility bundle linting every inline template via
processInlineTemplates (verified firing with a planted violation, one real
hit fixed in rich-text-editor); docs/wcag-checklist.md and Foundations/
Accessibility MDX tying the four a11y layers (axe, lint, play tests,
manual checklist) together. The checklist pass already earned its keep —
it found a real 320px overflow in aanvraag-block's warning alert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 08:26:01 +02:00

4.9 KiB
Raw Permalink Blame History

WP-17 — App-level a11y: route focus/scroll, template lint, WCAG checklist + MDX

Status: done (pending commit) Phase: 4 — a11y

Why

Three app-level gaps close the WCAG story:

  • No route-change focus/scroll managementapp.config.ts has only provideRouter(routes, withViewTransitions()); after navigation, focus stays wherever it was and scroll position is unmanaged. (The wizard manages focus within steps; the skip link is the only cross-page mechanism.)
  • No template a11y linting@angular-eslint is entirely absent.
  • User decision: a manual WCAG checklist documents what automation can't test.

Read first

  • src/app/app.config.ts
  • eslint.config.mjs, package.json
  • src/app/shared/layout/shell/shell.component.ts (#main target), page-shell/page-shell.component.ts (candidate focus target)

Decisions (pre-made, don't relitigate)

  • Scroll: withInMemoryScrolling({ scrollPositionRestoration: 'enabled' }).
  • Focus: on NavigationEnd, move focus to the new page's h1 (or #main fallback, tabindex="-1"), deferred via afterNextRender so it coexists with view transitions; skip the initial load. Implement as a small root service/effect wired in app.config.ts — not per page.
  • Lint: angular-eslint at the version matching Angular 22 (~v21+ — check npm view angular-eslint peerDependencies), with processInlineTemplates (this repo has no .html templates). Rules: alt-text, label-has-associated-control, interactive-supports-focus, click-events-have-key-events, valid-aria, elements-content. Verify the processor fires by planting a known-bad pattern before trusting green.
  • The checklist is a living doc with per-page status columns — not prose.

Files

  • src/app/app.config.ts + a new small shared/ focus-on-navigation service
  • eslint.config.mjs, package.json (angular-eslint)
  • Any template the new lint rules flag
  • New docs/wcag-checklist.md
  • New src/docs/a11y.mdx — title Foundations/Accessibility

Steps

  1. Router scroll + focus service; manual keyboard smoke (tab through, navigate, confirm focus lands on the new page's heading).
  2. Add angular-eslint; enable the template rules on inline templates; plant a bad pattern, see it fail, remove it; fix real hits.
  3. docs/wcag-checklist.md: manual checks per page (dashboard, wizards, brief, login) — keyboard walk & focus order, no traps, 200% zoom/reflow, NVDA or VoiceOver pass, visible focus, error announcement; status columns (page × check).
  4. src/docs/a11y.mdx: the layered approach — axe gate (WP-01) + template lint + play tests (WP-16) + manual checklist; the alert-role rationale; where the a11y.disable skip register lives and its rules.

Acceptance criteria

  • Navigating between routes moves focus to the new page's heading; scroll resets; view transitions still play.
  • Template a11y rules active and proven to fire; lint green.
  • Checklist checked in with an initial pass filled in for the dashboard at minimum.
  • a11y.mdx renders; links to checklist and WP-01 skip rules.

Deviation from the original plan

  • Used the official angular.configs.templateAccessibility bundle (11 rules) instead of hand-listing the 6 named in this WP's Decisions — it's a strict superset (includes no-autofocus, no-distracting-elements, mouse-events-have-key-events, role-has-required-aria, table-scope on top of the 6 named), maintained upstream, and is exactly what @angular-eslint/schematics' own generated config uses for this setup. Less code to hand-maintain, same coverage plus more.
  • The dashboard's checklist pass surfaced a real bug: aanvraag-block's warning app-alert (two app-button actions) overflows the viewport at 320px — its .feedback flex row doesn't wrap. Documented in docs/wcag-checklist.md with the root cause, not fixed — fixing live component CSS found via the checklist is the "full manual audit" scope this WP's Out-of-scope section explicitly defers, not this WP's own deliverable. Flagged here so it isn't lost.
  • "Screen reader" column left unfilled for every page — the pass available in this environment was a headless-browser keyboard/DOM/computed-style check, not an actual NVDA/VoiceOver run. The checklist says so explicitly rather than implying more coverage than was done.

Verification

GREEN + npm run test-storybook:ci. Manual: keyboard-only walk of login → dashboard → wizard → brief.

Out of scope

Full manual audit of every page (the checklist enables it; filling it beyond the dashboard is ongoing work, not this WP).

Risks

  • angular-eslint's inline-template processor silently not running → the planted-violation step is mandatory.
  • Focus-move vs withViewTransitions timing → afterNextRender + test in a real browser (npm start), not just unit-level.