@surea11y/core 1.1.1 → 1.2.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 +29 -0
- package/README.md +3 -0
- package/bin/core.js +107 -3
- package/docs/API_STABILITY.md +61 -0
- package/docs/BASELINE.md +66 -0
- package/docs/CLI.md +26 -0
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/INTEGRATION.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/REPORT.md +33 -0
- package/docs/RULE_AUTHORING.md +31 -0
- package/package.json +1 -1
- package/src/baseline.js +0 -0
- package/src/checks/automatic/aria-allowed-attr.js +2 -3
- package/src/checks/automatic/aria-allowed-role.js +2 -3
- package/src/checks/automatic/aria-braille-equivalent.js +3 -4
- package/src/checks/automatic/aria-conditional-attr.js +3 -4
- package/src/checks/automatic/aria-deprecated-role.js +2 -3
- package/src/checks/automatic/aria-hidden-body.js +9 -1
- package/src/checks/automatic/aria-prohibited-attr.js +2 -3
- package/src/checks/automatic/aria-prohibited-children.js +73 -17
- package/src/checks/automatic/aria-required-attr.js +2 -3
- package/src/checks/automatic/aria-required-children.js +2 -3
- package/src/checks/automatic/aria-required-parent.js +3 -4
- package/src/checks/automatic/aria-roles-valid.js +2 -3
- package/src/checks/automatic/aria-valid-attr-value.js +2 -3
- package/src/checks/automatic/aria-valid-attr.js +2 -3
- package/src/checks/automatic/autocomplete-valid.js +2 -3
- package/src/checks/automatic/avoid-inline-spacing.js +2 -3
- package/src/checks/automatic/binary-control-name-present.js +2 -3
- package/src/checks/automatic/button-name-present.js +14 -4
- package/src/checks/automatic/bypass-blocks-present.js +44 -8
- package/src/checks/automatic/combobox-name-present.js +2 -3
- package/src/checks/automatic/css-orientation-lock.js +9 -1
- package/src/checks/automatic/definition-list-children-valid.js +2 -3
- package/src/checks/automatic/deprecated-elements-not-used.js +2 -3
- package/src/checks/automatic/dialog-name-present.js +2 -3
- package/src/checks/automatic/dlitem-parent-valid.js +2 -3
- package/src/checks/automatic/form-control-single-label.js +2 -3
- package/src/checks/automatic/html-xml-lang-mismatch.js +9 -1
- package/src/checks/automatic/iframe-focusable-content.js +2 -3
- package/src/checks/automatic/iframe-name-present.js +2 -3
- package/src/checks/automatic/iframe-title-unique.js +2 -3
- package/src/checks/automatic/label-in-name.js +7 -8
- package/src/checks/automatic/language-page-present.js +9 -1
- package/src/checks/automatic/link-in-text-block.js +2 -3
- package/src/checks/automatic/link-name-present.js +16 -4
- package/src/checks/automatic/list-children-valid.js +2 -3
- package/src/checks/automatic/listbox-name-present.js +2 -3
- package/src/checks/automatic/listitem-parent-valid.js +2 -3
- package/src/checks/automatic/menuitem-name-present.js +2 -3
- package/src/checks/automatic/meta-refresh-no-exceptions.js +9 -1
- package/src/checks/automatic/meta-refresh-timing-absent.js +9 -1
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +9 -1
- package/src/checks/automatic/meter-name-present.js +2 -3
- package/src/checks/automatic/nested-interactive-controls-absent.js +2 -3
- package/src/checks/automatic/option-name-present.js +2 -3
- package/src/checks/automatic/page-title-present.js +9 -1
- package/src/checks/automatic/progressbar-name-present.js +2 -3
- package/src/checks/automatic/searchbox-name-present.js +2 -3
- package/src/checks/automatic/server-side-image-map-absent.js +2 -3
- package/src/checks/automatic/slider-name-present.js +2 -3
- package/src/checks/automatic/spinbutton-name-present.js +2 -3
- package/src/checks/automatic/summary-name-present.js +2 -3
- package/src/checks/automatic/tab-name-present.js +2 -3
- package/src/checks/automatic/table-headers-attr-valid.js +2 -3
- package/src/checks/automatic/table-th-has-data-cells.js +2 -3
- package/src/checks/automatic/td-has-header.js +2 -3
- package/src/checks/automatic/textbox-name-present.js +2 -3
- package/src/checks/automatic/tooltip-name-present.js +2 -3
- package/src/checks/automatic/treeitem-name-present.js +2 -3
- package/src/checks/automatic/valid-lang.js +2 -3
- package/src/checks/manual/accesskeys-manual.js +1 -7
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +3 -4
- package/src/checks/manual/aria-text-manual.js +2 -3
- package/src/checks/manual/empty-heading-manual.js +1 -6
- package/src/checks/manual/empty-table-header-manual.js +5 -9
- package/src/checks/manual/focus-order-semantics-manual.js +2 -3
- package/src/checks/manual/heading-order-manual.js +1 -6
- package/src/checks/manual/identical-links-same-purpose-manual.js +2 -3
- package/src/checks/manual/image-redundant-alt-manual.js +2 -3
- package/src/checks/manual/label-title-only-manual.js +2 -3
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +30 -14
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +30 -14
- package/src/checks/manual/landmark-main-is-top-level-manual.js +30 -14
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +25 -13
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +25 -13
- package/src/checks/manual/landmark-one-main-manual.js +9 -1
- package/src/checks/manual/landmark-unique-manual.js +32 -30
- package/src/checks/manual/link-name-quality-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +9 -1
- package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -3
- package/src/checks/manual/no-autoplay-audio-manual.js +2 -3
- package/src/checks/manual/p-as-heading-manual.js +2 -3
- package/src/checks/manual/page-has-heading-one-manual.js +40 -3
- package/src/checks/manual/page-title-patterns-manual.js +9 -1
- package/src/checks/manual/presentation-role-conflict-manual.js +3 -4
- package/src/checks/manual/region-manual.js +34 -14
- package/src/checks/manual/scope-attr-valid-manual.js +2 -7
- package/src/checks/manual/scrollable-region-focusable-manual.js +2 -3
- package/src/checks/manual/skip-link-manual.js +91 -9
- package/src/checks/manual/tabindex-manual.js +2 -7
- package/src/checks/manual/table-duplicate-name-manual.js +2 -3
- package/src/checks/manual/table-fake-caption-manual.js +2 -3
- package/src/checks/manual/video-caption-manual.js +2 -3
- package/src/core/aria-helpers.js +82 -18
- package/src/core/dom-helpers.js +88 -3
- package/src/core/dom-runner.js +23 -0
- package/src/core/rule-meta.js +19 -0
- package/src/core.js +2632 -885
- package/src/i18n/en.js +6 -2
- package/src/i18n/fr.js +6 -2
- package/src/report.js +482 -0
|
@@ -45,15 +45,14 @@ const meta = {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
function runInPage(ctx) {
|
|
48
|
-
const { document,
|
|
49
|
-
const safeRoot = root || document;
|
|
48
|
+
const { document, helpers, rule } = ctx;
|
|
50
49
|
|
|
51
50
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
52
51
|
if (!ariaHelpers) {
|
|
53
52
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
55
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
57
56
|
|
|
58
57
|
const occurrences = [];
|
|
59
58
|
let applicableCount = 0;
|
|
@@ -48,6 +48,14 @@ const meta = {
|
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
// This check is inherently whole-document (does the PAGE have this
|
|
52
|
+
// property?), not evaluable per-subtree -- notApplicable when contextSelector
|
|
53
|
+
// scoped this run narrower than the whole document, or when
|
|
54
|
+
// engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
|
|
55
|
+
function applicability(ctx) {
|
|
56
|
+
return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
function runInPage(ctx) {
|
|
52
60
|
const { document, helpers, rule } = ctx;
|
|
53
61
|
|
|
@@ -84,4 +92,4 @@ function runInPage(ctx) {
|
|
|
84
92
|
return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'critical', occurrences };
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
module.exports = { id, meta, runInPage };
|
|
95
|
+
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -83,8 +83,7 @@ const meta = {
|
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
function runInPage(ctx) {
|
|
86
|
-
const { document,
|
|
87
|
-
const safeRoot = root || document;
|
|
86
|
+
const { document, helpers, rule } = ctx;
|
|
88
87
|
|
|
89
88
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
90
89
|
if (!ariaHelpers) {
|
|
@@ -104,7 +103,7 @@ function runInPage(ctx) {
|
|
|
104
103
|
|
|
105
104
|
const PROHIBITED_NAMING_ATTRS = ['aria-label', 'aria-labelledby'];
|
|
106
105
|
|
|
107
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
106
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
108
107
|
|
|
109
108
|
const occurrences = [];
|
|
110
109
|
let applicableCount = 0;
|
|
@@ -52,12 +52,42 @@
|
|
|
52
52
|
* focusability) are static, declarative markup facts with no live-DOM/
|
|
53
53
|
* hydration risk, unlike e.g. `aria-checked-state-mismatch`'s DOM-
|
|
54
54
|
* property comparison.
|
|
55
|
+
* - Fixed 2026-07-30: a roleless-but-focusable descendant's message and
|
|
56
|
+
* `data.details.attr` used to always claim "carries tabindex" even when
|
|
57
|
+
* the element had no tabindex attribute at all and was only focusable
|
|
58
|
+
* natively (e.g. an <a href> link). `helpers.getFocusableInfo`'s
|
|
59
|
+
* `mechanism` field ('tabindex' | 'native' | ...) is now used to tell
|
|
60
|
+
* the two apart, with a distinct `nativeFocusable` attr/message for the
|
|
61
|
+
* native case. Found via a real Angular app: a routerLink <a> inside a
|
|
62
|
+
* role="list" was reported as "carries tabindex" though the rendered
|
|
63
|
+
* markup had no such attribute.
|
|
55
64
|
* - Recursion stops at the first non-transparent role boundary, same as
|
|
56
65
|
* that reference engine: a nested container with its own real role (e.g. a
|
|
57
66
|
* <div role="listbox"> inside a menubar) is evaluated as ITS OWN
|
|
58
67
|
* owned-role entry against the outer container (and, separately, gets
|
|
59
68
|
* its own applicability pass as a container in the same rule run) —
|
|
60
69
|
* its descendants are never misattributed to the outer container.
|
|
70
|
+
* - Fixed 2026-07-31: child-role resolution used `ariaHelpers.getExplicitRole`
|
|
71
|
+
* (explicit role="" attribute only), unlike aria-required-children's
|
|
72
|
+
* descendant matching which uses `ariaHelpers.getContainmentRole` (explicit
|
|
73
|
+
* role, falling back to the native-tag map — li/tr/td/th/tbody/ul/ol/
|
|
74
|
+
* table/select/input[type=radio] — see that helper's own header comment).
|
|
75
|
+
* A bare `<li>` with no role="" attribute — the common CSS-reset
|
|
76
|
+
* workaround `<ul role="list"><li>...</li></ul>` that getContainmentRole
|
|
77
|
+
* exists specifically to handle — was therefore read as roleless here,
|
|
78
|
+
* making it structurally transparent: the walk recursed straight through
|
|
79
|
+
* the listitem boundary into its subtree and could report a focusable
|
|
80
|
+
* descendant several levels down as a disallowed owned child of the list,
|
|
81
|
+
* instead of stopping at the (implicit) listitem the way
|
|
82
|
+
* aria-required-children already does. Switched to getContainmentRole so
|
|
83
|
+
* both rules resolve an owned child's role identically. This is a general
|
|
84
|
+
* fix, not list/listitem-specific: it applies to every container role in
|
|
85
|
+
* REQUIRED_OWNED_ROLES whose native-tag counterpart the child map covers
|
|
86
|
+
* (e.g. a bare `<tr>`/`<td>` under a role="table"/"grid"/"row" container
|
|
87
|
+
* with no explicit role="" was subject to the same flattening bug). Found
|
|
88
|
+
* via a real Angular Material-style component library: an `<a routerlink>`
|
|
89
|
+
* several DOM levels inside a bare `<li>` under `<ul role="list">` was
|
|
90
|
+
* reported as an unallowed owned child of the list.
|
|
61
91
|
* - Gated on isAccTreeEligible for the container itself, matching the fix
|
|
62
92
|
* applied to aria-required-children (see that rule's header): the
|
|
63
93
|
* original "not gated" note here just cited that rule's reasoning
|
|
@@ -100,8 +130,7 @@ const meta = {
|
|
|
100
130
|
};
|
|
101
131
|
|
|
102
132
|
function runInPage(ctx) {
|
|
103
|
-
const { document,
|
|
104
|
-
const safeRoot = root || document;
|
|
133
|
+
const { document, helpers, rule } = ctx;
|
|
105
134
|
|
|
106
135
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
107
136
|
if (!ariaHelpers) {
|
|
@@ -152,6 +181,11 @@ function runInPage(ctx) {
|
|
|
152
181
|
// entry with role: null, which can never satisfy a required-role set)
|
|
153
182
|
// when it carries a global aria-* attribute or is focusable — matches
|
|
154
183
|
// a widely-used reference engine's own getOwnedRoles exactly (see header comment).
|
|
184
|
+
// kidRole comes from getContainmentRole, not getExplicitRole (see header
|
|
185
|
+
// comment's 2026-07-31 fix): "roleless" here means neither an explicit
|
|
186
|
+
// role="" NOR one of the native containment tags (li, tr, td, ...), so a
|
|
187
|
+
// bare <li>/<tr>/... is a real listitem/row boundary, not a transparent
|
|
188
|
+
// wrapper the walk should pass through.
|
|
155
189
|
function collectOwnedRoles(el, requiredSet, out, depth) {
|
|
156
190
|
if (depth > MAX_DEPTH) return;
|
|
157
191
|
const kids = el.children ? Array.prototype.slice.call(el.children) : [];
|
|
@@ -159,21 +193,27 @@ function runInPage(ctx) {
|
|
|
159
193
|
if (!kid || kid.nodeType !== 1) continue;
|
|
160
194
|
if (!isEligibleAcc(kid)) continue;
|
|
161
195
|
|
|
162
|
-
const kidRole = ariaHelpers.
|
|
196
|
+
const kidRole = ariaHelpers.getContainmentRole(kid);
|
|
163
197
|
const isPresentational = kidRole === 'presentation' || kidRole === 'none';
|
|
164
198
|
const isTransparentGroup = (kidRole === 'group' || kidRole === 'rowgroup') && requiredSet.has(kidRole);
|
|
165
199
|
|
|
166
200
|
if (!kidRole && !isPresentational) {
|
|
167
201
|
const globalAttr = getGlobalAriaAttr(kid);
|
|
168
|
-
let
|
|
202
|
+
let mechanism = 'none';
|
|
169
203
|
try {
|
|
170
204
|
const fi = helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo(kid, ctx) : null;
|
|
171
|
-
|
|
205
|
+
mechanism = (fi && fi.focusable && fi.mechanism) || 'none';
|
|
172
206
|
} catch {
|
|
173
|
-
|
|
207
|
+
mechanism = 'none';
|
|
174
208
|
}
|
|
175
|
-
if (globalAttr ||
|
|
176
|
-
|
|
209
|
+
if (globalAttr || mechanism !== 'none') {
|
|
210
|
+
// `mechanism` distinguishes an actual tabindex="" attribute from
|
|
211
|
+
// native focusability (e.g. <a href>, <button>, <input>) — these
|
|
212
|
+
// are different facts and must not be reported as the same
|
|
213
|
+
// "carries tabindex" claim (a native anchor with no tabindex
|
|
214
|
+
// attribute at all is not "carrying tabindex").
|
|
215
|
+
const attr = globalAttr || (mechanism === 'tabindex' ? 'tabindex' : 'nativeFocusable');
|
|
216
|
+
out.push({ el: kid, role: null, attr });
|
|
177
217
|
continue; // real accessible-tree node: stop here, do not recurse further
|
|
178
218
|
}
|
|
179
219
|
}
|
|
@@ -190,7 +230,7 @@ function runInPage(ctx) {
|
|
|
190
230
|
}
|
|
191
231
|
}
|
|
192
232
|
|
|
193
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
233
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
194
234
|
|
|
195
235
|
const occurrences = [];
|
|
196
236
|
let applicableCount = 0;
|
|
@@ -220,12 +260,28 @@ function runInPage(ctx) {
|
|
|
220
260
|
const containerSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
221
261
|
|
|
222
262
|
const isRoleless = !entry.role;
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
263
|
+
const isNativeFocusable = entry.attr === 'nativeFocusable';
|
|
264
|
+
|
|
265
|
+
let summary;
|
|
266
|
+
let hint;
|
|
267
|
+
let summaryKey;
|
|
268
|
+
let hintKey;
|
|
269
|
+
if (isNativeFocusable) {
|
|
270
|
+
summary = `This element has no explicit role but is natively focusable, making it a real accessible-tree node that is not an allowed owned child of the enclosing role="${role}" container.`;
|
|
271
|
+
hint = `Give this element role="presentation"/"none", remove its native focusability (e.g. drop the href/tabindex-granting attribute), or move it outside the ${role} container.`;
|
|
272
|
+
summaryKey = 'ariaProhibitedChildren_summary_fail_native_focusable';
|
|
273
|
+
hintKey = 'ariaProhibitedChildren_hint_fail_native_focusable';
|
|
274
|
+
} else if (isRoleless) {
|
|
275
|
+
summary = `This element has no explicit role but carries ${entry.attr}, making it a real accessible-tree node that is not an allowed owned child of the enclosing role="${role}" container.`;
|
|
276
|
+
hint = `Remove ${entry.attr} (or the role="${role}" container ownership), or give this element role="presentation"/"none" if it isn't meant to be its own accessible-tree node.`;
|
|
277
|
+
summaryKey = 'ariaProhibitedChildren_summary_fail_roleless';
|
|
278
|
+
hintKey = 'ariaProhibitedChildren_hint_fail_roleless';
|
|
279
|
+
} else {
|
|
280
|
+
summary = `This element has role="${entry.role}", which is not an allowed owned child of the enclosing role="${role}" container.`;
|
|
281
|
+
hint = `Remove or change this role so it matches one of the container's allowed owned roles (${requiredOwned.join(', ')}), or move this element outside the ${role} container.`;
|
|
282
|
+
summaryKey = 'ariaProhibitedChildren_summary_fail';
|
|
283
|
+
hintKey = 'ariaProhibitedChildren_hint_fail';
|
|
284
|
+
}
|
|
229
285
|
|
|
230
286
|
occurrences.push({
|
|
231
287
|
selector: stableSelector,
|
|
@@ -233,8 +289,8 @@ function runInPage(ctx) {
|
|
|
233
289
|
summary,
|
|
234
290
|
hint,
|
|
235
291
|
i18n: {
|
|
236
|
-
summaryKey
|
|
237
|
-
hintKey
|
|
292
|
+
summaryKey,
|
|
293
|
+
hintKey,
|
|
238
294
|
params: isRoleless
|
|
239
295
|
? { attr: entry.attr, containerRole: role }
|
|
240
296
|
: { childRole: entry.role, containerRole: role, allowedRoles: requiredOwned.join(', ') }
|
|
@@ -69,8 +69,7 @@ const meta = {
|
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
function runInPage(ctx) {
|
|
72
|
-
const { document,
|
|
73
|
-
const safeRoot = root || document;
|
|
72
|
+
const { document, helpers, rule } = ctx;
|
|
74
73
|
|
|
75
74
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
76
75
|
if (!ariaHelpers) {
|
|
@@ -94,7 +93,7 @@ function runInPage(ctx) {
|
|
|
94
93
|
return v != null && String(v).trim().toLowerCase() === 'true';
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
96
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
98
97
|
|
|
99
98
|
const occurrences = [];
|
|
100
99
|
let applicableCount = 0;
|
|
@@ -80,8 +80,7 @@ const meta = {
|
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
function runInPage(ctx) {
|
|
83
|
-
const { document,
|
|
84
|
-
const safeRoot = root || document;
|
|
83
|
+
const { document, helpers, rule } = ctx;
|
|
85
84
|
|
|
86
85
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
87
86
|
if (!ariaHelpers) {
|
|
@@ -112,7 +111,7 @@ function runInPage(ctx) {
|
|
|
112
111
|
// ("runInPage MUST be self-contained").
|
|
113
112
|
const CANDIDATE_SELECTOR = '[role], li, option, tr, td, th, thead, tbody, tfoot, ul, ol, table, select, input[type="radio"]';
|
|
114
113
|
|
|
115
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
114
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
116
115
|
|
|
117
116
|
const occurrences = [];
|
|
118
117
|
let applicableCount = 0;
|
|
@@ -66,8 +66,7 @@ const meta = {
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
function runInPage(ctx) {
|
|
69
|
-
const { document,
|
|
70
|
-
const safeRoot = root || document;
|
|
69
|
+
const { document, helpers, rule } = ctx;
|
|
71
70
|
|
|
72
71
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
73
72
|
if (!ariaHelpers) {
|
|
@@ -153,7 +152,7 @@ function runInPage(ctx) {
|
|
|
153
152
|
const idTok = elId && String(elId).trim();
|
|
154
153
|
if (!idTok) return false;
|
|
155
154
|
|
|
156
|
-
const owners = helpers.queryAllSmart ? helpers.queryAllSmart('[aria-owns]'
|
|
155
|
+
const owners = helpers.queryAllSmart ? helpers.queryAllSmart('[aria-owns]') : helpers.queryAll('[aria-owns]');
|
|
157
156
|
for (const owner of owners) {
|
|
158
157
|
if (!owner || !owner.getAttribute) continue;
|
|
159
158
|
const ownsAttr = owner.getAttribute('aria-owns') || '';
|
|
@@ -166,7 +165,7 @@ function runInPage(ctx) {
|
|
|
166
165
|
return false;
|
|
167
166
|
}
|
|
168
167
|
|
|
169
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
168
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
170
169
|
|
|
171
170
|
const occurrences = [];
|
|
172
171
|
let applicableCount = 0;
|
|
@@ -44,15 +44,14 @@ const meta = {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
function runInPage(ctx) {
|
|
47
|
-
const { document,
|
|
48
|
-
const safeRoot = root || document;
|
|
47
|
+
const { document, helpers, rule } = ctx;
|
|
49
48
|
|
|
50
49
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
51
50
|
if (!ariaHelpers) {
|
|
52
51
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
54
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
56
55
|
|
|
57
56
|
const occurrences = [];
|
|
58
57
|
let applicableCount = 0;
|
|
@@ -57,15 +57,14 @@ const meta = {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
function runInPage(ctx) {
|
|
60
|
-
const { document,
|
|
61
|
-
const safeRoot = root || document;
|
|
60
|
+
const { document, helpers, rule } = ctx;
|
|
62
61
|
|
|
63
62
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
64
63
|
if (!ariaHelpers) {
|
|
65
64
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*'
|
|
67
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*') : helpers.queryAll('*');
|
|
69
68
|
|
|
70
69
|
const occurrences = [];
|
|
71
70
|
let applicableCount = 0;
|
|
@@ -46,15 +46,14 @@ const meta = {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
function runInPage(ctx) {
|
|
49
|
-
const { document,
|
|
50
|
-
const safeRoot = root || document;
|
|
49
|
+
const { document, helpers, rule } = ctx;
|
|
51
50
|
|
|
52
51
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
53
52
|
if (!ariaHelpers) {
|
|
54
53
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*'
|
|
56
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*') : helpers.queryAll('*');
|
|
58
57
|
|
|
59
58
|
const occurrences = [];
|
|
60
59
|
let applicableCount = 0;
|
|
@@ -52,8 +52,7 @@ const meta = {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
function runInPage(ctx) {
|
|
55
|
-
const { document,
|
|
56
|
-
const safeRoot = root || document;
|
|
55
|
+
const { document, helpers, rule } = ctx;
|
|
57
56
|
|
|
58
57
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
59
58
|
// ("runInPage MUST be self-contained").
|
|
@@ -88,7 +87,7 @@ function runInPage(ctx) {
|
|
|
88
87
|
return FIELD_NAMES.has(remaining[0]);
|
|
89
88
|
}
|
|
90
89
|
|
|
91
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('input, select, textarea'
|
|
90
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('input, select, textarea') : helpers.queryAll('input, select, textarea');
|
|
92
91
|
|
|
93
92
|
const occurrences = [];
|
|
94
93
|
let applicableCount = 0;
|
|
@@ -46,12 +46,11 @@ const meta = {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
function runInPage(ctx) {
|
|
49
|
-
const { document,
|
|
50
|
-
const safeRoot = root || document;
|
|
49
|
+
const { document, helpers, rule } = ctx;
|
|
51
50
|
|
|
52
51
|
const SPACING_PROPS = ['line-height', 'letter-spacing', 'word-spacing'];
|
|
53
52
|
|
|
54
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[style]'
|
|
53
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[style]') : helpers.queryAll('[style]');
|
|
55
54
|
|
|
56
55
|
const occurrences = [];
|
|
57
56
|
let applicableCount = 0;
|
|
@@ -23,11 +23,10 @@ const meta = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
function runInPage(ctx) {
|
|
26
|
-
const { document,
|
|
26
|
+
const { document, helpers, rule } = ctx;
|
|
27
27
|
const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
|
|
28
28
|
? helpers.getEligibilityInfo
|
|
29
29
|
: null;
|
|
30
|
-
const safeRoot = root || document;
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
function normalizeWs(s) {
|
|
@@ -159,7 +158,7 @@ function runInPage(ctx) {
|
|
|
159
158
|
let applicableCount = 0;
|
|
160
159
|
|
|
161
160
|
const selector = 'input[type="checkbox"], input[type="radio"], [role="checkbox"], [role="radio"], [role="switch"]';
|
|
162
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
161
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
163
162
|
|
|
164
163
|
|
|
165
164
|
// Precompute label[for] associations once for speed/determinism.
|
|
@@ -25,8 +25,7 @@ const meta = {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
function runInPage(ctx) {
|
|
28
|
-
const { document,
|
|
29
|
-
const safeRoot = root || document;
|
|
28
|
+
const { document, helpers, rule } = ctx;
|
|
30
29
|
|
|
31
30
|
const occurrences = [];
|
|
32
31
|
let applicableCount = 0;
|
|
@@ -63,7 +62,7 @@ function runInPage(ctx) {
|
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
const selector = 'button, input[type="button"], input[type="submit"], input[type="reset"], [role="button"]';
|
|
66
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
65
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
67
66
|
|
|
68
67
|
for (const el of nodes) {
|
|
69
68
|
// isAccTreeEligible returns { eligible, reasons }, not a boolean.
|
|
@@ -75,6 +74,7 @@ function runInPage(ctx) {
|
|
|
75
74
|
|
|
76
75
|
const tag = (el.tagName || '').toLowerCase();
|
|
77
76
|
const role = el.getAttribute ? el.getAttribute('role') : null;
|
|
77
|
+
const roleNorm = normalizeWs(role).toLowerCase();
|
|
78
78
|
|
|
79
79
|
const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
|
|
80
80
|
|
|
@@ -90,7 +90,17 @@ function runInPage(ctx) {
|
|
|
90
90
|
inputValueName = getInputButtonValueName(el);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
// A native <button> (or [role="button"]) whose role has been overridden to
|
|
94
|
+
// one of these roles is no longer semantically a button — per the WAI-ARIA
|
|
95
|
+
// Accessible Name and Description Computation spec these roles are
|
|
96
|
+
// name-from-author-only, and their rendered content represents a VALUE,
|
|
97
|
+
// not a NAME (mirrors axe-core's controlValueRoles, verified against its
|
|
98
|
+
// source). Found on a real page (Spotify's "sort by" control): a
|
|
99
|
+
// <button role="combobox">List</button> where "List" is the combobox's
|
|
100
|
+
// currently selected value, not a label for what the combobox is —
|
|
101
|
+
// crediting it as the accessible name masked a real missing-name bug.
|
|
102
|
+
const VALUE_ROLES = ['textbox', 'progressbar', 'scrollbar', 'slider', 'spinbutton', 'combobox', 'listbox'];
|
|
103
|
+
const isContentNameCandidate = (tag === 'button' || role === 'button') && !VALUE_ROLES.includes(roleNorm);
|
|
94
104
|
const contentName =
|
|
95
105
|
(!trustedProgrammaticName && !inputValueName && isContentNameCandidate)
|
|
96
106
|
? getConservativeSubtreeText(el)
|
|
@@ -67,6 +67,14 @@ const meta = {
|
|
|
67
67
|
coverage: { facetsBySc: { '2.4.1': ['bypass-blocks-present'] } }
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
// This check is inherently whole-document (does the PAGE have this
|
|
71
|
+
// property?), not evaluable per-subtree -- notApplicable when contextSelector
|
|
72
|
+
// scoped this run narrower than the whole document, or when
|
|
73
|
+
// engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
|
|
74
|
+
function applicability(ctx) {
|
|
75
|
+
return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
function runInPage(ctx) {
|
|
71
79
|
const { document, helpers, rule } = ctx;
|
|
72
80
|
|
|
@@ -75,14 +83,43 @@ function runInPage(ctx) {
|
|
|
75
83
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
76
84
|
}
|
|
77
85
|
|
|
78
|
-
function
|
|
86
|
+
const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
87
|
+
|
|
88
|
+
function isExposedToAt(el) {
|
|
89
|
+
if (!isAccTreeEligible) return true;
|
|
90
|
+
try {
|
|
91
|
+
const r = isAccTreeEligible(el, ctx);
|
|
92
|
+
if (typeof r === 'boolean') return r;
|
|
93
|
+
return !!(r && r.eligible);
|
|
94
|
+
} catch {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function queryAll(selector) {
|
|
79
100
|
try {
|
|
80
|
-
return
|
|
101
|
+
return helpers && typeof helpers.queryAllSmart === 'function'
|
|
102
|
+
? helpers.queryAllSmart(selector)
|
|
103
|
+
: document.querySelectorAll(selector);
|
|
81
104
|
} catch {
|
|
82
|
-
return
|
|
105
|
+
return [];
|
|
83
106
|
}
|
|
84
107
|
}
|
|
85
108
|
|
|
109
|
+
// Filters through isAccTreeEligible (hidden/aria-hidden/display:none/inert
|
|
110
|
+
// don't count as "a bypass mechanism is present") -- see page-has-heading-
|
|
111
|
+
// one-manual.js's identical fix for the real-world trigger (CDC's flu
|
|
112
|
+
// page, 2026-07-30: its only <h1> sits inside a display:none ancestor,
|
|
113
|
+
// unreachable by sighted and screen reader users alike). A fully
|
|
114
|
+
// non-rendered <main>/heading was previously credited here too, wrongly
|
|
115
|
+
// returning `pass` for a page with zero actual bypass mechanisms.
|
|
116
|
+
function hasMainLandmark() {
|
|
117
|
+
for (const el of queryAll('main, [role="main"]')) {
|
|
118
|
+
if (el && isExposedToAt(el)) return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
86
123
|
function hasWorkingAnchorLink() {
|
|
87
124
|
let links = [];
|
|
88
125
|
try {
|
|
@@ -122,11 +159,10 @@ function runInPage(ctx) {
|
|
|
122
159
|
}
|
|
123
160
|
|
|
124
161
|
function hasHeading() {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
} catch {
|
|
128
|
-
return false;
|
|
162
|
+
for (const el of queryAll('h1, h2, h3, h4, h5, h6, [role="heading"]')) {
|
|
163
|
+
if (el && isExposedToAt(el)) return true;
|
|
129
164
|
}
|
|
165
|
+
return false;
|
|
130
166
|
}
|
|
131
167
|
|
|
132
168
|
const mainLandmark = hasMainLandmark();
|
|
@@ -159,4 +195,4 @@ function runInPage(ctx) {
|
|
|
159
195
|
return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
|
|
160
196
|
}
|
|
161
197
|
|
|
162
|
-
module.exports = { id, meta, runInPage };
|
|
198
|
+
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -23,11 +23,10 @@ const meta = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
function runInPage(ctx) {
|
|
26
|
-
const { document,
|
|
26
|
+
const { document, helpers, rule } = ctx;
|
|
27
27
|
const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
|
|
28
28
|
? helpers.getEligibilityInfo
|
|
29
29
|
: null;
|
|
30
|
-
const safeRoot = root || document;
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
function normalizeWs(s) {
|
|
@@ -159,7 +158,7 @@ function runInPage(ctx) {
|
|
|
159
158
|
let applicableCount = 0;
|
|
160
159
|
|
|
161
160
|
const selector = '[role="combobox"]';
|
|
162
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
161
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
163
162
|
|
|
164
163
|
// Precompute label[for] map for combobox elements that are labelable
|
|
165
164
|
// native form controls (e.g. <input role="combobox">).
|
|
@@ -73,6 +73,14 @@ const meta = {
|
|
|
73
73
|
coverage: { facetsBySc: { '1.3.4': ['css-orientation-lock'] } }
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
+
// This check is inherently whole-document (does the PAGE have this
|
|
77
|
+
// property?), not evaluable per-subtree -- notApplicable when contextSelector
|
|
78
|
+
// scoped this run narrower than the whole document, or when
|
|
79
|
+
// engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
|
|
80
|
+
function applicability(ctx) {
|
|
81
|
+
return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
|
|
82
|
+
}
|
|
83
|
+
|
|
76
84
|
function runInPage(ctx) {
|
|
77
85
|
const { document, helpers, rule } = ctx;
|
|
78
86
|
|
|
@@ -203,4 +211,4 @@ function runInPage(ctx) {
|
|
|
203
211
|
return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
|
|
204
212
|
}
|
|
205
213
|
|
|
206
|
-
module.exports = { id, meta, runInPage };
|
|
214
|
+
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -54,14 +54,13 @@ const meta = {
|
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
function runInPage(ctx) {
|
|
57
|
-
const { document,
|
|
58
|
-
const safeRoot = root || document;
|
|
57
|
+
const { document, helpers, rule } = ctx;
|
|
59
58
|
|
|
60
59
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
61
60
|
// ("runInPage MUST be self-contained").
|
|
62
61
|
const PASSTHROUGH_TAGS = new Set(['dt', 'dd', 'script', 'template', 'style']);
|
|
63
62
|
|
|
64
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dl'
|
|
63
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dl') : helpers.queryAll('dl');
|
|
65
64
|
|
|
66
65
|
const occurrences = [];
|
|
67
66
|
let applicableCount = 0;
|
|
@@ -48,10 +48,9 @@ const meta = {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
function runInPage(ctx) {
|
|
51
|
-
const { document,
|
|
52
|
-
const safeRoot = root || document;
|
|
51
|
+
const { document, helpers, rule } = ctx;
|
|
53
52
|
|
|
54
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('blink, marquee'
|
|
53
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('blink, marquee') : helpers.queryAll('blink, marquee');
|
|
55
54
|
|
|
56
55
|
const occurrences = [];
|
|
57
56
|
let applicableCount = 0;
|
|
@@ -23,11 +23,10 @@ const meta = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
function runInPage(ctx) {
|
|
26
|
-
const { document,
|
|
26
|
+
const { document, helpers, rule } = ctx;
|
|
27
27
|
const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
|
|
28
28
|
? helpers.getEligibilityInfo
|
|
29
29
|
: null;
|
|
30
|
-
const safeRoot = root || document;
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
function normalizeWs(s) {
|
|
@@ -141,7 +140,7 @@ function runInPage(ctx) {
|
|
|
141
140
|
let applicableCount = 0;
|
|
142
141
|
|
|
143
142
|
const selector = '[role="dialog"],[role="alertdialog"]';
|
|
144
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
143
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
145
144
|
|
|
146
145
|
|
|
147
146
|
function evaluate(el) {
|
|
@@ -42,10 +42,9 @@ const meta = {
|
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
function runInPage(ctx) {
|
|
45
|
-
const { document,
|
|
46
|
-
const safeRoot = root || document;
|
|
45
|
+
const { document, helpers, rule } = ctx;
|
|
47
46
|
|
|
48
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dt, dd'
|
|
47
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dt, dd') : helpers.queryAll('dt, dd');
|
|
49
48
|
|
|
50
49
|
const occurrences = [];
|
|
51
50
|
let applicableCount = 0;
|
|
@@ -46,11 +46,10 @@ const meta = {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
function runInPage(ctx) {
|
|
49
|
-
const { document,
|
|
50
|
-
const safeRoot = root || document;
|
|
49
|
+
const { document, helpers, rule } = ctx;
|
|
51
50
|
|
|
52
51
|
const selector = 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
|
|
53
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
52
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
54
53
|
|
|
55
54
|
// Build a for-value -> label[] map once (avoids per-control dynamic
|
|
56
55
|
// attribute-selector construction / CSS.escape, which is not guaranteed
|