@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
|
/**
|
|
@@ -47,7 +49,8 @@ const id = 'aria-required-parent';
|
|
|
47
49
|
|
|
48
50
|
const meta = {
|
|
49
51
|
title: 'Roles requiring a specific context role must be in that context',
|
|
50
|
-
description:
|
|
52
|
+
description:
|
|
53
|
+
'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
54
|
i18n: {
|
|
52
55
|
titleKey: 'ariaRequiredParent_title',
|
|
53
56
|
descriptionKey: 'ariaRequiredParent_description'
|
|
@@ -56,7 +59,13 @@ const meta = {
|
|
|
56
59
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
57
60
|
wcagSc: ['4.1.2'],
|
|
58
61
|
normativeMappings: [
|
|
59
|
-
{
|
|
62
|
+
{
|
|
63
|
+
standard: 'WCAG',
|
|
64
|
+
version: '2.2',
|
|
65
|
+
requirement: '4.1.2',
|
|
66
|
+
title: 'Name, Role, Value',
|
|
67
|
+
conformanceLevel: 'A'
|
|
68
|
+
}
|
|
60
69
|
],
|
|
61
70
|
defaultSeverity: 'moderate',
|
|
62
71
|
category: 'robust',
|
|
@@ -66,7 +75,7 @@ const meta = {
|
|
|
66
75
|
};
|
|
67
76
|
|
|
68
77
|
function runInPage(ctx) {
|
|
69
|
-
const {
|
|
78
|
+
const { helpers, rule } = ctx;
|
|
70
79
|
|
|
71
80
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
72
81
|
if (!ariaHelpers) {
|
|
@@ -74,7 +83,8 @@ function runInPage(ctx) {
|
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
function isEligibleAcc(el) {
|
|
77
|
-
const fn =
|
|
86
|
+
const fn =
|
|
87
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
78
88
|
if (!fn) return true;
|
|
79
89
|
try {
|
|
80
90
|
const r = fn(el, ctx);
|
|
@@ -86,24 +96,20 @@ function runInPage(ctx) {
|
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
// Roles that may host a nested listitem/treeitem group without breaking
|
|
89
|
-
// the required-context chain
|
|
90
|
-
//
|
|
91
|
-
// roles via its ownGroupRoles option).
|
|
99
|
+
// the required-context chain — the group role is transparent for exactly
|
|
100
|
+
// these two roles.
|
|
92
101
|
const GROUP_TRANSPARENT_FOR_ROLES = new Set(['listitem', 'treeitem']);
|
|
93
102
|
|
|
94
103
|
// A real ancestor role — not "no role at all" and not the two roles that
|
|
95
104
|
// strip an element from the accessibility tree's parent/child chain
|
|
96
|
-
// entirely (presentation/none) — stops the search
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// <ul role="tablist">: that reference engine correctly fails this (the tablist is never the
|
|
105
|
-
// tab's accessible-tree parent, listitem is), which the old "walk every
|
|
106
|
-
// ancestor" version here missed entirely.
|
|
105
|
+
// entirely (presentation/none) — stops the search. This is stricter than
|
|
106
|
+
// "any ancestor with the right role anywhere up the tree": the
|
|
107
|
+
// required-context relationship is about the accessibility tree's actual
|
|
108
|
+
// PARENT, so an intervening ancestor with its OWN distinct real role
|
|
109
|
+
// (e.g. a plain <li>'s native "listitem" role) blocks the search even if
|
|
110
|
+
// a further-up ancestor has the correct role. E.g. a <button role="tab">
|
|
111
|
+
// inside a plain <li> (native listitem) inside <ul role="tablist"> fails:
|
|
112
|
+
// the tablist is never the tab's accessible-tree parent, the listitem is.
|
|
107
113
|
function getRealContextRole(el) {
|
|
108
114
|
const role = ariaHelpers.getContainmentRole(el);
|
|
109
115
|
if (!role || role === 'presentation' || role === 'none') return '';
|
|
@@ -113,21 +119,29 @@ function runInPage(ctx) {
|
|
|
113
119
|
// Flat-tree ancestor walk (ctx.helpers.composedParent — assignedSlot wins
|
|
114
120
|
// over parentNode, then shadow host). A slotted light-DOM element's real
|
|
115
121
|
// rendered ancestor is whatever the shadow tree wraps its <slot> in (e.g.
|
|
116
|
-
// a role="list" container), not its own light-DOM parentElement
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
// a role="list" container), not its own light-DOM parentElement.
|
|
123
|
+
// composedParent can return a non-Element node (a ShadowRoot, nodeType
|
|
124
|
+
// 11) when climbing out of a shadow tree that has no further light-DOM
|
|
125
|
+
// parent — skip those and keep climbing rather than treating them as a
|
|
126
|
+
// (roleless) context.
|
|
127
|
+
const getComposedParent =
|
|
128
|
+
helpers && typeof helpers.composedParent === 'function'
|
|
129
|
+
? helpers.composedParent
|
|
130
|
+
: function (n) {
|
|
131
|
+
return n && n.parentElement ? n.parentElement : null;
|
|
132
|
+
};
|
|
126
133
|
|
|
127
134
|
function hasAcceptableAncestorContext(el, acceptableRoles, ownRole) {
|
|
128
135
|
const allowsGroup = acceptableRoles.has('group');
|
|
129
136
|
let cur = getComposedParent(el);
|
|
130
137
|
let guard = 0;
|
|
138
|
+
// Mutable working copy: passing a transparent "group" ancestor also
|
|
139
|
+
// makes the element's OWN role an acceptable context from that point on
|
|
140
|
+
// (a nested treeitem-under-group-under-treeitem chain is a normal,
|
|
141
|
+
// arbitrarily-deep ARIA tree/list, not just one level). Cloned lazily
|
|
142
|
+
// so the caller's Set (built once per element in runInPage) is never
|
|
143
|
+
// mutated.
|
|
144
|
+
let roles = acceptableRoles;
|
|
131
145
|
while (cur && guard++ < 200) {
|
|
132
146
|
if (cur.nodeType !== 1) {
|
|
133
147
|
cur = getComposedParent(cur);
|
|
@@ -139,10 +153,12 @@ function runInPage(ctx) {
|
|
|
139
153
|
continue;
|
|
140
154
|
}
|
|
141
155
|
if (role === 'group' && allowsGroup && GROUP_TRANSPARENT_FOR_ROLES.has(ownRole)) {
|
|
156
|
+
if (roles === acceptableRoles) roles = new Set(acceptableRoles);
|
|
157
|
+
roles.add(ownRole);
|
|
142
158
|
cur = getComposedParent(cur);
|
|
143
159
|
continue;
|
|
144
160
|
}
|
|
145
|
-
return
|
|
161
|
+
return roles.has(role);
|
|
146
162
|
}
|
|
147
163
|
return false;
|
|
148
164
|
}
|
|
@@ -152,7 +168,9 @@ function runInPage(ctx) {
|
|
|
152
168
|
const idTok = elId && String(elId).trim();
|
|
153
169
|
if (!idTok) return false;
|
|
154
170
|
|
|
155
|
-
const owners = helpers.queryAllSmart
|
|
171
|
+
const owners = helpers.queryAllSmart
|
|
172
|
+
? helpers.queryAllSmart('[aria-owns]')
|
|
173
|
+
: helpers.queryAll('[aria-owns]');
|
|
156
174
|
for (const owner of owners) {
|
|
157
175
|
if (!owner || !owner.getAttribute) continue;
|
|
158
176
|
const ownsAttr = owner.getAttribute('aria-owns') || '';
|
|
@@ -165,7 +183,9 @@ function runInPage(ctx) {
|
|
|
165
183
|
return false;
|
|
166
184
|
}
|
|
167
185
|
|
|
168
|
-
const nodes = helpers.queryAllSmart
|
|
186
|
+
const nodes = helpers.queryAllSmart
|
|
187
|
+
? helpers.queryAllSmart('[role]')
|
|
188
|
+
: helpers.queryAll('[role]');
|
|
169
189
|
|
|
170
190
|
const occurrences = [];
|
|
171
191
|
let applicableCount = 0;
|
|
@@ -191,7 +211,7 @@ function runInPage(ctx) {
|
|
|
191
211
|
if (hasContext) continue;
|
|
192
212
|
|
|
193
213
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
194
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
214
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
195
215
|
|
|
196
216
|
occurrences.push({
|
|
197
217
|
selector: stableSelector,
|
|
@@ -204,7 +224,11 @@ function runInPage(ctx) {
|
|
|
204
224
|
params: { role, requiredRoles: requiredContext.join(', ') }
|
|
205
225
|
},
|
|
206
226
|
data: {
|
|
207
|
-
details: {
|
|
227
|
+
details: {
|
|
228
|
+
reasonCode: 'ARIA_REQUIRED_PARENT_MISSING',
|
|
229
|
+
role,
|
|
230
|
+
requiredContextRoles: requiredContext
|
|
231
|
+
}
|
|
208
232
|
}
|
|
209
233
|
});
|
|
210
234
|
}
|
|
@@ -213,9 +237,14 @@ function runInPage(ctx) {
|
|
|
213
237
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
214
238
|
}
|
|
215
239
|
if (occurrences.length) {
|
|
216
|
-
return {
|
|
240
|
+
return {
|
|
241
|
+
ruleId: rule.ruleId,
|
|
242
|
+
outcome: 'fail',
|
|
243
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
244
|
+
occurrences
|
|
245
|
+
};
|
|
217
246
|
}
|
|
218
247
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
219
248
|
}
|
|
220
249
|
|
|
221
|
-
module.exports = { id, meta, runInPage };
|
|
250
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -15,7 +17,8 @@ const id = 'aria-role-name-present';
|
|
|
15
17
|
|
|
16
18
|
const meta = {
|
|
17
19
|
title: 'ARIA widget/container roles have an accessible name',
|
|
18
|
-
description:
|
|
20
|
+
description:
|
|
21
|
+
'Checks that selected ARIA widget/container roles expose a non-empty accessible name.',
|
|
19
22
|
i18n: {
|
|
20
23
|
titleKey: 'ariaRoleNamePresent_title',
|
|
21
24
|
descriptionKey: 'ariaRoleNamePresent_description'
|
|
@@ -24,7 +27,13 @@ const meta = {
|
|
|
24
27
|
tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name'],
|
|
25
28
|
wcagSc: ['4.1.2'],
|
|
26
29
|
normativeMappings: [
|
|
27
|
-
{
|
|
30
|
+
{
|
|
31
|
+
standard: 'WCAG',
|
|
32
|
+
version: '2.2',
|
|
33
|
+
requirement: '4.1.2',
|
|
34
|
+
title: 'Name, Role, Value',
|
|
35
|
+
conformanceLevel: 'A'
|
|
36
|
+
}
|
|
28
37
|
],
|
|
29
38
|
defaultSeverity: 'serious',
|
|
30
39
|
category: 'robust',
|
|
@@ -41,17 +50,19 @@ function runInPage(ctx) {
|
|
|
41
50
|
const { document, root, helpers, rule } = ctx;
|
|
42
51
|
const safeRoot = root || document;
|
|
43
52
|
|
|
44
|
-
const queryAllSmart =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const queryAllSmart =
|
|
54
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
55
|
+
const queryAll =
|
|
56
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
57
|
+
? helpers.queryAll
|
|
58
|
+
: (sel, rt) => {
|
|
59
|
+
try {
|
|
60
|
+
const scope = rt || safeRoot;
|
|
61
|
+
return scope && scope.querySelectorAll ? Array.from(scope.querySelectorAll(sel)) : [];
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
};
|
|
55
66
|
|
|
56
67
|
const isAccTreeEligible =
|
|
57
68
|
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
@@ -60,9 +71,14 @@ function runInPage(ctx) {
|
|
|
60
71
|
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
61
72
|
|
|
62
73
|
const getAriaLabelledByInfo =
|
|
63
|
-
helpers && typeof helpers.getAriaLabelledByInfo === 'function'
|
|
74
|
+
helpers && typeof helpers.getAriaLabelledByInfo === 'function'
|
|
75
|
+
? helpers.getAriaLabelledByInfo
|
|
76
|
+
: null;
|
|
64
77
|
|
|
65
|
-
const normalizeWs = (s) =>
|
|
78
|
+
const normalizeWs = (s) =>
|
|
79
|
+
String(s || '')
|
|
80
|
+
.replace(/\s+/g, ' ')
|
|
81
|
+
.trim();
|
|
66
82
|
|
|
67
83
|
const getAttr = (el, name) => {
|
|
68
84
|
try {
|
|
@@ -73,7 +89,6 @@ function runInPage(ctx) {
|
|
|
73
89
|
}
|
|
74
90
|
};
|
|
75
91
|
|
|
76
|
-
|
|
77
92
|
// aria-labelledby resolution: delegate to the shared helper (used by
|
|
78
93
|
// button-name-present et al.), which correctly includes hidden/aria-hidden
|
|
79
94
|
// referenced nodes per the Accessible Name and Description Computation
|
|
@@ -120,7 +135,9 @@ function runInPage(ctx) {
|
|
|
120
135
|
|
|
121
136
|
const nodes = (() => {
|
|
122
137
|
try {
|
|
123
|
-
return (
|
|
138
|
+
return (
|
|
139
|
+
(queryAllSmart ? queryAllSmart(selector, safeRoot) : queryAll(selector, safeRoot)) || []
|
|
140
|
+
);
|
|
124
141
|
} catch {
|
|
125
142
|
return queryAll(selector, safeRoot);
|
|
126
143
|
}
|
|
@@ -158,9 +175,15 @@ function runInPage(ctx) {
|
|
|
158
175
|
const ok = !!(ariaLabel || labelled || title);
|
|
159
176
|
if (ok) continue;
|
|
160
177
|
|
|
161
|
-
const eligInfo = getEligibilityInfo
|
|
162
|
-
|
|
163
|
-
|
|
178
|
+
const eligInfo = getEligibilityInfo
|
|
179
|
+
? (() => {
|
|
180
|
+
try {
|
|
181
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
})()
|
|
186
|
+
: null;
|
|
164
187
|
|
|
165
188
|
const baseOccurrence = {
|
|
166
189
|
summary: 'This element has no accessible name.',
|
|
@@ -195,7 +218,12 @@ function runInPage(ctx) {
|
|
|
195
218
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
196
219
|
}
|
|
197
220
|
|
|
198
|
-
return {
|
|
221
|
+
return {
|
|
222
|
+
ruleId: rule.ruleId,
|
|
223
|
+
outcome: 'fail',
|
|
224
|
+
severity: rule.defaultSeverity || 'minor',
|
|
225
|
+
occurrences
|
|
226
|
+
};
|
|
199
227
|
}
|
|
200
228
|
|
|
201
229
|
module.exports = { id, meta, runInPage };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -25,7 +27,8 @@ const id = 'aria-roles-valid';
|
|
|
25
27
|
|
|
26
28
|
const meta = {
|
|
27
29
|
title: 'role attribute must be a valid, non-abstract ARIA role',
|
|
28
|
-
description:
|
|
30
|
+
description:
|
|
31
|
+
'Checks that an explicit role="" attribute resolves to a real, non-abstract WAI-ARIA role.',
|
|
29
32
|
i18n: {
|
|
30
33
|
titleKey: 'ariaRolesValid_title',
|
|
31
34
|
descriptionKey: 'ariaRolesValid_description'
|
|
@@ -34,7 +37,13 @@ const meta = {
|
|
|
34
37
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
35
38
|
wcagSc: ['4.1.2'],
|
|
36
39
|
normativeMappings: [
|
|
37
|
-
{
|
|
40
|
+
{
|
|
41
|
+
standard: 'WCAG',
|
|
42
|
+
version: '2.2',
|
|
43
|
+
requirement: '4.1.2',
|
|
44
|
+
title: 'Name, Role, Value',
|
|
45
|
+
conformanceLevel: 'A'
|
|
46
|
+
}
|
|
38
47
|
],
|
|
39
48
|
defaultSeverity: 'serious',
|
|
40
49
|
category: 'robust',
|
|
@@ -44,14 +53,16 @@ const meta = {
|
|
|
44
53
|
};
|
|
45
54
|
|
|
46
55
|
function runInPage(ctx) {
|
|
47
|
-
const {
|
|
56
|
+
const { helpers, rule } = ctx;
|
|
48
57
|
|
|
49
58
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
50
59
|
if (!ariaHelpers) {
|
|
51
60
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
52
61
|
}
|
|
53
62
|
|
|
54
|
-
const nodes = helpers.queryAllSmart
|
|
63
|
+
const nodes = helpers.queryAllSmart
|
|
64
|
+
? helpers.queryAllSmart('[role]')
|
|
65
|
+
: helpers.queryAll('[role]');
|
|
55
66
|
|
|
56
67
|
const occurrences = [];
|
|
57
68
|
let applicableCount = 0;
|
|
@@ -71,7 +82,7 @@ function runInPage(ctx) {
|
|
|
71
82
|
|
|
72
83
|
const reasonCode = !isKnown ? 'ARIA_ROLE_INVALID' : 'ARIA_ROLE_ABSTRACT';
|
|
73
84
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
74
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
85
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
75
86
|
|
|
76
87
|
occurrences.push({
|
|
77
88
|
selector: stableSelector,
|
|
@@ -83,12 +94,8 @@ function runInPage(ctx) {
|
|
|
83
94
|
? 'Use a valid ARIA role token, or remove the role attribute if none applies.'
|
|
84
95
|
: 'Replace this abstract role with a concrete role appropriate for the widget/structure.',
|
|
85
96
|
i18n: {
|
|
86
|
-
summaryKey: !isKnown
|
|
87
|
-
|
|
88
|
-
: 'ariaRolesValid_summary_abstract',
|
|
89
|
-
hintKey: !isKnown
|
|
90
|
-
? 'ariaRolesValid_hint_invalid'
|
|
91
|
-
: 'ariaRolesValid_hint_abstract',
|
|
97
|
+
summaryKey: !isKnown ? 'ariaRolesValid_summary_invalid' : 'ariaRolesValid_summary_abstract',
|
|
98
|
+
hintKey: !isKnown ? 'ariaRolesValid_hint_invalid' : 'ariaRolesValid_hint_abstract',
|
|
92
99
|
params: { role }
|
|
93
100
|
},
|
|
94
101
|
data: {
|
|
@@ -101,7 +108,12 @@ function runInPage(ctx) {
|
|
|
101
108
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
102
109
|
}
|
|
103
110
|
if (occurrences.length) {
|
|
104
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
ruleId: rule.ruleId,
|
|
113
|
+
outcome: 'fail',
|
|
114
|
+
severity: rule.defaultSeverity || 'serious',
|
|
115
|
+
occurrences
|
|
116
|
+
};
|
|
105
117
|
}
|
|
106
118
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
107
119
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -13,32 +15,34 @@
|
|
|
13
15
|
* @expectation
|
|
14
16
|
* Each attribute's value conforms to its WAI-ARIA-declared value type:
|
|
15
17
|
* boolean ("true"/"false"), tristate ("true"/"false"/"mixed"), a token
|
|
16
|
-
* from a fixed enumerated set, an integer, a real number, or
|
|
17
|
-
* ID reference (list) that resolves to an existing element in
|
|
18
|
-
* document.
|
|
18
|
+
* from a fixed enumerated set, an integer, a real number, or an empty
|
|
19
|
+
* value or ID reference (list) that resolves to an existing element in
|
|
20
|
+
* the document.
|
|
19
21
|
* @implementation-notes
|
|
20
22
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
21
23
|
* property, while engine-level hidden-subtree filtering still applies
|
|
22
24
|
* unless engineOptions.includeHiddenElements is true.
|
|
23
|
-
* - ID-reference resolution (
|
|
24
|
-
*
|
|
25
|
-
* aria-
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
25
|
+
* - ID-reference resolution (see aria-helpers.js's idExists) only flags
|
|
26
|
+
* idref-list attributes (aria-labelledby, aria-describedby,
|
|
27
|
+
* aria-controls, aria-owns, etc.) when NONE of the space-separated ids
|
|
28
|
+
* resolve — a partially-dangling list (some ids exist, some don't) is
|
|
29
|
+
* left unflagged (only invalidate when every token fails to resolve).
|
|
30
|
+
* Single-idref attributes (aria-activedescendant, aria-errormessage) are
|
|
31
|
+
* flagged whenever their one id doesn't resolve.
|
|
32
|
+
* - An explicitly-EMPTY idref/idref-list value (e.g.
|
|
33
|
+
* `aria-describedby=""`) is valid, not a violation — `allowEmpty` holds
|
|
34
|
+
* for every idref/idref-list ARIA attribute. A common, deliberate pattern
|
|
35
|
+
* in templated markup (e.g. React conditionally rendering
|
|
36
|
+
* `aria-describedby={hasError ? errorId : ''}`); flagging it would be a
|
|
37
|
+
* false positive.
|
|
35
38
|
*/
|
|
36
39
|
|
|
37
40
|
const id = 'aria-valid-attr-value';
|
|
38
41
|
|
|
39
42
|
const meta = {
|
|
40
43
|
title: 'aria-* attribute values must match their declared type',
|
|
41
|
-
description:
|
|
44
|
+
description:
|
|
45
|
+
'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
46
|
i18n: {
|
|
43
47
|
titleKey: 'ariaValidAttrValue_title',
|
|
44
48
|
descriptionKey: 'ariaValidAttrValue_description'
|
|
@@ -47,7 +51,13 @@ const meta = {
|
|
|
47
51
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
48
52
|
wcagSc: ['4.1.2'],
|
|
49
53
|
normativeMappings: [
|
|
50
|
-
{
|
|
54
|
+
{
|
|
55
|
+
standard: 'WCAG',
|
|
56
|
+
version: '2.2',
|
|
57
|
+
requirement: '4.1.2',
|
|
58
|
+
title: 'Name, Role, Value',
|
|
59
|
+
conformanceLevel: 'A'
|
|
60
|
+
}
|
|
51
61
|
],
|
|
52
62
|
defaultSeverity: 'serious',
|
|
53
63
|
category: 'robust',
|
|
@@ -57,7 +67,7 @@ const meta = {
|
|
|
57
67
|
};
|
|
58
68
|
|
|
59
69
|
function runInPage(ctx) {
|
|
60
|
-
const {
|
|
70
|
+
const { helpers, rule } = ctx;
|
|
61
71
|
|
|
62
72
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
63
73
|
if (!ariaHelpers) {
|
|
@@ -85,14 +95,18 @@ function runInPage(ctx) {
|
|
|
85
95
|
const result = ariaHelpers.validateAttrValue(name, rawValue);
|
|
86
96
|
if (!result.valid) {
|
|
87
97
|
if (!invalid) invalid = [];
|
|
88
|
-
invalid.push({
|
|
98
|
+
invalid.push({
|
|
99
|
+
name,
|
|
100
|
+
value: rawValue == null ? '' : String(rawValue),
|
|
101
|
+
reason: result.reason
|
|
102
|
+
});
|
|
89
103
|
}
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
if (!invalid || !invalid.length) continue;
|
|
93
107
|
|
|
94
108
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
95
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
109
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
96
110
|
|
|
97
111
|
for (const item of invalid) {
|
|
98
112
|
occurrences.push({
|
|
@@ -106,7 +120,12 @@ function runInPage(ctx) {
|
|
|
106
120
|
params: { attr: item.name, value: item.value }
|
|
107
121
|
},
|
|
108
122
|
data: {
|
|
109
|
-
details: {
|
|
123
|
+
details: {
|
|
124
|
+
reasonCode: 'ARIA_ATTR_VALUE_INVALID',
|
|
125
|
+
attr: item.name,
|
|
126
|
+
value: item.value,
|
|
127
|
+
valueReason: item.reason
|
|
128
|
+
}
|
|
110
129
|
}
|
|
111
130
|
});
|
|
112
131
|
}
|
|
@@ -116,7 +135,12 @@ function runInPage(ctx) {
|
|
|
116
135
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
117
136
|
}
|
|
118
137
|
if (occurrences.length) {
|
|
119
|
-
return {
|
|
138
|
+
return {
|
|
139
|
+
ruleId: rule.ruleId,
|
|
140
|
+
outcome: 'fail',
|
|
141
|
+
severity: rule.defaultSeverity || 'serious',
|
|
142
|
+
occurrences
|
|
143
|
+
};
|
|
120
144
|
}
|
|
121
145
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
122
146
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -27,7 +29,8 @@ const id = 'aria-valid-attr';
|
|
|
27
29
|
|
|
28
30
|
const meta = {
|
|
29
31
|
title: 'aria-* attributes must be real, defined ARIA attributes',
|
|
30
|
-
description:
|
|
32
|
+
description:
|
|
33
|
+
'Checks that every aria-* attribute name present in the DOM is a real attribute defined by the WAI-ARIA specification.',
|
|
31
34
|
i18n: {
|
|
32
35
|
titleKey: 'ariaValidAttr_title',
|
|
33
36
|
descriptionKey: 'ariaValidAttr_description'
|
|
@@ -36,7 +39,13 @@ const meta = {
|
|
|
36
39
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
37
40
|
wcagSc: ['4.1.2'],
|
|
38
41
|
normativeMappings: [
|
|
39
|
-
{
|
|
42
|
+
{
|
|
43
|
+
standard: 'WCAG',
|
|
44
|
+
version: '2.2',
|
|
45
|
+
requirement: '4.1.2',
|
|
46
|
+
title: 'Name, Role, Value',
|
|
47
|
+
conformanceLevel: 'A'
|
|
48
|
+
}
|
|
40
49
|
],
|
|
41
50
|
defaultSeverity: 'serious',
|
|
42
51
|
category: 'robust',
|
|
@@ -46,7 +55,7 @@ const meta = {
|
|
|
46
55
|
};
|
|
47
56
|
|
|
48
57
|
function runInPage(ctx) {
|
|
49
|
-
const {
|
|
58
|
+
const { helpers, rule } = ctx;
|
|
50
59
|
|
|
51
60
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
52
61
|
if (!ariaHelpers) {
|
|
@@ -78,7 +87,7 @@ function runInPage(ctx) {
|
|
|
78
87
|
if (!invalidNames || !invalidNames.length) continue;
|
|
79
88
|
|
|
80
89
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
81
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
90
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
82
91
|
|
|
83
92
|
for (const name of invalidNames) {
|
|
84
93
|
occurrences.push({
|
|
@@ -102,7 +111,12 @@ function runInPage(ctx) {
|
|
|
102
111
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
103
112
|
}
|
|
104
113
|
if (occurrences.length) {
|
|
105
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
ruleId: rule.ruleId,
|
|
116
|
+
outcome: 'fail',
|
|
117
|
+
severity: rule.defaultSeverity || 'serious',
|
|
118
|
+
occurrences
|
|
119
|
+
};
|
|
106
120
|
}
|
|
107
121
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
108
122
|
}
|