@surea11y/core 1.4.1 → 1.6.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 +212 -128
- package/README.md +46 -9
- package/docs/ACT_RULE_MAPPING.md +243 -0
- package/docs/API_STABILITY.md +4 -4
- package/docs/BINDING_AUTHORS_GUIDE.md +3 -3
- package/docs/DESIGN_CHALLENGES.md +301 -0
- package/docs/ENGINE_OPTIONS.md +30 -14
- package/docs/I18N.md +176 -20
- package/docs/INTEGRATION.md +29 -7
- package/docs/LIMITATIONS.md +6 -4
- package/docs/OUTPUT_SCHEMA.md +13 -3
- package/docs/REPORT.md +3 -1
- package/docs/RULE_AUTHORING.md +104 -23
- package/docs/RULE_CATALOG.md +1878 -169
- package/docs/RULE_TAXONOMY.md +2 -2
- package/docs/TROUBLESHOOTING.md +4 -4
- package/docs/WCAG_CONFORMANCE.md +25 -9
- package/package.json +8 -7
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +95 -40
- package/src/checks/automatic/aria-allowed-role.js +16 -18
- package/src/checks/automatic/aria-braille-equivalent.js +19 -21
- package/src/checks/automatic/aria-conditional-attr.js +22 -24
- package/src/checks/automatic/aria-deprecated-role.js +63 -50
- package/src/checks/automatic/aria-hidden-body.js +4 -11
- package/src/checks/automatic/aria-hidden-focus.js +104 -23
- package/src/checks/automatic/aria-prohibited-attr.js +71 -72
- package/src/checks/automatic/aria-prohibited-children.js +154 -61
- package/src/checks/automatic/aria-required-attr.js +74 -29
- package/src/checks/automatic/aria-required-children.js +38 -34
- package/src/checks/automatic/aria-required-parent.js +78 -29
- package/src/checks/automatic/aria-role-name-present.js +36 -22
- package/src/checks/automatic/aria-roles-valid.js +37 -23
- package/src/checks/automatic/aria-valid-attr-value.js +33 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -18
- package/src/checks/automatic/autocomplete-valid.js +17 -19
- package/src/checks/automatic/avoid-inline-spacing.js +14 -16
- package/src/checks/automatic/binary-control-name-present.js +46 -26
- package/src/checks/automatic/button-name-present.js +115 -34
- package/src/checks/automatic/combobox-name-present.js +40 -22
- package/src/checks/automatic/contrast-computable.js +32 -0
- package/src/checks/automatic/contrast-enhanced.js +21 -1
- package/src/checks/automatic/contrast-minimum.js +21 -1
- package/src/checks/automatic/css-orientation-lock.js +118 -41
- package/src/checks/automatic/definition-list-children-valid.js +25 -29
- package/src/checks/automatic/deprecated-elements-not-used.js +15 -17
- package/src/checks/automatic/dialog-name-present.js +36 -20
- package/src/checks/automatic/dlitem-parent-valid.js +15 -17
- package/src/checks/automatic/duplicate-id-aria.js +50 -40
- package/src/checks/automatic/duplicate-id.js +198 -0
- package/src/checks/automatic/embed-text-alternative-present.js +2 -2
- package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
- package/src/checks/automatic/form-control-single-label.js +39 -41
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
- package/src/checks/automatic/iframe-focusable-content.js +92 -38
- package/src/checks/automatic/iframe-name-present.js +53 -21
- package/src/checks/automatic/iframe-title-unique.js +19 -24
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +198 -49
- package/src/checks/automatic/link-in-text-block.js +29 -31
- package/src/checks/automatic/link-name-present.js +47 -31
- package/src/checks/automatic/list-children-valid.js +21 -23
- package/src/checks/automatic/listbox-name-present.js +42 -24
- package/src/checks/automatic/listitem-parent-valid.js +18 -21
- package/src/checks/automatic/menuitem-name-present.js +36 -20
- package/src/checks/automatic/meta-refresh-no-exceptions.js +39 -31
- package/src/checks/automatic/meta-refresh-timing-absent.js +29 -25
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
- package/src/checks/automatic/meter-name-present.js +38 -21
- package/src/checks/automatic/nested-interactive-controls-absent.js +22 -24
- package/src/checks/automatic/option-name-present.js +39 -22
- package/src/checks/automatic/page-title-present.js +21 -3
- package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
- package/src/checks/automatic/progressbar-name-present.js +41 -24
- package/src/checks/automatic/role-img-alt-present.js +64 -16
- package/src/checks/automatic/searchbox-name-present.js +46 -24
- package/src/checks/automatic/server-side-image-map-absent.js +16 -19
- package/src/checks/automatic/slider-name-present.js +42 -23
- package/src/checks/automatic/spinbutton-name-present.js +46 -24
- package/src/checks/automatic/summary-name-present.js +34 -20
- package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
- package/src/checks/automatic/svg-text-alternative-present.js +13 -10
- package/src/checks/automatic/tab-name-present.js +37 -20
- package/src/checks/automatic/table-headers-attr-valid.js +57 -24
- package/src/checks/automatic/table-th-has-data-cells.js +76 -24
- package/src/checks/automatic/target-size-minimum.js +172 -131
- package/src/checks/automatic/td-has-header.js +20 -25
- package/src/checks/automatic/textbox-name-present.js +42 -24
- package/src/checks/automatic/tooltip-name-present.js +37 -20
- package/src/checks/automatic/treeitem-name-present.js +39 -22
- package/src/checks/automatic/valid-lang.js +107 -24
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +21 -22
- package/src/checks/manual/area-alt-decorative-manual.js +7 -0
- package/src/checks/manual/area-alt-quality-manual.js +6 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +23 -26
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +48 -38
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/empty-heading-manual.js +73 -28
- package/src/checks/manual/empty-table-header-manual.js +33 -36
- package/src/checks/manual/focus-order-semantics-manual.js +15 -15
- package/src/checks/manual/form-control-label-quality-manual.js +453 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +4 -11
- package/src/checks/manual/heading-order-manual.js +20 -25
- package/src/checks/manual/heading-quality-manual.js +338 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
- package/src/checks/manual/image-redundant-alt-manual.js +18 -21
- package/src/checks/manual/img-alt-decorative-manual.js +211 -52
- package/src/checks/manual/img-alt-quality-manual.js +7 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
- package/src/checks/manual/label-title-only-manual.js +19 -21
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +21 -24
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +23 -26
- package/src/checks/manual/landmark-main-is-top-level-manual.js +20 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +8 -13
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +8 -13
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +4 -9
- package/src/checks/manual/landmark-one-main-manual.js +8 -15
- package/src/checks/manual/landmark-unique-manual.js +31 -36
- package/src/checks/manual/link-name-quality-manual.js +162 -35
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +16 -19
- package/src/checks/manual/mouse-only-event-handlers-manual.js +26 -28
- package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
- package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/p-as-heading-manual.js +4 -4
- package/src/checks/manual/page-has-heading-one-manual.js +8 -15
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/presentation-role-conflict-manual.js +74 -46
- package/src/checks/manual/region-manual.js +32 -25
- package/src/checks/manual/scope-attr-valid-manual.js +16 -19
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +44 -52
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +16 -19
- package/src/checks/manual/table-duplicate-name-manual.js +16 -19
- package/src/checks/manual/table-fake-caption-manual.js +2 -2
- package/src/checks/manual/video-caption-manual.js +3 -3
- package/src/checks/manual-review.js +17 -1
- package/src/core.js +13086 -5169
- package/src/report.js +16 -2
- package/surea11y.browser.js +5665 -4219
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
- package/bin/surea11y-core.js +0 -20
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* ancestor/owner's aria-owns) whose effective role is one of the
|
|
20
20
|
* acceptable context roles for this element's role.
|
|
21
21
|
* @implementation-notes
|
|
22
|
-
* -
|
|
22
|
+
* - Scoped to REQUIRED_CONTEXT_ROLE in src/core/aria-helpers.js
|
|
23
23
|
* (see that file's header for the conservative-scope rationale); roles
|
|
24
24
|
* with an explicitly empty entry (e.g. tabpanel) are left unconstrained.
|
|
25
25
|
* - Context-role matching uses ariaHelpers.getContainmentRole, which
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
* independent path via a reverse lookup over the search root.
|
|
35
35
|
* - Gated on isAccTreeEligible for the element itself. The ancestor-role
|
|
36
36
|
* walk itself doesn't care about visibility (a hidden ancestor's role is
|
|
37
|
-
* still found by plain DOM/composed-tree containment, so a
|
|
38
|
-
* correctly-nested-but-hidden widget was never at risk here)
|
|
37
|
+
* still found by plain DOM/composed-tree containment, so a
|
|
38
|
+
* correctly-nested-but-hidden widget was never at risk here), the
|
|
39
39
|
* remaining false-positive shape is an element whose required ancestor
|
|
40
40
|
* context doesn't exist YET because it (and its wrapping context) are
|
|
41
41
|
* assembled together at reveal time (e.g. a portal-rendered item staged
|
|
@@ -96,13 +96,57 @@ function runInPage(ctx) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// Roles that may host a nested listitem/treeitem group without breaking
|
|
99
|
-
// the required-context chain
|
|
99
|
+
// the required-context chain, the group role is transparent for exactly
|
|
100
100
|
// these two roles.
|
|
101
101
|
const GROUP_TRANSPARENT_FOR_ROLES = new Set(['listitem', 'treeitem']);
|
|
102
102
|
|
|
103
|
-
//
|
|
103
|
+
// The WAI-ARIA "Global States and Properties" set (same list as
|
|
104
|
+
// aria-prohibited-children.js's GLOBAL_ARIA_ATTRS, duplicated, not
|
|
105
|
+
// imported, since runInPage must be self-contained per
|
|
106
|
+
// scripts/build-core.js). A roleless ancestor carrying any of these is
|
|
107
|
+
// still "included in the accessibility tree" and is therefore a real
|
|
108
|
+
// (generic) parent, not a transparent one: ACT's ff89c9 test corpus
|
|
109
|
+
// covers exactly this with role="listitem" whose actual DOM parent is a
|
|
110
|
+
// roleless <div aria-live="polite">, itself inside a role="list", the
|
|
111
|
+
// required-context chain is broken by that included-but-roleless div,
|
|
112
|
+
// even though a role="list" ancestor does exist further up.
|
|
113
|
+
const GLOBAL_ARIA_ATTRS = [
|
|
114
|
+
'aria-atomic',
|
|
115
|
+
'aria-braillelabel',
|
|
116
|
+
'aria-brailleroledescription',
|
|
117
|
+
'aria-busy',
|
|
118
|
+
'aria-controls',
|
|
119
|
+
'aria-current',
|
|
120
|
+
'aria-describedby',
|
|
121
|
+
'aria-description',
|
|
122
|
+
'aria-details',
|
|
123
|
+
'aria-disabled',
|
|
124
|
+
'aria-dropeffect',
|
|
125
|
+
'aria-errormessage',
|
|
126
|
+
'aria-flowto',
|
|
127
|
+
'aria-grabbed',
|
|
128
|
+
'aria-haspopup',
|
|
129
|
+
'aria-hidden',
|
|
130
|
+
'aria-invalid',
|
|
131
|
+
'aria-keyshortcuts',
|
|
132
|
+
'aria-label',
|
|
133
|
+
'aria-labelledby',
|
|
134
|
+
'aria-live',
|
|
135
|
+
'aria-owns',
|
|
136
|
+
'aria-relevant',
|
|
137
|
+
'aria-roledescription'
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
function hasGlobalAriaAttr(el) {
|
|
141
|
+
for (const attr of GLOBAL_ARIA_ATTRS) {
|
|
142
|
+
if (el.getAttribute && el.getAttribute(attr) != null) return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// A real ancestor role, not "no role at all" and not the two roles that
|
|
104
148
|
// strip an element from the accessibility tree's parent/child chain
|
|
105
|
-
// entirely (presentation/none)
|
|
149
|
+
// entirely (presentation/none), stops the search. This is stricter than
|
|
106
150
|
// "any ancestor with the right role anywhere up the tree": the
|
|
107
151
|
// required-context relationship is about the accessibility tree's actual
|
|
108
152
|
// PARENT, so an intervening ancestor with its OWN distinct real role
|
|
@@ -110,19 +154,27 @@ function runInPage(ctx) {
|
|
|
110
154
|
// a further-up ancestor has the correct role. E.g. a <button role="tab">
|
|
111
155
|
// inside a plain <li> (native listitem) inside <ul role="tablist"> fails:
|
|
112
156
|
// the tablist is never the tab's accessible-tree parent, the listitem is.
|
|
157
|
+
//
|
|
158
|
+
// A roleless ancestor is normally transparent (it isn't a node in the
|
|
159
|
+
// accessibility tree at all), UNLESS it carries a global ARIA attribute
|
|
160
|
+
// -- that alone includes it in the tree as a real, roleless (generic)
|
|
161
|
+
// parent, which still blocks the search the same way a distinct real
|
|
162
|
+
// role would.
|
|
113
163
|
function getRealContextRole(el) {
|
|
114
164
|
const role = ariaHelpers.getContainmentRole(el);
|
|
115
|
-
if (!role || role === 'presentation' || role === 'none')
|
|
165
|
+
if (!role || role === 'presentation' || role === 'none') {
|
|
166
|
+
return hasGlobalAriaAttr(el) ? 'generic' : '';
|
|
167
|
+
}
|
|
116
168
|
return role;
|
|
117
169
|
}
|
|
118
170
|
|
|
119
|
-
// Flat-tree ancestor walk (ctx.helpers.composedParent
|
|
171
|
+
// Flat-tree ancestor walk (ctx.helpers.composedParent, assignedSlot wins
|
|
120
172
|
// over parentNode, then shadow host). A slotted light-DOM element's real
|
|
121
173
|
// rendered ancestor is whatever the shadow tree wraps its <slot> in (e.g.
|
|
122
174
|
// a role="list" container), not its own light-DOM parentElement.
|
|
123
175
|
// composedParent can return a non-Element node (a ShadowRoot, nodeType
|
|
124
176
|
// 11) when climbing out of a shadow tree that has no further light-DOM
|
|
125
|
-
// parent
|
|
177
|
+
// parent, skip those and keep climbing rather than treating them as a
|
|
126
178
|
// (roleless) context.
|
|
127
179
|
const getComposedParent =
|
|
128
180
|
helpers && typeof helpers.composedParent === 'function'
|
|
@@ -210,27 +262,24 @@ function runInPage(ctx) {
|
|
|
210
262
|
|
|
211
263
|
if (hasContext) continue;
|
|
212
264
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
reasonCode: 'ARIA_REQUIRED_PARENT_MISSING',
|
|
229
|
-
role,
|
|
230
|
-
requiredContextRoles: requiredContext
|
|
265
|
+
occurrences.push(
|
|
266
|
+
helpers.reportOccurrence(el, {
|
|
267
|
+
summary: 'This role requires a specific ancestor/owner context role, which was not found.',
|
|
268
|
+
hint: 'Place this element inside (or aria-owns-reference it from) an element with an acceptable context role.',
|
|
269
|
+
i18n: {
|
|
270
|
+
summaryKey: 'ariaRequiredParent_summary_fail',
|
|
271
|
+
hintKey: 'ariaRequiredParent_hint_fail',
|
|
272
|
+
params: { role, requiredRoles: requiredContext.join(', ') }
|
|
273
|
+
},
|
|
274
|
+
data: {
|
|
275
|
+
details: {
|
|
276
|
+
reasonCode: 'ARIA_REQUIRED_PARENT_MISSING',
|
|
277
|
+
role,
|
|
278
|
+
requiredContextRoles: requiredContext
|
|
279
|
+
}
|
|
231
280
|
}
|
|
232
|
-
}
|
|
233
|
-
|
|
281
|
+
})
|
|
282
|
+
);
|
|
234
283
|
}
|
|
235
284
|
|
|
236
285
|
if (applicableCount === 0) {
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Generic name-presence rule for
|
|
6
|
+
* Generic name-presence rule for the ARIA roles WAI-ARIA *requires* an
|
|
7
|
+
* accessible name for.
|
|
7
8
|
*
|
|
8
9
|
* Notes:
|
|
9
10
|
* - This rule intentionally focuses on author-provided naming mechanisms:
|
|
@@ -11,14 +12,34 @@
|
|
|
11
12
|
* - It does NOT treat descendant text content as a valid name source for these roles
|
|
12
13
|
* (to avoid false passes from labelled children inside composite widgets).
|
|
13
14
|
* - Eligibility is based on helpers.isAccTreeEligible(node, ctx) per engine checks.
|
|
15
|
+
* - The role set is generated from aria-query by
|
|
16
|
+
* scripts/generate-aria-tables.js; see that script for why each role is in
|
|
17
|
+
* or out, and for the name-required roles no rule covers yet.
|
|
18
|
+
*
|
|
19
|
+
* @applicability
|
|
20
|
+
* Applies to elements whose role attribute is exactly one of grid, meter,
|
|
21
|
+
* progressbar, radiogroup or tree, and that are included in the
|
|
22
|
+
* accessibility tree. Membership is decided by WAI-ARIA's own "Accessible
|
|
23
|
+
* Name Required: True" characteristic, not by whether a role merely permits
|
|
24
|
+
* a name: tablist, toolbar, menu, menubar and scrollbar are name-from-author
|
|
25
|
+
* roles the spec does not require a name for, and are out of
|
|
26
|
+
* scope. meter and progressbar are also covered by meter-name-present and
|
|
27
|
+
* progressbar-name-present, which map to SC 1.1.1; this rule is what gives
|
|
28
|
+
* those two roles their 4.1.2 coverage.
|
|
29
|
+
* @expectation
|
|
30
|
+
* The element has a non-empty aria-label, an aria-labelledby that resolves
|
|
31
|
+
* to non-empty text, or a non-empty title. Every role in the set is
|
|
32
|
+
* name-from-author-only, so descendant text is not accepted:
|
|
33
|
+
* a labelled child inside a composite widget would otherwise pass the
|
|
34
|
+
* container that has no name of its own.
|
|
14
35
|
*/
|
|
15
36
|
|
|
16
37
|
const id = 'aria-role-name-present';
|
|
17
38
|
|
|
18
39
|
const meta = {
|
|
19
|
-
title: 'ARIA
|
|
40
|
+
title: 'ARIA roles that require an accessible name have one',
|
|
20
41
|
description:
|
|
21
|
-
'Checks that
|
|
42
|
+
'Checks that the ARIA roles WAI-ARIA requires an accessible name for expose a non-empty one.',
|
|
22
43
|
i18n: {
|
|
23
44
|
titleKey: 'ariaRoleNamePresent_title',
|
|
24
45
|
descriptionKey: 'ariaRoleNamePresent_description'
|
|
@@ -93,7 +114,7 @@ function runInPage(ctx) {
|
|
|
93
114
|
// button-name-present et al.), which correctly includes hidden/aria-hidden
|
|
94
115
|
// referenced nodes per the Accessible Name and Description Computation
|
|
95
116
|
// spec (a hidden node directly referenced by aria-labelledby still
|
|
96
|
-
// supplies its text
|
|
117
|
+
// supplies its text, this is a standard visually-hidden-label pattern).
|
|
97
118
|
const resolveLabelledby = (el, maxRefs) => {
|
|
98
119
|
if (!getAriaLabelledByInfo) return '';
|
|
99
120
|
try {
|
|
@@ -115,23 +136,16 @@ function runInPage(ctx) {
|
|
|
115
136
|
}
|
|
116
137
|
};
|
|
117
138
|
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
'menubar',
|
|
129
|
-
'meter',
|
|
130
|
-
'progressbar'
|
|
131
|
-
]);
|
|
132
|
-
|
|
133
|
-
const selector =
|
|
134
|
-
'[role="scrollbar"],[role="toolbar"],[role="tablist"],[role="radiogroup"],[role="tree"],[role="grid"],[role="menu"],[role="menubar"],[role="meter"],[role="progressbar"]';
|
|
139
|
+
// Roles WAI-ARIA marks "Accessible Name Required: True" and names from the
|
|
140
|
+
// author only. Generated from aria-query by scripts/generate-aria-tables.js:
|
|
141
|
+
// a role that merely *allows* an author name (tablist, toolbar, menu,
|
|
142
|
+
// menubar, scrollbar) is not a 4.1.2 failure when unnamed and is not listed.
|
|
143
|
+
// <generated:aria-name-required-roles>
|
|
144
|
+
const NAME_REQUIRED_ROLES = new Set(['grid', 'meter', 'progressbar', 'radiogroup', 'tree']);
|
|
145
|
+
// </generated:aria-name-required-roles>
|
|
146
|
+
|
|
147
|
+
// Derived from the set above so the two cannot drift apart.
|
|
148
|
+
const selector = [...NAME_REQUIRED_ROLES].map((r) => `[role="${r}"]`).join(',');
|
|
135
149
|
|
|
136
150
|
const nodes = (() => {
|
|
137
151
|
try {
|
|
@@ -161,7 +175,7 @@ function runInPage(ctx) {
|
|
|
161
175
|
return '';
|
|
162
176
|
}
|
|
163
177
|
})();
|
|
164
|
-
if (!
|
|
178
|
+
if (!NAME_REQUIRED_ROLES.has(role)) continue;
|
|
165
179
|
|
|
166
180
|
if (!isEligible(el)) continue;
|
|
167
181
|
|
|
@@ -68,14 +68,28 @@ function runInPage(ctx) {
|
|
|
68
68
|
let applicableCount = 0;
|
|
69
69
|
|
|
70
70
|
// Programmatically hidden per the ACT glossary: display:none, visibility not
|
|
71
|
-
// visible, or aria-hidden on the element or an ancestor.
|
|
71
|
+
// visible, or aria-hidden on the element or an ancestor. inert is treated
|
|
72
|
+
// the same although the glossary predates it -- an inert subtree is out of
|
|
73
|
+
// the accessibility tree entirely, so a role on it reaches no one.
|
|
72
74
|
function isHidden(el) {
|
|
73
75
|
try {
|
|
74
76
|
if (typeof helpers.isDomVisibleEligible === 'function') {
|
|
75
77
|
if (!helpers.isDomVisibleEligible(el, ctx)) return true;
|
|
76
78
|
}
|
|
77
|
-
|
|
79
|
+
// Walk the composed tree, not parentElement: that stops at a shadow
|
|
80
|
+
// root, so a host carrying aria-hidden or inert would never be seen
|
|
81
|
+
// from inside its own shadow content.
|
|
82
|
+
const up =
|
|
83
|
+
typeof helpers.composedParent === 'function'
|
|
84
|
+
? helpers.composedParent
|
|
85
|
+
: (n) => n.parentElement;
|
|
86
|
+
|
|
87
|
+
// A shadow root has no getAttribute, so skip past it rather than
|
|
88
|
+
// stopping: the host one step further up is the node that matters.
|
|
89
|
+
for (let n = el; n; n = up(n)) {
|
|
90
|
+
if (!n.getAttribute) continue;
|
|
78
91
|
if (String(n.getAttribute('aria-hidden') || '').toLowerCase() === 'true') return true;
|
|
92
|
+
if (n.hasAttribute && n.hasAttribute('inert')) return true;
|
|
79
93
|
}
|
|
80
94
|
} catch {
|
|
81
95
|
return false;
|
|
@@ -106,27 +120,27 @@ function runInPage(ctx) {
|
|
|
106
120
|
const role = tokens[0];
|
|
107
121
|
const isKnown = tokens.some((t) => ariaHelpers.isKnownRole(t));
|
|
108
122
|
const reasonCode = !isKnown ? 'ARIA_ROLE_INVALID' : 'ARIA_ROLE_ABSTRACT';
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
123
|
+
|
|
124
|
+
occurrences.push(
|
|
125
|
+
helpers.reportOccurrence(el, {
|
|
126
|
+
summary: !isKnown
|
|
127
|
+
? 'The role attribute value is not a recognized ARIA role.'
|
|
128
|
+
: 'The role attribute value is an abstract ARIA role, which must not be used directly.',
|
|
129
|
+
hint: !isKnown
|
|
130
|
+
? 'Use a valid ARIA role token, or remove the role attribute if none applies.'
|
|
131
|
+
: 'Replace this abstract role with a concrete role appropriate for the widget/structure.',
|
|
132
|
+
i18n: {
|
|
133
|
+
summaryKey: !isKnown
|
|
134
|
+
? 'ariaRolesValid_summary_invalid'
|
|
135
|
+
: 'ariaRolesValid_summary_abstract',
|
|
136
|
+
hintKey: !isKnown ? 'ariaRolesValid_hint_invalid' : 'ariaRolesValid_hint_abstract',
|
|
137
|
+
params: { role }
|
|
138
|
+
},
|
|
139
|
+
data: {
|
|
140
|
+
details: { reasonCode, role }
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
);
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
if (applicableCount === 0) {
|
|
@@ -15,9 +15,14 @@
|
|
|
15
15
|
* @expectation
|
|
16
16
|
* Each attribute's value conforms to its WAI-ARIA-declared value type:
|
|
17
17
|
* boolean ("true"/"false"), tristate ("true"/"false"/"mixed"), a token
|
|
18
|
-
* from a fixed enumerated set, an integer, a real number, or an
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* from a fixed enumerated set, an integer, a real number, or an ID
|
|
19
|
+
* reference (list) that resolves to an existing element in the document.
|
|
20
|
+
* Per ACT 6a7281's own applicability ("any state or property that is
|
|
21
|
+
* NOT empty"), an explicitly empty value, including a bare boolean-style
|
|
22
|
+
* attribute with no "=value" at all, e.g. `aria-checked` alone, is out
|
|
23
|
+
* of scope for every value type, not a violation: a common, deliberate
|
|
24
|
+
* pattern in templated markup (e.g. React conditionally rendering
|
|
25
|
+
* `aria-describedby={hasError ? errorId : ''}`).
|
|
21
26
|
* @implementation-notes
|
|
22
27
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
23
28
|
* property, while engine-level hidden-subtree filtering still applies
|
|
@@ -25,16 +30,14 @@
|
|
|
25
30
|
* - ID-reference resolution (see aria-helpers.js's idExists) only flags
|
|
26
31
|
* idref-list attributes (aria-labelledby, aria-describedby,
|
|
27
32
|
* aria-controls, aria-owns, etc.) when NONE of the space-separated ids
|
|
28
|
-
* resolve
|
|
33
|
+
* resolve, a partially-dangling list (some ids exist, some don't) is
|
|
29
34
|
* left unflagged (only invalidate when every token fails to resolve).
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* `aria-describedby={hasError ? errorId : ''}`); flagging it would be a
|
|
37
|
-
* false positive.
|
|
35
|
+
* Of the two single-idref attributes, aria-activedescendant is flagged
|
|
36
|
+
* whenever its one id doesn't resolve; aria-errormessage's existence is
|
|
37
|
+
* never checked (format only), ACT 6a7281's own Background
|
|
38
|
+
* text names it as a non-required property whose target "may be created
|
|
39
|
+
* in response to an event that may or may not happen" (a validation
|
|
40
|
+
* error message rendered only once the error actually occurs).
|
|
38
41
|
*/
|
|
39
42
|
|
|
40
43
|
const id = 'aria-valid-attr-value';
|
|
@@ -105,29 +108,26 @@ function runInPage(ctx) {
|
|
|
105
108
|
|
|
106
109
|
if (!invalid || !invalid.length) continue;
|
|
107
110
|
|
|
108
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
109
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
110
|
-
|
|
111
111
|
for (const item of invalid) {
|
|
112
|
-
occurrences.push(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
112
|
+
occurrences.push(
|
|
113
|
+
helpers.reportOccurrence(el, {
|
|
114
|
+
summary: 'This element has an ARIA attribute with an invalid value.',
|
|
115
|
+
hint: 'Use a value that matches the attribute’s expected type (see the WAI-ARIA specification for this attribute).',
|
|
116
|
+
i18n: {
|
|
117
|
+
summaryKey: 'ariaValidAttrValue_summary_fail',
|
|
118
|
+
hintKey: 'ariaValidAttrValue_hint_fail',
|
|
119
|
+
params: { attr: item.name, value: item.value }
|
|
120
|
+
},
|
|
121
|
+
data: {
|
|
122
|
+
details: {
|
|
123
|
+
reasonCode: 'ARIA_ATTR_VALUE_INVALID',
|
|
124
|
+
attr: item.name,
|
|
125
|
+
value: item.value,
|
|
126
|
+
valueReason: item.reason
|
|
127
|
+
}
|
|
128
128
|
}
|
|
129
|
-
}
|
|
130
|
-
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* deterministic defect).
|
|
19
19
|
* @implementation-notes
|
|
20
20
|
* - Distinct from aria-valid-attr-value (which validates the VALUE
|
|
21
|
-
* of a recognized attribute)
|
|
21
|
+
* of a recognized attribute), this rule only validates the attribute
|
|
22
22
|
* NAME.
|
|
23
23
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
24
24
|
* property, while engine-level hidden-subtree filtering still applies
|
|
@@ -86,24 +86,21 @@ function runInPage(ctx) {
|
|
|
86
86
|
|
|
87
87
|
if (!invalidNames || !invalidNames.length) continue;
|
|
88
88
|
|
|
89
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
90
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
91
|
-
|
|
92
89
|
for (const name of invalidNames) {
|
|
93
|
-
occurrences.push(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
90
|
+
occurrences.push(
|
|
91
|
+
helpers.reportOccurrence(el, {
|
|
92
|
+
summary: 'This element has an attribute that is not a recognized ARIA attribute.',
|
|
93
|
+
hint: 'Correct the attribute name (check for typos), or remove it if not needed.',
|
|
94
|
+
i18n: {
|
|
95
|
+
summaryKey: 'ariaValidAttr_summary_fail',
|
|
96
|
+
hintKey: 'ariaValidAttr_hint_fail',
|
|
97
|
+
params: { attr: name }
|
|
98
|
+
},
|
|
99
|
+
data: {
|
|
100
|
+
details: { reasonCode: 'ARIA_ATTR_INVALID', attr: name }
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
);
|
|
107
104
|
}
|
|
108
105
|
}
|
|
109
106
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* order) with the full fixed field-name vocabulary from the HTML
|
|
27
27
|
* Standard, rather than validating every field-specific constraint
|
|
28
28
|
* (e.g. which contact-modality tokens are legal for which field
|
|
29
|
-
* names)
|
|
29
|
+
* names), matches this engine's established "scoped"
|
|
30
30
|
* precedent (see aria-helpers.js) for keeping high-confidence fail
|
|
31
31
|
* without reimplementing the entire spec.
|
|
32
32
|
*/
|
|
@@ -63,7 +63,7 @@ const meta = {
|
|
|
63
63
|
function runInPage(ctx) {
|
|
64
64
|
const { helpers, rule } = ctx;
|
|
65
65
|
|
|
66
|
-
// Declared inside runInPage
|
|
66
|
+
// Declared inside runInPage, see scripts/build-core.js header
|
|
67
67
|
// ("runInPage MUST be self-contained").
|
|
68
68
|
const FIELD_NAMES = new Set([
|
|
69
69
|
'name',
|
|
@@ -193,23 +193,21 @@ function runInPage(ctx) {
|
|
|
193
193
|
if (isValidAutocomplete(raw)) continue;
|
|
194
194
|
|
|
195
195
|
const tag = el.tagName.toLowerCase();
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
});
|
|
196
|
+
|
|
197
|
+
occurrences.push(
|
|
198
|
+
helpers.reportOccurrence(el, {
|
|
199
|
+
summary: 'This autocomplete attribute value is not a valid autofill value.',
|
|
200
|
+
hint: 'Use "on"/"off", or a valid autofill token list (e.g. "shipping street-address", "cc-number").',
|
|
201
|
+
i18n: {
|
|
202
|
+
summaryKey: 'autocompleteValid_summary_fail',
|
|
203
|
+
hintKey: 'autocompleteValid_hint_fail',
|
|
204
|
+
params: { element: tag, value: raw }
|
|
205
|
+
},
|
|
206
|
+
data: {
|
|
207
|
+
details: { reasonCode: 'AUTOCOMPLETE_VALUE_INVALID', element: tag, value: raw }
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
);
|
|
213
211
|
}
|
|
214
212
|
|
|
215
213
|
if (applicableCount === 0) {
|
|
@@ -245,23 +245,21 @@ function runInPage(ctx) {
|
|
|
245
245
|
if (!flagged.length) continue;
|
|
246
246
|
|
|
247
247
|
const tag = el.tagName.toLowerCase();
|
|
248
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
249
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
250
248
|
|
|
251
|
-
occurrences.push(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
249
|
+
occurrences.push(
|
|
250
|
+
helpers.reportOccurrence(el, {
|
|
251
|
+
summary: `This element's inline style forces ${flagged.join(', ')} with !important below the WCAG text-spacing metric, so the user cannot raise it.`,
|
|
252
|
+
hint: 'Remove !important from line-height/letter-spacing/word-spacing in inline styles, or set a value that already meets the metric (line-height 1.5, letter-spacing 0.12em, word-spacing 0.16em).',
|
|
253
|
+
i18n: {
|
|
254
|
+
summaryKey: 'avoidInlineSpacing_summary_fail',
|
|
255
|
+
hintKey: 'avoidInlineSpacing_hint_fail',
|
|
256
|
+
params: { element: tag, properties: flagged.join(', ') }
|
|
257
|
+
},
|
|
258
|
+
data: {
|
|
259
|
+
details: { reasonCode: 'INLINE_SPACING_IMPORTANT', element: tag, properties: flagged }
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
);
|
|
265
263
|
}
|
|
266
264
|
|
|
267
265
|
if (applicableCount === 0) {
|