ultra11y 2.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/docker/Dockerfile +7 -0
- package/docker/package.json +9 -0
- package/docker/runner.mjs +23 -0
- package/package.json +73 -0
- package/scripts/bench.mjs +151 -0
- package/scripts/build-act-corpus.mjs +143 -0
- package/scripts/build-adjudication.mjs +503 -0
- package/scripts/build-pack-rgaa.mjs +397 -0
- package/scripts/build-standards.mjs +548 -0
- package/scripts/copy-bundle.mjs +39 -0
- package/scripts/import-pack.mjs +107 -0
- package/scripts/sync-engine.mjs +84 -0
- package/scripts/sync-version.mjs +43 -0
- package/scripts/ultra11y.mjs +52739 -0
- package/scripts/vendor/act-testcases.json +12297 -0
- package/scripts/vendor/rgaa/SOURCE.json +8 -0
- package/scripts/vendor/rgaa/criteres.json +4144 -0
- package/scripts/vendor/rgaa/glossaire.json +480 -0
- package/scripts/vendor/wcag-2.2-fr.json +82 -0
- package/scripts/vendor/wcag-2.2-sc-universe.json +950 -0
- package/scripts/vendor/wcag-2.2-sc.json +574 -0
- package/scripts/verify-rgaa-source.mjs +126 -0
- package/scripts/verify-skill-bundle.mjs +91 -0
- package/skills/review-a11y/SKILL.md +123 -0
- package/skills/review-a11y/scripts/ultra11y.mjs +52739 -0
- package/skills/ultra11y/SKILL.md +292 -0
- package/skills/ultra11y/references/act.md +163 -0
- package/skills/ultra11y/references/adjudication.md +599 -0
- package/skills/ultra11y/references/audit.md +100 -0
- package/skills/ultra11y/references/authoring.md +37 -0
- package/skills/ultra11y/references/automation.md +113 -0
- package/skills/ultra11y/references/correction.md +41 -0
- package/skills/ultra11y/references/criteria.md +129 -0
- package/skills/ultra11y/references/cross-file.md +82 -0
- package/skills/ultra11y/references/dynamic.md +152 -0
- package/skills/ultra11y/references/false-positives.md +81 -0
- package/skills/ultra11y/references/fix.md +46 -0
- package/skills/ultra11y/references/focus-and-logic.md +64 -0
- package/skills/ultra11y/references/forbidden-patterns.md +61 -0
- package/skills/ultra11y/references/forms-and-errors.md +147 -0
- package/skills/ultra11y/references/guidance.md +55 -0
- package/skills/ultra11y/references/judgment.md +90 -0
- package/skills/ultra11y/references/media-and-motion.md +116 -0
- package/skills/ultra11y/references/methodology.md +72 -0
- package/skills/ultra11y/references/naming.md +117 -0
- package/skills/ultra11y/references/orchestration.md +44 -0
- package/skills/ultra11y/references/packs.md +193 -0
- package/skills/ultra11y/references/prd.md +104 -0
- package/skills/ultra11y/references/rendered.md +110 -0
- package/skills/ultra11y/references/scale.md +49 -0
- package/skills/ultra11y/references/standards.md +97 -0
- package/skills/ultra11y/references/structure.md +138 -0
- package/skills/ultra11y/references/verify.md +88 -0
- package/skills/ultra11y/references/widgets.md +106 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +52739 -0
- package/src/act.ts +441 -0
- package/src/adjudicate.ts +630 -0
- package/src/aria.ts +230 -0
- package/src/audit.ts +396 -0
- package/src/auditor.ts +399 -0
- package/src/autofill.ts +102 -0
- package/src/axe-map.ts +294 -0
- package/src/baseline.ts +75 -0
- package/src/capture.ts +182 -0
- package/src/check.ts +243 -0
- package/src/cli.ts +1927 -0
- package/src/color.ts +102 -0
- package/src/config.ts +182 -0
- package/src/crawl.ts +75 -0
- package/src/criteria.ts +183 -0
- package/src/data/adjudication.json +770 -0
- package/src/data/guidance/rgaa.json +2395 -0
- package/src/data/standards/rgaa.glossary.json +478 -0
- package/src/data/standards/rgaa.json +3454 -0
- package/src/data/wcag-universe.json +530 -0
- package/src/data/wcag.json +1178 -0
- package/src/discover.ts +203 -0
- package/src/fix/codemods.ts +76 -0
- package/src/fix/diff.ts +64 -0
- package/src/fix/edits.ts +126 -0
- package/src/fix.ts +215 -0
- package/src/gh.ts +214 -0
- package/src/glob.ts +206 -0
- package/src/graph/build.ts +114 -0
- package/src/graph/engine-resolve.ts +0 -0
- package/src/graph/graph.ts +126 -0
- package/src/graph/imports.ts +359 -0
- package/src/graph/resolve.ts +73 -0
- package/src/graph/tsconfig.ts +113 -0
- package/src/grounding.ts +119 -0
- package/src/guidance/index.ts +45 -0
- package/src/guidance/types.ts +36 -0
- package/src/init.ts +113 -0
- package/src/mcp/handlers.ts +388 -0
- package/src/mcp/http.ts +260 -0
- package/src/mcp/prompts.ts +183 -0
- package/src/mcp/protocol.ts +187 -0
- package/src/mcp/resources.ts +136 -0
- package/src/mcp/server.ts +228 -0
- package/src/mcp/stdio.ts +119 -0
- package/src/mcp/tools.ts +347 -0
- package/src/messages.ts +1011 -0
- package/src/name.ts +174 -0
- package/src/orchestrate-templates.ts +231 -0
- package/src/orchestrate.ts +186 -0
- package/src/output.ts +108 -0
- package/src/pack.ts +187 -0
- package/src/parse/html.ts +258 -0
- package/src/parse/jsx-ast.ts +67 -0
- package/src/parse/jsx-bridge.ts +247 -0
- package/src/parse/jsx.ts +19 -0
- package/src/parse/source.ts +75 -0
- package/src/prd.ts +406 -0
- package/src/project-lock.ts +42 -0
- package/src/render.ts +307 -0
- package/src/report.ts +399 -0
- package/src/rules/colors.ts +125 -0
- package/src/rules/cross-registry.ts +264 -0
- package/src/rules/cross-rule.ts +83 -0
- package/src/rules/forms.ts +590 -0
- package/src/rules/frames.ts +33 -0
- package/src/rules/headings.ts +227 -0
- package/src/rules/images.ts +200 -0
- package/src/rules/links.ts +193 -0
- package/src/rules/mandatory.ts +173 -0
- package/src/rules/multimedia.ts +77 -0
- package/src/rules/navigation.ts +190 -0
- package/src/rules/presentation.ts +75 -0
- package/src/rules/registry.ts +57 -0
- package/src/rules/rule.ts +122 -0
- package/src/rules/scripts-aria.ts +636 -0
- package/src/rules/tables.ts +264 -0
- package/src/rules/timing.ts +55 -0
- package/src/sample.ts +176 -0
- package/src/scan-local.ts +866 -0
- package/src/scan.ts +538 -0
- package/src/standards/derive.ts +195 -0
- package/src/standards/index.ts +31 -0
- package/src/standards/pack-rules.ts +111 -0
- package/src/standards/pack.ts +81 -0
- package/src/standards/registry.ts +100 -0
- package/src/standards/types.ts +202 -0
- package/src/standards/validate.ts +544 -0
- package/src/standards/vocabulary.ts +75 -0
- package/src/types.ts +355 -0
- package/src/util.ts +32 -0
- package/src/vendor/codeindex-engine.d.mts +1009 -0
- package/src/vendor/codeindex-engine.mjs +13701 -0
- package/src/vendor/engine.meta.json +9 -0
- package/src/verify.ts +275 -0
- package/src/wcag.ts +128 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Dynamic tier (axe-core) — optional
|
|
2
|
+
|
|
3
|
+
The static engine leaves some criteria "to assess" because they need a **render**: computed
|
|
4
|
+
contrast (1.4.3), focus visible (2.4.7), reflow/zoom (1.4.4/1.4.10), text spacing (1.4.12),
|
|
5
|
+
content on hover (1.4.13), target size (2.5.8). The dynamic tier decides them by running
|
|
6
|
+
**axe-core in a real headless browser** (Playwright). Two runtimes, same finding shape:
|
|
7
|
+
|
|
8
|
+
- **`--runtime local`** (recommended): resolves a host/target Playwright + `@axe-core/playwright`
|
|
9
|
+
**at runtime** from `--cwd` (no Docker, no global install). Most projects with Playwright
|
|
10
|
+
e2e tests already have both deps. Adds the residual-criteria **probes** (below).
|
|
11
|
+
- **`--runtime docker`**: runs axe-core in a Docker image auto-built on first use (runner +
|
|
12
|
+
Dockerfile embedded in the bundle). No host deps beyond Docker. Axe + 320px reflow only.
|
|
13
|
+
- **`--runtime auto`** (default): local if Playwright resolves from `--cwd`, else Docker, else
|
|
14
|
+
an actionable error.
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
- **local**: a project reachable from `--cwd` with `@playwright/test` + `@axe-core/playwright`
|
|
19
|
+
installed, and a Chromium browser (`npx playwright install chromium`).
|
|
20
|
+
- **docker**: Docker running.
|
|
21
|
+
|
|
22
|
+
The rest of the skill (static audit) needs neither.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
# auto runtime (local if available, else Docker)
|
|
28
|
+
node scripts/ultra11y.mjs scan https://example.com --json
|
|
29
|
+
|
|
30
|
+
# explicit local runtime, resolving deps + browser from a project (e.g. a monorepo package)
|
|
31
|
+
node scripts/ultra11y.mjs scan http://localhost:3000 --runtime local --cwd packages/app --json
|
|
32
|
+
|
|
33
|
+
# authenticated pages: pass a Playwright storageState JSON (cookies/localStorage)
|
|
34
|
+
node scripts/ultra11y.mjs scan http://localhost:3000/dashboard --runtime local \
|
|
35
|
+
--cwd packages/app --storage-state packages/app/test-results/.auth/user.json --json
|
|
36
|
+
|
|
37
|
+
# several explicit URLs at once (one browser, one context per page)
|
|
38
|
+
node scripts/ultra11y.mjs scan http://localhost:3000/ http://localhost:3000/login --runtime local --cwd packages/app
|
|
39
|
+
|
|
40
|
+
# merge with a static audit: the "to assess" criteria turn C/NC
|
|
41
|
+
node scripts/ultra11y.mjs audit "src/**/*.tsx" --jsx --out audits --json > /dev/null
|
|
42
|
+
node scripts/ultra11y.mjs scan http://localhost:3000 --runtime local --cwd packages/app --merge audits/audit-latest.json --out audits
|
|
43
|
+
node scripts/ultra11y.mjs report --in audits/audit-latest.json --out audits
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`--storage-state` is **local-only**. Combining it with an **explicit** `--runtime docker` (or
|
|
47
|
+
the `--docker` alias) is an unsupported combination and errors out (exit 2) — the Docker tier
|
|
48
|
+
cannot use a Playwright storageState, and scanning unauthenticated would silently defeat the
|
|
49
|
+
flag. Under `--runtime auto` that happens to fall back to Docker (no local Playwright
|
|
50
|
+
resolved), it degrades with a warning instead — you didn't ask for Docker specifically.
|
|
51
|
+
Produce the file with Playwright (e.g. an e2e auth setup that logs in and
|
|
52
|
+
`context.storageState({ path })`).
|
|
53
|
+
|
|
54
|
+
### Cover many pages (crawl)
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
# every URL listed in a sitemap.xml
|
|
58
|
+
node scripts/ultra11y.mjs scan --sitemap https://example.com/sitemap.xml --json
|
|
59
|
+
|
|
60
|
+
# BFS of served same-origin links, from an entry page
|
|
61
|
+
node scripts/ultra11y.mjs scan --crawl https://example.com --depth 2 --max 50 --json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Each finding keeps the **page** it came from (`--merge` reports that URL as `file`). `--crawl`
|
|
65
|
+
follows links in the **served HTML** (SSR/MPA); for a pure SPA, use `--sitemap` or pass the
|
|
66
|
+
URLs explicitly. The crawl fetch is unauthenticated — for authed pages pass explicit URLs +
|
|
67
|
+
`--storage-state` instead.
|
|
68
|
+
|
|
69
|
+
## What the dynamic tier adds
|
|
70
|
+
|
|
71
|
+
- **Real contrast (1.4.3)** — axe computes the rendered colours (the main win, both runtimes).
|
|
72
|
+
- **Reflow (1.4.10)** — no horizontal scroll at 320px wide (both runtimes).
|
|
73
|
+
- **Cross-check** — axe re-validates the structural criteria (alt, labels, ARIA, headings…) at
|
|
74
|
+
render; a render finding is **authoritative** and turns the criterion NC.
|
|
75
|
+
|
|
76
|
+
axe findings map to WCAG success criteria via a curated table (`axe-rule → SC`), completed by
|
|
77
|
+
axe's **native WCAG tags** (`wcag<abc>`). On merge (`--merge`), a `manual` criterion the tier
|
|
78
|
+
decides leaves the residual risks and becomes `C`/`NC` (ruleId `axe:<rule>` for axe findings).
|
|
79
|
+
|
|
80
|
+
### Residual-criteria probes (local runtime only)
|
|
81
|
+
|
|
82
|
+
Beyond axe, the local runtime runs bespoke Playwright probes for the criteria axe alone cannot
|
|
83
|
+
decide. Each raises a **definite NC only when the failure is observed** in the rendered page; a
|
|
84
|
+
clean probe leaves the SC `manual` (never silently conforming). Merged findings get a
|
|
85
|
+
`dyn-<engine>` ruleId.
|
|
86
|
+
|
|
87
|
+
| Probe | SC | How |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| focus visibility | 2.4.7 | Tab through focusables; flag any whose computed style (outline/box-shadow/border/background) is unchanged when focused |
|
|
90
|
+
| 200% zoom | 1.4.4 | Enlarge text to 200%; flag page-level horizontal scroll or text clipped in an `overflow:hidden` container |
|
|
91
|
+
| text spacing | 1.4.12 | Inject the WCAG 1.4.12 spacing override; flag clipped/truncated text |
|
|
92
|
+
| content on hover | 1.4.13 | For `aria-describedby` triggers whose target is hidden, hover to reveal then check it is dismissible (Escape) |
|
|
93
|
+
|
|
94
|
+
Visually-hidden (`clip`/1px sr-only) elements are excluded from these probes. **Target size
|
|
95
|
+
(2.5.8)** is intentionally left to axe-core's own `target-size` rule, which applies the inline
|
|
96
|
+
and 24px-spacing exceptions correctly (a hand-rolled probe was strictly noisier on real pages).
|
|
97
|
+
|
|
98
|
+
These probes are heuristic (conservative severities: focus + zoom `majeur`, the rest `mineur`)
|
|
99
|
+
and **local-only** — the Docker `RUNNER` is kept byte-identical to `docker/runner.mjs`
|
|
100
|
+
(`docker-sync` test), so mirroring the probes into the Docker path is deferred. Adversarially
|
|
101
|
+
verify probe findings (a `verify` pass) before filing them.
|
|
102
|
+
|
|
103
|
+
### Stateful interaction probes (local runtime, interactions ON by default)
|
|
104
|
+
|
|
105
|
+
The read-only probes above measure a page **as served**. Some non-conformities only appear
|
|
106
|
+
once the user has *interacted* — a filled field that overflows its cell, a status message
|
|
107
|
+
that never reaches a live region. The local runtime therefore also runs a **stateful** pass
|
|
108
|
+
that drives the page, then restores it. **Safety contract**: only NON-navigating actions are
|
|
109
|
+
performed — fill text inputs (a long representative value, respecting `maxlength`), toggle
|
|
110
|
+
checkbox/radio, click `button[type="button"]`. **Never** a link, a submit button, or a form
|
|
111
|
+
submit; every interaction records `location.href` first and aborts + restores if it changed;
|
|
112
|
+
every loop is bounded; original state is always restored.
|
|
113
|
+
|
|
114
|
+
| Stateful probe | SC | What it adds |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| fill-inputs → re-measure | 1.4.4 / 1.4.10 / 1.4.12 | fills visible text-like inputs with real content, then re-runs the zoom/reflow/spacing stress probes so an overflow that only occurs *when the field holds the value the auditor must type* is caught |
|
|
117
|
+
| live-region | 4.1.3 | triggers safe interactions and checks that a resulting status message lands in an `aria-live`/`role="status"`/`role="alert"` region (status-messages) — the extra SC `localTestedScs` reports only when interactions are on |
|
|
118
|
+
|
|
119
|
+
- **`--no-interact`** disables the whole stateful pass (fill + live-region), leaving only the
|
|
120
|
+
read-only probes — use it when even bounded, non-navigating interaction is unwelcome.
|
|
121
|
+
- **Authenticated-scan click policy.** When a `--storage-state` session is loaded, the
|
|
122
|
+
live-region probe does **not** click buttons by default (even a `type="button"` click can
|
|
123
|
+
trigger a server mutation the `location.href` assertion cannot see). Fill/toggle still run.
|
|
124
|
+
**`--interact-clicks`** re-enables the clicks explicitly; unauthenticated scans keep clicks
|
|
125
|
+
on. Defense-in-depth on top: a button whose accessible name matches a
|
|
126
|
+
destructive/submitting verb (delete, remove, send, submit, confirm, pay…) is **never**
|
|
127
|
+
clicked, in either mode.
|
|
128
|
+
|
|
129
|
+
## Scan a normative page sample (`scan --sample`)
|
|
130
|
+
|
|
131
|
+
A real country-standard audit runs over a declared **page sample** (échantillon), not one
|
|
132
|
+
URL. Declare it in `.ultra11yrc.json` under `sample.pages` (+ `transverse`), then:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
node scripts/ultra11y.mjs sample check # lint the sample's coverage vs the standard's required kinds
|
|
136
|
+
node scripts/ultra11y.mjs scan --sample --runtime local --cwd packages/app --merge audits/audit-latest.json --out audits
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`scan --sample` iterates every configured sample page (per-page `--storage-state` supported
|
|
140
|
+
for authenticated pages), keeps each finding's originating **page name + auth flag** as
|
|
141
|
+
provenance (surfaced in the auditor ticket's *Pages / URLs impactées* and *Contexte de
|
|
142
|
+
reproduction*), and `--merge`s them into the audit. `sample check` is an **advisory** lint —
|
|
143
|
+
it reports which required page kinds the sample lacks (a malformed `sample` block is a hard
|
|
144
|
+
error, exit 2; a merely-incomplete one is guidance, exit 0). See `references/audit.md`
|
|
145
|
+
(sample concept) and `references/packs.md` (`sampleMethodology`).
|
|
146
|
+
|
|
147
|
+
## Limits
|
|
148
|
+
|
|
149
|
+
Even with the local probes, **reading order**, **alt relevance** and the other judgment criteria
|
|
150
|
+
are the AI agent's to adjudicate (gated, `verify --manual`), not the dynamic tier's. The probes
|
|
151
|
+
reduce — but do not eliminate — the residual on 2.4.7/1.4.4/1.4.12/1.4.13/2.5.8: confirm a sample
|
|
152
|
+
on screen (optional human oversight). pa11y can be added as a second source if needed.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Known false-positive traps — what to scrutinise before trusting a finding
|
|
2
|
+
|
|
3
|
+
The static engine reads SOURCE, not the rendered DOM. On component-based code that
|
|
4
|
+
creates a few predictable false-positive (FP) classes. The engine now **auto-suppresses**
|
|
5
|
+
most of them; this page tells you (the agent) where it still cannot be sure, so you can
|
|
6
|
+
**confirm or refute** instead of trusting blindly. Run the refutation loop:
|
|
7
|
+
`verify --report <md> [--semantic]` → rule each item against the cited `file:line` →
|
|
8
|
+
`verify --apply <verdicts.json>` (refuted/unsupported entries fail the gate).
|
|
9
|
+
|
|
10
|
+
## How the engine already protects you
|
|
11
|
+
|
|
12
|
+
- **Components are skipped.** A PascalCase tag (`<Button>`, `<HistoryEntry>`) keeps its
|
|
13
|
+
case in JSX **and** in `.vue/.svelte/.astro` (parsed with case preserved). Single-file
|
|
14
|
+
rules only fire on intrinsic HTML tags, so a component child of `<ul>`, a `<Select>`,
|
|
15
|
+
or a `role` prop on `<UChatMessage>` is no longer mis-flagged.
|
|
16
|
+
- **Injected content is not "empty".** A `<slot>`, `{@render children()}`, `{children}`,
|
|
17
|
+
or a child component means the name/content/options may be supplied at runtime —
|
|
18
|
+
`empty-heading`, `button-empty-name`, `link-empty-name`, `select-has-option`,
|
|
19
|
+
`list-structure`, `data-table-no-headers` and `h1-missing` all bail instead of asserting.
|
|
20
|
+
- **Dynamic bindings/values count as present, never invalid.** `:aria-label`,
|
|
21
|
+
`v-bind:aria-label`, `bind:value`, `:alt`, `:scope` (Vue/Svelte/Alpine) are read as
|
|
22
|
+
"present, value unknown"; a dynamic `role={…}` is skipped (not parsed as a bogus role);
|
|
23
|
+
a `v-bind="…"` / `{...rest}` / `{shorthand}` spread suppresses missing-name/label/alt.
|
|
24
|
+
- **Cross-file references resolve under `--graph`.** An `aria-controls`/`for`/heading
|
|
25
|
+
target defined in another file (including a `const X = "id"; id={X}`) is proven to exist
|
|
26
|
+
and the finding is suppressed; an icon-only component whose definition supplies its own
|
|
27
|
+
`aria-label` is not flagged "unnamed". **Always run component code with `--graph`.**
|
|
28
|
+
- **Conditional branches & interleaved components aren't crossed.** Headings in different
|
|
29
|
+
arms of `{cond ? … : …}` are mutually exclusive, and a child component between two
|
|
30
|
+
headings may inject an intermediate one — so `heading-order-skip` won't compare across them.
|
|
31
|
+
- **Framework shell templates are recognized.** A SPA mount shell (`<div id="app"></div>`)
|
|
32
|
+
or a build placeholder (`%sveltekit.body%`/`%sveltekit.head%`, `{{ }}`, `<%= %>`) means
|
|
33
|
+
the real content/title is injected later — `h1-missing`/`title-missing-empty` don't fire.
|
|
34
|
+
- **Slot-projected required content is not "missing".** A `<slot>`/`{@render children()}`
|
|
35
|
+
passthrough suppresses `fieldset-legend-missing` and `aria-required-children`; `<th>` in a
|
|
36
|
+
`<thead>` carries implicit column scope.
|
|
37
|
+
- **Test/story markup is out of scope by default.** `*.test.*`, `*.spec.*`, `*.stories.*`
|
|
38
|
+
and `__tests__/` are excluded (bad-by-design fixtures). Re-include with
|
|
39
|
+
`--no-default-excludes` or by naming the file.
|
|
40
|
+
|
|
41
|
+
## Where the AI agent must still adjudicate
|
|
42
|
+
|
|
43
|
+
A finding's `preliminary: true` (in `--json`) and a `scope.sourceTemplate` / `scope.rendered`
|
|
44
|
+
caveat in the report mean **the verdict is provisional** — the AI agent confirms it against the
|
|
45
|
+
rendered output (rendering criteria go to `scan`), recording a verdict either way.
|
|
46
|
+
|
|
47
|
+
| Rule / situation | Why it may still be wrong | How to confirm or refute |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| Any finding on `.vue/.svelte/.astro` SOURCE | Slots, snippets, `{#if}`, `v-for`, scoped logic and provide/inject are invisible to the template parser | Audit the **rendered** HTML (Storybook/build output) or `scan` a live page; refute the source finding if the rendered DOM is conformant |
|
|
50
|
+
| Any finding on library-rendered JSX (DSFR/MUI…) | The component's produced HTML lives in `node_modules` | `render` → build/SSR → `audit` the output, or `scan` |
|
|
51
|
+
| `control-label-missing` in an SFC | The label may be wired via provide/inject or a sibling `<Label>` component the parser can't link | Read the form component + its field wrapper; refute if a programmatic label exists at render |
|
|
52
|
+
| `heading-order-skip` via `{#if}`/`v-if` in SFCs (no AST) | SFC conditionals are opaque text, so arms can't be separated like in JSX | Reason about each render path; flag only a skip that occurs in a single path |
|
|
53
|
+
| `contrast` (1.4.3), focus, zoom, reflow | Needs computed styles | `scan` (Docker tier) — never decide from source |
|
|
54
|
+
| `aria-*` referencing a runtime-built id (`id={`\`x-${i}\``}`) | The id is dynamic; cross-file resolution only handles `const` string ids | Read the component; refute if the id is generated and wired correctly |
|
|
55
|
+
|
|
56
|
+
## Normativity false positives — a "problem" with no failing normative test
|
|
57
|
+
|
|
58
|
+
A finding can cite a real element yet still not be a **non-conformity**, because no test of
|
|
59
|
+
the active standard actually fails. These were the professional-auditor + official-Ara
|
|
60
|
+
corrections against ultra11y's own RGAA audit of egapro; each is now handled as a
|
|
61
|
+
**recommendation (non-normative)** or suppressed outright — never a normative NC.
|
|
62
|
+
|
|
63
|
+
| Case | Why it is NOT an NC | Current handling |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| An **empty `<th>`** (layout/spacer header cell) | No RGAA/WCAG test says every `<th>` must have content; a best-practice-only axe `empty-table-header` violation carries no `wcag*` tag | merges as **advisory**, attaches to no criterion as NC |
|
|
66
|
+
| A **download link** (`…report.pdf`) whose text doesn't name the format/weight | Good practice (DSFR), but no failing normative test under 6.1/2.4.4 | the RGAA `download-link-format` pack rule is **`advisory: true`** → « Recommandation (non normative) », never flips 6.1 |
|
|
67
|
+
| **Two `<h1>`** on a page / a page **starting at `<h2>`** | "one `<h1>` per page" is not a rule in HTML, a11y, or SEO; no heading-order skip occurs | `h1-multiple` / `h1-missing` are **advisory** (no normative NC) |
|
|
68
|
+
| A **lorem-ipsum tooltip** | Content-quality (UX), not accessibility | emits **no finding** |
|
|
69
|
+
|
|
70
|
+
The distinction is the same one you apply in the judgment phase (`references/judgment.md`):
|
|
71
|
+
an **NC needs a `normativeRef`** citing the precise failed test of the active standard; a
|
|
72
|
+
good practice without one is a `recommendations[]` entry, and a pure UX concern is neither.
|
|
73
|
+
When you adjudicate, don't "promote" a recommendation to an NC to look thorough.
|
|
74
|
+
|
|
75
|
+
## The rule of thumb
|
|
76
|
+
|
|
77
|
+
The engine raises **confirmed candidates**, never silent conformance. When a finding is
|
|
78
|
+
`preliminary`, or sits on SFC/library source, treat it as "verify, don't trust": open the
|
|
79
|
+
`file:line`, check whether the missing thing is injected/rendered elsewhere, and record a
|
|
80
|
+
`supported`/`refuted` verdict via `verify --apply`. A refuted finding is dropped — that is
|
|
81
|
+
the anti-hallucination contract working as intended.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Put the fixes in place — `fix` (hybrid, native-first)
|
|
2
|
+
|
|
3
|
+
`fix` applies what is **mechanically safe**, prepares what needs a word filled in, and
|
|
4
|
+
**proposes** what is judgment — without ever inventing content (anti-hallucination doctrine).
|
|
5
|
+
By default it is a **dry-run** (diff); `--write` applies.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
node scripts/ultra11y.mjs fix "src/**/*.html" # dry-run: diff + list
|
|
9
|
+
node scripts/ultra11y.mjs fix "src/**/*.html" --write # apply the safe fixes
|
|
10
|
+
node scripts/ultra11y.mjs fix --changed --write # only the git diff
|
|
11
|
+
node scripts/ultra11y.mjs fix page.html --only positive-tabindex --write
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Three classes of fix
|
|
15
|
+
|
|
16
|
+
- **auto** — deterministic, no judgment, written as-is:
|
|
17
|
+
`positive-tabindex` (→ `tabindex="0"`), `redundant-aria` (drop the redundant `role`),
|
|
18
|
+
`meta-viewport-zoom-block` (drop blocking `user-scalable=no` / `maximum-scale`).
|
|
19
|
+
- **fill-in (placeholder)** — inserts a **valid** attribute with a `TODO` sentinel that **you**
|
|
20
|
+
replace with a relevant, in-context value: `html-lang-missing` (`lang="TODO"`),
|
|
21
|
+
`iframe-title-missing` (`title="TODO"`), `img-alt-missing` (`alt="TODO"` or
|
|
22
|
+
`aria-label="TODO"`), `control-label-missing` (`aria-label="TODO"`).
|
|
23
|
+
- **judgment (proposal)** — the engine never fixes on its own: alt relevance (1.1.1), link/
|
|
24
|
+
button purpose (2.4.4/4.1.2), contrast, table/fieldset structure… the AI agent writes the
|
|
25
|
+
content and adjudicates the criterion (`verify --manual`, gated).
|
|
26
|
+
|
|
27
|
+
## Guarantees
|
|
28
|
+
|
|
29
|
+
- **Edit by source range**: codemods re-read the opening tag locally (attribute offsets are not
|
|
30
|
+
provided by the parser) and apply edits **back-to-front**; edits that overlap on one element
|
|
31
|
+
are dropped, never misapplied.
|
|
32
|
+
- **Anti-regression gate**: after `--write`, the file is re-audited; if a fix **introduced** a
|
|
33
|
+
new non-conformity, it is **not** written.
|
|
34
|
+
- **JSX/TSX: safe codemods only.** `.jsx`/`.tsx` parse to a real AST whose offsets index the
|
|
35
|
+
original file, so `fix --write` applies the codemods marked **jsxSafe** (attribute removal /
|
|
36
|
+
insertion of React-valid props) — it never rewrites an attribute name (`tabIndex={5}` is
|
|
37
|
+
never turned into `tabindex="0"`). Only the rare **lossy** fallback (a file the AST parser
|
|
38
|
+
could not parse at all) stays proposal-only.
|
|
39
|
+
|
|
40
|
+
## Fix order (native-first)
|
|
41
|
+
|
|
42
|
+
native semantics → accessible names / labels → keyboard access → visible focus → meaning not
|
|
43
|
+
carried by colour alone → media. See `references/authoring.md` and
|
|
44
|
+
`references/forbidden-patterns.md`. After fixing, re-audit; the AI agent adjudicates the judgment
|
|
45
|
+
criteria (`verify --manual`, gated) and the rendering criteria go to `scan` — never marked
|
|
46
|
+
conforming without a recorded, gated justification.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Focus & interaction logic — the agent reads the source and adjudicates the interaction logic
|
|
2
|
+
|
|
3
|
+
A static engine sees *structure*. It cannot see what happens when a user **tabs, types, or
|
|
4
|
+
opens a dialog** — focus visibility, focus order, focus traps, what changes on focus/input,
|
|
5
|
+
whether a mouse interaction has a keyboard equivalent. ultra11y therefore marks every such
|
|
6
|
+
criterion **`manual` / residual risk** (never silently "conforming") and hands it to **the AI
|
|
7
|
+
agent**, which adjudicates it via `verify --manual` (gated). This is the interaction-logic half
|
|
8
|
+
of the division of labour, and it is where reading the *actual source* matters most.
|
|
9
|
+
|
|
10
|
+
> **Read the whole component, not just the report.** The audit parses every element in scope
|
|
11
|
+
> (`audit --graph` resolves imports across files), but a focus/logic verdict requires reading
|
|
12
|
+
> the real source of each interactive component — the JSX, the event handlers, the effects,
|
|
13
|
+
> the state — and reasoning about what a keyboard/screen-reader user experiences.
|
|
14
|
+
|
|
15
|
+
## The criteria you own (surfaced as residual risks)
|
|
16
|
+
|
|
17
|
+
| SC | What to reason about, reading the source |
|
|
18
|
+
|---|---|
|
|
19
|
+
| **2.1.1 Keyboard** | Every action reachable by mouse is reachable by keyboard. Mouse-only handlers (`onMouseEnter`/`onMouseLeave`/`onClick` on a non-interactive element) need a focus/key equivalent (`onFocus`/`onKeyDown`) or a native interactive element. |
|
|
20
|
+
| **2.1.2 No keyboard trap** | Focus can leave every widget (modals, menus, date pickers, embeds). Trace the trap logic and the Escape/return path. |
|
|
21
|
+
| **2.4.3 Focus order** | Tab order follows meaning. Watch DOM order vs visual order, portals/modals, and `tabIndex` (positive is already flagged; reason about roving `tabIndex` in menus/tabs/grids). |
|
|
22
|
+
| **2.4.7 Focus visible** | A visible focus indicator on every focusable element — check CSS (`:focus`/`:focus-visible`, never a bare `outline: none`) at render. |
|
|
23
|
+
| **2.4.11 Focus not obscured** | The focused element isn't hidden behind sticky headers/footers/overlays. |
|
|
24
|
+
| **3.2.1 On focus / 3.2.2 On input** | Focusing or changing a control doesn't trigger an unexpected context change (auto-submit, navigation, surprise modal). Trace `onFocus`/`onChange` handlers. |
|
|
25
|
+
| **2.5.3 Label in name** | The visible label text is contained in the accessible name (so voice control works). |
|
|
26
|
+
|
|
27
|
+
## Component patterns to trace (where focus logic lives)
|
|
28
|
+
|
|
29
|
+
- **Dialog / modal**: focus moves into it on open, is trapped while open, returns to the
|
|
30
|
+
trigger on close, Escape closes it, `aria-modal`/`role="dialog"` + a label.
|
|
31
|
+
- **Menu / dropdown / combobox / listbox**: roving `tabIndex` (one tab stop), arrow-key
|
|
32
|
+
navigation, Escape, `aria-expanded`/`aria-activedescendant`, focus returned to the trigger.
|
|
33
|
+
- **Tabs / accordion**: arrow keys move between tabs, focus follows selection appropriately.
|
|
34
|
+
- **Toast / live region**: announced without stealing focus.
|
|
35
|
+
- **Custom controls built on `<div>`/`<span>`**: prefer a native element; if not, they need
|
|
36
|
+
`role`, `tabindex="0"`, key handlers (Enter/Space), and state in ARIA — the engine flags
|
|
37
|
+
the clickable non-interactive element (2.1.1), but YOU verify the full keyboard contract.
|
|
38
|
+
|
|
39
|
+
## Workflow
|
|
40
|
+
|
|
41
|
+
1. **Audit the full scope** with `audit --graph` so every component/import is captured.
|
|
42
|
+
2. **Build the worklists**. `verify --manual --in audit.json` lists each residual (manual)
|
|
43
|
+
focus/logic criterion, pre-loaded with harvested evidence (ARIA state, `tabindex`, control
|
|
44
|
+
labels), for the AI agent to adjudicate; `verify --report … --semantic` lists the engine's
|
|
45
|
+
detected candidates (e.g. a clickable non-interactive element flagged under 2.1.1) to confirm
|
|
46
|
+
or refute — both grounded in the W3C Understanding reference (see `references/verify.md` /
|
|
47
|
+
`references/judgment.md`).
|
|
48
|
+
3. **For each focus/logic item, open the real source** of the relevant components and reason
|
|
49
|
+
about the keyboard/focus/interaction behaviour above. Adjudicate a residual criterion in
|
|
50
|
+
`ADJUDICATE.todo.json`: `C` (justified) / `NC` (with a groundable `file:line` finding: what
|
|
51
|
+
the keyboard user experiences) / `NA` (justified) / `manual` (with a `reason`). Confirm or
|
|
52
|
+
refute an engine candidate in `VERIFY.todo.json`: `supported` / `partial` / `refuted` /
|
|
53
|
+
`unsupported`, with a concrete cited reason. What needs a rendered DOM (focus *visible*, *not
|
|
54
|
+
obscured*) is marked `needs-rendered-dom` and goes through the `scan` dynamic tier
|
|
55
|
+
(`references/dynamic.md`). The local runtime (`scan --runtime local`) now **probes focus
|
|
56
|
+
visibility (2.4.7)** directly — tabbing through focusables and flagging any whose computed
|
|
57
|
+
style does not change on focus — alongside 200% zoom (1.4.4), text spacing (1.4.12) and
|
|
58
|
+
content-on-hover (1.4.13). A probe finding is a real NC; a clean probe still leaves the SC
|
|
59
|
+
`manual` — never marked conforming on a guess.
|
|
60
|
+
4. **`verify --apply`** to gate FAIL-CLOSED: an unadjudicated, refuted or unsupported focus/logic
|
|
61
|
+
claim — or an agent verdict missing its justification/finding/reason — fails.
|
|
62
|
+
|
|
63
|
+
The rule that never bends: a focus/interaction criterion the AI agent could not decide by reading
|
|
64
|
+
the source (or rendering) is an **explicit residual risk** with a recorded reason, not a silent pass.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Forbidden anti-patterns (accessible HTML/CSS)
|
|
2
|
+
|
|
3
|
+
15 common traps. Format: ❌ bad → why → ✅ native fix → WCAG success criterion (engine
|
|
4
|
+
rule when statically detected).
|
|
5
|
+
|
|
6
|
+
### 1. Clickable `<div>`/`<span>`
|
|
7
|
+
❌ `<div onClick={…}>` → not focusable, not keyboard-operable, no role.
|
|
8
|
+
✅ native `<button type="button">`. WCAG 2.1.1 / 4.1.2 (`clickable-noninteractive`).
|
|
9
|
+
|
|
10
|
+
### 2. `<html>` without `lang`
|
|
11
|
+
❌ default language undeclared → wrong screen-reader pronunciation.
|
|
12
|
+
✅ `<html lang="en">`. WCAG 3.1.1 (`html-lang-missing`).
|
|
13
|
+
|
|
14
|
+
### 3. Page without `<title>` (or empty)
|
|
15
|
+
❌ missing tab/history title. ✅ a relevant `<title>`. WCAG 2.4.2 (`title-missing-empty`).
|
|
16
|
+
|
|
17
|
+
### 4. Empty link or button
|
|
18
|
+
❌ `<a href="/"></a>`, `<button></button>` → no accessible name.
|
|
19
|
+
✅ visible text, or `aria-label`. WCAG 2.4.4 / 4.1.2 (`link-empty-name`, `button-empty-name`).
|
|
20
|
+
|
|
21
|
+
### 5. Unnamed icon-only control
|
|
22
|
+
❌ `<button><svg/></button>` with no name. ✅ `aria-label` or visually-hidden text.
|
|
23
|
+
WCAG 2.4.4 / 4.1.2 (`icon-only-control-unnamed`).
|
|
24
|
+
|
|
25
|
+
### 6. `<label>` not associated with the field
|
|
26
|
+
❌ a visual label with no `for`/wrapping. ✅ `<label for="id">` or a wrapping label.
|
|
27
|
+
WCAG 4.1.2 (`control-label-missing`).
|
|
28
|
+
|
|
29
|
+
### 7. `placeholder` used as a label
|
|
30
|
+
❌ placeholder alone → disappears on input, not a label. ✅ a real `<label>`.
|
|
31
|
+
WCAG 4.1.2 (`placeholder-as-label`).
|
|
32
|
+
|
|
33
|
+
### 8. `outline: none` / `outline: 0` on a focusable element
|
|
34
|
+
❌ removes the keyboard focus indicator. ✅ keep/style a visible focus.
|
|
35
|
+
WCAG 2.4.7 (needs-rendering — verify manually).
|
|
36
|
+
|
|
37
|
+
### 9. Positive `tabindex`
|
|
38
|
+
❌ `tabindex="1"` breaks the logical DOM order. ✅ `tabindex="0"` or nothing.
|
|
39
|
+
WCAG 2.4.3 (`positive-tabindex`).
|
|
40
|
+
|
|
41
|
+
### 10. Informative text baked into an image
|
|
42
|
+
❌ text-as-image with no alternative. ✅ real text styled with CSS. WCAG 1.4.5 (judgment).
|
|
43
|
+
|
|
44
|
+
### 11. Meaning carried by CSS alone (`::before`/`::after`)
|
|
45
|
+
❌ information injected by styles only. ✅ text in the HTML. WCAG 1.3.1 (judgment/rendering).
|
|
46
|
+
|
|
47
|
+
### 12. Skip link hidden with `display:none`
|
|
48
|
+
❌ missing target or non-focusable skip link. ✅ a real target + visible on focus.
|
|
49
|
+
WCAG 2.4.1 (`skip-link-target-missing` for the target).
|
|
50
|
+
|
|
51
|
+
### 13. Navigation not structured as a list
|
|
52
|
+
❌ a run of links with no `<ul>`. ✅ `<nav><ul><li>…`. WCAG 1.3.1 (judgment).
|
|
53
|
+
|
|
54
|
+
### 14. Data table without headers
|
|
55
|
+
❌ `<table>` without `<th>`/`scope`/`<caption>`. ✅ headers + `scope` + caption.
|
|
56
|
+
WCAG 1.3.1 (`data-table-no-headers`, `table-caption-missing`).
|
|
57
|
+
|
|
58
|
+
### 15. Redundant or broken ARIA
|
|
59
|
+
❌ `<button role="button">`, `aria-labelledby="nonexistent-id"`, an invented role.
|
|
60
|
+
✅ native semantics; valid ARIA references. WCAG 4.1.2 (`redundant-aria`,
|
|
61
|
+
`aria-ref-missing-id`, `invalid-aria-role`).
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Forms, errors and authentication
|
|
2
|
+
|
|
3
|
+
Forms are where accessibility failures cost the most: a broken image is an annoyance, an
|
|
4
|
+
unusable form is a transaction the user cannot complete. This page covers 1.3.5, 3.2.2,
|
|
5
|
+
3.3.1–3.3.4 and WCAG 2.2's 3.3.7/3.3.8.
|
|
6
|
+
|
|
7
|
+
## Labels and instructions (3.3.2, 4.1.2)
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<!-- ✓ the association is programmatic, and clicking the text focuses the field -->
|
|
11
|
+
<label for="email">Email address</label>
|
|
12
|
+
<input id="email" type="email" name="email" autocomplete="email"
|
|
13
|
+
aria-describedby="email-hint" required>
|
|
14
|
+
<p id="email-hint">We use it only to send your receipt.</p>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- A `placeholder` is **not** a label: it disappears on input, is often too low-contrast, and
|
|
18
|
+
is announced inconsistently. Keep it for an example (`e.g. name@example.com`), never for
|
|
19
|
+
the label. Engine rule: `placeholder-as-label`.
|
|
20
|
+
- State the format **before** the error, not only in it: `aria-describedby` on the field.
|
|
21
|
+
- `required` on the control is what assistive tech announces. A red asterisk alone is a
|
|
22
|
+
1.4.1 failure too — pair it with text ("Required fields are marked *", or a real word).
|
|
23
|
+
- Group related controls: radios and checkboxes in `<fieldset><legend>`, a date split across
|
|
24
|
+
three inputs likewise. Engine rules: `fieldset-legend-missing`,
|
|
25
|
+
`radio-checkbox-group-ungrouped`, `date-fields-ungrouped`.
|
|
26
|
+
|
|
27
|
+
## 1.3.5 Identify Input Purpose — the token list
|
|
28
|
+
|
|
29
|
+
A field collecting information **about the user** must declare its purpose. The token has to
|
|
30
|
+
come from the HTML autofill vocabulary; an invented one reads as compliant and fills nothing
|
|
31
|
+
(engine rule: `autocomplete-token-invalid`).
|
|
32
|
+
|
|
33
|
+
| Group | Tokens |
|
|
34
|
+
|---|---|
|
|
35
|
+
| Identity | `name` `honorific-prefix` `given-name` `additional-name` `family-name` `honorific-suffix` `nickname` `organization-title` `organization` `sex` `bday` `bday-day` `bday-month` `bday-year` `photo` `language` |
|
|
36
|
+
| Credentials | `username` `new-password` `current-password` `one-time-code` |
|
|
37
|
+
| Address | `street-address` `address-line1` `address-line2` `address-line3` `address-level1`…`address-level4` `country` `country-name` `postal-code` |
|
|
38
|
+
| Payment | `cc-name` `cc-given-name` `cc-additional-name` `cc-family-name` `cc-number` `cc-exp` `cc-exp-month` `cc-exp-year` `cc-csc` `cc-type` `transaction-currency` `transaction-amount` |
|
|
39
|
+
| Contact | `tel` `tel-country-code` `tel-national` `tel-area-code` `tel-local` `tel-local-prefix` `tel-local-suffix` `tel-extension` `email` `impp` `url` |
|
|
40
|
+
|
|
41
|
+
Qualifiers may precede the token: `section-*` (to separate two addresses on one form), then
|
|
42
|
+
`shipping`/`billing`, then `home`/`work`/`mobile`/`fax`/`pager` (contact fields only).
|
|
43
|
+
|
|
44
|
+
```html
|
|
45
|
+
<input autocomplete="shipping postal-code"> <!-- ✓ -->
|
|
46
|
+
<input autocomplete="section-work work email"> <!-- ✓ -->
|
|
47
|
+
<input autocomplete="postcode"> <!-- ✗ not a token -->
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Applicability:** the criterion covers the USER's own data. An order number, a search term
|
|
51
|
+
or a quantity is not in scope — that is a legitimate `NA` on the field.
|
|
52
|
+
|
|
53
|
+
## 3.3.1 / 3.3.3 — identifying and fixing errors
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<label for="dob">Date of birth</label>
|
|
57
|
+
<input id="dob" name="dob" aria-invalid="true"
|
|
58
|
+
aria-describedby="dob-err" aria-errormessage="dob-err">
|
|
59
|
+
<p id="dob-err">Enter the date as DD/MM/YYYY — for example 04/03/1990.</p>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- The error must be **in text**, not only a red border (1.4.1 + 3.3.1).
|
|
63
|
+
- It must be **associated** with its field. Engine rules: `error-not-associated`,
|
|
64
|
+
`aria-invalid-no-description`.
|
|
65
|
+
- 3.3.3 asks for the **correction** when it is knowable: "Enter DD/MM/YYYY" beats "Invalid
|
|
66
|
+
date". Withhold only where suggesting it would compromise security.
|
|
67
|
+
|
|
68
|
+
**Error summary pattern** — for a form with several errors, put a summary at the top, move
|
|
69
|
+
focus to it on submit, and link each entry to its field:
|
|
70
|
+
|
|
71
|
+
```html
|
|
72
|
+
<div role="alert" tabindex="-1" id="errors">
|
|
73
|
+
<h2>There are 2 problems with this form</h2>
|
|
74
|
+
<ul>
|
|
75
|
+
<li><a href="#dob">Date of birth: enter the date as DD/MM/YYYY</a></li>
|
|
76
|
+
<li><a href="#email">Email address: enter an address with an @</a></li>
|
|
77
|
+
</ul>
|
|
78
|
+
</div>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Focus the summary (not the first field): the user hears how many problems there are before
|
|
82
|
+
being dropped into one of them.
|
|
83
|
+
|
|
84
|
+
## 3.2.2 On Input / 3.2.1 On Focus
|
|
85
|
+
|
|
86
|
+
Changing a value, or merely receiving focus, must not by itself change the context.
|
|
87
|
+
|
|
88
|
+
```html
|
|
89
|
+
<!-- ✗ selecting an option navigates; keyboard users trigger it while browsing the list -->
|
|
90
|
+
<select onchange="location = this.value">…</select>
|
|
91
|
+
|
|
92
|
+
<!-- ✓ the change is explicit, and announced beforehand -->
|
|
93
|
+
<label for="lang">Language</label>
|
|
94
|
+
<select id="lang">…</select>
|
|
95
|
+
<button type="submit">Change language</button>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Engine rule: `on-input-context-change`. Auto-advancing between one-character code fields is
|
|
99
|
+
the same failure in a friendlier costume — allow it, but keep the form usable without it.
|
|
100
|
+
|
|
101
|
+
## 3.3.4 Error Prevention (legal, financial, data)
|
|
102
|
+
|
|
103
|
+
For submissions that are legally binding, financial, or that modify/delete user data, one of
|
|
104
|
+
these must hold: **reversible**, **checked** (errors detected and a chance to correct), or
|
|
105
|
+
**confirmed** (a review step before the final commit). A one-click irreversible delete fails.
|
|
106
|
+
|
|
107
|
+
## 3.3.7 Redundant Entry (WCAG 2.2)
|
|
108
|
+
|
|
109
|
+
Information already entered in the same process must not be asked for again — auto-populate
|
|
110
|
+
it, or offer it for selection. Exceptions: re-entry is essential (confirming a password),
|
|
111
|
+
the earlier information is no longer valid, or it would compromise security.
|
|
112
|
+
|
|
113
|
+
```html
|
|
114
|
+
<!-- ✓ the shipping address is offered rather than retyped -->
|
|
115
|
+
<label><input type="checkbox" name="same" checked> Billing address is the same as shipping</label>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 3.3.8 Accessible Authentication (WCAG 2.2)
|
|
119
|
+
|
|
120
|
+
No step of an authentication process may require a **cognitive function test** — memorising,
|
|
121
|
+
transcribing, or solving a puzzle — unless an alternative, a mechanism to help, object
|
|
122
|
+
recognition, or user-provided content is available.
|
|
123
|
+
|
|
124
|
+
```html
|
|
125
|
+
<!-- ✗ blocking paste turns login into a memory test and breaks password managers -->
|
|
126
|
+
<input type="password" onpaste="return false">
|
|
127
|
+
<!-- ✗ autofill switched off on a credential field -->
|
|
128
|
+
<input type="password" autocomplete="off">
|
|
129
|
+
|
|
130
|
+
<!-- ✓ declare the real purpose; let the manager fill and the user paste -->
|
|
131
|
+
<input type="password" autocomplete="current-password">
|
|
132
|
+
<input type="text" inputmode="numeric" autocomplete="one-time-code">
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Engine rule: `credential-entry-blocked`. Also in scope: a transcription CAPTCHA with no
|
|
136
|
+
alternative, and a code the user must retype from another screen with no way to paste.
|
|
137
|
+
|
|
138
|
+
## What the engine settles, and what stays yours
|
|
139
|
+
|
|
140
|
+
| Decided by the engine | Yours to adjudicate |
|
|
141
|
+
|---|---|
|
|
142
|
+
| Missing/duplicate labels, placeholder-as-label, ungrouped radios and date parts | Whether a label is **descriptive** (2.4.6) and an instruction sufficient (3.3.2) |
|
|
143
|
+
| `autocomplete` token outside the vocabulary | Whether the field collects **user** data at all (1.3.5 applicability) |
|
|
144
|
+
| Error text not associated, `aria-invalid` with no description | Whether the message says **how to fix it** (3.3.3) |
|
|
145
|
+
| Paste/autofill blocked on a credential field | Whether the whole auth flow imposes a cognitive test (3.3.8) |
|
|
146
|
+
| `onchange` that submits or navigates | Whether a context change was **announced beforehand** (3.2.2) |
|
|
147
|
+
| — | Redundant entry across a multi-step flow (3.3.7) |
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Implementation guidance — concrete before/after rules attached to criteria
|
|
2
|
+
|
|
3
|
+
A standards pack says *what* a criterion requires; **guidance** says *how to implement
|
|
4
|
+
it*. It is the layer of concrete, how-to-implement rules — the RGAA SocialGouv/etalab
|
|
5
|
+
good/bad code patterns — keyed to a pack criterion and the WCAG success criteria it maps
|
|
6
|
+
onto. It enriches output without ever changing the verdict.
|
|
7
|
+
|
|
8
|
+
## Where it shows up
|
|
9
|
+
|
|
10
|
+
Guidance is looked up at **render time** by criterion / WCAG SC, so the canonical
|
|
11
|
+
`audit` JSON stays untouched (no schema change, no new finding). It surfaces in:
|
|
12
|
+
|
|
13
|
+
- **`prd`** — each backlog item gains a before/after example and an effort estimate; the
|
|
14
|
+
`--format doc` PRD weaves it into the user stories and acceptance criteria.
|
|
15
|
+
- **`criteria`** lookups for a pack criterion.
|
|
16
|
+
|
|
17
|
+
Because attachment is presentation-only, guidance never marks anything conforming and
|
|
18
|
+
never invents a non-conformity — it is illustration, not detection.
|
|
19
|
+
|
|
20
|
+
## Dataset shape
|
|
21
|
+
|
|
22
|
+
A guidance dataset is JSON: `{ pack, source, license, attribution, entries[] }`, where
|
|
23
|
+
each entry is `{ id, criterionId, wcag[], title, summary, impact?, examples[], reference }`
|
|
24
|
+
and an example is `{ lang: "html"|"jsx"|"css", bad?, good?, note? }`. The built-in RGAA
|
|
25
|
+
guidance ships at `src/data/guidance/rgaa.json` (its entry ids mirror the SocialGouv rule
|
|
26
|
+
files). Load external guidance with `--pack <dir>` (a `guidance.json` beside `pack.json`)
|
|
27
|
+
or a `.ultra11yrc.json` `guidance` list. `pack check --guidance` validates that every
|
|
28
|
+
entry resolves to a real criterion, maps to recognized WCAG SCs, and that every example
|
|
29
|
+
parses.
|
|
30
|
+
|
|
31
|
+
## Honesty rule — guidance is NOT a free detector
|
|
32
|
+
|
|
33
|
+
A pattern earns a **WCAG-core detector** only when it is both statically decidable **and**
|
|
34
|
+
maps to a success criterion present in the WCAG 2.2 **AA** core. Everything else lives
|
|
35
|
+
here as guidance (and, where the country standard covers it, in the pack), never as a
|
|
36
|
+
silent "conforming".
|
|
37
|
+
|
|
38
|
+
The headline example: **opening a new window** (`target="_blank"` without warning). Its
|
|
39
|
+
only clean WCAG home is **3.2.5 Change on Request — which is AAA, absent from the AA
|
|
40
|
+
core** (the dataset jumps 3.2.4 → 3.2.6). So ultra11y does **not** ship a WCAG-core
|
|
41
|
+
detector for it — no static rule flags `target="_blank"` under any standard. It ships as
|
|
42
|
+
RGAA guidance (criterion 13.2 → WCAG 3.2.1): look it up with `criteria --standard rgaa 13.2`,
|
|
43
|
+
and it attaches to the relevant `prd`/`report` entry — it is never raised as an audit
|
|
44
|
+
non-conformity, and never force-mapped onto a wrong AA criterion. (`--standard` is a
|
|
45
|
+
`report`/`prd`/`criteria` flag, not an `audit` one.) Judgment- and
|
|
46
|
+
rendering-dependent rules (alt relevance, computed contrast, reading order) stay guidance
|
|
47
|
+
+ residual risk for the same reason.
|
|
48
|
+
|
|
49
|
+
## Attribution
|
|
50
|
+
|
|
51
|
+
RGAA 4.1.2 © DINUM — Licence Ouverte / Etalab 2.0. The before/after implementation
|
|
52
|
+
patterns are adapted from SocialGouv/skills (`rgaa-html-css`) and the official RGAA
|
|
53
|
+
méthode; the dataset stores short derived summaries + minimal examples, not verbatim prose
|
|
54
|
+
(see `NOTICE`). When ingesting another source, record its license and attribution in the
|
|
55
|
+
dataset header and `NOTICE` before redistributing any text.
|