docs: archive the finished backlogs (RD-30)
Two backlog trees are complete: `docs/project/backlog/` (75 files, every WP done) and `docs/project/refactor-backlog-setup/` (the arc before it). Move both under `docs/project/archive/` with `git mv`, so history stays intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them, because it points at the now-archived backlog README. Add `docs/project/archive/README.md`. It states that these trees are historical and names the two directories that are still live. Repoint every inbound reference named in RD-30's Files table: CLAUDE.md, the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the `document-feature` and `new-ssp` skills, and the readable-codebase PLAN, README, and RD-19 ticket. Fix two upward-relative links inside the moved WP files (WP-68, WP-69) that gained a directory level and would otherwise break. Repoint `.prettierignore`'s two agent-prompt exclusions to their new path, so prettier keeps leaving those files' exact wording alone. Mark RD-30 done and check off its acceptance criteria; flip its README row to done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# WP-16 — Component a11y: description wiring + alert role
|
||||
|
||||
Status: done (pending commit)
|
||||
Phase: 4 — a11y
|
||||
|
||||
## Why
|
||||
|
||||
Audit findings axe can't (fully) catch:
|
||||
|
||||
- `form-field` renders a description `<div [id]="fieldId()+'-desc'">` that **no control
|
||||
ever references** — `text-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
|
||||
|
||||
- [x] Description text is programmatically associated in the canonical composition;
|
||||
login-form BSN hint announced.
|
||||
- [x] `-error` id appended exactly when invalid; order stable.
|
||||
- [x] 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.
|
||||
Reference in New Issue
Block a user