@surea11y/core 1.4.1 → 1.6.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 +212 -128
- package/README.md +46 -9
- package/docs/ACT_RULE_MAPPING.md +243 -0
- package/docs/API_STABILITY.md +4 -4
- package/docs/BINDING_AUTHORS_GUIDE.md +3 -3
- package/docs/DESIGN_CHALLENGES.md +301 -0
- package/docs/ENGINE_OPTIONS.md +30 -14
- package/docs/I18N.md +176 -20
- package/docs/INTEGRATION.md +29 -7
- package/docs/LIMITATIONS.md +6 -4
- package/docs/OUTPUT_SCHEMA.md +13 -3
- package/docs/REPORT.md +3 -1
- package/docs/RULE_AUTHORING.md +104 -23
- package/docs/RULE_CATALOG.md +1878 -169
- package/docs/RULE_TAXONOMY.md +2 -2
- package/docs/TROUBLESHOOTING.md +4 -4
- package/docs/WCAG_CONFORMANCE.md +25 -9
- package/package.json +8 -7
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +95 -40
- package/src/checks/automatic/aria-allowed-role.js +16 -18
- package/src/checks/automatic/aria-braille-equivalent.js +19 -21
- package/src/checks/automatic/aria-conditional-attr.js +22 -24
- package/src/checks/automatic/aria-deprecated-role.js +63 -50
- package/src/checks/automatic/aria-hidden-body.js +4 -11
- package/src/checks/automatic/aria-hidden-focus.js +104 -23
- package/src/checks/automatic/aria-prohibited-attr.js +71 -72
- package/src/checks/automatic/aria-prohibited-children.js +154 -61
- package/src/checks/automatic/aria-required-attr.js +74 -29
- package/src/checks/automatic/aria-required-children.js +38 -34
- package/src/checks/automatic/aria-required-parent.js +78 -29
- package/src/checks/automatic/aria-role-name-present.js +36 -22
- package/src/checks/automatic/aria-roles-valid.js +37 -23
- package/src/checks/automatic/aria-valid-attr-value.js +33 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -18
- package/src/checks/automatic/autocomplete-valid.js +17 -19
- package/src/checks/automatic/avoid-inline-spacing.js +14 -16
- package/src/checks/automatic/binary-control-name-present.js +46 -26
- package/src/checks/automatic/button-name-present.js +115 -34
- package/src/checks/automatic/combobox-name-present.js +40 -22
- package/src/checks/automatic/contrast-computable.js +32 -0
- package/src/checks/automatic/contrast-enhanced.js +21 -1
- package/src/checks/automatic/contrast-minimum.js +21 -1
- package/src/checks/automatic/css-orientation-lock.js +118 -41
- package/src/checks/automatic/definition-list-children-valid.js +25 -29
- package/src/checks/automatic/deprecated-elements-not-used.js +15 -17
- package/src/checks/automatic/dialog-name-present.js +36 -20
- package/src/checks/automatic/dlitem-parent-valid.js +15 -17
- package/src/checks/automatic/duplicate-id-aria.js +50 -40
- package/src/checks/automatic/duplicate-id.js +198 -0
- package/src/checks/automatic/embed-text-alternative-present.js +2 -2
- package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
- package/src/checks/automatic/form-control-single-label.js +39 -41
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
- package/src/checks/automatic/iframe-focusable-content.js +92 -38
- package/src/checks/automatic/iframe-name-present.js +53 -21
- package/src/checks/automatic/iframe-title-unique.js +19 -24
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +198 -49
- package/src/checks/automatic/link-in-text-block.js +29 -31
- package/src/checks/automatic/link-name-present.js +47 -31
- package/src/checks/automatic/list-children-valid.js +21 -23
- package/src/checks/automatic/listbox-name-present.js +42 -24
- package/src/checks/automatic/listitem-parent-valid.js +18 -21
- package/src/checks/automatic/menuitem-name-present.js +36 -20
- package/src/checks/automatic/meta-refresh-no-exceptions.js +39 -31
- package/src/checks/automatic/meta-refresh-timing-absent.js +29 -25
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
- package/src/checks/automatic/meter-name-present.js +38 -21
- package/src/checks/automatic/nested-interactive-controls-absent.js +22 -24
- package/src/checks/automatic/option-name-present.js +39 -22
- package/src/checks/automatic/page-title-present.js +21 -3
- package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
- package/src/checks/automatic/progressbar-name-present.js +41 -24
- package/src/checks/automatic/role-img-alt-present.js +64 -16
- package/src/checks/automatic/searchbox-name-present.js +46 -24
- package/src/checks/automatic/server-side-image-map-absent.js +16 -19
- package/src/checks/automatic/slider-name-present.js +42 -23
- package/src/checks/automatic/spinbutton-name-present.js +46 -24
- package/src/checks/automatic/summary-name-present.js +34 -20
- package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
- package/src/checks/automatic/svg-text-alternative-present.js +13 -10
- package/src/checks/automatic/tab-name-present.js +37 -20
- package/src/checks/automatic/table-headers-attr-valid.js +57 -24
- package/src/checks/automatic/table-th-has-data-cells.js +76 -24
- package/src/checks/automatic/target-size-minimum.js +172 -131
- package/src/checks/automatic/td-has-header.js +20 -25
- package/src/checks/automatic/textbox-name-present.js +42 -24
- package/src/checks/automatic/tooltip-name-present.js +37 -20
- package/src/checks/automatic/treeitem-name-present.js +39 -22
- package/src/checks/automatic/valid-lang.js +107 -24
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +21 -22
- package/src/checks/manual/area-alt-decorative-manual.js +7 -0
- package/src/checks/manual/area-alt-quality-manual.js +6 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +23 -26
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +48 -38
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/empty-heading-manual.js +73 -28
- package/src/checks/manual/empty-table-header-manual.js +33 -36
- package/src/checks/manual/focus-order-semantics-manual.js +15 -15
- package/src/checks/manual/form-control-label-quality-manual.js +453 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +4 -11
- package/src/checks/manual/heading-order-manual.js +20 -25
- package/src/checks/manual/heading-quality-manual.js +338 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
- package/src/checks/manual/image-redundant-alt-manual.js +18 -21
- package/src/checks/manual/img-alt-decorative-manual.js +211 -52
- package/src/checks/manual/img-alt-quality-manual.js +7 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
- package/src/checks/manual/label-title-only-manual.js +19 -21
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +21 -24
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +23 -26
- package/src/checks/manual/landmark-main-is-top-level-manual.js +20 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +8 -13
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +8 -13
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +4 -9
- package/src/checks/manual/landmark-one-main-manual.js +8 -15
- package/src/checks/manual/landmark-unique-manual.js +31 -36
- package/src/checks/manual/link-name-quality-manual.js +162 -35
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +16 -19
- package/src/checks/manual/mouse-only-event-handlers-manual.js +26 -28
- package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
- package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/p-as-heading-manual.js +4 -4
- package/src/checks/manual/page-has-heading-one-manual.js +8 -15
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/presentation-role-conflict-manual.js +74 -46
- package/src/checks/manual/region-manual.js +32 -25
- package/src/checks/manual/scope-attr-valid-manual.js +16 -19
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +44 -52
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +16 -19
- package/src/checks/manual/table-duplicate-name-manual.js +16 -19
- package/src/checks/manual/table-fake-caption-manual.js +2 -2
- package/src/checks/manual/video-caption-manual.js +3 -3
- package/src/checks/manual-review.js +17 -1
- package/src/core.js +13086 -5169
- package/src/report.js +16 -2
- package/surea11y.browser.js +5665 -4219
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
- package/bin/surea11y-core.js +0 -20
package/docs/RULE_TAXONOMY.md
CHANGED
|
@@ -24,7 +24,7 @@ Manual rules MUST NOT make normative failure decisions.
|
|
|
24
24
|
### 1.2 Intent
|
|
25
25
|
Encoded by: **rule id suffix**
|
|
26
26
|
|
|
27
|
-
Illustrative intents (from the image-alternatives family used as the running example in §2, not an exhaustive list — the ruleset's
|
|
27
|
+
Illustrative intents (from the image-alternatives family used as the running example in §2, not an exhaustive list — the ruleset's 130 rules use dozens of distinct suffixes; `docs/RULE_CATALOG.md` is the generated, always-current list):
|
|
28
28
|
|
|
29
29
|
- `present`
|
|
30
30
|
- Verifies that a required **mechanism exists**
|
|
@@ -43,7 +43,7 @@ Intent determines whether a rule can be automatic.
|
|
|
43
43
|
### 1.3 Target Family
|
|
44
44
|
Encoded by: **rule id prefix**
|
|
45
45
|
|
|
46
|
-
Illustrative families, from the image-alternatives cluster (WCAG 1.1.1) used as the running example in §2 — not an exhaustive list. The ruleset's
|
|
46
|
+
Illustrative families, from the image-alternatives cluster (WCAG 1.1.1) used as the running example in §2 — not an exhaustive list. The ruleset's 130 rules span dozens of families (`aria-*`, `contrast-*`, `dialog-*`, `iframe-*`, `label-*`, `link-*`, `list-*`, `landmark-*`, and more); see `docs/RULE_CATALOG.md` for the generated, always-current list:
|
|
47
47
|
|
|
48
48
|
- `img`
|
|
49
49
|
- `area`
|
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
|
|
|
@@ -18,7 +18,7 @@ Check the result's `error` field first — if it says `"<something> is not defin
|
|
|
18
18
|
|
|
19
19
|
## "A geometry-dependent rule (e.g. `target-size-minimum`) always says `notApplicable`"
|
|
20
20
|
|
|
21
|
-
Plain jsdom (no real browser) doesn't implement CSS layout — `getBoundingClientRect()` always returns zero geometry. Rules that need real layout
|
|
21
|
+
Plain jsdom (no real browser) doesn't implement CSS layout — `getBoundingClientRect()` always returns zero geometry. Rules that need real layout report `notApplicable` under jsdom rather than guess. Run through a real browser instead (Puppeteer/Playwright — see [`INTEGRATION.md`](./INTEGRATION.md) Pattern 2) to get real findings from these rules. See [`LIMITATIONS.md`](./LIMITATIONS.md).
|
|
22
22
|
|
|
23
23
|
## "I only see `fail`/`cantTell` occurrences — where's the list of elements that passed?"
|
|
24
24
|
|
|
@@ -28,7 +28,7 @@ By design, this engine never enumerates the elements a rule *passed* — only th
|
|
|
28
28
|
|
|
29
29
|
Two common causes, in order of likelihood:
|
|
30
30
|
|
|
31
|
-
1. **The element is excluded from the accessibility tree** — `aria-hidden="true"`, `display: none`, `visibility: hidden`, `hidden`, or an `inert` ancestor. Most rules
|
|
31
|
+
1. **The element is excluded from the accessibility tree** — `aria-hidden="true"`, `display: none`, `visibility: hidden`, `hidden`, or an `inert` ancestor. Most rules skip content that's already invisible to assistive technology, since checking a hidden element would be meaningless and could produce a misleading `fail` on content no user encounters. Some rules explicitly opt out of this gating when it wouldn't make sense to (e.g. `no-autoplay-audio` — hidden audio still plays sound) — check the specific rule's file header comment (`@applicability`) in `src/checks/`.
|
|
32
32
|
2. **`excludeSelectors`** — if you've configured this (directly or inherited from a shared config), confirm the element in question isn't matched by it. Remember this can also be scoped to a single rule via `engineOptions.rules[ruleId].excludeSelectors` (see [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md#rule-scoped-excludeselectors)) — if a rule you expect to fire keeps coming back `notApplicable`/`pass` for one element only, check whether that rule specifically has its own exclude list configured, not just the global one.
|
|
33
33
|
|
|
34
34
|
## "Does a clean scan (`pass` everywhere) mean the page is WCAG conformant?"
|
|
@@ -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/docs/WCAG_CONFORMANCE.md
CHANGED
|
@@ -4,9 +4,9 @@ How individual rule results relate to a WCAG Success Criterion (SC), and what su
|
|
|
4
4
|
|
|
5
5
|
## The three layers
|
|
6
6
|
|
|
7
|
-
1. **Atomic rules** (`checksResults[]`) — one normative decision each, e.g. "does this `<img>` have an `alt` attribute." See [`RULE_CATALOG.md`](./RULE_CATALOG.md) for all
|
|
7
|
+
1. **Atomic rules** (`checksResults[]`) — one normative decision each, e.g. "does this `<img>` have an `alt` attribute." See [`RULE_CATALOG.md`](./RULE_CATALOG.md) for all 130.
|
|
8
8
|
2. **Facets** — a WCAG SC is usually bigger than any one rule can decide deterministically. Internally, each SC is broken into named "facets" (e.g. 1.1.1 Non-text Content has facets like `img-alt-attr-present`, `text-alternative-quality`, `decorative-null`) tracked in `src/coverage/wcag-facets.js`, each marked `full` (a rule decides it with high confidence), `partial` (a rule decides *part* of it — see each rule's own scope notes), or `manual` (no safe automated heuristic exists at all). Run `npm run coverage` to regenerate `coverage/coverage-report.md`, the per-SC facet breakdown.
|
|
9
|
-
3. **Composite (WCAG-SC rollup) rules** (`rulesResults[]`) — a generated aggregate of every atomic rule mapped to one SC, giving you one pass/fail/cantTell/notApplicable verdict per SC instead of having to roll up dozens of atomic results yourself. See [`RULE_CATALOG.md`](./RULE_CATALOG.md#composite-wcag-sc-rollup-rules-
|
|
9
|
+
3. **Composite (WCAG-SC rollup) rules** (`rulesResults[]`) — a generated aggregate of every atomic rule mapped to one SC, giving you one pass/fail/cantTell/notApplicable verdict per SC instead of having to roll up dozens of atomic results yourself. See [`RULE_CATALOG.md`](./RULE_CATALOG.md#composite-wcag-sc-rollup-rules-33) for the full list (e.g. `wcag-1.1.1-non-text-content` rolls up 22 atomic rules).
|
|
10
10
|
|
|
11
11
|
## How a composite's outcome is computed
|
|
12
12
|
|
|
@@ -25,17 +25,33 @@ A composite's `data.details.contributors` array (see [`OUTPUT_SCHEMA.md`](./OUTP
|
|
|
25
25
|
|
|
26
26
|
## Targeting a conformance level (A / AA / AAA)
|
|
27
27
|
|
|
28
|
-
Pass `runOnly.tags` (or `engineOptions.tags.include`) with
|
|
28
|
+
Pass `runOnly.tags` (or `engineOptions.tags.include`) with the level tags you want:
|
|
29
29
|
|
|
30
30
|
```js
|
|
31
|
-
|
|
31
|
+
// WCAG 2.0 A and AA. Both tags are required: they are not cumulative.
|
|
32
|
+
runDomRulesInPage(url, null, {}, { tags: ['wcag2a', 'wcag2aa'] });
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
**Level tags do not nest, and this is the easiest thing to get wrong here.** A rule
|
|
36
|
+
carries one level tag per Success Criterion it maps to, and nothing more: a rule
|
|
37
|
+
mapped only to an AA criterion is tagged `wcag2aa` and *not* `wcag2a`. Asking for
|
|
38
|
+
`{ tags: ['wcag2aa'] }` on its own therefore runs the 10 rules mapped to a 2.0 AA
|
|
39
|
+
criterion, not the ~100 that make up an A + AA target. List every level you mean.
|
|
40
|
+
|
|
41
|
+
The same applies across WCAG versions — a criterion introduced in 2.1 or 2.2 carries
|
|
42
|
+
only its own origin tag — so a full conformance target is a union of tag sets. See
|
|
43
|
+
[`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md#filtering-by-wcag-version-21-vs-22) for the
|
|
44
|
+
ready-made sets per version, including the one criterion WCAG 2.2 removed rather than
|
|
45
|
+
added.
|
|
46
|
+
|
|
47
|
+
Composites, unlike atomic rules, *are* filtered cumulatively. The runner reads the
|
|
48
|
+
highest level named in `tags` and drops every composite above it, so requesting
|
|
49
|
+
`['wcag2a', 'wcag2aa']` returns no `rulesResults` entry for an AAA-only SC. That is
|
|
50
|
+
`inferTargetLevelFromRunOnly`/`isAllowedByTargetLevel` in `src/core/dom-runner.js` if
|
|
51
|
+
you need the exact precedence.
|
|
52
|
+
|
|
53
|
+
Omit `tags` entirely (the default) and every rule at every level runs, with no composite
|
|
54
|
+
suppression.
|
|
39
55
|
|
|
40
56
|
## What this engine cannot tell you
|
|
41
57
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surea11y/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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",
|
|
@@ -25,9 +25,6 @@
|
|
|
25
25
|
"vitest"
|
|
26
26
|
],
|
|
27
27
|
"main": "src/index.js",
|
|
28
|
-
"bin": {
|
|
29
|
-
"surea11y-core": "bin/surea11y-core.js"
|
|
30
|
-
},
|
|
31
28
|
"exports": {
|
|
32
29
|
".": "./src/index.js",
|
|
33
30
|
"./baseline": "./src/baseline.js",
|
|
@@ -59,8 +56,8 @@
|
|
|
59
56
|
"src/report.js",
|
|
60
57
|
"src/sarif.js",
|
|
61
58
|
"src/checks/**/*.js",
|
|
62
|
-
"bin/surea11y-core.js",
|
|
63
59
|
"surea11y.browser.js",
|
|
60
|
+
"surea11y.i18n.*.js",
|
|
64
61
|
"docs/**/*.md",
|
|
65
62
|
"README.md",
|
|
66
63
|
"LICENSE",
|
|
@@ -73,8 +70,8 @@
|
|
|
73
70
|
"scripts": {
|
|
74
71
|
"lint": "eslint .",
|
|
75
72
|
"lint:fix": "eslint . --fix",
|
|
76
|
-
"format": "prettier --write \"src/**/*.js\" \"
|
|
77
|
-
"format:check": "prettier --check \"src/**/*.js\" \"
|
|
73
|
+
"format": "prettier --write \"src/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"eslint.config.js\"",
|
|
74
|
+
"format:check": "prettier --check \"src/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"eslint.config.js\"",
|
|
78
75
|
"build": "node scripts/build-core.js && node scripts/build-browser.js",
|
|
79
76
|
"pretest": "playwright install chromium",
|
|
80
77
|
"test": "npm run format:check && npm run build && node scripts/run-tests.js",
|
|
@@ -87,11 +84,15 @@
|
|
|
87
84
|
"test:typography-helpers": "node tests/contrast-typography.test.js",
|
|
88
85
|
"coverage": "node scripts/generate-wcag-coverage.js --rulesDir src/checks",
|
|
89
86
|
"coverage:strict": "node scripts/generate-wcag-coverage.js --rulesDir src/checks --strictFacets",
|
|
87
|
+
"coverage:check": "node scripts/generate-wcag-coverage.js --rulesDir src/checks --check",
|
|
90
88
|
"fixtures:index": "node scripts/generate-fixture-index.js",
|
|
91
89
|
"docs:rule-catalog": "npm run build && node scripts/generate-rule-catalog.js",
|
|
92
90
|
"validate:automatic-rules": "npm run build && node scripts/validate-all-rules.js src/checks/automatic",
|
|
93
91
|
"validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual",
|
|
92
|
+
"validate:rules": "npm run validate:automatic-rules && npm run validate:manual-rules",
|
|
94
93
|
"i18n:new": "node scripts/i18n-scaffold.js",
|
|
94
|
+
"i18n:sync": "node scripts/i18n-sync.js",
|
|
95
|
+
"i18n:check": "node scripts/i18n-sync.js --check",
|
|
95
96
|
"i18n:report": "node scripts/i18n-report.js"
|
|
96
97
|
},
|
|
97
98
|
"devDependencies": {
|
package/src/baseline.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
// docs/BASELINE.md: identity for one violation occurrence is
|
|
6
|
-
// `ruleId + reasonCode + html
|
|
6
|
+
// `ruleId + reasonCode + html`, on purpose NOT `selector`/`structuralPath`
|
|
7
7
|
// (both position-derived, so they shift when unrelated markup changes
|
|
8
8
|
// elsewhere on the page; see buildSelector/buildStructuralPath in
|
|
9
9
|
// src/core/dom-helpers.js). Unlike src/explain/group.js's computeGroupKey,
|
|
10
|
-
// this does not use a coarse structural signature: that's
|
|
10
|
+
// this does not use a coarse structural signature: that's lossy on purpose
|
|
11
11
|
// for AI-explanation dedup (one prompt per shape), which would risk a CI gate
|
|
12
|
-
// silently treating
|
|
12
|
+
// silently treating an actually new violation as "known" just because it
|
|
13
13
|
// shares tag/class shape with an old baselined one -- the wrong failure mode
|
|
14
14
|
// here. Content-based matching survives incidental DOM changes elsewhere on
|
|
15
15
|
// the page; its known limitation is a flagged element with dynamic content
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
const id = 'area-alt-present';
|
|
22
22
|
|
|
23
23
|
const meta = {
|
|
24
|
-
title: '
|
|
24
|
+
title: '<area> must have an alt attribute',
|
|
25
25
|
description:
|
|
26
|
-
'Checks that
|
|
26
|
+
'Checks that <area> elements provide an alt attribute to support a text alternative mechanism.',
|
|
27
27
|
i18n: {
|
|
28
28
|
titleKey: 'area_altPresent_title',
|
|
29
29
|
descriptionKey: 'area_altPresent_description'
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 4.1.2
|
|
11
11
|
* @applicability
|
|
12
|
-
* Applies to elements
|
|
13
|
-
*
|
|
12
|
+
* Applies to elements carrying at least one recognized, non-global
|
|
13
|
+
* aria-* attribute, judged against the role they actually have: an
|
|
14
|
+
* explicit valid role, else the implicit role of their tag, else, for
|
|
15
|
+
* the elements HTML-AAM maps to no role at all, nothing.
|
|
14
16
|
* @expectation
|
|
15
17
|
* Every recognized aria-* attribute present is either: (a) globally
|
|
16
18
|
* supported on any element (the "global" ARIA states/properties, e.g.
|
|
@@ -20,10 +22,19 @@
|
|
|
20
22
|
* still allowed: it is reported as CANTTELL (see helpers.aria.isDeprecatedAttr)
|
|
21
23
|
* so the author decides, not as a not-allowed FAIL.
|
|
22
24
|
* @implementation-notes
|
|
23
|
-
* -
|
|
24
|
-
*
|
|
25
|
-
* role
|
|
26
|
-
*
|
|
25
|
+
* - Three tiers of role resolution, in order: an explicit `role`; the
|
|
26
|
+
* implicit role of the tag (IMPLICIT_ROLE_BY_ELEMENT, generated only for
|
|
27
|
+
* elements whose role is the same in every context, see
|
|
28
|
+
* `scripts/generate-aria-tables.js` for what is excluded and why); and
|
|
29
|
+
* ROLELESS_ELEMENTS, the tags HTML-AAM gives no role at all. A
|
|
30
|
+
* role-specific attribute on one of those is supported by nothing, which
|
|
31
|
+
* is exactly ACT 5c01ea's `<audio controls aria-orientation="horizontal">`.
|
|
32
|
+
* An element whose role is context-dependent (`<a>`, `<section>`,
|
|
33
|
+
* `<td>`, ...) is still skipped rather than guessed at.
|
|
34
|
+
* - `<div>`/`<span>` resolve to `generic`, whose supported set is empty, so
|
|
35
|
+
* `<div aria-expanded="true">` is reported: the attribute announces
|
|
36
|
+
* nothing on an element with no widget semantics, which is a real defect
|
|
37
|
+
* and not a spec technicality.
|
|
27
38
|
* - SUPPORTED_ATTRS_BY_ROLE holds unambiguous, well-established ARIA facts:
|
|
28
39
|
* subclass relationships like `searchbox`==`textbox`; same-family widget
|
|
29
40
|
* properties like `columnheader`/`rowheader` sort/col-row-index/span
|
|
@@ -82,7 +93,7 @@ function runInPage(ctx) {
|
|
|
82
93
|
// Global ARIA states/properties supported on (almost) any element,
|
|
83
94
|
// regardless of role, per the WAI-ARIA "Global States and Properties" list.
|
|
84
95
|
// Declared inside runInPage (rather than at module scope) because the
|
|
85
|
-
// build inlines only this function's own source text
|
|
96
|
+
// build inlines only this function's own source text, see
|
|
86
97
|
// scripts/build-core.js header ("runInPage MUST be self-contained").
|
|
87
98
|
// <generated:aria-global-attrs>
|
|
88
99
|
const GLOBAL_ATTRS = [
|
|
@@ -109,8 +120,8 @@ function runInPage(ctx) {
|
|
|
109
120
|
];
|
|
110
121
|
// </generated:aria-global-attrs>
|
|
111
122
|
|
|
112
|
-
// Per-role supported (non-global) states/properties.
|
|
113
|
-
// conservative
|
|
123
|
+
// Per-role supported (non-global) states/properties.
|
|
124
|
+
// conservative, see src/core/aria-helpers.js file header for the same
|
|
114
125
|
// confidence-scoping rationale; only well-established, unambiguous
|
|
115
126
|
// role/attribute pairings from the WAI-ARIA role definitions are listed.
|
|
116
127
|
// <generated:aria-implicit-roles>
|
|
@@ -125,6 +136,7 @@ function runInPage(ctx) {
|
|
|
125
136
|
details: 'group',
|
|
126
137
|
dfn: 'term',
|
|
127
138
|
dialog: 'dialog',
|
|
139
|
+
div: 'generic',
|
|
128
140
|
dt: 'term',
|
|
129
141
|
em: 'emphasis',
|
|
130
142
|
fieldset: 'group',
|
|
@@ -149,6 +161,7 @@ function runInPage(ctx) {
|
|
|
149
161
|
p: 'paragraph',
|
|
150
162
|
progress: 'progressbar',
|
|
151
163
|
strong: 'strong',
|
|
164
|
+
span: 'generic',
|
|
152
165
|
sub: 'subscript',
|
|
153
166
|
sup: 'superscript',
|
|
154
167
|
textarea: 'textbox',
|
|
@@ -173,6 +186,10 @@ function runInPage(ctx) {
|
|
|
173
186
|
'[aria-activedescendant], [aria-autocomplete], [aria-checked], [aria-colcount], [aria-colindex], [aria-colspan], [aria-disabled], [aria-errormessage], [aria-expanded], [aria-haspopup], [aria-invalid], [aria-level], [aria-modal], [aria-multiline], [aria-multiselectable], [aria-orientation], [aria-placeholder], [aria-posinset], [aria-pressed], [aria-readonly], [aria-required], [aria-rowcount], [aria-rowindex], [aria-rowspan], [aria-selected], [aria-setsize], [aria-sort], [aria-valuemax], [aria-valuemin], [aria-valuenow], [aria-valuetext]';
|
|
174
187
|
// </generated:aria-implicit-roles>
|
|
175
188
|
|
|
189
|
+
// <generated:aria-roleless-elements>
|
|
190
|
+
const ROLELESS_ELEMENTS = new Set(['audio', 'video']);
|
|
191
|
+
// </generated:aria-roleless-elements>
|
|
192
|
+
|
|
176
193
|
// <generated:aria-role-attrs>
|
|
177
194
|
const SUPPORTED_ATTRS_BY_ROLE = {
|
|
178
195
|
alert: [],
|
|
@@ -806,6 +823,39 @@ function runInPage(ctx) {
|
|
|
806
823
|
const cantTellOccurrences = [];
|
|
807
824
|
let applicableCount = 0;
|
|
808
825
|
|
|
826
|
+
// An element HTML-AAM maps to no role has nothing to support a
|
|
827
|
+
// role-specific attribute, so every non-global one present is reported.
|
|
828
|
+
// Returns how many recognized aria-* attributes it carried, for the
|
|
829
|
+
// applicable count.
|
|
830
|
+
function rolelessOccurrences(el, tag) {
|
|
831
|
+
let seen = 0;
|
|
832
|
+
const attrs = el.attributes;
|
|
833
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
834
|
+
const name = String(attrs[i].name || '').toLowerCase();
|
|
835
|
+
if (name.slice(0, 5) !== 'aria-') continue;
|
|
836
|
+
if (!ariaHelpers.isValidAriaAttrName(name)) continue; // aria-valid-attr's concern
|
|
837
|
+
|
|
838
|
+
seen += 1;
|
|
839
|
+
if (globalSet.has(name)) continue;
|
|
840
|
+
|
|
841
|
+
failOccurrences.push(
|
|
842
|
+
helpers.reportOccurrence(el, {
|
|
843
|
+
summary: `<${tag}> has no ARIA role, so nothing supports the ${name} attribute on it.`,
|
|
844
|
+
hint: `Remove ${name}, or move it to an element whose role supports it. A role-specific ARIA attribute on an element with no role is ignored by assistive technology.`,
|
|
845
|
+
i18n: {
|
|
846
|
+
summaryKey: 'ariaAllowedAttr_summary_fail_roleless',
|
|
847
|
+
hintKey: 'ariaAllowedAttr_hint_fail_roleless',
|
|
848
|
+
params: { attr: name, element: tag }
|
|
849
|
+
},
|
|
850
|
+
data: {
|
|
851
|
+
details: { reasonCode: 'ARIA_ATTR_NOT_ALLOWED_ROLELESS', attr: name, element: tag }
|
|
852
|
+
}
|
|
853
|
+
})
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
return seen;
|
|
857
|
+
}
|
|
858
|
+
|
|
809
859
|
for (const el of nodes) {
|
|
810
860
|
if (!el || !el.attributes) continue;
|
|
811
861
|
|
|
@@ -824,6 +874,14 @@ function runInPage(ctx) {
|
|
|
824
874
|
role = Object.prototype.hasOwnProperty.call(IMPLICIT_ROLE_BY_ELEMENT, key)
|
|
825
875
|
? IMPLICIT_ROLE_BY_ELEMENT[key]
|
|
826
876
|
: '';
|
|
877
|
+
// No role from either source: for a tag HTML-AAM maps to no role at
|
|
878
|
+
// all, that IS the answer. Nothing supports a role-specific
|
|
879
|
+
// attribute here. Any other tag has a role this table does not model
|
|
880
|
+
// (context-dependent ones), so it stays out of scope.
|
|
881
|
+
if (!role && ROLELESS_ELEMENTS.has(tag)) {
|
|
882
|
+
applicableCount += rolelessOccurrences(el, tag);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
827
885
|
}
|
|
828
886
|
if (!role || !ariaHelpers.isValidConcreteRole(role)) continue; // aria-roles-valid's concern
|
|
829
887
|
|
|
@@ -858,49 +916,46 @@ function runInPage(ctx) {
|
|
|
858
916
|
|
|
859
917
|
if (!disallowed || !disallowed.length) continue;
|
|
860
918
|
|
|
861
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
862
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
863
|
-
|
|
864
919
|
for (const name of disallowed) {
|
|
865
920
|
// A property ARIA deprecated (rather than prohibited) on this role is
|
|
866
|
-
// still allowed
|
|
921
|
+
// still allowed, surfaced as cantTell for the author to decide, not a
|
|
867
922
|
// not-allowed fail.
|
|
868
923
|
const deprecated =
|
|
869
924
|
typeof ariaHelpers.isDeprecatedAttr === 'function' &&
|
|
870
925
|
ariaHelpers.isDeprecatedAttr(name, role);
|
|
871
926
|
if (deprecated) {
|
|
872
|
-
cantTellOccurrences.push(
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
927
|
+
cantTellOccurrences.push(
|
|
928
|
+
helpers.reportOccurrence(el, {
|
|
929
|
+
summary: 'This ARIA attribute is deprecated for this element’s role.',
|
|
930
|
+
hint: 'It is still allowed but discouraged; remove it or use a role that supports it, as a future ARIA version may disallow it.',
|
|
931
|
+
occurrenceOutcome: 'cantTell',
|
|
932
|
+
i18n: {
|
|
933
|
+
summaryKey: 'ariaAllowedAttr_summary_cantTell',
|
|
934
|
+
hintKey: 'ariaAllowedAttr_hint_cantTell',
|
|
935
|
+
params: { attr: name, role }
|
|
936
|
+
},
|
|
937
|
+
data: {
|
|
938
|
+
details: { reasonCode: 'ARIA_ATTR_DEPRECATED', attr: name, role }
|
|
939
|
+
}
|
|
940
|
+
})
|
|
941
|
+
);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
failOccurrences.push(
|
|
945
|
+
helpers.reportOccurrence(el, {
|
|
946
|
+
summary: 'This ARIA attribute is not permitted for this element’s role.',
|
|
947
|
+
hint: 'Remove this attribute, or use a role that supports it.',
|
|
948
|
+
occurrenceOutcome: 'fail',
|
|
878
949
|
i18n: {
|
|
879
|
-
summaryKey: '
|
|
880
|
-
hintKey: '
|
|
950
|
+
summaryKey: 'ariaAllowedAttr_summary_fail',
|
|
951
|
+
hintKey: 'ariaAllowedAttr_hint_fail',
|
|
881
952
|
params: { attr: name, role }
|
|
882
953
|
},
|
|
883
954
|
data: {
|
|
884
|
-
details: { reasonCode: '
|
|
955
|
+
details: { reasonCode: 'ARIA_ATTR_NOT_ALLOWED', attr: name, role }
|
|
885
956
|
}
|
|
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
|
-
});
|
|
957
|
+
})
|
|
958
|
+
);
|
|
904
959
|
}
|
|
905
960
|
}
|
|
906
961
|
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* The explicit role is one of the roles the ARIA-in-HTML specification
|
|
18
18
|
* permits for that host element.
|
|
19
19
|
* @implementation-notes
|
|
20
|
-
* -
|
|
20
|
+
* - Scoped to elements present in ALLOWED_ROLES_BY_ELEMENT;
|
|
21
21
|
* elements without an asserted constraint are treated as "no constraint"
|
|
22
|
-
* (not flagged) rather than guessed at
|
|
22
|
+
* (not flagged) rather than guessed at, see that table's header comment.
|
|
23
23
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
24
24
|
* property, while engine-level hidden-subtree filtering still applies
|
|
25
25
|
* unless engineOptions.includeHiddenElements is true.
|
|
@@ -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) {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* - `aria-braillelabel`/`aria-brailleroledescription` do not participate
|
|
28
28
|
* in the standard accessible-name computation, so
|
|
29
29
|
* `helpers.getAccessibleNameInfo` already reflects only the "regular"
|
|
30
|
-
* (non-braille) name
|
|
30
|
+
* (non-braille) name, no special-casing needed there.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
const id = 'aria-braille-equivalent';
|
|
@@ -67,7 +67,7 @@ function runInPage(ctx) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function getConservativeSubtreeText(container) {
|
|
70
|
-
// "Name from content"
|
|
70
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
71
71
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
72
72
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
73
73
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -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, so 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
|
|
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
* other than `"false"` (i.e. `"true"`, `"grammar"`, or `"spelling"`).
|
|
17
17
|
* An element with `aria-errormessage` but `aria-invalid` absent or
|
|
18
18
|
* `"false"` silently drops the error message from the accessibility
|
|
19
|
-
* tree
|
|
19
|
+
* tree, authors almost always intend it to be exposed.
|
|
20
20
|
* @implementation-notes
|
|
21
|
-
* - This is
|
|
21
|
+
* - This is narrow: the broader space is a table of many
|
|
22
22
|
* attribute/condition pairs. This rule implements only the one pairing
|
|
23
23
|
* (`aria-errormessage` / `aria-invalid`) that is unambiguous and
|
|
24
|
-
* explicitly stated in the ARIA spec, to keep `fail` high-confidence
|
|
24
|
+
* explicitly stated in the ARIA spec, to keep `fail` high-confidence,
|
|
25
25
|
* matches this repo's established pattern (see `aria-required-attr`/
|
|
26
26
|
* `aria-prohibited-attr` for the same "narrow but zero false positives"
|
|
27
27
|
* trade-off).
|
|
28
28
|
* - Does not check whether the `aria-errormessage` ID reference itself
|
|
29
|
-
* resolves to an existing element
|
|
29
|
+
* resolves to an existing element. That's `aria-valid-attr-value`'s
|
|
30
30
|
* concern, not this rule's.
|
|
31
31
|
*/
|
|
32
32
|
|
|
@@ -35,7 +35,7 @@ const id = 'aria-conditional-attr';
|
|
|
35
35
|
const meta = {
|
|
36
36
|
title: 'aria-errormessage requires aria-invalid to be set to a non-false value',
|
|
37
37
|
description:
|
|
38
|
-
'Checks that elements with aria-errormessage also have aria-invalid set to "true", "grammar", or "spelling"
|
|
38
|
+
'Checks that elements with aria-errormessage also have aria-invalid set to "true", "grammar", or "spelling"; otherwise the error message is dropped from the accessibility tree.',
|
|
39
39
|
i18n: {
|
|
40
40
|
titleKey: 'ariaConditionalAttr_title',
|
|
41
41
|
descriptionKey: 'ariaConditionalAttr_description'
|
|
@@ -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) {
|