The folder now equals the layer, as CLAUDE.md decision 2 requires. 33 directories move by git mv (25 flat, plus upload/'s 8 subfolders split across all three layers). 28 distinct @shared/ui/* specifiers rewrite across 73 files, longest-first. Five relative imports inside upload/ become @shared/ui aliases because their sibling now lives in a different layer; two stay relative because both ends stay in the same layer. Four .mdx docs get their seven broken story imports fixed; atomic-design.mdx's page-shell import is untouched, because layout/ does not move. No component, template, story title, or layer-tag comment changes. That is RD-28's job. Verified against the ticket's acceptance commands: the 26 flat directories become exactly 3 layer folders with the counts the ticket names, only three @shared/ui/* prefixes remain (atoms, molecules, organisms), the .mdx import count holds at 7, and the relative-import count inside ui/ drops from 7 to 2 as decision 4 requires. The @shared/ui/ occurrence count moves from 200 to 205: decision 4 mandates turning 5 of those 7 relative imports into @shared/ui/* aliases, which decision 3's "200 before, 200 after" check does not account for. The 5-occurrence gap is exactly the 5 conversions decision 4 names, not a lost or duplicated specifier. npm run ci --full passes: lint, typecheck, dep:check, format, tokens, seam, both apps' + both libraries' tests, both apps' localized build, audit, backend tests, all three generated-artifact drift checks, and both Storybook instances' build + axe-core a11y suite (67+45 suites, 198+112 tests, all green). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
70 lines
3.9 KiB
Plaintext
70 lines
3.9 KiB
Plaintext
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
|
|
import * as AlertStories from '../src/ui/atoms/alert/alert.stories';
|
|
import * as FormFieldStories from '../src/ui/molecules/form-field/form-field.stories';
|
|
|
|
<Meta title="Foundations/Accessibility" />
|
|
|
|
# Accessibility
|
|
|
|
No single tool catches every a11y class of bug, so this repo layers four, each catching
|
|
what the ones below/above it can't.
|
|
|
|
## The layers
|
|
|
|
1. **Axe on every story** — `@storybook/addon-a11y` in the panel, plus
|
|
`@storybook/test-runner` + `axe-playwright` gating CI (`npm run test-storybook:ci`).
|
|
Catches structural/contrast/ARIA-shape violations on every component, automatically,
|
|
as soon as a story exists. Escape hatch: `parameters: { a11y: { disable: true } }`,
|
|
only with an inline justification comment + a cross-reference to the WP that will fix
|
|
it (see e.g. `task-list.stories.ts`).
|
|
2. **Template a11y lint** — `angular-eslint`'s `templateAccessibility` config
|
|
(`alt-text`, `label-has-associated-control`, `click`/`mouse-events-have-key-events`,
|
|
`interactive-supports-focus`, `valid-aria`, `no-autofocus`, …) running on every inline
|
|
template via `angular.processInlineTemplates` (this repo has no `.html` files — every
|
|
template is a string in the `@Component` decorator; the processor extracts each one
|
|
into a virtual file the template rules can lint). Catches missing alt text, unlabelled
|
|
controls, and interactive elements that can't be reached by keyboard — at lint time,
|
|
before a story even exists.
|
|
3. **Play tests** — Storybook stories assert the wiring axe/lint can't see:
|
|
`form-field.stories.ts`'s canonical composition asserts `aria-describedby` joins
|
|
`-desc`/`-error` in the right order; `alert.stories.ts` asserts `role="alert"` for
|
|
errors vs `role="status"` for info/ok/warning. These run as part of the same
|
|
`test-storybook:ci` gate as the axe checks, so a regression fails CI, not just a panel.
|
|
4. **Manual WCAG checklist** (`docs/reference/wcag-checklist.md`) — what none of the above can see:
|
|
tab order across a whole page, focus traps, 200%-zoom reflow, and how a real screen
|
|
reader narrates a flow. A living per-page checklist, not a one-time audit — it already
|
|
caught a real bug (a dashboard alert overflowing at 320px) that no automated layer here
|
|
would have flagged.
|
|
|
|
## Component wiring this protects
|
|
|
|
<Canvas of={FormFieldStories.WithDescriptionAndError} />
|
|
|
|
The description (`-desc`) and error (`-error`) ids are joined in a pinned order so a
|
|
screen reader announces the hint, then the error, never neither. See
|
|
`text-input.component.ts`'s `describedBy()`.
|
|
|
|
<Canvas of={AlertStories.Error} />
|
|
|
|
Errors are `role="alert"` (assertive — interrupts, because the user needs to know
|
|
_now_); info/ok/warning stay `role="status"` (polite) so they don't interrupt whatever
|
|
the user is doing. See `alert.component.ts`.
|
|
|
|
## Route-change focus
|
|
|
|
Client-side routing has no page (re)load, so a screen reader/keyboard user's focus stays
|
|
wherever it was — usually the link they just clicked, now detached from any content that
|
|
matters. `shared/layout/route-focus.ts` moves focus to the new page's `<h1>` (every page
|
|
has exactly one via `page-shell`) on every navigation after the initial load, deferred via
|
|
`afterNextRender` so it doesn't race the view-transition DOM swap. Scroll position resets
|
|
the same way (`withInMemoryScrolling`), both wired once in `app.config.ts` — not per page.
|
|
|
|
## Where the skip register lives
|
|
|
|
`npm run lint` fails the build on a real template a11y violation, and `test-storybook:ci`
|
|
fails it on a real axe violation. Both can be locally disabled — the lint rule via a
|
|
normal ESLint disable comment, axe via `parameters: { a11y: { disable: true } }` — but
|
|
only with a comment naming _why_ and a cross-reference to the WP expected to remove the
|
|
skip (see `docs/project/backlog/WP-13-cibg-gap-register.md`'s marker convention, reused here).
|
|
Grep `a11y: { disable: true }` in `*.stories.ts` for the current list.
|