@surea11y/core 1.4.1 → 1.5.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/CHANGELOG.md +47 -7
- package/README.md +19 -3
- package/docs/API_STABILITY.md +2 -2
- package/docs/ENGINE_OPTIONS.md +14 -10
- package/docs/I18N.md +176 -20
- package/docs/INTEGRATION.md +28 -6
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +13 -3
- package/docs/REPORT.md +2 -0
- package/docs/RULE_AUTHORING.md +53 -0
- package/docs/TROUBLESHOOTING.md +2 -2
- package/package.json +6 -1
- package/src/checks/automatic/aria-allowed-attr.js +27 -30
- package/src/checks/automatic/aria-allowed-role.js +14 -16
- package/src/checks/automatic/aria-braille-equivalent.js +17 -19
- package/src/checks/automatic/aria-conditional-attr.js +17 -19
- package/src/checks/automatic/aria-deprecated-role.js +62 -49
- package/src/checks/automatic/aria-hidden-body.js +2 -9
- package/src/checks/automatic/aria-hidden-focus.js +99 -18
- package/src/checks/automatic/aria-prohibited-attr.js +54 -55
- package/src/checks/automatic/aria-prohibited-children.js +26 -26
- package/src/checks/automatic/aria-required-attr.js +14 -17
- package/src/checks/automatic/aria-required-children.js +17 -20
- package/src/checks/automatic/aria-required-parent.js +17 -20
- package/src/checks/automatic/aria-roles-valid.js +37 -23
- package/src/checks/automatic/aria-valid-attr-value.js +18 -21
- package/src/checks/automatic/aria-valid-attr.js +14 -17
- package/src/checks/automatic/autocomplete-valid.js +15 -17
- package/src/checks/automatic/avoid-inline-spacing.js +14 -16
- package/src/checks/automatic/binary-control-name-present.js +19 -21
- package/src/checks/automatic/button-name-present.js +23 -28
- package/src/checks/automatic/combobox-name-present.js +15 -17
- package/src/checks/automatic/css-orientation-lock.js +22 -22
- package/src/checks/automatic/definition-list-children-valid.js +18 -21
- package/src/checks/automatic/deprecated-elements-not-used.js +14 -16
- package/src/checks/automatic/dialog-name-present.js +16 -18
- package/src/checks/automatic/dlitem-parent-valid.js +15 -17
- package/src/checks/automatic/duplicate-id-aria.js +45 -37
- package/src/checks/automatic/form-control-single-label.js +38 -40
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
- package/src/checks/automatic/iframe-focusable-content.js +29 -31
- package/src/checks/automatic/iframe-name-present.js +15 -17
- package/src/checks/automatic/iframe-title-unique.js +18 -23
- package/src/checks/automatic/label-in-name.js +31 -36
- package/src/checks/automatic/link-in-text-block.js +19 -21
- package/src/checks/automatic/link-name-present.js +25 -30
- package/src/checks/automatic/list-children-valid.js +15 -17
- package/src/checks/automatic/listbox-name-present.js +15 -17
- package/src/checks/automatic/listitem-parent-valid.js +14 -17
- package/src/checks/automatic/menuitem-name-present.js +16 -18
- package/src/checks/automatic/meta-refresh-no-exceptions.js +15 -18
- package/src/checks/automatic/meta-refresh-timing-absent.js +14 -17
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
- package/src/checks/automatic/meter-name-present.js +16 -18
- package/src/checks/automatic/nested-interactive-controls-absent.js +17 -19
- package/src/checks/automatic/option-name-present.js +16 -18
- package/src/checks/automatic/progressbar-name-present.js +19 -21
- package/src/checks/automatic/searchbox-name-present.js +19 -17
- package/src/checks/automatic/server-side-image-map-absent.js +15 -18
- package/src/checks/automatic/slider-name-present.js +15 -17
- package/src/checks/automatic/spinbutton-name-present.js +19 -17
- package/src/checks/automatic/summary-name-present.js +16 -18
- package/src/checks/automatic/tab-name-present.js +16 -18
- package/src/checks/automatic/table-headers-attr-valid.js +15 -17
- package/src/checks/automatic/table-th-has-data-cells.js +15 -19
- package/src/checks/automatic/target-size-minimum.js +104 -81
- package/src/checks/automatic/td-has-header.js +15 -20
- package/src/checks/automatic/textbox-name-present.js +15 -17
- package/src/checks/automatic/tooltip-name-present.js +16 -18
- package/src/checks/automatic/treeitem-name-present.js +16 -18
- package/src/checks/automatic/valid-lang.js +15 -17
- package/src/checks/manual/accesskeys-manual.js +18 -19
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +19 -22
- package/src/checks/manual/bypass-blocks-present-manual.js +4 -12
- package/src/checks/manual/empty-heading-manual.js +15 -17
- package/src/checks/manual/empty-table-header-manual.js +27 -30
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -9
- package/src/checks/manual/heading-order-manual.js +17 -22
- package/src/checks/manual/image-redundant-alt-manual.js +14 -17
- package/src/checks/manual/label-title-only-manual.js +15 -17
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-main-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -7
- package/src/checks/manual/landmark-one-main-manual.js +2 -9
- package/src/checks/manual/landmark-unique-manual.js +22 -27
- package/src/checks/manual/link-name-quality-manual.js +15 -17
- package/src/checks/manual/meta-viewport-large-manual.js +14 -17
- package/src/checks/manual/mouse-only-event-handlers-manual.js +17 -19
- package/src/checks/manual/page-has-heading-one-manual.js +2 -9
- package/src/checks/manual/presentation-role-conflict-manual.js +19 -21
- package/src/checks/manual/region-manual.js +13 -6
- package/src/checks/manual/scope-attr-valid-manual.js +14 -17
- package/src/checks/manual/skip-link-manual.js +39 -47
- package/src/checks/manual/tabindex-manual.js +14 -17
- package/src/checks/manual/table-duplicate-name-manual.js +14 -17
- package/src/core.js +4203 -3604
- package/src/report.js +14 -0
- package/surea11y.browser.js +1960 -3671
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
package/docs/OUTPUT_SCHEMA.md
CHANGED
|
@@ -15,7 +15,11 @@ This is the exact shape of the object returned by `runDomRulesInPage(...)` / `ru
|
|
|
15
15
|
|
|
16
16
|
```ts
|
|
17
17
|
{
|
|
18
|
-
engine: {
|
|
18
|
+
engine: {
|
|
19
|
+
tag: string,
|
|
20
|
+
schemaVersion: string,
|
|
21
|
+
locale: { requested: string, resolved: string, reason: string }
|
|
22
|
+
},
|
|
19
23
|
url: string | null,
|
|
20
24
|
title: string | null,
|
|
21
25
|
timestamp: string | null,
|
|
@@ -31,6 +35,8 @@ This is the exact shape of the object returned by `runDomRulesInPage(...)` / `ru
|
|
|
31
35
|
|---|---|
|
|
32
36
|
| `engine.tag` | The engine's own identity tag, currently `"a11ycore"`. Every rule (built-in or custom) carries it in `meta.tags` — rule `ruleId`s themselves are bare (no prefix). |
|
|
33
37
|
| `engine.schemaVersion` | The result-schema version (`"1.0.0"`). Bump-worthy if this document's shape ever changes incompatibly — pin to it if you're parsing output programmatically. See [`API_STABILITY.md`](./API_STABILITY.md) for the full stable/unstable field list and version-bump policy. |
|
|
38
|
+
| `engine.locale` | Which dictionary the run actually used. `requested` is your `engineOptions.locale` after trimming (`"en"` if you passed nothing or a non-string); `resolved` is the locale whose dictionary was used; `reason` explains the pairing. Because locale fallback is graceful and per-string, asking for a language the build does not carry produces English text rather than an error — this field is how you find that out without reading the strings. Reported once per result: a run uses one dictionary throughout. |
|
|
39
|
+
| `engine.locale.reason` | `"ok"` — you got the dictionary you asked for, and it carries every key. `"primary-subtag"` — your code had a subtag with no dictionary of its own, so its base language was used: `"de-DE"` resolves to `"de"`. `"dictionary-not-loaded"` — the project ships that language, but this build does not carry it and none was supplied (the standalone browser bundle, without its locale side file). `"unknown-locale"` — the project has no such translation at all. `"partial-dictionary"` — the dictionary was used but is missing keys English has, so those strings fell back to English. Treat the set as open; later releases can add to it. |
|
|
34
40
|
| `url` | The `pageUrl` argument you passed in, or `document.location.href` if you passed `null`/omitted it, or `null` if neither is available. |
|
|
35
41
|
| `title` | `document.title` at scan time, or `null`. |
|
|
36
42
|
| `timestamp` | **Not auto-generated.** Only set if you pass `engineOptions.timestamp` as a non-empty string — the engine has no built-in clock (deterministic-by-design). If you want a scan timestamp in the result, supply it yourself. |
|
|
@@ -97,7 +103,7 @@ Notes:
|
|
|
97
103
|
|
|
98
104
|
- **`outcome` vs `outcomeNormalized`**: identical except `notApplicable` becomes `"inapplicable"` in `outcomeNormalized`. Both are provided so you can match either your own vocabulary or the engine's internal one.
|
|
99
105
|
- **`type: "manual"` rules can never report `outcome: "fail"`.** If a manual rule's own logic would have said `fail`, the engine coerces it to `cantTell` and appends an explanatory note to `error` — this is enforced centrally (`policy.coerceManualFailToCantTell`, on by default under the `a11y` policy contract; see [`POLICY.md`](./POLICY.md)), not something each rule has to remember. `fail` is reserved for deterministic, high-confidence, `type: "automatic"` findings only.
|
|
100
|
-
- **`meta.normativeMappings`** is how a check result ties back to a WCAG Success Criterion — `[]` for rules with no formal WCAG mapping (
|
|
106
|
+
- **`meta.normativeMappings`** is how a check result ties back to a WCAG Success Criterion — `[]` for rules with no formal WCAG mapping (this engine calls them advisory `type: "manual"` rules). See [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md) for how these roll up.
|
|
101
107
|
- **`error`**: only present if the rule implementation threw an uncaught exception, or if the manual-fail coercion above fired. A thrown rule always surfaces as `outcome: "cantTell"` with `occurrences: []` and `error` set to the exception message — the engine never lets one broken rule crash the whole scan.
|
|
102
108
|
- **`engineOptions`** on each result is the *resolved* options object (after locale/contrast defaults were applied), not literally what you passed in — useful for confirming what a given rule actually saw, especially the resolved `locale` and `contrast.mode`/`contrast.rootCanvasFallback`.
|
|
103
109
|
|
|
@@ -185,7 +191,11 @@ const result = runDomRulesInPage(
|
|
|
185
191
|
|
|
186
192
|
```json
|
|
187
193
|
{
|
|
188
|
-
"engine": {
|
|
194
|
+
"engine": {
|
|
195
|
+
"tag": "a11ycore",
|
|
196
|
+
"schemaVersion": "1.0.0",
|
|
197
|
+
"locale": { "requested": "en", "resolved": "en", "reason": "ok" }
|
|
198
|
+
},
|
|
189
199
|
"url": "https://example.test/",
|
|
190
200
|
"title": "Example",
|
|
191
201
|
"timestamp": null,
|
package/docs/REPORT.md
CHANGED
|
@@ -15,6 +15,8 @@ Open `report.html` directly from disk. Works alongside any other output mode —
|
|
|
15
15
|
- **WCAG rollup**: grouped by conformance level (A / AA / AAA), sourced directly from the engine's own `rulesResults[]` composite rollups (`docs/WCAG_CONFORMANCE.md`) — one row per Success Criterion, its outcome, a pass/fail/needs-review/n/a breakdown, and which atomic rules contributed. This is real engine data, not an invented grouping — the same rollup you'd get from the raw JSON's `rulesResults`.
|
|
16
16
|
- **Full technical data** (collapsed by default): a scorecard (tiles per outcome) and a searchable, filterable (by outcome), paginated table of every individual occurrence across the whole scan.
|
|
17
17
|
|
|
18
|
+
The meta bar under the header carries the rule count, occurrence count, engine tag, schema version, and the locale the scan resolved to. Locale fallback is per-string and invisible in the text itself, so a report requested in a language the engine does not carry reads as an ordinary English one — the chip names the requested locale alongside the resolved one when the two differ. See [`I18N.md`](./I18N.md).
|
|
19
|
+
|
|
18
20
|
## Library usage
|
|
19
21
|
|
|
20
22
|
```js
|
package/docs/RULE_AUTHORING.md
CHANGED
|
@@ -132,6 +132,10 @@ The build/runtime resolves i18n by:
|
|
|
132
132
|
2) falling back to `en` if missing,
|
|
133
133
|
3) falling back to the literal `title`/`description` strings if still missing.
|
|
134
134
|
|
|
135
|
+
Add the key and its English text to `src/i18n/en.json`, then run
|
|
136
|
+
`npm run i18n:sync` so every other locale picks it up. `npm test` fails if you
|
|
137
|
+
forget. See [`I18N.md`](./I18N.md).
|
|
138
|
+
|
|
135
139
|
#### `meta.tags`
|
|
136
140
|
Tags are used for grouping/filtering. Typical tag families in this ruleset include:
|
|
137
141
|
- WCAG tagging: `wcag2a`, `wcag111`
|
|
@@ -163,6 +167,32 @@ const meta = {
|
|
|
163
167
|
|
|
164
168
|
---
|
|
165
169
|
|
|
170
|
+
## 4.3 Reporting an occurrence
|
|
171
|
+
|
|
172
|
+
Build occurrences with `helpers.reportOccurrence(element, { summary, hint, i18n, data })`
|
|
173
|
+
rather than assembling the object by hand:
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
occurrences.push(helpers.reportOccurrence(el, { summary: '…', hint: '…' }));
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
It attaches the element for the engine to finalize, which is how `selector`,
|
|
180
|
+
`html` and `structuralPath` get filled in centrally instead of in each of the
|
|
181
|
+
124 rules.
|
|
182
|
+
|
|
183
|
+
**This is a performance contract, not just a convenience.** Every occurrence
|
|
184
|
+
gets a `structuralPath`. Given the element, the engine computes it directly.
|
|
185
|
+
Given only a hand-built occurrence, it re-finds the element with
|
|
186
|
+
`document.querySelector(selector)` — one DOM query per occurrence. That is
|
|
187
|
+
fine for a rule reporting a single document-level finding, and quadratic for
|
|
188
|
+
one reporting many: `region` hand-built its occurrences and took four minutes
|
|
189
|
+
on a thousand-element page, against under a second afterwards.
|
|
190
|
+
|
|
191
|
+
`perfStats.counters['structuralPath.selectorFallback']` counts how often the
|
|
192
|
+
engine had to re-find an element, so a slow rule can be spotted without
|
|
193
|
+
guessing. `tests/structural-path-fallback.test.js` fails if that count starts
|
|
194
|
+
growing with page size.
|
|
195
|
+
|
|
166
196
|
## 5) i18n in occurrences (repo reality)
|
|
167
197
|
|
|
168
198
|
Occurrences also support i18n via keys + params.
|
|
@@ -189,6 +219,29 @@ At normalization time, the engine:
|
|
|
189
219
|
Translation strings use `{{paramName}}` placeholders.
|
|
190
220
|
`params` is shallow-copied and passed into interpolation.
|
|
191
221
|
|
|
222
|
+
**A param carries a value, never prose.** Element names, roles, attribute names,
|
|
223
|
+
selectors, ids, counts and ratios are values: they read the same in every
|
|
224
|
+
language, because the author will search their own source for them. An English
|
|
225
|
+
word or sentence is not, and passing one means it stays English in every locale
|
|
226
|
+
— with nothing to reveal it, since the key is present everywhere and coverage
|
|
227
|
+
reports look complete.
|
|
228
|
+
|
|
229
|
+
When a message varies by case, give each case its own key rather than
|
|
230
|
+
interpolating the differing text:
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
// wrong: the sentence lives in the rule, so no locale can reach it
|
|
234
|
+
i18n: { hintKey: 'myRule_hint_fail', params: { advice: 'Replace it with role="list".' } }
|
|
235
|
+
|
|
236
|
+
// right: one key per case, each translatable on its own
|
|
237
|
+
i18n: { hintKey: 'myRule_hint_fail_directory', params: { role } }
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`tests/i18n/i18n-translatable-strings.test.js` fails any dictionary value with
|
|
241
|
+
no translatable text of its own, which catches the `"{{advice}}"` shape above.
|
|
242
|
+
It cannot catch a param carrying prose into an otherwise-normal sentence, so
|
|
243
|
+
that one is on you.
|
|
244
|
+
|
|
192
245
|
---
|
|
193
246
|
|
|
194
247
|
## 6) Helpers contract used by rules (ctx.helpers)
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## "I passed `runOnly: ['some-rule-id']` but every rule still ran"
|
|
4
4
|
|
|
5
|
-
`runOnly` must be an object, not a bare array — `runOnly: ['img-alt-present']` is silently ignored (the engine falls through to "run everything"), because that shape has none of the fields the engine actually checks (`includeRuleIds`, `tags`, etc.).
|
|
5
|
+
`runOnly` must be an object, not a bare array — `runOnly: ['img-alt-present']` is silently ignored (the engine falls through to "run everything"), because that shape has none of the fields the engine actually checks (`includeRuleIds`, `tags`, etc.). A bare array is easy to reach for, so this is worth checking first.
|
|
6
6
|
|
|
7
7
|
Fix:
|
|
8
8
|
|
|
@@ -41,7 +41,7 @@ Treat it as "needs a human to look" — it's neither pass nor fail by design. Mo
|
|
|
41
41
|
|
|
42
42
|
## "What happens if a locale is only partially translated?"
|
|
43
43
|
|
|
44
|
-
Missing keys fall back to English per-string (never a blank or broken result), so a partial locale degrades gracefully rather than failing outright — see [`I18N.md`](./I18N.md) for the mechanism and current coverage.
|
|
44
|
+
Missing keys fall back to English per-string (never a blank or broken result), so a partial locale degrades gracefully rather than failing outright — see [`I18N.md`](./I18N.md) for the mechanism and current coverage. Key parity is enforced rather than hoped for: `npm run i18n:sync` carries any new or renamed `en.json` key into every other locale file, and the build fails if one is out of step. A key it adds holds the English text until someone translates it, so a locale can be behind on wording without ever being behind on keys.
|
|
45
45
|
|
|
46
46
|
## "`runDomRulesInPage` vs `runa11yCoreInPage` — which one do I want?"
|
|
47
47
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surea11y/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Deterministic WCAG 2.2 accessibility engine that tells you what it can't tell you. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"src/checks/**/*.js",
|
|
62
62
|
"bin/surea11y-core.js",
|
|
63
63
|
"surea11y.browser.js",
|
|
64
|
+
"surea11y.i18n.*.js",
|
|
64
65
|
"docs/**/*.md",
|
|
65
66
|
"README.md",
|
|
66
67
|
"LICENSE",
|
|
@@ -87,11 +88,15 @@
|
|
|
87
88
|
"test:typography-helpers": "node tests/contrast-typography.test.js",
|
|
88
89
|
"coverage": "node scripts/generate-wcag-coverage.js --rulesDir src/checks",
|
|
89
90
|
"coverage:strict": "node scripts/generate-wcag-coverage.js --rulesDir src/checks --strictFacets",
|
|
91
|
+
"coverage:check": "node scripts/generate-wcag-coverage.js --rulesDir src/checks --check",
|
|
90
92
|
"fixtures:index": "node scripts/generate-fixture-index.js",
|
|
91
93
|
"docs:rule-catalog": "npm run build && node scripts/generate-rule-catalog.js",
|
|
92
94
|
"validate:automatic-rules": "npm run build && node scripts/validate-all-rules.js src/checks/automatic",
|
|
93
95
|
"validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual",
|
|
96
|
+
"validate:rules": "npm run validate:automatic-rules && npm run validate:manual-rules",
|
|
94
97
|
"i18n:new": "node scripts/i18n-scaffold.js",
|
|
98
|
+
"i18n:sync": "node scripts/i18n-sync.js",
|
|
99
|
+
"i18n:check": "node scripts/i18n-sync.js --check",
|
|
95
100
|
"i18n:report": "node scripts/i18n-report.js"
|
|
96
101
|
},
|
|
97
102
|
"devDependencies": {
|
|
@@ -858,9 +858,6 @@ function runInPage(ctx) {
|
|
|
858
858
|
|
|
859
859
|
if (!disallowed || !disallowed.length) continue;
|
|
860
860
|
|
|
861
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
862
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
863
|
-
|
|
864
861
|
for (const name of disallowed) {
|
|
865
862
|
// A property ARIA deprecated (rather than prohibited) on this role is
|
|
866
863
|
// still allowed — surfaced as cantTell for the author to decide, not a
|
|
@@ -869,38 +866,38 @@ function runInPage(ctx) {
|
|
|
869
866
|
typeof ariaHelpers.isDeprecatedAttr === 'function' &&
|
|
870
867
|
ariaHelpers.isDeprecatedAttr(name, role);
|
|
871
868
|
if (deprecated) {
|
|
872
|
-
cantTellOccurrences.push(
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
869
|
+
cantTellOccurrences.push(
|
|
870
|
+
helpers.reportOccurrence(el, {
|
|
871
|
+
summary: 'This ARIA attribute is deprecated for this element’s role.',
|
|
872
|
+
hint: 'It is still allowed but discouraged; remove it or use a role that supports it, as a future ARIA version may disallow it.',
|
|
873
|
+
occurrenceOutcome: 'cantTell',
|
|
874
|
+
i18n: {
|
|
875
|
+
summaryKey: 'ariaAllowedAttr_summary_cantTell',
|
|
876
|
+
hintKey: 'ariaAllowedAttr_hint_cantTell',
|
|
877
|
+
params: { attr: name, role }
|
|
878
|
+
},
|
|
879
|
+
data: {
|
|
880
|
+
details: { reasonCode: 'ARIA_ATTR_DEPRECATED', attr: name, role }
|
|
881
|
+
}
|
|
882
|
+
})
|
|
883
|
+
);
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
failOccurrences.push(
|
|
887
|
+
helpers.reportOccurrence(el, {
|
|
888
|
+
summary: 'This ARIA attribute is not permitted for this element’s role.',
|
|
889
|
+
hint: 'Remove this attribute, or use a role that supports it.',
|
|
890
|
+
occurrenceOutcome: 'fail',
|
|
878
891
|
i18n: {
|
|
879
|
-
summaryKey: '
|
|
880
|
-
hintKey: '
|
|
892
|
+
summaryKey: 'ariaAllowedAttr_summary_fail',
|
|
893
|
+
hintKey: 'ariaAllowedAttr_hint_fail',
|
|
881
894
|
params: { attr: name, role }
|
|
882
895
|
},
|
|
883
896
|
data: {
|
|
884
|
-
details: { reasonCode: '
|
|
897
|
+
details: { reasonCode: 'ARIA_ATTR_NOT_ALLOWED', attr: name, role }
|
|
885
898
|
}
|
|
886
|
-
})
|
|
887
|
-
|
|
888
|
-
}
|
|
889
|
-
failOccurrences.push({
|
|
890
|
-
selector: stableSelector,
|
|
891
|
-
html,
|
|
892
|
-
summary: 'This ARIA attribute is not permitted for this element’s role.',
|
|
893
|
-
hint: 'Remove this attribute, or use a role that supports it.',
|
|
894
|
-
occurrenceOutcome: 'fail',
|
|
895
|
-
i18n: {
|
|
896
|
-
summaryKey: 'ariaAllowedAttr_summary_fail',
|
|
897
|
-
hintKey: 'ariaAllowedAttr_hint_fail',
|
|
898
|
-
params: { attr: name, role }
|
|
899
|
-
},
|
|
900
|
-
data: {
|
|
901
|
-
details: { reasonCode: 'ARIA_ATTR_NOT_ALLOWED', attr: name, role }
|
|
902
|
-
}
|
|
903
|
-
});
|
|
899
|
+
})
|
|
900
|
+
);
|
|
904
901
|
}
|
|
905
902
|
}
|
|
906
903
|
|
|
@@ -82,24 +82,22 @@ function runInPage(ctx) {
|
|
|
82
82
|
|
|
83
83
|
if (info.allowed) continue;
|
|
84
84
|
|
|
85
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
86
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
87
85
|
const tag = (el.tagName || '').toLowerCase();
|
|
88
86
|
|
|
89
|
-
occurrences.push(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
87
|
+
occurrences.push(
|
|
88
|
+
helpers.reportOccurrence(el, {
|
|
89
|
+
summary: 'This role is not permitted on this element.',
|
|
90
|
+
hint: 'Use a role permitted for this element, or change the host element.',
|
|
91
|
+
i18n: {
|
|
92
|
+
summaryKey: 'ariaAllowedRole_summary_fail',
|
|
93
|
+
hintKey: 'ariaAllowedRole_hint_fail',
|
|
94
|
+
params: { role, element: tag }
|
|
95
|
+
},
|
|
96
|
+
data: {
|
|
97
|
+
details: { reasonCode: 'ARIA_ROLE_NOT_ALLOWED_FOR_ELEMENT', role, element: tag }
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
);
|
|
103
101
|
}
|
|
104
102
|
|
|
105
103
|
if (applicableCount === 0) {
|
|
@@ -119,29 +119,27 @@ function runInPage(ctx) {
|
|
|
119
119
|
|
|
120
120
|
if (!missing.length) continue;
|
|
121
121
|
|
|
122
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
123
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
124
122
|
const tag = (el.tagName || '').toLowerCase();
|
|
125
123
|
|
|
126
124
|
for (const m of missing) {
|
|
127
|
-
occurrences.push(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
125
|
+
occurrences.push(
|
|
126
|
+
helpers.reportOccurrence(el, {
|
|
127
|
+
summary: `This element has ${m.attr} but no ${m.requires}, its non-braille equivalent.`,
|
|
128
|
+
hint: `${m.attr} is a Braille-specific supplement, not a replacement — also provide ${m.requires}.`,
|
|
129
|
+
i18n: {
|
|
130
|
+
summaryKey: 'ariaBrailleEquivalent_summary_fail',
|
|
131
|
+
hintKey: 'ariaBrailleEquivalent_hint_fail',
|
|
132
|
+
params: { element: tag, attr: m.attr, requires: m.requires }
|
|
133
|
+
},
|
|
134
|
+
data: {
|
|
135
|
+
details: {
|
|
136
|
+
reasonCode: 'BRAILLE_ATTR_WITHOUT_EQUIVALENT',
|
|
137
|
+
attr: m.attr,
|
|
138
|
+
requires: m.requires
|
|
139
|
+
}
|
|
142
140
|
}
|
|
143
|
-
}
|
|
144
|
-
|
|
141
|
+
})
|
|
142
|
+
);
|
|
145
143
|
}
|
|
146
144
|
}
|
|
147
145
|
|
|
@@ -86,28 +86,26 @@ function runInPage(ctx) {
|
|
|
86
86
|
const invalidValue = trim(el.getAttribute('aria-invalid')).toLowerCase();
|
|
87
87
|
if (TRUTHY_INVALID_VALUES.has(invalidValue)) continue;
|
|
88
88
|
|
|
89
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
90
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
91
89
|
const tag = (el.tagName || '').toLowerCase();
|
|
92
90
|
|
|
93
|
-
occurrences.push(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
'
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
occurrences.push(
|
|
92
|
+
helpers.reportOccurrence(el, {
|
|
93
|
+
summary:
|
|
94
|
+
'This element has aria-errormessage but aria-invalid is missing or "false", so the error message is not exposed.',
|
|
95
|
+
hint: 'Set aria-invalid to "true" (or "grammar"/"spelling") whenever aria-errormessage should be exposed to assistive technology.',
|
|
96
|
+
i18n: {
|
|
97
|
+
summaryKey: 'ariaConditionalAttr_summary_fail',
|
|
98
|
+
hintKey: 'ariaConditionalAttr_hint_fail',
|
|
99
|
+
params: { element: tag, ariaInvalid: invalidValue || '(absent)' }
|
|
100
|
+
},
|
|
101
|
+
data: {
|
|
102
|
+
details: {
|
|
103
|
+
reasonCode: 'ARIA_ERRORMESSAGE_WITHOUT_TRUTHY_INVALID',
|
|
104
|
+
ariaInvalid: invalidValue
|
|
105
|
+
}
|
|
108
106
|
}
|
|
109
|
-
}
|
|
110
|
-
|
|
107
|
+
})
|
|
108
|
+
);
|
|
111
109
|
}
|
|
112
110
|
|
|
113
111
|
if (applicableCount === 0) {
|
|
@@ -81,8 +81,20 @@ function runInPage(ctx) {
|
|
|
81
81
|
if (typeof helpers.isDomVisibleEligible === 'function') {
|
|
82
82
|
if (!helpers.isDomVisibleEligible(el, ctx)) return true;
|
|
83
83
|
}
|
|
84
|
-
|
|
84
|
+
// Walk the composed tree, not parentElement: that stops at a shadow
|
|
85
|
+
// root, so a host carrying aria-hidden or inert would never be seen
|
|
86
|
+
// from inside its own shadow content.
|
|
87
|
+
const up =
|
|
88
|
+
typeof helpers.composedParent === 'function'
|
|
89
|
+
? helpers.composedParent
|
|
90
|
+
: (n) => n.parentElement;
|
|
91
|
+
|
|
92
|
+
// A shadow root has no getAttribute, so skip past it rather than
|
|
93
|
+
// stopping: the host one step further up is the node that matters.
|
|
94
|
+
for (let n = el; n; n = up(n)) {
|
|
95
|
+
if (!n.getAttribute) continue;
|
|
85
96
|
if (String(n.getAttribute('aria-hidden') || '').toLowerCase() === 'true') return true;
|
|
97
|
+
if (n.hasAttribute && n.hasAttribute('inert')) return true;
|
|
86
98
|
}
|
|
87
99
|
} catch {
|
|
88
100
|
return false;
|
|
@@ -113,63 +125,64 @@ function runInPage(ctx) {
|
|
|
113
125
|
ariaHelpers.isAuthorProhibitedRole(role);
|
|
114
126
|
if (!deprecated && !discouraged && !prohibited) continue;
|
|
115
127
|
|
|
116
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
117
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
118
128
|
const guidance = ariaHelpers.getDeprecatedRoleGuidance
|
|
119
129
|
? ariaHelpers.getDeprecatedRoleGuidance(role)
|
|
120
|
-
:
|
|
130
|
+
: {
|
|
131
|
+
key: 'ariaDeprecatedRole_guidance_default',
|
|
132
|
+
text: 'Replace the deprecated role with its recommended replacement.'
|
|
133
|
+
};
|
|
121
134
|
|
|
122
135
|
if (prohibited) {
|
|
123
136
|
// Author MUST NOT: the usage is non-conforming, not merely discouraged.
|
|
124
|
-
failOccurrences.push(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
137
|
+
failOccurrences.push(
|
|
138
|
+
helpers.reportOccurrence(el, {
|
|
139
|
+
summary: `This element uses role="${role}", which authors must not explicitly declare.`,
|
|
140
|
+
hint: guidance.text,
|
|
141
|
+
occurrenceOutcome: 'fail',
|
|
142
|
+
i18n: {
|
|
143
|
+
summaryKey: 'ariaDeprecatedRole_summary_fail',
|
|
144
|
+
hintKey: guidance.key,
|
|
145
|
+
params: { role }
|
|
146
|
+
},
|
|
147
|
+
data: {
|
|
148
|
+
details: { reasonCode: 'ARIA_ROLE_AUTHOR_PROHIBITED', role, guidance: guidance.text }
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
);
|
|
139
152
|
} else if (discouraged) {
|
|
140
153
|
// Reserved for user-agent-internal use, at SHOULD NOT strength.
|
|
141
|
-
cantTellOccurrences.push(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
154
|
+
cantTellOccurrences.push(
|
|
155
|
+
helpers.reportOccurrence(el, {
|
|
156
|
+
summary: `This element uses role="${role}", which is reserved for user agents (still valid, but discouraged).`,
|
|
157
|
+
hint: guidance.text,
|
|
158
|
+
occurrenceOutcome: 'cantTell',
|
|
159
|
+
i18n: {
|
|
160
|
+
summaryKey: 'ariaDeprecatedRole_summary_cantTell_discouraged',
|
|
161
|
+
hintKey: guidance.key,
|
|
162
|
+
params: { role }
|
|
163
|
+
},
|
|
164
|
+
data: {
|
|
165
|
+
details: { reasonCode: 'ARIA_ROLE_AUTHOR_DISCOURAGED', role, guidance: guidance.text }
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
);
|
|
156
169
|
} else {
|
|
157
170
|
// Deprecated but still valid: surfaced for the author to decide.
|
|
158
|
-
cantTellOccurrences.push(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
171
|
+
cantTellOccurrences.push(
|
|
172
|
+
helpers.reportOccurrence(el, {
|
|
173
|
+
summary: `This element uses role="${role}", which is deprecated in WAI-ARIA.`,
|
|
174
|
+
hint: guidance.text,
|
|
175
|
+
occurrenceOutcome: 'cantTell',
|
|
176
|
+
i18n: {
|
|
177
|
+
summaryKey: 'ariaDeprecatedRole_summary_cantTell',
|
|
178
|
+
hintKey: guidance.key,
|
|
179
|
+
params: { role }
|
|
180
|
+
},
|
|
181
|
+
data: {
|
|
182
|
+
details: { reasonCode: 'ARIA_ROLE_DEPRECATED', role, guidance: guidance.text }
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
);
|
|
173
186
|
}
|
|
174
187
|
}
|
|
175
188
|
|
|
@@ -86,15 +86,8 @@ function runInPage(ctx) {
|
|
|
86
86
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
|
|
90
|
-
const html = helpers.getOuterHtmlSnippet
|
|
91
|
-
? helpers.getOuterHtmlSnippet(body)
|
|
92
|
-
: (body.outerHTML || '').slice(0, 200);
|
|
93
|
-
|
|
94
89
|
const occurrences = [
|
|
95
|
-
{
|
|
96
|
-
selector: stableSelector,
|
|
97
|
-
html,
|
|
90
|
+
helpers.reportOccurrence(body, {
|
|
98
91
|
summary:
|
|
99
92
|
'The document body has aria-hidden="true", which hides the entire page from assistive technologies.',
|
|
100
93
|
hint: 'Remove aria-hidden from <body>. Hide specific elements instead, if that was the intent.',
|
|
@@ -106,7 +99,7 @@ function runInPage(ctx) {
|
|
|
106
99
|
data: {
|
|
107
100
|
details: { reasonCode: 'ARIA_HIDDEN_BODY' }
|
|
108
101
|
}
|
|
109
|
-
}
|
|
102
|
+
})
|
|
110
103
|
];
|
|
111
104
|
|
|
112
105
|
return {
|