Wires text-input's aria-describedby to the form-field description div (the BSN hint was rendered but never announced), pins desc-before-error ordering, and switches alert to role=alert for errors vs role=status for info/ok/warning. Composition contract enforced by story play tests (form-field+text-input, alert per variant) run in the WP-01 CI gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
4.1 KiB
WP-16 — Component a11y: description wiring + alert role
Status: done (pending commit) Phase: 4 — a11y
Why
Audit findings axe can't (fully) catch:
form-fieldrenders a description<div [id]="fieldId()+'-desc'">that no control ever references —text-inputsetsaria-describedbyonly to…-errorand 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 (
fieldIdmust equal the input'sname/id) with nothing enforcing it. - The
alertatom is alwaysrole="status"— error alerts are announced politely while other errors in the app userole="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.tssrc/app/shared/ui/alert/alert.component.tssrc/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:-descalways when a description exists +-errorwhen 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 keeprole="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
- Implement the describedby join in the input atoms; form-field renders
-desconly when a description input is set (it already does — verify). - Write the play tests (form-field composition + alert roles).
- Alert role switch + rationale comment.
- 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.
-errorid appended exactly when invalid; order stable.- Error alerts are
role="alert"; play tests assert both behaviors and run in the CI gate.
Deviation from the original plan
radio-group/checkbox were left unchanged — grepping every call site found zero
consumers pairing either with a form-field description (only the login-form BSN
field, which uses text-input). The WP's own Files note ("describedby joins; only
where the component takes a hint") already carved out this exact case — adding
hasDescription to atoms with no live description consumer would be unused surface,
not a fix. radio-group already had correct -error-only wiring; untouched.
describedBy() was added directly on text-input rather than factored into a shared
shared/kernel helper — one consumer, ~5 lines, not worth the indirection yet.
Manual screen-reader spot check (optional per the WP) skipped; the play test is the
enforced check going forward.
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.