@surea11y/core 1.6.0 → 1.8.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 +140 -0
- package/README.md +179 -90
- package/docs/ACT_RULE_MAPPING.md +10 -8
- package/docs/API_STABILITY.md +67 -6
- package/docs/BINDING_AUTHORS_GUIDE.md +104 -2
- package/docs/CI_INTEGRATIONS.md +43 -0
- package/docs/DESIGN_CHALLENGES.md +162 -2
- package/docs/EARL.md +100 -0
- package/docs/ENGINE_OPTIONS.md +109 -5
- package/docs/I18N.md +62 -20
- package/docs/INTEGRATION.md +4 -2
- package/docs/JUNIT.md +73 -0
- package/docs/LIMITATIONS.md +4 -1
- package/docs/OUTPUT_SCHEMA.md +62 -11
- package/docs/POLICY.md +1 -1
- package/docs/REPORT.md +7 -2
- package/docs/RULE_AUTHORING.md +83 -17
- package/docs/RULE_CATALOG.md +212 -139
- package/docs/RULE_EXAMPLES.md +2189 -0
- package/docs/RULE_HELPERS.md +390 -0
- package/docs/RULE_TAXONOMY.md +27 -6
- package/docs/SARIF.md +23 -3
- package/docs/WCAG_CONFORMANCE.md +64 -3
- package/package.json +41 -12
- package/profiles/index.js +14 -0
- package/src/checks/automatic/area-alt-present.js +87 -31
- package/src/checks/automatic/aria-allowed-attr.js +6 -0
- package/src/checks/automatic/aria-allowed-role.js +32 -23
- package/src/checks/automatic/aria-braille-equivalent.js +43 -17
- package/src/checks/automatic/aria-conditional-attr.js +17 -10
- package/src/checks/automatic/aria-deprecated-role.js +12 -0
- package/src/checks/automatic/aria-hidden-body.js +1 -1
- package/src/checks/automatic/aria-hidden-focus.js +74 -18
- package/src/checks/automatic/aria-prohibited-attr.js +22 -4
- package/src/checks/automatic/aria-prohibited-children.js +6 -6
- package/src/checks/automatic/aria-required-attr.js +88 -12
- package/src/checks/automatic/aria-required-children.js +33 -16
- package/src/checks/automatic/aria-required-parent.js +32 -6
- package/src/checks/automatic/aria-role-name-present.js +20 -3
- package/src/checks/automatic/aria-roles-valid.js +52 -21
- package/src/checks/automatic/aria-valid-attr-value.js +89 -24
- package/src/checks/automatic/aria-valid-attr.js +14 -9
- package/src/checks/automatic/autocomplete-valid.js +152 -26
- package/src/checks/automatic/avoid-inline-spacing.js +207 -15
- package/src/checks/automatic/button-name-present.js +2 -1
- package/src/checks/automatic/canvas-text-alternative-present.js +105 -14
- package/src/checks/automatic/combobox-name-present.js +34 -51
- package/src/checks/automatic/contrast-computable.js +45 -4
- package/src/checks/automatic/contrast-enhanced.js +16 -4
- package/src/checks/automatic/contrast-minimum.js +57 -11
- package/src/checks/automatic/css-orientation-lock.js +171 -12
- package/src/checks/automatic/definition-list-children-valid.js +67 -23
- package/src/checks/automatic/deprecated-elements-not-used.js +43 -38
- package/src/checks/automatic/dialog-name-present.js +28 -9
- package/src/checks/automatic/duplicate-id-aria.js +5 -0
- package/src/checks/automatic/duplicate-id.js +19 -10
- package/src/checks/automatic/form-control-single-label.js +9 -0
- package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
- package/src/checks/automatic/iframe-focusable-content.js +12 -4
- package/src/checks/automatic/iframe-title-unique.js +36 -81
- package/src/checks/automatic/input-image-alt-present.js +32 -20
- package/src/checks/automatic/label-in-name.js +78 -69
- package/src/checks/automatic/language-page-present.js +12 -6
- package/src/checks/automatic/link-in-text-block.js +512 -44
- package/src/checks/automatic/link-name-present.js +13 -5
- package/src/checks/automatic/list-children-valid.js +18 -1
- package/src/checks/automatic/listbox-name-present.js +19 -49
- package/src/checks/automatic/listitem-parent-valid.js +4 -3
- package/src/checks/automatic/meta-refresh-no-exceptions.js +3 -3
- package/src/checks/automatic/page-title-present.js +16 -4
- package/src/checks/automatic/progressbar-name-present.js +11 -1
- package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +9 -5
- package/src/checks/automatic/searchbox-name-present.js +32 -49
- package/src/checks/automatic/server-side-image-map-absent.js +48 -28
- package/src/checks/automatic/slider-name-present.js +38 -52
- package/src/checks/automatic/spinbutton-name-present.js +32 -49
- package/src/checks/automatic/target-size-minimum.js +84 -16
- package/src/checks/automatic/td-has-header.js +60 -23
- package/src/checks/automatic/text-spacing-content-loss.js +548 -0
- package/src/checks/automatic/textbox-name-present.js +32 -49
- package/src/checks/automatic/valid-lang.js +15 -10
- package/src/checks/manual/area-alt-quality-manual.js +113 -31
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +0 -2
- package/src/checks/manual/css-focus-indicator-suppressed-manual.js +23 -10
- package/src/checks/manual/css-hidden-focus.js +215 -7
- package/src/checks/manual/form-control-label-quality-manual.js +243 -29
- package/src/checks/manual/heading-order-manual.js +9 -1
- package/src/checks/manual/heading-quality-manual.js +143 -9
- package/src/checks/manual/img-alt-decorative-manual.js +6 -3
- package/src/checks/manual/input-image-alt-quality-manual.js +81 -13
- package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
- package/src/checks/manual/link-name-quality-manual.js +130 -4
- package/src/checks/manual/media-transcript-present-manual.js +65 -8
- package/src/checks/manual/mouse-only-event-handlers-manual.js +66 -5
- package/src/checks/manual/no-autoplay-audio-manual.js +93 -6
- package/src/checks/manual/p-as-heading-manual.js +89 -44
- package/src/checks/manual/page-title-patterns-manual.js +77 -8
- package/src/checks/manual/password-paste-enabled-manual.js +255 -0
- package/src/checks/manual/skip-link-manual.js +42 -14
- package/src/checks/manual/table-fake-caption-manual.js +32 -1
- package/src/checks/manual/video-caption-manual.js +47 -24
- package/src/checks/manual-review.js +0 -4
- package/src/core.js +18061 -46194
- package/src/coverage/en301549-map.js +187 -0
- package/src/coverage/standards.js +279 -0
- package/src/coverage/wcag-facets.js +1119 -0
- package/src/coverage/wcag-version-map.js +101 -0
- package/src/earl.js +144 -0
- package/src/en301549.js +33 -0
- package/src/junit.js +321 -0
- package/src/profile-kit.js +163 -0
- package/src/report.js +343 -74
- package/src/sarif.js +56 -5
- package/src/wcag.js +105 -0
- package/surea11y.browser.js +11 -41039
- package/surea11y.i18n.de.js +2 -21
- package/surea11y.i18n.es.js +2 -21
- package/surea11y.i18n.fr.js +2 -21
- package/surea11y.i18n.ja.js +3 -0
- package/src/checks/manual/area-alt-decorative-manual.js +0 -255
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surea11y/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"aria",
|
|
15
15
|
"contrast",
|
|
16
16
|
"sarif",
|
|
17
|
+
"junit",
|
|
17
18
|
"zero-dependencies",
|
|
18
19
|
"jsdom",
|
|
19
20
|
"playwright",
|
|
@@ -30,7 +31,12 @@
|
|
|
30
31
|
"./baseline": "./src/baseline.js",
|
|
31
32
|
"./report": "./src/report.js",
|
|
32
33
|
"./sarif": "./src/sarif.js",
|
|
34
|
+
"./junit": "./src/junit.js",
|
|
35
|
+
"./earl": "./src/earl.js",
|
|
36
|
+
"./en301549": "./src/en301549.js",
|
|
37
|
+
"./wcag": "./src/wcag.js",
|
|
33
38
|
"./browser": "./surea11y.browser.js",
|
|
39
|
+
"./i18n/*": "./surea11y.i18n.*.js",
|
|
34
40
|
"./package.json": "./package.json"
|
|
35
41
|
},
|
|
36
42
|
"author": "Jorge Rumoroso",
|
|
@@ -39,7 +45,7 @@
|
|
|
39
45
|
"type": "git",
|
|
40
46
|
"url": "git+https://github.com/SureA11y/core.git"
|
|
41
47
|
},
|
|
42
|
-
"homepage": "https://
|
|
48
|
+
"homepage": "https://surea11y.dev/",
|
|
43
49
|
"bugs": {
|
|
44
50
|
"url": "https://github.com/SureA11y/core/issues"
|
|
45
51
|
},
|
|
@@ -55,6 +61,18 @@
|
|
|
55
61
|
"src/baseline.js",
|
|
56
62
|
"src/report.js",
|
|
57
63
|
"src/sarif.js",
|
|
64
|
+
"src/junit.js",
|
|
65
|
+
"src/earl.js",
|
|
66
|
+
"src/en301549.js",
|
|
67
|
+
"src/coverage/en301549-map.js",
|
|
68
|
+
"src/coverage/standards.js",
|
|
69
|
+
"src/wcag.js",
|
|
70
|
+
"src/profile-kit.js",
|
|
71
|
+
"src/coverage/wcag-facets.js",
|
|
72
|
+
"src/coverage/wcag-version-map.js",
|
|
73
|
+
"profiles/index.js",
|
|
74
|
+
"profiles/*/*.js",
|
|
75
|
+
"profiles/*/rules/**/*.js",
|
|
58
76
|
"src/checks/**/*.js",
|
|
59
77
|
"surea11y.browser.js",
|
|
60
78
|
"surea11y.i18n.*.js",
|
|
@@ -70,27 +88,37 @@
|
|
|
70
88
|
"scripts": {
|
|
71
89
|
"lint": "eslint .",
|
|
72
90
|
"lint:fix": "eslint . --fix",
|
|
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\"",
|
|
91
|
+
"format": "prettier --write \"src/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"profiles/**/*.js\" \"eslint.config.js\"",
|
|
92
|
+
"format:check": "prettier --check \"src/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"profiles/**/*.js\" \"eslint.config.js\"",
|
|
75
93
|
"build": "node scripts/build-core.js && node scripts/build-browser.js",
|
|
76
94
|
"pretest": "playwright install chromium",
|
|
77
|
-
"test": "npm run format:check && npm run build && node scripts/run-tests.js",
|
|
78
|
-
"test:coverage": "npm run build && node scripts/run-tests.js --experimental-test-coverage --test-coverage-include=src/**/*.js",
|
|
95
|
+
"test": "npm run lint && npm run format:check && npm run build && npm run validate:rules && node scripts/run-tests.js",
|
|
96
|
+
"test:coverage": "npm run build && node scripts/run-tests.js --experimental-test-coverage --test-coverage-include=src/**/*.js --test-coverage-include=profiles/**/*.js",
|
|
79
97
|
"test:contrast-helpers": "node tests/contrast-helpers.test.js",
|
|
80
98
|
"helpers-perf-bench": "node --expose-gc scripts/dom-helpers-perf-bench.js",
|
|
81
99
|
"engine-perf-bench": "node --expose-gc scripts/engine-perf-bench.js --profileRules=true --iters=25 --top=15",
|
|
82
100
|
"engine-perf-bench-transparent": "node --expose-gc scripts/engine-perf-bench.js --profileRules=true --top=15 --iters=25 --generated=contrastTransparent",
|
|
83
101
|
"engine-perf-bench-opaque": "node --expose-gc scripts/engine-perf-bench.js --profileRules=true --top=15 --iters=25 --generated=contrastOpaque",
|
|
84
102
|
"test:typography-helpers": "node tests/contrast-typography.test.js",
|
|
85
|
-
"coverage": "node scripts/generate-wcag-coverage.js
|
|
86
|
-
"coverage:strict": "node scripts/generate-wcag-coverage.js --
|
|
87
|
-
"coverage:check": "node scripts/generate-wcag-coverage.js --
|
|
103
|
+
"coverage": "node scripts/generate-wcag-coverage.js",
|
|
104
|
+
"coverage:strict": "node scripts/generate-wcag-coverage.js --strictFacets",
|
|
105
|
+
"coverage:check": "node scripts/generate-wcag-coverage.js --check",
|
|
106
|
+
"finding-ids": "npm run build && node scripts/generate-finding-ids.js",
|
|
107
|
+
"finding-ids:release": "node scripts/freeze-finding-ids.js",
|
|
88
108
|
"fixtures:index": "node scripts/generate-fixture-index.js",
|
|
109
|
+
"fixtures:check": "node scripts/generate-fixture-index.js --check",
|
|
110
|
+
"fixtures:markers": "npm run build && node scripts/generate-fixture-markers.js",
|
|
111
|
+
"fixtures:markers:check": "npm run build && node scripts/generate-fixture-markers.js --check",
|
|
89
112
|
"docs:rule-catalog": "npm run build && node scripts/generate-rule-catalog.js",
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
113
|
+
"docs:rule-catalog:check": "npm run build && node scripts/generate-rule-catalog.js --check",
|
|
114
|
+
"rule-examples:coverage": "npm run build && node scripts/generate-rule-examples-coverage.js",
|
|
115
|
+
"rule-examples:coverage:check": "npm run build && node scripts/generate-rule-examples-coverage.js --check",
|
|
116
|
+
"docs:rule-review": "npm run build && node scripts/generate-rule-review.js",
|
|
117
|
+
"validate:automatic-rules": "npm run build && node scripts/validate-all-rules.js src/checks/automatic profiles/*/rules/automatic",
|
|
118
|
+
"validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual profiles/*/rules/manual",
|
|
119
|
+
"validate:rules": "npm run build && node scripts/validate-all-rules.js",
|
|
93
120
|
"i18n:new": "node scripts/i18n-scaffold.js",
|
|
121
|
+
"profile:new": "node scripts/profile-new.js",
|
|
94
122
|
"i18n:sync": "node scripts/i18n-sync.js",
|
|
95
123
|
"i18n:check": "node scripts/i18n-sync.js --check",
|
|
96
124
|
"i18n:report": "node scripts/i18n-report.js"
|
|
@@ -98,6 +126,7 @@
|
|
|
98
126
|
"devDependencies": {
|
|
99
127
|
"@eslint/js": "^10.0.1",
|
|
100
128
|
"aria-query": "^5.3.2",
|
|
129
|
+
"esbuild": "^0.28.2",
|
|
101
130
|
"eslint": "^10.8.0",
|
|
102
131
|
"eslint-config-prettier": "^10.1.8",
|
|
103
132
|
"globals": "^17.8.0",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The profiles built into the engine, in registry order. Each one is a
|
|
7
|
+
* standard whose verdicts are not WCAG's renumbered (see profiles/README.md),
|
|
8
|
+
* and exports `standard`, its registry entry, which src/coverage/standards.js
|
|
9
|
+
* appends after the standards core keeps for itself, `rulesDir`, the folder
|
|
10
|
+
* of its own rules, which the build compiles with core's, and `i18nDir`, the
|
|
11
|
+
* folder of its dictionaries, which the build adds to core's.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
module.exports = [];
|
|
@@ -11,19 +11,30 @@
|
|
|
11
11
|
* @applicability
|
|
12
12
|
* Applies to <area> elements that:
|
|
13
13
|
* 1) are in a <map> that is referenced by an <img usemap>, AND
|
|
14
|
-
* 2)
|
|
15
|
-
*
|
|
14
|
+
* 2) carry a non-empty href (an <area> with no href is not a hyperlink
|
|
15
|
+
* at all per the HTML spec, and has nothing for this rule to name), AND
|
|
16
|
+
* 3) the referencing <img> is actually rendered (hidden/display:none/
|
|
17
|
+
* visibility exclude it; aria-hidden does not, since <area> is not a
|
|
18
|
+
* DOM descendant of <img>), AND
|
|
19
|
+
* 4) the <area> itself is eligible in the accessibility tree. hidden,
|
|
20
|
+
* display:none and inert on the <area> or its <map> do not exclude it:
|
|
21
|
+
* neither element generates a box, so a real browser's image-map
|
|
22
|
+
* hit-testing ignores all three there (verified against Chromium and
|
|
23
|
+
* Firefox); only those mechanisms on a genuine ancestor of the whole
|
|
24
|
+
* <img>+<map> pairing do.
|
|
16
25
|
* @expectation
|
|
17
|
-
* Each applicable <area> element has
|
|
18
|
-
*
|
|
26
|
+
* Each applicable <area> element has a non-empty accessible name, from alt,
|
|
27
|
+
* aria-label/aria-labelledby, or title. An <area> in a used map is always
|
|
28
|
+
* a link, so alt="" is not decorative here as it is on <img>: an empty alt
|
|
29
|
+
* fails the same as a missing one unless another mechanism names it.
|
|
19
30
|
*/
|
|
20
31
|
|
|
21
32
|
const id = 'area-alt-present';
|
|
22
33
|
|
|
23
34
|
const meta = {
|
|
24
|
-
title: '<area> must have an
|
|
35
|
+
title: '<area> must have an accessible name',
|
|
25
36
|
description:
|
|
26
|
-
'Checks that <area> elements
|
|
37
|
+
'Checks that <area> elements have a non-empty accessible name via alt, aria-label/aria-labelledby, or title.',
|
|
27
38
|
i18n: {
|
|
28
39
|
titleKey: 'area_altPresent_title',
|
|
29
40
|
descriptionKey: 'area_altPresent_description'
|
|
@@ -76,6 +87,11 @@ function runInPage(ctx) {
|
|
|
76
87
|
const isAccTreeEligible =
|
|
77
88
|
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
78
89
|
|
|
90
|
+
const isDomVisibleEligible =
|
|
91
|
+
helpers && typeof helpers.isDomVisibleEligible === 'function'
|
|
92
|
+
? helpers.isDomVisibleEligible
|
|
93
|
+
: null;
|
|
94
|
+
|
|
79
95
|
const getAriaNameInfo =
|
|
80
96
|
helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
|
|
81
97
|
|
|
@@ -156,17 +172,32 @@ function runInPage(ctx) {
|
|
|
156
172
|
const img = getReferencingImgForArea(el);
|
|
157
173
|
if (!img) continue;
|
|
158
174
|
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
175
|
+
// 0b) Without href an <area> is not a hyperlink at all per the HTML
|
|
176
|
+
// spec -- just a shape with no associated action -- so it has nothing
|
|
177
|
+
// for this rule to name.
|
|
178
|
+
const hrefRaw = el.getAttribute('href');
|
|
179
|
+
if (!hrefRaw || !hrefRaw.trim()) continue;
|
|
180
|
+
|
|
181
|
+
// 1) The referencing <img> must actually be rendered: a used map's
|
|
182
|
+
// hotspots depend on the img's box, not its accessibility-tree
|
|
183
|
+
// exposure. <area> is not a DOM descendant of <img> -- only linked by
|
|
184
|
+
// the usemap IDREF -- so aria-hidden on the img has nothing to
|
|
185
|
+
// propagate along (verified against Chromium and Firefox: the area
|
|
186
|
+
// stays reachable regardless). hidden/display:none/visibility on the
|
|
187
|
+
// img itself still excludes it, since that removes the box the
|
|
188
|
+
// hotspot geometry depends on.
|
|
189
|
+
if (isDomVisibleEligible) {
|
|
190
|
+
const imgVis = (() => {
|
|
163
191
|
try {
|
|
164
|
-
return
|
|
192
|
+
return isDomVisibleEligible(img, ctx, {
|
|
193
|
+
visibilityMode: 'styleOnly',
|
|
194
|
+
disableGeometry: true
|
|
195
|
+
});
|
|
165
196
|
} catch {
|
|
166
197
|
return { eligible: true, reasons: [] };
|
|
167
198
|
}
|
|
168
199
|
})();
|
|
169
|
-
if (
|
|
200
|
+
if (imgVis && imgVis.eligible === false) continue;
|
|
170
201
|
}
|
|
171
202
|
|
|
172
203
|
// 2) The <area> itself must be eligible (aria-hidden/inert exceptions handled by helper).
|
|
@@ -184,8 +215,18 @@ function runInPage(ctx) {
|
|
|
184
215
|
// From here: applicable
|
|
185
216
|
applicableCount += 1;
|
|
186
217
|
|
|
187
|
-
|
|
188
|
-
|
|
218
|
+
let altRaw;
|
|
219
|
+
try {
|
|
220
|
+
altRaw = el.getAttribute('alt');
|
|
221
|
+
} catch {
|
|
222
|
+
altRaw = null;
|
|
223
|
+
}
|
|
224
|
+
const hasAltAttr = altRaw !== null;
|
|
225
|
+
const altGivesName = hasAltAttr && String(altRaw).trim() !== '';
|
|
226
|
+
if (altGivesName) continue;
|
|
227
|
+
|
|
228
|
+
// alt="" is not decorative on an <area>: a used map's area is always a
|
|
229
|
+
// link, so it needs a name from elsewhere or it fails.
|
|
189
230
|
|
|
190
231
|
// aria-label / aria-labelledby is also a valid, standards-recognized
|
|
191
232
|
// text-alternative mechanism for <area> (HTML-AAM accessible name
|
|
@@ -201,8 +242,8 @@ function runInPage(ctx) {
|
|
|
201
242
|
}
|
|
202
243
|
|
|
203
244
|
// A non-empty title attribute is HTML-AAM's own next fallback naming
|
|
204
|
-
// source once alt
|
|
205
|
-
// for <img title="..."> with no alt.
|
|
245
|
+
// source once alt gives no name (missing or empty). Same gap
|
|
246
|
+
// img-alt-present handles for <img title="..."> with no alt.
|
|
206
247
|
const titleRaw = (() => {
|
|
207
248
|
try {
|
|
208
249
|
return el.getAttribute('title');
|
|
@@ -214,21 +255,36 @@ function runInPage(ctx) {
|
|
|
214
255
|
|
|
215
256
|
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
216
257
|
|
|
217
|
-
const baseOccurrence =
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
258
|
+
const baseOccurrence = hasAltAttr
|
|
259
|
+
? {
|
|
260
|
+
// Leave selector/html empty so the engine can fill them from __node.
|
|
261
|
+
selector: '',
|
|
262
|
+
html: '',
|
|
263
|
+
summary: 'Empty alt attribute leaves <area> link with no accessible name.',
|
|
264
|
+
hint: 'Describe the link destination in alt, or add aria-label/aria-labelledby (an <area> cannot be decorative once its map is used).',
|
|
265
|
+
i18n: {
|
|
266
|
+
summaryKey: 'area_altPresent_summary_fail_empty',
|
|
267
|
+
hintKey: 'area_altPresent_hint_fail_empty',
|
|
268
|
+
params: { element: 'area' }
|
|
269
|
+
},
|
|
270
|
+
data: {
|
|
271
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
: {
|
|
275
|
+
selector: '',
|
|
276
|
+
html: '',
|
|
277
|
+
summary: 'Missing alt attribute on <area>.',
|
|
278
|
+
hint: 'Add an alt attribute describing the link destination (an <area> cannot be decorative).',
|
|
279
|
+
i18n: {
|
|
280
|
+
summaryKey: 'area_altPresent_summary_fail',
|
|
281
|
+
hintKey: 'area_altPresent_hint_fail',
|
|
282
|
+
params: { element: 'area' }
|
|
283
|
+
},
|
|
284
|
+
data: {
|
|
285
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
286
|
+
}
|
|
287
|
+
};
|
|
232
288
|
|
|
233
289
|
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
234
290
|
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
@@ -934,6 +934,12 @@ function runInPage(ctx) {
|
|
|
934
934
|
hintKey: 'ariaAllowedAttr_hint_cantTell',
|
|
935
935
|
params: { attr: name, role }
|
|
936
936
|
},
|
|
937
|
+
uncertainty: {
|
|
938
|
+
code: 'spec-only',
|
|
939
|
+
needed:
|
|
940
|
+
'Whether the deprecated attribute is still honoured by the assistive technology in use.',
|
|
941
|
+
evidence: { attribute: name, role, status: 'deprecated-for-role' }
|
|
942
|
+
},
|
|
937
943
|
data: {
|
|
938
944
|
details: { reasonCode: 'ARIA_ATTR_DEPRECATED', attr: name, role }
|
|
939
945
|
}
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* @check aria-allowed-role
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary Explicit role must be permitted by the ARIA-in-HTML spec for its host element
|
|
9
|
-
* @standard WCAG
|
|
10
|
-
* @sc 4.1.2
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
11
10
|
* @applicability
|
|
12
11
|
* Applies to elements with an explicit, valid, non-abstract role, where
|
|
13
12
|
* the host element/attribute combination has an asserted permitted-roles
|
|
@@ -16,7 +15,18 @@
|
|
|
16
15
|
* @expectation
|
|
17
16
|
* The explicit role is one of the roles the ARIA-in-HTML specification
|
|
18
17
|
* permits for that host element.
|
|
18
|
+
* Reported at CANTTELL rather than FAIL: ARIA-in-HTML's permitted-roles
|
|
19
|
+
* table is an author conformance requirement with no ACT rule and no WCAG
|
|
20
|
+
* mapping in any source. The role the author asked for is still the role
|
|
21
|
+
* assistive technology exposes, so whether the combination harms anyone
|
|
22
|
+
* depends on the widget, not on the table.
|
|
19
23
|
* @implementation-notes
|
|
24
|
+
* - Not WCAG-normative. ARIA-in-HTML's permitted-roles table is an author
|
|
25
|
+
* conformance requirement of that specification; no ACT rule covers it and
|
|
26
|
+
* no source maps it to a Success Criterion, so the rule reports the
|
|
27
|
+
* violation without claiming a criterion is failed. Deterministic all the
|
|
28
|
+
* same, so it stays `type: 'automatic'` and keeps deciding rather than
|
|
29
|
+
* deferring to a reviewer -- see docs/RULE_TAXONOMY.md 1.1.
|
|
20
30
|
* - Scoped to elements present in ALLOWED_ROLES_BY_ELEMENT;
|
|
21
31
|
* elements without an asserted constraint are treated as "no constraint"
|
|
22
32
|
* (not flagged) rather than guessed at, see that table's header comment.
|
|
@@ -36,22 +46,14 @@ const meta = {
|
|
|
36
46
|
descriptionKey: 'ariaAllowedRole_description'
|
|
37
47
|
},
|
|
38
48
|
helpUrl: null,
|
|
39
|
-
tags: ['
|
|
40
|
-
wcagSc: [
|
|
41
|
-
normativeMappings: [
|
|
42
|
-
{
|
|
43
|
-
standard: 'WCAG',
|
|
44
|
-
version: '2.2',
|
|
45
|
-
requirement: '4.1.2',
|
|
46
|
-
title: 'Name, Role, Value',
|
|
47
|
-
conformanceLevel: 'A'
|
|
48
|
-
}
|
|
49
|
-
],
|
|
49
|
+
tags: ['best-practice', 'aria', 'structure', 'atomic', 'automatic'],
|
|
50
|
+
wcagSc: [],
|
|
51
|
+
normativeMappings: [],
|
|
50
52
|
defaultSeverity: 'moderate',
|
|
51
53
|
category: 'robust',
|
|
52
54
|
type: 'automatic',
|
|
53
55
|
defaultConfidence: 'high',
|
|
54
|
-
coverage: {
|
|
56
|
+
coverage: {}
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
function runInPage(ctx) {
|
|
@@ -93,6 +95,16 @@ function runInPage(ctx) {
|
|
|
93
95
|
hintKey: 'ariaAllowedRole_hint_fail',
|
|
94
96
|
params: { role, element: tag }
|
|
95
97
|
},
|
|
98
|
+
uncertainty: {
|
|
99
|
+
code: 'spec-only',
|
|
100
|
+
needed: 'Whether the role misstates this element to assistive technology in practice.',
|
|
101
|
+
evidence: {
|
|
102
|
+
role,
|
|
103
|
+
element: tag,
|
|
104
|
+
source: 'ARIA in HTML permitted-roles table',
|
|
105
|
+
wcagSc: []
|
|
106
|
+
}
|
|
107
|
+
},
|
|
96
108
|
data: {
|
|
97
109
|
details: { reasonCode: 'ARIA_ROLE_NOT_ALLOWED_FOR_ELEMENT', role, element: tag }
|
|
98
110
|
}
|
|
@@ -103,15 +115,12 @@ function runInPage(ctx) {
|
|
|
103
115
|
if (applicableCount === 0) {
|
|
104
116
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
105
117
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
118
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
119
|
+
[],
|
|
120
|
+
occurrences,
|
|
121
|
+
rule.defaultSeverity || 'moderate'
|
|
122
|
+
);
|
|
123
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
module.exports = { id, meta, runInPage };
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
* Using either braille-specific attribute as the ONLY naming mechanism
|
|
24
24
|
* leaves non-braille assistive technology (most screen readers, voice
|
|
25
25
|
* control, etc.) with no accessible name/role description at all.
|
|
26
|
+
* Reported at CANTTELL rather than FAIL: aria-brailleroledescription
|
|
27
|
+
* without aria-roledescription reaches no user at all, and a missing
|
|
28
|
+
* accessible name is the naming rules' decision for the roles that require
|
|
29
|
+
* one. The braille attribute being unpaired is worth surfacing, but it is
|
|
30
|
+
* not itself a criterion failing.
|
|
26
31
|
* @implementation-notes
|
|
27
32
|
* - `aria-braillelabel`/`aria-brailleroledescription` do not participate
|
|
28
33
|
* in the standard accessible-name computation, so
|
|
@@ -52,7 +57,7 @@ const meta = {
|
|
|
52
57
|
conformanceLevel: 'A'
|
|
53
58
|
}
|
|
54
59
|
],
|
|
55
|
-
defaultSeverity: '
|
|
60
|
+
defaultSeverity: 'moderate',
|
|
56
61
|
category: 'robust',
|
|
57
62
|
type: 'automatic',
|
|
58
63
|
defaultConfidence: 'high',
|
|
@@ -108,13 +113,28 @@ function runInPage(ctx) {
|
|
|
108
113
|
nameInfo && typeof nameInfo.value === 'string' ? nameInfo.value : ''
|
|
109
114
|
);
|
|
110
115
|
const name = programmaticName || getConservativeSubtreeText(el);
|
|
111
|
-
if (!name)
|
|
116
|
+
if (!name)
|
|
117
|
+
missing.push({
|
|
118
|
+
attr: 'aria-braillelabel',
|
|
119
|
+
requires: 'an accessible name',
|
|
120
|
+
messageKey: 'label',
|
|
121
|
+
summary:
|
|
122
|
+
'This element has aria-braillelabel but no accessible name, its non-braille equivalent.',
|
|
123
|
+
hint: 'aria-braillelabel is a Braille-specific supplement, not a replacement, so also give the element an accessible name (for example visible text, aria-label or aria-labelledby).'
|
|
124
|
+
});
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
if (brailleRoleDesc) {
|
|
115
128
|
const roleDesc = trim(el.getAttribute('aria-roledescription'));
|
|
116
129
|
if (!roleDesc)
|
|
117
|
-
missing.push({
|
|
130
|
+
missing.push({
|
|
131
|
+
attr: 'aria-brailleroledescription',
|
|
132
|
+
requires: 'aria-roledescription',
|
|
133
|
+
messageKey: 'roleDescription',
|
|
134
|
+
summary:
|
|
135
|
+
'This element has aria-brailleroledescription but not aria-roledescription, its non-braille equivalent.',
|
|
136
|
+
hint: 'aria-brailleroledescription is a Braille-specific supplement, not a replacement, so also provide aria-roledescription.'
|
|
137
|
+
});
|
|
118
138
|
}
|
|
119
139
|
|
|
120
140
|
if (!missing.length) continue;
|
|
@@ -124,12 +144,21 @@ function runInPage(ctx) {
|
|
|
124
144
|
for (const m of missing) {
|
|
125
145
|
occurrences.push(
|
|
126
146
|
helpers.reportOccurrence(el, {
|
|
127
|
-
|
|
128
|
-
|
|
147
|
+
// One message per missing equivalent rather than a shared template:
|
|
148
|
+
// `requires` is English prose for the label case, and interpolating
|
|
149
|
+
// it put English into every translated sentence.
|
|
150
|
+
summary: m.summary,
|
|
151
|
+
hint: m.hint,
|
|
129
152
|
i18n: {
|
|
130
|
-
summaryKey:
|
|
131
|
-
hintKey:
|
|
132
|
-
params: { element: tag, attr: m.attr
|
|
153
|
+
summaryKey: `ariaBrailleEquivalent_summary_fail_${m.messageKey}`,
|
|
154
|
+
hintKey: `ariaBrailleEquivalent_hint_fail_${m.messageKey}`,
|
|
155
|
+
params: { element: tag, attr: m.attr }
|
|
156
|
+
},
|
|
157
|
+
uncertainty: {
|
|
158
|
+
code: 'spec-only',
|
|
159
|
+
needed:
|
|
160
|
+
'Whether any user reaches this attribute, given no braille equivalent is exposed.',
|
|
161
|
+
evidence: { element: tag, attribute: m.attr, requires: m.requires }
|
|
133
162
|
},
|
|
134
163
|
data: {
|
|
135
164
|
details: {
|
|
@@ -146,15 +175,12 @@ function runInPage(ctx) {
|
|
|
146
175
|
if (applicableCount === 0) {
|
|
147
176
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
148
177
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
178
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
179
|
+
[],
|
|
180
|
+
occurrences,
|
|
181
|
+
rule.defaultSeverity || 'moderate'
|
|
182
|
+
);
|
|
183
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
158
184
|
}
|
|
159
185
|
|
|
160
186
|
module.exports = { id, meta, runInPage };
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
* An element with `aria-errormessage` but `aria-invalid` absent or
|
|
18
18
|
* `"false"` silently drops the error message from the accessibility
|
|
19
19
|
* tree, authors almost always intend it to be exposed.
|
|
20
|
+
* Reported at CANTTELL rather than FAIL: aria-errormessage is only
|
|
21
|
+
* exposed once aria-invalid is set, so the reference is currently inert.
|
|
22
|
+
* Whether that costs the user anything depends on whether the message is
|
|
23
|
+
* conveyed some other way (visible text next to the field, aria-describedby),
|
|
24
|
+
* which static markup does not settle.
|
|
20
25
|
* @implementation-notes
|
|
21
26
|
* - This is narrow: the broader space is a table of many
|
|
22
27
|
* attribute/condition pairs. This rule implements only the one pairing
|
|
@@ -52,7 +57,7 @@ const meta = {
|
|
|
52
57
|
conformanceLevel: 'A'
|
|
53
58
|
}
|
|
54
59
|
],
|
|
55
|
-
defaultSeverity: '
|
|
60
|
+
defaultSeverity: 'moderate',
|
|
56
61
|
category: 'robust',
|
|
57
62
|
type: 'automatic',
|
|
58
63
|
defaultConfidence: 'high',
|
|
@@ -98,6 +103,11 @@ function runInPage(ctx) {
|
|
|
98
103
|
hintKey: 'ariaConditionalAttr_hint_fail',
|
|
99
104
|
params: { element: tag, ariaInvalid: invalidValue || '(absent)' }
|
|
100
105
|
},
|
|
106
|
+
uncertainty: {
|
|
107
|
+
code: 'spec-only',
|
|
108
|
+
needed: 'Whether the field is ever in an invalid state that should expose this message.',
|
|
109
|
+
evidence: { element: tag, ariaInvalid: invalidValue || null }
|
|
110
|
+
},
|
|
101
111
|
data: {
|
|
102
112
|
details: {
|
|
103
113
|
reasonCode: 'ARIA_ERRORMESSAGE_WITHOUT_TRUTHY_INVALID',
|
|
@@ -111,15 +121,12 @@ function runInPage(ctx) {
|
|
|
111
121
|
if (applicableCount === 0) {
|
|
112
122
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
113
123
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
124
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
125
|
+
[],
|
|
126
|
+
occurrences,
|
|
127
|
+
rule.defaultSeverity || 'moderate'
|
|
128
|
+
);
|
|
129
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
module.exports = { id, meta, runInPage };
|
|
@@ -161,6 +161,12 @@ function runInPage(ctx) {
|
|
|
161
161
|
hintKey: guidance.key,
|
|
162
162
|
params: { role }
|
|
163
163
|
},
|
|
164
|
+
uncertainty: {
|
|
165
|
+
code: 'spec-only',
|
|
166
|
+
needed:
|
|
167
|
+
'Whether this user-agent-reserved role misleads the assistive technology in use.',
|
|
168
|
+
evidence: { role, ariaStatus: 'discouraged-for-authors' }
|
|
169
|
+
},
|
|
164
170
|
data: {
|
|
165
171
|
details: { reasonCode: 'ARIA_ROLE_AUTHOR_DISCOURAGED', role, guidance: guidance.text }
|
|
166
172
|
}
|
|
@@ -178,6 +184,12 @@ function runInPage(ctx) {
|
|
|
178
184
|
hintKey: guidance.key,
|
|
179
185
|
params: { role }
|
|
180
186
|
},
|
|
187
|
+
uncertainty: {
|
|
188
|
+
code: 'spec-only',
|
|
189
|
+
needed:
|
|
190
|
+
'Whether the deprecated role is still honoured, and what it should be replaced with.',
|
|
191
|
+
evidence: { role, ariaStatus: 'deprecated' }
|
|
192
|
+
},
|
|
181
193
|
data: {
|
|
182
194
|
details: { reasonCode: 'ARIA_ROLE_DEPRECATED', role, guidance: guidance.text }
|
|
183
195
|
}
|
|
@@ -33,7 +33,7 @@ const meta = {
|
|
|
33
33
|
descriptionKey: 'ariaHiddenBody_description'
|
|
34
34
|
},
|
|
35
35
|
helpUrl: null,
|
|
36
|
-
tags: ['wcag2a', 'wcag131', 'wcag412', 'structure', 'atomic', 'automatic'],
|
|
36
|
+
tags: ['wcag2a', 'wcag131', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
37
37
|
wcagSc: ['1.3.1', '4.1.2'],
|
|
38
38
|
normativeMappings: [
|
|
39
39
|
{
|