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

103 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 management** — `app.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
- [x] Navigating between routes moves focus to the new page's heading; scroll resets;
view transitions still play.
- [x] Template a11y rules active and _proven_ to fire; lint green.
- [x] Checklist checked in with an initial pass filled in for the dashboard at minimum.
- [x] `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.