@surea11y/core 1.1.2 → 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 +48 -4
- package/LICENSE +373 -21
- package/README.md +70 -1
- package/bin/core.js +240 -11
- package/docs/API_STABILITY.md +61 -0
- package/docs/BASELINE.md +66 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +80 -1
- package/docs/ENGINE_OPTIONS.md +4 -0
- package/docs/INTEGRATION.md +19 -1
- package/docs/OUTPUT_SCHEMA.md +2 -2
- package/docs/REPORT.md +33 -0
- package/docs/RULE_AUTHORING.md +31 -0
- 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 +50 -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 +125 -29
- 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 +50 -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 +39 -9
- 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 +43 -19
- 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 +117 -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 +37 -8
- package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -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 +26 -7
- 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 -12
- package/src/checks/manual/empty-table-header-manual.js +21 -14
- 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 -12
- 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 +95 -29
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
- package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +35 -21
- package/src/checks/manual/landmark-unique-manual.js +73 -45
- 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 +25 -6
- 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 +40 -23
- package/src/checks/manual/page-title-patterns-manual.js +87 -51
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +275 -62
- package/src/checks/manual/scope-attr-valid-manual.js +11 -9
- 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 +11 -9
- 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 +1128 -823
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4176 -3886
- package/src/core/dom-runner.js +720 -596
- 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 +71 -35
- package/src/core.js +37939 -29121
- package/src/i18n/en.js +1194 -887
- package/src/i18n/fr.js +1136 -793
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +502 -0
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -50,7 +50,8 @@ const id = 'aria-required-attr';
|
|
|
50
50
|
|
|
51
51
|
const meta = {
|
|
52
52
|
title: 'Roles with a required ARIA state/property must carry it',
|
|
53
|
-
description:
|
|
53
|
+
description:
|
|
54
|
+
'Checks that elements with an explicit role carry every unambiguous, context-independent required aria-* state/property for that role (e.g. role="checkbox" must have aria-checked).',
|
|
54
55
|
i18n: {
|
|
55
56
|
titleKey: 'ariaRequiredAttr_title',
|
|
56
57
|
descriptionKey: 'ariaRequiredAttr_description'
|
|
@@ -59,7 +60,13 @@ const meta = {
|
|
|
59
60
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
60
61
|
wcagSc: ['4.1.2'],
|
|
61
62
|
normativeMappings: [
|
|
62
|
-
{
|
|
63
|
+
{
|
|
64
|
+
standard: 'WCAG',
|
|
65
|
+
version: '2.2',
|
|
66
|
+
requirement: '4.1.2',
|
|
67
|
+
title: 'Name, Role, Value',
|
|
68
|
+
conformanceLevel: 'A'
|
|
69
|
+
}
|
|
63
70
|
],
|
|
64
71
|
defaultSeverity: 'serious',
|
|
65
72
|
category: 'robust',
|
|
@@ -69,7 +76,7 @@ const meta = {
|
|
|
69
76
|
};
|
|
70
77
|
|
|
71
78
|
function runInPage(ctx) {
|
|
72
|
-
const {
|
|
79
|
+
const { helpers, rule } = ctx;
|
|
73
80
|
|
|
74
81
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
75
82
|
if (!ariaHelpers) {
|
|
@@ -77,7 +84,8 @@ function runInPage(ctx) {
|
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
function isEligibleAcc(el) {
|
|
80
|
-
const fn =
|
|
87
|
+
const fn =
|
|
88
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
81
89
|
if (!fn) return true;
|
|
82
90
|
try {
|
|
83
91
|
const r = fn(el, ctx);
|
|
@@ -93,7 +101,9 @@ function runInPage(ctx) {
|
|
|
93
101
|
return v != null && String(v).trim().toLowerCase() === 'true';
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
const nodes = helpers.queryAllSmart
|
|
104
|
+
const nodes = helpers.queryAllSmart
|
|
105
|
+
? helpers.queryAllSmart('[role]')
|
|
106
|
+
: helpers.queryAll('[role]');
|
|
97
107
|
|
|
98
108
|
const occurrences = [];
|
|
99
109
|
let applicableCount = 0;
|
|
@@ -121,7 +131,7 @@ function runInPage(ctx) {
|
|
|
121
131
|
if (!missing.length) continue;
|
|
122
132
|
|
|
123
133
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
124
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
134
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
125
135
|
|
|
126
136
|
for (const attr of missing) {
|
|
127
137
|
occurrences.push({
|
|
@@ -145,9 +155,14 @@ function runInPage(ctx) {
|
|
|
145
155
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
146
156
|
}
|
|
147
157
|
if (occurrences.length) {
|
|
148
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
ruleId: rule.ruleId,
|
|
160
|
+
outcome: 'fail',
|
|
161
|
+
severity: rule.defaultSeverity || 'serious',
|
|
162
|
+
occurrences
|
|
163
|
+
};
|
|
149
164
|
}
|
|
150
165
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
151
166
|
}
|
|
152
167
|
|
|
153
|
-
module.exports = { id, meta, runInPage };
|
|
168
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -61,7 +61,8 @@ const id = 'aria-required-children';
|
|
|
61
61
|
|
|
62
62
|
const meta = {
|
|
63
63
|
title: 'Container roles must own at least one required child role',
|
|
64
|
-
description:
|
|
64
|
+
description:
|
|
65
|
+
'Checks that container roles with a documented "required owned elements" entry (list, listbox, menu, radiogroup, table, grid, tablist, tree, row, ...) contain at least one descendant or aria-owns-referenced element with an acceptable owned role.',
|
|
65
66
|
i18n: {
|
|
66
67
|
titleKey: 'ariaRequiredChildren_title',
|
|
67
68
|
descriptionKey: 'ariaRequiredChildren_description'
|
|
@@ -70,7 +71,13 @@ const meta = {
|
|
|
70
71
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
71
72
|
wcagSc: ['4.1.2'],
|
|
72
73
|
normativeMappings: [
|
|
73
|
-
{
|
|
74
|
+
{
|
|
75
|
+
standard: 'WCAG',
|
|
76
|
+
version: '2.2',
|
|
77
|
+
requirement: '4.1.2',
|
|
78
|
+
title: 'Name, Role, Value',
|
|
79
|
+
conformanceLevel: 'A'
|
|
80
|
+
}
|
|
74
81
|
],
|
|
75
82
|
defaultSeverity: 'moderate',
|
|
76
83
|
category: 'robust',
|
|
@@ -80,7 +87,7 @@ const meta = {
|
|
|
80
87
|
};
|
|
81
88
|
|
|
82
89
|
function runInPage(ctx) {
|
|
83
|
-
const {
|
|
90
|
+
const { helpers, rule } = ctx;
|
|
84
91
|
|
|
85
92
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
86
93
|
if (!ariaHelpers) {
|
|
@@ -88,7 +95,8 @@ function runInPage(ctx) {
|
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
function isEligibleAcc(el) {
|
|
91
|
-
const fn =
|
|
98
|
+
const fn =
|
|
99
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
92
100
|
if (!fn) return true;
|
|
93
101
|
try {
|
|
94
102
|
const r = fn(el, ctx);
|
|
@@ -109,9 +117,12 @@ function runInPage(ctx) {
|
|
|
109
117
|
// (kept in sync with aria-helpers.js NATIVE_CONTAINMENT_ROLE_BY_ELEMENT).
|
|
110
118
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
111
119
|
// ("runInPage MUST be self-contained").
|
|
112
|
-
const CANDIDATE_SELECTOR =
|
|
120
|
+
const CANDIDATE_SELECTOR =
|
|
121
|
+
'[role], li, option, tr, td, th, thead, tbody, tfoot, ul, ol, table, select, input[type="radio"]';
|
|
113
122
|
|
|
114
|
-
const nodes = helpers.queryAllSmart
|
|
123
|
+
const nodes = helpers.queryAllSmart
|
|
124
|
+
? helpers.queryAllSmart('[role]')
|
|
125
|
+
: helpers.queryAll('[role]');
|
|
115
126
|
|
|
116
127
|
const occurrences = [];
|
|
117
128
|
let applicableCount = 0;
|
|
@@ -139,8 +150,11 @@ function runInPage(ctx) {
|
|
|
139
150
|
if (out.length >= limit) return;
|
|
140
151
|
if (seen.has(child)) continue;
|
|
141
152
|
|
|
142
|
-
if (
|
|
143
|
-
|
|
153
|
+
if (
|
|
154
|
+
(child.tagName || '').toLowerCase() === 'slot' &&
|
|
155
|
+
typeof child.assignedElements === 'function'
|
|
156
|
+
) {
|
|
157
|
+
let assigned;
|
|
144
158
|
try {
|
|
145
159
|
assigned = child.assignedElements({ flatten: true }) || [];
|
|
146
160
|
} catch {
|
|
@@ -184,7 +198,7 @@ function runInPage(ctx) {
|
|
|
184
198
|
// selector, exactly as before this fix — covers the overwhelming
|
|
185
199
|
// majority of containers (no shadow DOM involved at all) with zero
|
|
186
200
|
// added cost.
|
|
187
|
-
let descendants
|
|
201
|
+
let descendants;
|
|
188
202
|
try {
|
|
189
203
|
descendants = el.querySelectorAll(CANDIDATE_SELECTOR);
|
|
190
204
|
} catch {
|
|
@@ -202,7 +216,7 @@ function runInPage(ctx) {
|
|
|
202
216
|
// <slot> somewhere in the subtree to expand — bounds the extra cost to
|
|
203
217
|
// exactly the containers that could possibly need it.
|
|
204
218
|
if (!found) {
|
|
205
|
-
let hasSlot
|
|
219
|
+
let hasSlot;
|
|
206
220
|
try {
|
|
207
221
|
hasSlot = !!el.querySelector('slot');
|
|
208
222
|
} catch {
|
|
@@ -243,7 +257,7 @@ function runInPage(ctx) {
|
|
|
243
257
|
if (found) continue;
|
|
244
258
|
|
|
245
259
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
246
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
260
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
247
261
|
|
|
248
262
|
occurrences.push({
|
|
249
263
|
selector: stableSelector,
|
|
@@ -256,7 +270,11 @@ function runInPage(ctx) {
|
|
|
256
270
|
params: { role, requiredRoles: requiredOwned.join(', ') }
|
|
257
271
|
},
|
|
258
272
|
data: {
|
|
259
|
-
details: {
|
|
273
|
+
details: {
|
|
274
|
+
reasonCode: 'ARIA_REQUIRED_CHILD_MISSING',
|
|
275
|
+
role,
|
|
276
|
+
requiredOwnedRoles: requiredOwned
|
|
277
|
+
}
|
|
260
278
|
}
|
|
261
279
|
});
|
|
262
280
|
}
|
|
@@ -265,9 +283,14 @@ function runInPage(ctx) {
|
|
|
265
283
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
266
284
|
}
|
|
267
285
|
if (occurrences.length) {
|
|
268
|
-
return {
|
|
286
|
+
return {
|
|
287
|
+
ruleId: rule.ruleId,
|
|
288
|
+
outcome: 'fail',
|
|
289
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
290
|
+
occurrences
|
|
291
|
+
};
|
|
269
292
|
}
|
|
270
293
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
271
294
|
}
|
|
272
295
|
|
|
273
|
-
module.exports = { id, meta, runInPage };
|
|
296
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -47,7 +47,8 @@ const id = 'aria-required-parent';
|
|
|
47
47
|
|
|
48
48
|
const meta = {
|
|
49
49
|
title: 'Roles requiring a specific context role must be in that context',
|
|
50
|
-
description:
|
|
50
|
+
description:
|
|
51
|
+
'Checks that roles with a documented "required context role" entry (listitem, option, tab, treeitem, row, cell, ...) have an ancestor or aria-owns owner with an acceptable context role.',
|
|
51
52
|
i18n: {
|
|
52
53
|
titleKey: 'ariaRequiredParent_title',
|
|
53
54
|
descriptionKey: 'ariaRequiredParent_description'
|
|
@@ -56,7 +57,13 @@ const meta = {
|
|
|
56
57
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
57
58
|
wcagSc: ['4.1.2'],
|
|
58
59
|
normativeMappings: [
|
|
59
|
-
{
|
|
60
|
+
{
|
|
61
|
+
standard: 'WCAG',
|
|
62
|
+
version: '2.2',
|
|
63
|
+
requirement: '4.1.2',
|
|
64
|
+
title: 'Name, Role, Value',
|
|
65
|
+
conformanceLevel: 'A'
|
|
66
|
+
}
|
|
60
67
|
],
|
|
61
68
|
defaultSeverity: 'moderate',
|
|
62
69
|
category: 'robust',
|
|
@@ -66,7 +73,7 @@ const meta = {
|
|
|
66
73
|
};
|
|
67
74
|
|
|
68
75
|
function runInPage(ctx) {
|
|
69
|
-
const {
|
|
76
|
+
const { helpers, rule } = ctx;
|
|
70
77
|
|
|
71
78
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
72
79
|
if (!ariaHelpers) {
|
|
@@ -74,7 +81,8 @@ function runInPage(ctx) {
|
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
function isEligibleAcc(el) {
|
|
77
|
-
const fn =
|
|
84
|
+
const fn =
|
|
85
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
78
86
|
if (!fn) return true;
|
|
79
87
|
try {
|
|
80
88
|
const r = fn(el, ctx);
|
|
@@ -120,14 +128,25 @@ function runInPage(ctx) {
|
|
|
120
128
|
// non-Element node (a ShadowRoot, nodeType 11) when climbing out of a
|
|
121
129
|
// shadow tree that has no further light-DOM parent — skip those and keep
|
|
122
130
|
// climbing rather than treating them as a (roleless) context.
|
|
123
|
-
const getComposedParent =
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
const getComposedParent =
|
|
132
|
+
helpers && typeof helpers.composedParent === 'function'
|
|
133
|
+
? helpers.composedParent
|
|
134
|
+
: function (n) {
|
|
135
|
+
return n && n.parentElement ? n.parentElement : null;
|
|
136
|
+
};
|
|
126
137
|
|
|
127
138
|
function hasAcceptableAncestorContext(el, acceptableRoles, ownRole) {
|
|
128
139
|
const allowsGroup = acceptableRoles.has('group');
|
|
129
140
|
let cur = getComposedParent(el);
|
|
130
141
|
let guard = 0;
|
|
142
|
+
// Mutable working copy: passing a transparent "group" ancestor also
|
|
143
|
+
// makes the element's OWN role an acceptable context from that point on
|
|
144
|
+
// (a nested treeitem-under-group-under-treeitem chain is a normal,
|
|
145
|
+
// arbitrarily-deep ARIA tree/list, not just one level) — mirroring that
|
|
146
|
+
// reference engine's getMissingContext, which pushes explicitRole into
|
|
147
|
+
// reqContext at the same point. Cloned lazily so the caller's Set (built
|
|
148
|
+
// once per element in runInPage) is never mutated.
|
|
149
|
+
let roles = acceptableRoles;
|
|
131
150
|
while (cur && guard++ < 200) {
|
|
132
151
|
if (cur.nodeType !== 1) {
|
|
133
152
|
cur = getComposedParent(cur);
|
|
@@ -139,10 +158,12 @@ function runInPage(ctx) {
|
|
|
139
158
|
continue;
|
|
140
159
|
}
|
|
141
160
|
if (role === 'group' && allowsGroup && GROUP_TRANSPARENT_FOR_ROLES.has(ownRole)) {
|
|
161
|
+
if (roles === acceptableRoles) roles = new Set(acceptableRoles);
|
|
162
|
+
roles.add(ownRole);
|
|
142
163
|
cur = getComposedParent(cur);
|
|
143
164
|
continue;
|
|
144
165
|
}
|
|
145
|
-
return
|
|
166
|
+
return roles.has(role);
|
|
146
167
|
}
|
|
147
168
|
return false;
|
|
148
169
|
}
|
|
@@ -152,7 +173,9 @@ function runInPage(ctx) {
|
|
|
152
173
|
const idTok = elId && String(elId).trim();
|
|
153
174
|
if (!idTok) return false;
|
|
154
175
|
|
|
155
|
-
const owners = helpers.queryAllSmart
|
|
176
|
+
const owners = helpers.queryAllSmart
|
|
177
|
+
? helpers.queryAllSmart('[aria-owns]')
|
|
178
|
+
: helpers.queryAll('[aria-owns]');
|
|
156
179
|
for (const owner of owners) {
|
|
157
180
|
if (!owner || !owner.getAttribute) continue;
|
|
158
181
|
const ownsAttr = owner.getAttribute('aria-owns') || '';
|
|
@@ -165,7 +188,9 @@ function runInPage(ctx) {
|
|
|
165
188
|
return false;
|
|
166
189
|
}
|
|
167
190
|
|
|
168
|
-
const nodes = helpers.queryAllSmart
|
|
191
|
+
const nodes = helpers.queryAllSmart
|
|
192
|
+
? helpers.queryAllSmart('[role]')
|
|
193
|
+
: helpers.queryAll('[role]');
|
|
169
194
|
|
|
170
195
|
const occurrences = [];
|
|
171
196
|
let applicableCount = 0;
|
|
@@ -191,7 +216,7 @@ function runInPage(ctx) {
|
|
|
191
216
|
if (hasContext) continue;
|
|
192
217
|
|
|
193
218
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
194
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
219
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
195
220
|
|
|
196
221
|
occurrences.push({
|
|
197
222
|
selector: stableSelector,
|
|
@@ -204,7 +229,11 @@ function runInPage(ctx) {
|
|
|
204
229
|
params: { role, requiredRoles: requiredContext.join(', ') }
|
|
205
230
|
},
|
|
206
231
|
data: {
|
|
207
|
-
details: {
|
|
232
|
+
details: {
|
|
233
|
+
reasonCode: 'ARIA_REQUIRED_PARENT_MISSING',
|
|
234
|
+
role,
|
|
235
|
+
requiredContextRoles: requiredContext
|
|
236
|
+
}
|
|
208
237
|
}
|
|
209
238
|
});
|
|
210
239
|
}
|
|
@@ -213,9 +242,14 @@ function runInPage(ctx) {
|
|
|
213
242
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
214
243
|
}
|
|
215
244
|
if (occurrences.length) {
|
|
216
|
-
return {
|
|
245
|
+
return {
|
|
246
|
+
ruleId: rule.ruleId,
|
|
247
|
+
outcome: 'fail',
|
|
248
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
249
|
+
occurrences
|
|
250
|
+
};
|
|
217
251
|
}
|
|
218
252
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
219
253
|
}
|
|
220
254
|
|
|
221
|
-
module.exports = { id, meta, runInPage };
|
|
255
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -15,7 +15,8 @@ const id = 'aria-role-name-present';
|
|
|
15
15
|
|
|
16
16
|
const meta = {
|
|
17
17
|
title: 'ARIA widget/container roles have an accessible name',
|
|
18
|
-
description:
|
|
18
|
+
description:
|
|
19
|
+
'Checks that selected ARIA widget/container roles expose a non-empty accessible name.',
|
|
19
20
|
i18n: {
|
|
20
21
|
titleKey: 'ariaRoleNamePresent_title',
|
|
21
22
|
descriptionKey: 'ariaRoleNamePresent_description'
|
|
@@ -24,7 +25,13 @@ const meta = {
|
|
|
24
25
|
tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name'],
|
|
25
26
|
wcagSc: ['4.1.2'],
|
|
26
27
|
normativeMappings: [
|
|
27
|
-
{
|
|
28
|
+
{
|
|
29
|
+
standard: 'WCAG',
|
|
30
|
+
version: '2.2',
|
|
31
|
+
requirement: '4.1.2',
|
|
32
|
+
title: 'Name, Role, Value',
|
|
33
|
+
conformanceLevel: 'A'
|
|
34
|
+
}
|
|
28
35
|
],
|
|
29
36
|
defaultSeverity: 'serious',
|
|
30
37
|
category: 'robust',
|
|
@@ -41,17 +48,19 @@ function runInPage(ctx) {
|
|
|
41
48
|
const { document, root, helpers, rule } = ctx;
|
|
42
49
|
const safeRoot = root || document;
|
|
43
50
|
|
|
44
|
-
const queryAllSmart =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
const queryAllSmart =
|
|
52
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
53
|
+
const queryAll =
|
|
54
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
55
|
+
? helpers.queryAll
|
|
56
|
+
: (sel, rt) => {
|
|
57
|
+
try {
|
|
58
|
+
const scope = rt || safeRoot;
|
|
59
|
+
return scope && scope.querySelectorAll ? Array.from(scope.querySelectorAll(sel)) : [];
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
};
|
|
55
64
|
|
|
56
65
|
const isAccTreeEligible =
|
|
57
66
|
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
@@ -60,9 +69,14 @@ function runInPage(ctx) {
|
|
|
60
69
|
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
61
70
|
|
|
62
71
|
const getAriaLabelledByInfo =
|
|
63
|
-
helpers && typeof helpers.getAriaLabelledByInfo === 'function'
|
|
72
|
+
helpers && typeof helpers.getAriaLabelledByInfo === 'function'
|
|
73
|
+
? helpers.getAriaLabelledByInfo
|
|
74
|
+
: null;
|
|
64
75
|
|
|
65
|
-
const normalizeWs = (s) =>
|
|
76
|
+
const normalizeWs = (s) =>
|
|
77
|
+
String(s || '')
|
|
78
|
+
.replace(/\s+/g, ' ')
|
|
79
|
+
.trim();
|
|
66
80
|
|
|
67
81
|
const getAttr = (el, name) => {
|
|
68
82
|
try {
|
|
@@ -73,7 +87,6 @@ function runInPage(ctx) {
|
|
|
73
87
|
}
|
|
74
88
|
};
|
|
75
89
|
|
|
76
|
-
|
|
77
90
|
// aria-labelledby resolution: delegate to the shared helper (used by
|
|
78
91
|
// button-name-present et al.), which correctly includes hidden/aria-hidden
|
|
79
92
|
// referenced nodes per the Accessible Name and Description Computation
|
|
@@ -120,7 +133,9 @@ function runInPage(ctx) {
|
|
|
120
133
|
|
|
121
134
|
const nodes = (() => {
|
|
122
135
|
try {
|
|
123
|
-
return (
|
|
136
|
+
return (
|
|
137
|
+
(queryAllSmart ? queryAllSmart(selector, safeRoot) : queryAll(selector, safeRoot)) || []
|
|
138
|
+
);
|
|
124
139
|
} catch {
|
|
125
140
|
return queryAll(selector, safeRoot);
|
|
126
141
|
}
|
|
@@ -158,9 +173,15 @@ function runInPage(ctx) {
|
|
|
158
173
|
const ok = !!(ariaLabel || labelled || title);
|
|
159
174
|
if (ok) continue;
|
|
160
175
|
|
|
161
|
-
const eligInfo = getEligibilityInfo
|
|
162
|
-
|
|
163
|
-
|
|
176
|
+
const eligInfo = getEligibilityInfo
|
|
177
|
+
? (() => {
|
|
178
|
+
try {
|
|
179
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
})()
|
|
184
|
+
: null;
|
|
164
185
|
|
|
165
186
|
const baseOccurrence = {
|
|
166
187
|
summary: 'This element has no accessible name.',
|
|
@@ -195,7 +216,12 @@ function runInPage(ctx) {
|
|
|
195
216
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
196
217
|
}
|
|
197
218
|
|
|
198
|
-
return {
|
|
219
|
+
return {
|
|
220
|
+
ruleId: rule.ruleId,
|
|
221
|
+
outcome: 'fail',
|
|
222
|
+
severity: rule.defaultSeverity || 'minor',
|
|
223
|
+
occurrences
|
|
224
|
+
};
|
|
199
225
|
}
|
|
200
226
|
|
|
201
227
|
module.exports = { id, meta, runInPage };
|
|
@@ -25,7 +25,8 @@ const id = 'aria-roles-valid';
|
|
|
25
25
|
|
|
26
26
|
const meta = {
|
|
27
27
|
title: 'role attribute must be a valid, non-abstract ARIA role',
|
|
28
|
-
description:
|
|
28
|
+
description:
|
|
29
|
+
'Checks that an explicit role="" attribute resolves to a real, non-abstract WAI-ARIA role.',
|
|
29
30
|
i18n: {
|
|
30
31
|
titleKey: 'ariaRolesValid_title',
|
|
31
32
|
descriptionKey: 'ariaRolesValid_description'
|
|
@@ -34,7 +35,13 @@ const meta = {
|
|
|
34
35
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
35
36
|
wcagSc: ['4.1.2'],
|
|
36
37
|
normativeMappings: [
|
|
37
|
-
{
|
|
38
|
+
{
|
|
39
|
+
standard: 'WCAG',
|
|
40
|
+
version: '2.2',
|
|
41
|
+
requirement: '4.1.2',
|
|
42
|
+
title: 'Name, Role, Value',
|
|
43
|
+
conformanceLevel: 'A'
|
|
44
|
+
}
|
|
38
45
|
],
|
|
39
46
|
defaultSeverity: 'serious',
|
|
40
47
|
category: 'robust',
|
|
@@ -44,14 +51,16 @@ const meta = {
|
|
|
44
51
|
};
|
|
45
52
|
|
|
46
53
|
function runInPage(ctx) {
|
|
47
|
-
const {
|
|
54
|
+
const { helpers, rule } = ctx;
|
|
48
55
|
|
|
49
56
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
50
57
|
if (!ariaHelpers) {
|
|
51
58
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
const nodes = helpers.queryAllSmart
|
|
61
|
+
const nodes = helpers.queryAllSmart
|
|
62
|
+
? helpers.queryAllSmart('[role]')
|
|
63
|
+
: helpers.queryAll('[role]');
|
|
55
64
|
|
|
56
65
|
const occurrences = [];
|
|
57
66
|
let applicableCount = 0;
|
|
@@ -71,7 +80,7 @@ function runInPage(ctx) {
|
|
|
71
80
|
|
|
72
81
|
const reasonCode = !isKnown ? 'ARIA_ROLE_INVALID' : 'ARIA_ROLE_ABSTRACT';
|
|
73
82
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
74
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
83
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
75
84
|
|
|
76
85
|
occurrences.push({
|
|
77
86
|
selector: stableSelector,
|
|
@@ -83,12 +92,8 @@ function runInPage(ctx) {
|
|
|
83
92
|
? 'Use a valid ARIA role token, or remove the role attribute if none applies.'
|
|
84
93
|
: 'Replace this abstract role with a concrete role appropriate for the widget/structure.',
|
|
85
94
|
i18n: {
|
|
86
|
-
summaryKey: !isKnown
|
|
87
|
-
|
|
88
|
-
: 'ariaRolesValid_summary_abstract',
|
|
89
|
-
hintKey: !isKnown
|
|
90
|
-
? 'ariaRolesValid_hint_invalid'
|
|
91
|
-
: 'ariaRolesValid_hint_abstract',
|
|
95
|
+
summaryKey: !isKnown ? 'ariaRolesValid_summary_invalid' : 'ariaRolesValid_summary_abstract',
|
|
96
|
+
hintKey: !isKnown ? 'ariaRolesValid_hint_invalid' : 'ariaRolesValid_hint_abstract',
|
|
92
97
|
params: { role }
|
|
93
98
|
},
|
|
94
99
|
data: {
|
|
@@ -101,7 +106,12 @@ function runInPage(ctx) {
|
|
|
101
106
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
102
107
|
}
|
|
103
108
|
if (occurrences.length) {
|
|
104
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
ruleId: rule.ruleId,
|
|
111
|
+
outcome: 'fail',
|
|
112
|
+
severity: rule.defaultSeverity || 'serious',
|
|
113
|
+
occurrences
|
|
114
|
+
};
|
|
105
115
|
}
|
|
106
116
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
107
117
|
}
|
|
@@ -38,7 +38,8 @@ const id = 'aria-valid-attr-value';
|
|
|
38
38
|
|
|
39
39
|
const meta = {
|
|
40
40
|
title: 'aria-* attribute values must match their declared type',
|
|
41
|
-
description:
|
|
41
|
+
description:
|
|
42
|
+
'Checks that every recognized aria-* attribute has a value conforming to its WAI-ARIA-declared value type (boolean, tristate, token, integer, number, or ID reference).',
|
|
42
43
|
i18n: {
|
|
43
44
|
titleKey: 'ariaValidAttrValue_title',
|
|
44
45
|
descriptionKey: 'ariaValidAttrValue_description'
|
|
@@ -47,7 +48,13 @@ const meta = {
|
|
|
47
48
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
48
49
|
wcagSc: ['4.1.2'],
|
|
49
50
|
normativeMappings: [
|
|
50
|
-
{
|
|
51
|
+
{
|
|
52
|
+
standard: 'WCAG',
|
|
53
|
+
version: '2.2',
|
|
54
|
+
requirement: '4.1.2',
|
|
55
|
+
title: 'Name, Role, Value',
|
|
56
|
+
conformanceLevel: 'A'
|
|
57
|
+
}
|
|
51
58
|
],
|
|
52
59
|
defaultSeverity: 'serious',
|
|
53
60
|
category: 'robust',
|
|
@@ -57,7 +64,7 @@ const meta = {
|
|
|
57
64
|
};
|
|
58
65
|
|
|
59
66
|
function runInPage(ctx) {
|
|
60
|
-
const {
|
|
67
|
+
const { helpers, rule } = ctx;
|
|
61
68
|
|
|
62
69
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
63
70
|
if (!ariaHelpers) {
|
|
@@ -85,14 +92,18 @@ function runInPage(ctx) {
|
|
|
85
92
|
const result = ariaHelpers.validateAttrValue(name, rawValue);
|
|
86
93
|
if (!result.valid) {
|
|
87
94
|
if (!invalid) invalid = [];
|
|
88
|
-
invalid.push({
|
|
95
|
+
invalid.push({
|
|
96
|
+
name,
|
|
97
|
+
value: rawValue == null ? '' : String(rawValue),
|
|
98
|
+
reason: result.reason
|
|
99
|
+
});
|
|
89
100
|
}
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
if (!invalid || !invalid.length) continue;
|
|
93
104
|
|
|
94
105
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
95
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
106
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
96
107
|
|
|
97
108
|
for (const item of invalid) {
|
|
98
109
|
occurrences.push({
|
|
@@ -106,7 +117,12 @@ function runInPage(ctx) {
|
|
|
106
117
|
params: { attr: item.name, value: item.value }
|
|
107
118
|
},
|
|
108
119
|
data: {
|
|
109
|
-
details: {
|
|
120
|
+
details: {
|
|
121
|
+
reasonCode: 'ARIA_ATTR_VALUE_INVALID',
|
|
122
|
+
attr: item.name,
|
|
123
|
+
value: item.value,
|
|
124
|
+
valueReason: item.reason
|
|
125
|
+
}
|
|
110
126
|
}
|
|
111
127
|
});
|
|
112
128
|
}
|
|
@@ -116,7 +132,12 @@ function runInPage(ctx) {
|
|
|
116
132
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
117
133
|
}
|
|
118
134
|
if (occurrences.length) {
|
|
119
|
-
return {
|
|
135
|
+
return {
|
|
136
|
+
ruleId: rule.ruleId,
|
|
137
|
+
outcome: 'fail',
|
|
138
|
+
severity: rule.defaultSeverity || 'serious',
|
|
139
|
+
occurrences
|
|
140
|
+
};
|
|
120
141
|
}
|
|
121
142
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
122
143
|
}
|