Files
atomic-design-poc/docs/backlog/WP-16-component-a11y.md
Edwin van den Houdt f769242f76 docs: replace SHOWCASE-ROADMAP.md with docs/backlog/ (17 WPs)
Turns the prior roadmap sketch into ordered, gated work packages (enforcement
gates, FP/DDD consistency, CIBG fidelity, Storybook curriculum, a11y) from the
2026-07-02 showcase-hardening audit.
2026-07-02 17:07:59 +02:00

3.3 KiB

WP-16 — Component a11y: description wiring + alert role

Status: todo Phase: 4 — a11y

Why

Audit findings axe can't (fully) catch:

  • form-field renders a description <div [id]="fieldId()+'-desc'"> that no control ever referencestext-input sets aria-describedby only to …-error and only when invalid. Screen readers never announce field descriptions (e.g. the BSN hint on the login form).
  • The field↔control id pairing is manual (fieldId must equal the input's name/id) with nothing enforcing it.
  • The alert atom is always role="status" — error alerts are announced politely while other errors in the app use role="alert"; urgency is inconsistent.

Read first

  • src/app/shared/ui/form-field/form-field.component.ts (~line 15)
  • src/app/shared/ui/text-input/text-input.component.ts (~lines 17-18)
  • src/app/shared/ui/radio-group/radio-group.component.ts, checkbox/checkbox.component.ts
  • src/app/shared/ui/alert/alert.component.ts
  • src/app/auth/ui/login-form/login-form.component.ts (a live desc that's never wired)

Decisions (pre-made, don't relitigate)

  • aria-describedby = space-joined ids: -desc always when a description exists + -error when invalid; order pinned (desc first, error second).
  • Pairing contract: form-field exposes its fieldId; the composition contract (fieldId === control id/name) is documented in both components and enforced by a story play test on the canonical form-field+text-input composition (expect(input).toHaveAttribute('aria-describedby', 'x-desc'), flip validity, assert 'x-desc x-error'). Play tests run in the WP-01 test-runner for free.
  • DI-based auto-wiring (form-field providing the id via injection) is out of scope — more clever than this POC needs; the play test catches drift. Revisit only if the manual contract actually breaks in practice.
  • Alert: type === 'error'role="alert"; others keep role="status". Rationale comment in the atom.

Files

  • form-field.component.ts, text-input.component.ts, radio-group.component.ts, checkbox.component.ts (describedby joins; only where the component takes a hint)
  • alert.component.ts (+ story asserting the role per variant)
  • form-field.stories.ts (or a composition story) with the play test
  • Call sites that pass descriptions (verify login-form BSN hint is now announced)

Steps

  1. Implement the describedby join in the input atoms; form-field renders -desc only when a description input is set (it already does — verify).
  2. Write the play tests (form-field composition + alert roles).
  3. Alert role switch + rationale comment.
  4. Manual screen-reader spot check (optional but recommended — note result here).

Acceptance criteria

  • Description text is programmatically associated in the canonical composition; login-form BSN hint announced.
  • -error id appended exactly when invalid; order stable.
  • Error alerts are role="alert"; play tests assert both behaviors and run in the CI gate.

Verification

GREEN + npm run test-storybook:ci (includes the new play tests).

Out of scope

Route-change focus and template lint (WP-17); rewriting form-field's layout.

Risks

aria-describedby churn on validity flips re-announcing content — pinned order + play test coverage keeps it deterministic.