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