@surea11y/core 1.2.0 → 1.4.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 +81 -7
- package/LICENSE +373 -21
- package/README.md +175 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +27 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +19 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +63 -18
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +63 -31
- package/src/checks/automatic/aria-allowed-attr.js +204 -80
- package/src/checks/automatic/aria-allowed-role.js +23 -7
- package/src/checks/automatic/aria-braille-equivalent.js +34 -10
- package/src/checks/automatic/aria-conditional-attr.js +32 -14
- package/src/checks/automatic/aria-deprecated-role.js +26 -11
- package/src/checks/automatic/aria-hidden-body.js +48 -23
- package/src/checks/automatic/aria-hidden-focus.js +420 -66
- package/src/checks/automatic/aria-prohibited-attr.js +327 -60
- package/src/checks/automatic/aria-prohibited-children.js +111 -103
- package/src/checks/automatic/aria-required-attr.js +29 -15
- package/src/checks/automatic/aria-required-children.js +44 -24
- package/src/checks/automatic/aria-required-parent.js +64 -35
- package/src/checks/automatic/aria-role-name-present.js +49 -21
- package/src/checks/automatic/aria-roles-valid.js +24 -12
- package/src/checks/automatic/aria-valid-attr-value.js +46 -22
- package/src/checks/automatic/aria-valid-attr.js +19 -5
- package/src/checks/automatic/autocomplete-valid.js +76 -16
- package/src/checks/automatic/avoid-inline-spacing.js +23 -8
- package/src/checks/automatic/binary-control-name-present.js +62 -50
- package/src/checks/automatic/button-name-present.js +54 -24
- package/src/checks/automatic/bypass-blocks-present.js +51 -32
- package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
- package/src/checks/automatic/combobox-name-present.js +40 -45
- package/src/checks/automatic/contrast-computable.js +363 -341
- package/src/checks/automatic/contrast-enhanced.js +489 -466
- package/src/checks/automatic/contrast-minimum.js +488 -465
- package/src/checks/automatic/css-orientation-lock.js +51 -35
- package/src/checks/automatic/definition-list-children-valid.js +46 -25
- package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
- package/src/checks/automatic/dialog-name-present.js +47 -85
- package/src/checks/automatic/dlitem-parent-valid.js +25 -8
- package/src/checks/automatic/duplicate-id-aria.js +28 -9
- package/src/checks/automatic/embed-text-alternative-present.js +88 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
- package/src/checks/automatic/form-control-single-label.js +50 -14
- package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
- package/src/checks/automatic/iframe-focusable-content.js +265 -22
- package/src/checks/automatic/iframe-name-present.js +33 -9
- package/src/checks/automatic/iframe-title-unique.js +32 -9
- package/src/checks/automatic/img-alt-present.js +54 -52
- package/src/checks/automatic/input-image-alt-present.js +141 -112
- package/src/checks/automatic/label-in-name.js +65 -41
- package/src/checks/automatic/language-page-present.js +111 -109
- package/src/checks/automatic/link-in-text-block.js +61 -19
- package/src/checks/automatic/link-name-present.js +47 -14
- package/src/checks/automatic/list-children-valid.js +40 -33
- package/src/checks/automatic/listbox-name-present.js +41 -19
- package/src/checks/automatic/listitem-parent-valid.js +48 -13
- package/src/checks/automatic/menuitem-name-present.js +41 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
- package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
- package/src/checks/automatic/meter-name-present.js +40 -36
- package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
- package/src/checks/automatic/object-text-alternative-present.js +93 -39
- package/src/checks/automatic/option-name-present.js +40 -21
- package/src/checks/automatic/page-title-present.js +19 -6
- package/src/checks/automatic/progressbar-name-present.js +49 -44
- package/src/checks/automatic/role-img-alt-present.js +211 -159
- package/src/checks/automatic/searchbox-name-present.js +41 -19
- package/src/checks/automatic/server-side-image-map-absent.js +27 -11
- package/src/checks/automatic/slider-name-present.js +42 -47
- package/src/checks/automatic/spinbutton-name-present.js +41 -19
- package/src/checks/automatic/summary-name-present.js +39 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
- package/src/checks/automatic/svg-text-alternative-present.js +262 -230
- package/src/checks/automatic/tab-name-present.js +39 -60
- package/src/checks/automatic/table-headers-attr-valid.js +27 -10
- package/src/checks/automatic/table-th-has-data-cells.js +24 -8
- package/src/checks/automatic/target-size-minimum.js +123 -48
- package/src/checks/automatic/td-has-header.js +53 -12
- package/src/checks/automatic/textbox-name-present.js +41 -19
- package/src/checks/automatic/tooltip-name-present.js +39 -18
- package/src/checks/automatic/treeitem-name-present.js +40 -21
- package/src/checks/automatic/valid-lang.js +22 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
- package/src/checks/manual/accesskeys-manual.js +17 -6
- package/src/checks/manual/area-alt-decorative-manual.js +194 -193
- package/src/checks/manual/area-alt-quality-manual.js +184 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
- package/src/checks/manual/aria-text-manual.js +20 -11
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
- package/src/checks/manual/css-hidden-focus.js +375 -169
- package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
- package/src/checks/manual/empty-heading-manual.js +41 -24
- package/src/checks/manual/empty-table-header-manual.js +69 -31
- package/src/checks/manual/focus-order-semantics-manual.js +60 -13
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
- package/src/checks/manual/heading-order-manual.js +50 -8
- package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
- package/src/checks/manual/image-redundant-alt-manual.js +38 -8
- package/src/checks/manual/img-alt-decorative-manual.js +133 -96
- package/src/checks/manual/img-alt-quality-manual.js +178 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
- package/src/checks/manual/label-title-only-manual.js +44 -28
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
- package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
- package/src/checks/manual/landmark-one-main-manual.js +38 -43
- package/src/checks/manual/landmark-unique-manual.js +78 -67
- package/src/checks/manual/link-name-quality-manual.js +45 -12
- package/src/checks/manual/media-transcript-present-manual.js +37 -22
- package/src/checks/manual/meta-viewport-large-manual.js +19 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
- package/src/checks/manual/p-as-heading-manual.js +24 -7
- package/src/checks/manual/page-has-heading-one-manual.js +42 -32
- package/src/checks/manual/page-title-patterns-manual.js +80 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
- package/src/checks/manual/region-manual.js +244 -60
- package/src/checks/manual/scope-attr-valid-manual.js +13 -4
- package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
- package/src/checks/manual/skip-link-manual.js +42 -18
- package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
- package/src/checks/manual/tabindex-manual.js +13 -4
- package/src/checks/manual/table-duplicate-name-manual.js +22 -11
- package/src/checks/manual/table-fake-caption-manual.js +48 -10
- package/src/checks/manual/video-caption-manual.js +17 -4
- package/src/checks/manual-review.js +58 -12
- package/src/core.js +41705 -29650
- package/src/index.js +2 -0
- package/src/report.js +109 -47
- package/src/sarif.js +190 -0
- package/surea11y.browser.js +37774 -0
- package/bin/core.js +0 -348
- package/docs/CLI.md +0 -75
- package/src/catalogs/composites.wcag.js +0 -490
- package/src/checks/rules-and-tags.full.csv +0 -19
- package/src/checks/rules-and-tags.full.json +0 -259
- package/src/core/aria-helpers.js +0 -970
- package/src/core/contrast-helpers.js +0 -1147
- package/src/core/dom-helpers.js +0 -4235
- package/src/core/dom-runner.js +0 -671
- package/src/core/frame-messaging.js +0 -210
- package/src/core/frame-scan.js +0 -178
- package/src/core/rollup-composites.js +0 -135
- package/src/core/rule-meta.js +0 -159
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -923
- package/src/i18n/fr.js +0 -844
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -55
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'searchbox-name-present';
|
|
@@ -10,10 +12,16 @@ const meta = {
|
|
|
10
12
|
descriptionKey: 'searchboxNamePresent_description'
|
|
11
13
|
},
|
|
12
14
|
helpUrl: null,
|
|
13
|
-
tags: [
|
|
15
|
+
tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'searchbox'],
|
|
14
16
|
wcagSc: ['4.1.2'],
|
|
15
17
|
normativeMappings: [
|
|
16
|
-
{
|
|
18
|
+
{
|
|
19
|
+
standard: 'WCAG',
|
|
20
|
+
version: '2.2',
|
|
21
|
+
requirement: '4.1.2',
|
|
22
|
+
title: 'Name, Role, Value',
|
|
23
|
+
conformanceLevel: 'A'
|
|
24
|
+
}
|
|
17
25
|
],
|
|
18
26
|
defaultSeverity: 'serious',
|
|
19
27
|
category: 'robust',
|
|
@@ -24,20 +32,22 @@ const meta = {
|
|
|
24
32
|
|
|
25
33
|
function runInPage(ctx) {
|
|
26
34
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
35
|
+
const getEligibilityInfo =
|
|
36
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
37
|
|
|
32
38
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
39
|
+
return String(s || '')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
function getAttr(el, name) {
|
|
37
45
|
try {
|
|
38
46
|
if (!el || !el.getAttribute) return '';
|
|
39
47
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -52,7 +62,7 @@ function runInPage(ctx) {
|
|
|
52
62
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
63
|
return info && info.present ? info.value : '';
|
|
54
64
|
}
|
|
55
|
-
const t =
|
|
65
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
56
66
|
return t.replace(/\s+/g, ' ').trim();
|
|
57
67
|
}
|
|
58
68
|
|
|
@@ -100,7 +110,8 @@ function runInPage(ctx) {
|
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
function isEligibleAcc(helpers, el, ctx) {
|
|
103
|
-
const fn =
|
|
113
|
+
const fn =
|
|
114
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
104
115
|
if (!fn) return true;
|
|
105
116
|
try {
|
|
106
117
|
const r = fn(el, ctx);
|
|
@@ -126,12 +137,13 @@ function runInPage(ctx) {
|
|
|
126
137
|
return map;
|
|
127
138
|
}
|
|
128
139
|
|
|
129
|
-
|
|
130
140
|
const occurrences = [];
|
|
131
141
|
let applicableCount = 0;
|
|
132
142
|
|
|
133
|
-
const selector =
|
|
134
|
-
const nodes = helpers.queryAllSmart
|
|
143
|
+
const selector = '[role="searchbox"]';
|
|
144
|
+
const nodes = helpers.queryAllSmart
|
|
145
|
+
? helpers.queryAllSmart(selector)
|
|
146
|
+
: helpers.queryAll(selector);
|
|
135
147
|
|
|
136
148
|
// Precompute label[for] map for searchbox elements that are labelable
|
|
137
149
|
// native form controls (e.g. <input role="searchbox">).
|
|
@@ -200,16 +212,22 @@ function runInPage(ctx) {
|
|
|
200
212
|
if (res.ok) continue;
|
|
201
213
|
|
|
202
214
|
const eligInfo = getEligibilityInfo
|
|
203
|
-
|
|
204
|
-
|
|
215
|
+
? (() => {
|
|
216
|
+
try {
|
|
217
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
})()
|
|
222
|
+
: null;
|
|
205
223
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
206
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
224
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
207
225
|
|
|
208
226
|
occurrences.push({
|
|
209
227
|
selector: stableSelector,
|
|
210
228
|
html,
|
|
211
229
|
summary: 'This element has no accessible name.',
|
|
212
|
-
hint:
|
|
230
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this searchbox's accessible name.",
|
|
213
231
|
i18n: {
|
|
214
232
|
summaryKey: 'searchboxNamePresent_summary_fail',
|
|
215
233
|
hintKey: 'searchboxNamePresent_hint_fail',
|
|
@@ -222,12 +240,16 @@ function runInPage(ctx) {
|
|
|
222
240
|
});
|
|
223
241
|
}
|
|
224
242
|
|
|
225
|
-
|
|
226
243
|
if (applicableCount === 0) {
|
|
227
244
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
228
245
|
}
|
|
229
246
|
if (occurrences.length) {
|
|
230
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
ruleId: rule.ruleId,
|
|
249
|
+
outcome: 'fail',
|
|
250
|
+
severity: rule.defaultSeverity || 'minor',
|
|
251
|
+
occurrences
|
|
252
|
+
};
|
|
231
253
|
}
|
|
232
254
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
233
255
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -16,16 +18,16 @@
|
|
|
16
18
|
* users. Client-side image maps (<map>/<area>, each with real href/alt)
|
|
17
19
|
* are the accessible alternative and are not flagged by this rule.
|
|
18
20
|
* @implementation-notes
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
* alternative" exists elsewhere on the page).
|
|
21
|
+
* - Presence of ismap is itself the violation (there is no automatable way
|
|
22
|
+
* to verify a "usable alternative" exists elsewhere on the page).
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
25
|
const id = 'server-side-image-map-absent';
|
|
25
26
|
|
|
26
27
|
const meta = {
|
|
27
28
|
title: 'Images must not use a server-side image map',
|
|
28
|
-
description:
|
|
29
|
+
description:
|
|
30
|
+
'Checks that <img> elements do not carry the ismap attribute (server-side image maps have no keyboard-operable equivalent).',
|
|
29
31
|
i18n: {
|
|
30
32
|
titleKey: 'serverSideImageMapAbsent_title',
|
|
31
33
|
descriptionKey: 'serverSideImageMapAbsent_description'
|
|
@@ -34,7 +36,13 @@ const meta = {
|
|
|
34
36
|
tags: ['wcag2a', 'wcag211', 'structure', 'atomic', 'automatic', 'keyboard'],
|
|
35
37
|
wcagSc: ['2.1.1'],
|
|
36
38
|
normativeMappings: [
|
|
37
|
-
{
|
|
39
|
+
{
|
|
40
|
+
standard: 'WCAG',
|
|
41
|
+
version: '2.2',
|
|
42
|
+
requirement: '2.1.1',
|
|
43
|
+
title: 'Keyboard',
|
|
44
|
+
conformanceLevel: 'A'
|
|
45
|
+
}
|
|
38
46
|
],
|
|
39
47
|
defaultSeverity: 'serious',
|
|
40
48
|
category: 'operable',
|
|
@@ -44,9 +52,11 @@ const meta = {
|
|
|
44
52
|
};
|
|
45
53
|
|
|
46
54
|
function runInPage(ctx) {
|
|
47
|
-
const {
|
|
55
|
+
const { helpers, rule } = ctx;
|
|
48
56
|
|
|
49
|
-
const nodes = helpers.queryAllSmart
|
|
57
|
+
const nodes = helpers.queryAllSmart
|
|
58
|
+
? helpers.queryAllSmart('img[ismap]')
|
|
59
|
+
: helpers.queryAll('img[ismap]');
|
|
50
60
|
|
|
51
61
|
const occurrences = [];
|
|
52
62
|
let applicableCount = 0;
|
|
@@ -57,12 +67,13 @@ function runInPage(ctx) {
|
|
|
57
67
|
applicableCount += 1;
|
|
58
68
|
|
|
59
69
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
60
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
70
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
61
71
|
|
|
62
72
|
occurrences.push({
|
|
63
73
|
selector: stableSelector,
|
|
64
74
|
html,
|
|
65
|
-
summary:
|
|
75
|
+
summary:
|
|
76
|
+
'This image uses a server-side image map, which has no keyboard-operable equivalent.',
|
|
66
77
|
hint: 'Replace the server-side image map (ismap) with a client-side image map (<map>/<area>) or separate accessible links/buttons.',
|
|
67
78
|
i18n: {
|
|
68
79
|
summaryKey: 'serverSideImageMapAbsent_summary_fail',
|
|
@@ -79,9 +90,14 @@ function runInPage(ctx) {
|
|
|
79
90
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
80
91
|
}
|
|
81
92
|
if (occurrences.length) {
|
|
82
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
ruleId: rule.ruleId,
|
|
95
|
+
outcome: 'fail',
|
|
96
|
+
severity: rule.defaultSeverity || 'serious',
|
|
97
|
+
occurrences
|
|
98
|
+
};
|
|
83
99
|
}
|
|
84
100
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
85
101
|
}
|
|
86
102
|
|
|
87
|
-
module.exports = { id, meta, runInPage };
|
|
103
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -1,19 +1,28 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'slider-name-present';
|
|
4
6
|
|
|
5
7
|
const meta = {
|
|
6
8
|
title: 'Sliders have an accessible name',
|
|
7
|
-
description:
|
|
9
|
+
description:
|
|
10
|
+
'Checks that sliders (input[type="range"] and role="slider") expose a non-empty accessible name.',
|
|
8
11
|
i18n: {
|
|
9
12
|
titleKey: 'sliderNamePresent_title',
|
|
10
13
|
descriptionKey: 'sliderNamePresent_description'
|
|
11
14
|
},
|
|
12
15
|
helpUrl: null,
|
|
13
|
-
tags: ['wcag2a','wcag412','forms','atomic','automatic','name','slider'],
|
|
16
|
+
tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'slider'],
|
|
14
17
|
wcagSc: ['4.1.2'],
|
|
15
18
|
normativeMappings: [
|
|
16
|
-
{
|
|
19
|
+
{
|
|
20
|
+
standard: 'WCAG',
|
|
21
|
+
version: '2.2',
|
|
22
|
+
requirement: '4.1.2',
|
|
23
|
+
title: 'Name, Role, Value',
|
|
24
|
+
conformanceLevel: 'A'
|
|
25
|
+
}
|
|
17
26
|
],
|
|
18
27
|
defaultSeverity: 'serious',
|
|
19
28
|
category: 'robust',
|
|
@@ -24,38 +33,22 @@ const meta = {
|
|
|
24
33
|
|
|
25
34
|
function runInPage(ctx) {
|
|
26
35
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
36
|
+
const getEligibilityInfo =
|
|
37
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
38
|
|
|
32
39
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
40
|
+
return String(s || '')
|
|
41
|
+
.replace(/\s+/g, ' ')
|
|
42
|
+
.trim();
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
function getAttr(el, name) {
|
|
37
46
|
try {
|
|
38
47
|
if (!el || !el.getAttribute) return '';
|
|
39
48
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
function hasAttr(el, name) {
|
|
44
|
-
try { return !!(el && el.hasAttribute && el.hasAttribute(name)); } catch { return false; }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isExplicitProgrammatic(el) {
|
|
48
|
-
// Programmatic name mechanisms we treat as authoritative (presence-only):
|
|
49
|
-
// - aria-label (non-empty)
|
|
50
|
-
// - aria-labelledby (non-empty)
|
|
51
|
-
// - title (non-empty) [weak but allowed for presence]
|
|
52
|
-
const al = getAttr(el, 'aria-label');
|
|
53
|
-
if (al) return true;
|
|
54
|
-
const alb = getAttr(el, 'aria-labelledby');
|
|
55
|
-
if (alb) return true;
|
|
56
|
-
const t = getAttr(el, 'title');
|
|
57
|
-
if (t) return true;
|
|
58
|
-
return false;
|
|
49
|
+
} catch {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
59
52
|
}
|
|
60
53
|
|
|
61
54
|
function buildLabelForMap(doc) {
|
|
@@ -85,7 +78,7 @@ function runInPage(ctx) {
|
|
|
85
78
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
86
79
|
return info && info.present ? info.value : '';
|
|
87
80
|
}
|
|
88
|
-
const t =
|
|
81
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
89
82
|
return t.replace(/\s+/g, ' ').trim();
|
|
90
83
|
}
|
|
91
84
|
|
|
@@ -132,17 +125,9 @@ function runInPage(ctx) {
|
|
|
132
125
|
return '';
|
|
133
126
|
}
|
|
134
127
|
|
|
135
|
-
function getInputValueName(el) {
|
|
136
|
-
try {
|
|
137
|
-
const type = getAttr(el, 'type').toLowerCase();
|
|
138
|
-
if (type !== 'button' && type !== 'submit' && type !== 'reset') return '';
|
|
139
|
-
const v = getAttr(el, 'value');
|
|
140
|
-
return v;
|
|
141
|
-
} catch { return ''; }
|
|
142
|
-
}
|
|
143
|
-
|
|
144
128
|
function isEligibleAcc(helpers, el, ctx) {
|
|
145
|
-
const fn =
|
|
129
|
+
const fn =
|
|
130
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
146
131
|
if (!fn) return true;
|
|
147
132
|
try {
|
|
148
133
|
const r = fn(el, ctx);
|
|
@@ -153,13 +138,13 @@ function runInPage(ctx) {
|
|
|
153
138
|
}
|
|
154
139
|
}
|
|
155
140
|
|
|
156
|
-
|
|
157
141
|
const occurrences = [];
|
|
158
142
|
let applicableCount = 0;
|
|
159
143
|
|
|
160
144
|
const selector = 'input[type="range"], [role="slider"]';
|
|
161
|
-
const nodes = helpers.queryAllSmart
|
|
162
|
-
|
|
145
|
+
const nodes = helpers.queryAllSmart
|
|
146
|
+
? helpers.queryAllSmart(selector)
|
|
147
|
+
: helpers.queryAll(selector);
|
|
163
148
|
|
|
164
149
|
// Precompute label[for] map for native range inputs.
|
|
165
150
|
const labelForMap = buildLabelForMap(document);
|
|
@@ -229,7 +214,7 @@ function runInPage(ctx) {
|
|
|
229
214
|
const type = getAttr(el, 'type').toLowerCase();
|
|
230
215
|
const role = getAttr(el, 'role').toLowerCase();
|
|
231
216
|
|
|
232
|
-
let kind
|
|
217
|
+
let kind;
|
|
233
218
|
if (tag === 'input' && type === 'range') kind = 'native-slider';
|
|
234
219
|
else if (role === 'slider') kind = 'aria-slider';
|
|
235
220
|
else continue;
|
|
@@ -240,10 +225,16 @@ function runInPage(ctx) {
|
|
|
240
225
|
if (res.ok) continue;
|
|
241
226
|
|
|
242
227
|
const eligInfo = getEligibilityInfo
|
|
243
|
-
|
|
244
|
-
|
|
228
|
+
? (() => {
|
|
229
|
+
try {
|
|
230
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
})()
|
|
235
|
+
: null;
|
|
245
236
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
246
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
237
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
247
238
|
|
|
248
239
|
occurrences.push({
|
|
249
240
|
selector: stableSelector,
|
|
@@ -262,12 +253,16 @@ function runInPage(ctx) {
|
|
|
262
253
|
});
|
|
263
254
|
}
|
|
264
255
|
|
|
265
|
-
|
|
266
256
|
if (applicableCount === 0) {
|
|
267
257
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
268
258
|
}
|
|
269
259
|
if (occurrences.length) {
|
|
270
|
-
return {
|
|
260
|
+
return {
|
|
261
|
+
ruleId: rule.ruleId,
|
|
262
|
+
outcome: 'fail',
|
|
263
|
+
severity: rule.defaultSeverity || 'minor',
|
|
264
|
+
occurrences
|
|
265
|
+
};
|
|
271
266
|
}
|
|
272
267
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
273
268
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'spinbutton-name-present';
|
|
@@ -10,10 +12,16 @@ const meta = {
|
|
|
10
12
|
descriptionKey: 'spinbuttonNamePresent_description'
|
|
11
13
|
},
|
|
12
14
|
helpUrl: null,
|
|
13
|
-
tags: [
|
|
15
|
+
tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'spinbutton'],
|
|
14
16
|
wcagSc: ['4.1.2'],
|
|
15
17
|
normativeMappings: [
|
|
16
|
-
{
|
|
18
|
+
{
|
|
19
|
+
standard: 'WCAG',
|
|
20
|
+
version: '2.2',
|
|
21
|
+
requirement: '4.1.2',
|
|
22
|
+
title: 'Name, Role, Value',
|
|
23
|
+
conformanceLevel: 'A'
|
|
24
|
+
}
|
|
17
25
|
],
|
|
18
26
|
defaultSeverity: 'serious',
|
|
19
27
|
category: 'robust',
|
|
@@ -24,20 +32,22 @@ const meta = {
|
|
|
24
32
|
|
|
25
33
|
function runInPage(ctx) {
|
|
26
34
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
35
|
+
const getEligibilityInfo =
|
|
36
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
37
|
|
|
32
38
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
39
|
+
return String(s || '')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
function getAttr(el, name) {
|
|
37
45
|
try {
|
|
38
46
|
if (!el || !el.getAttribute) return '';
|
|
39
47
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -52,7 +62,7 @@ function runInPage(ctx) {
|
|
|
52
62
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
63
|
return info && info.present ? info.value : '';
|
|
54
64
|
}
|
|
55
|
-
const t =
|
|
65
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
56
66
|
return t.replace(/\s+/g, ' ').trim();
|
|
57
67
|
}
|
|
58
68
|
|
|
@@ -100,7 +110,8 @@ function runInPage(ctx) {
|
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
function isEligibleAcc(helpers, el, ctx) {
|
|
103
|
-
const fn =
|
|
113
|
+
const fn =
|
|
114
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
104
115
|
if (!fn) return true;
|
|
105
116
|
try {
|
|
106
117
|
const r = fn(el, ctx);
|
|
@@ -126,12 +137,13 @@ function runInPage(ctx) {
|
|
|
126
137
|
return map;
|
|
127
138
|
}
|
|
128
139
|
|
|
129
|
-
|
|
130
140
|
const occurrences = [];
|
|
131
141
|
let applicableCount = 0;
|
|
132
142
|
|
|
133
|
-
const selector =
|
|
134
|
-
const nodes = helpers.queryAllSmart
|
|
143
|
+
const selector = '[role="spinbutton"]';
|
|
144
|
+
const nodes = helpers.queryAllSmart
|
|
145
|
+
? helpers.queryAllSmart(selector)
|
|
146
|
+
: helpers.queryAll(selector);
|
|
135
147
|
|
|
136
148
|
// Precompute label[for] map for spinbutton elements that are labelable
|
|
137
149
|
// native form controls (e.g. <input role="spinbutton">).
|
|
@@ -200,16 +212,22 @@ function runInPage(ctx) {
|
|
|
200
212
|
if (res.ok) continue;
|
|
201
213
|
|
|
202
214
|
const eligInfo = getEligibilityInfo
|
|
203
|
-
|
|
204
|
-
|
|
215
|
+
? (() => {
|
|
216
|
+
try {
|
|
217
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
})()
|
|
222
|
+
: null;
|
|
205
223
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
206
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
224
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
207
225
|
|
|
208
226
|
occurrences.push({
|
|
209
227
|
selector: stableSelector,
|
|
210
228
|
html,
|
|
211
229
|
summary: 'This element has no accessible name.',
|
|
212
|
-
hint:
|
|
230
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this spinbutton's accessible name.",
|
|
213
231
|
i18n: {
|
|
214
232
|
summaryKey: 'spinbuttonNamePresent_summary_fail',
|
|
215
233
|
hintKey: 'spinbuttonNamePresent_hint_fail',
|
|
@@ -222,12 +240,16 @@ function runInPage(ctx) {
|
|
|
222
240
|
});
|
|
223
241
|
}
|
|
224
242
|
|
|
225
|
-
|
|
226
243
|
if (applicableCount === 0) {
|
|
227
244
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
228
245
|
}
|
|
229
246
|
if (occurrences.length) {
|
|
230
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
ruleId: rule.ruleId,
|
|
249
|
+
outcome: 'fail',
|
|
250
|
+
severity: rule.defaultSeverity || 'minor',
|
|
251
|
+
occurrences
|
|
252
|
+
};
|
|
231
253
|
}
|
|
232
254
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
233
255
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'summary-name-present';
|
|
@@ -13,7 +15,13 @@ const meta = {
|
|
|
13
15
|
tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name'],
|
|
14
16
|
wcagSc: ['4.1.2'],
|
|
15
17
|
normativeMappings: [
|
|
16
|
-
{
|
|
18
|
+
{
|
|
19
|
+
standard: 'WCAG',
|
|
20
|
+
version: '2.2',
|
|
21
|
+
requirement: '4.1.2',
|
|
22
|
+
title: 'Name, Role, Value',
|
|
23
|
+
conformanceLevel: 'A'
|
|
24
|
+
}
|
|
17
25
|
],
|
|
18
26
|
defaultSeverity: 'serious',
|
|
19
27
|
category: 'robust',
|
|
@@ -24,20 +32,22 @@ const meta = {
|
|
|
24
32
|
|
|
25
33
|
function runInPage(ctx) {
|
|
26
34
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
35
|
+
const getEligibilityInfo =
|
|
36
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
37
|
|
|
32
38
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
39
|
+
return String(s || '')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
function getAttr(el, name) {
|
|
37
45
|
try {
|
|
38
46
|
if (!el || !el.getAttribute) return '';
|
|
39
47
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -52,7 +62,7 @@ function runInPage(ctx) {
|
|
|
52
62
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
63
|
return info && info.present ? info.value : '';
|
|
54
64
|
}
|
|
55
|
-
const t =
|
|
65
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
56
66
|
return t.replace(/\s+/g, ' ').trim();
|
|
57
67
|
}
|
|
58
68
|
|
|
@@ -74,7 +84,8 @@ function runInPage(ctx) {
|
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
function isEligibleAcc(helpers, el, ctx) {
|
|
77
|
-
const fn =
|
|
87
|
+
const fn =
|
|
88
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
78
89
|
if (!fn) return true;
|
|
79
90
|
try {
|
|
80
91
|
const r = fn(el, ctx);
|
|
@@ -85,11 +96,12 @@ function runInPage(ctx) {
|
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
|
|
89
99
|
const occurrences = [];
|
|
90
100
|
let applicableCount = 0;
|
|
91
101
|
|
|
92
|
-
const nodes = helpers.queryAllSmart
|
|
102
|
+
const nodes = helpers.queryAllSmart
|
|
103
|
+
? helpers.queryAllSmart('summary')
|
|
104
|
+
: helpers.queryAll('summary');
|
|
93
105
|
|
|
94
106
|
function evaluate(el) {
|
|
95
107
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -117,10 +129,16 @@ function runInPage(ctx) {
|
|
|
117
129
|
if (res.ok) continue;
|
|
118
130
|
|
|
119
131
|
const eligInfo = getEligibilityInfo
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
? (() => {
|
|
133
|
+
try {
|
|
134
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
})()
|
|
139
|
+
: null;
|
|
122
140
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
123
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
141
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
124
142
|
|
|
125
143
|
occurrences.push({
|
|
126
144
|
selector: stableSelector,
|
|
@@ -139,14 +157,18 @@ function runInPage(ctx) {
|
|
|
139
157
|
});
|
|
140
158
|
}
|
|
141
159
|
|
|
142
|
-
|
|
143
160
|
if (applicableCount === 0) {
|
|
144
161
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
145
162
|
}
|
|
146
163
|
if (occurrences.length) {
|
|
147
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
ruleId: rule.ruleId,
|
|
166
|
+
outcome: 'fail',
|
|
167
|
+
severity: rule.defaultSeverity || 'minor',
|
|
168
|
+
occurrences
|
|
169
|
+
};
|
|
148
170
|
}
|
|
149
171
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
150
172
|
}
|
|
151
173
|
|
|
152
|
-
module.exports = { id, meta, runInPage };
|
|
174
|
+
module.exports = { id, meta, runInPage };
|