@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
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check link-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Links must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to <a href>, <area href> and elements with role="link" that are
|
|
13
|
+
* included in the accessibility tree. An <a> without an href is not a link
|
|
14
|
+
* and is not matched.
|
|
15
|
+
* @expectation
|
|
16
|
+
* The element has a non-empty accessible name. A programmatic name is
|
|
17
|
+
* taken first (aria-labelledby, aria-label, an associated <label>, title),
|
|
18
|
+
* and failing that the element falls back to its own subtree text,
|
|
19
|
+
* counting each descendant's own name (an <img alt>, aria-label or title),
|
|
20
|
+
* the shape behind the common <a><img alt="..."></a> logo link. The
|
|
21
|
+
* content fallback is suppressed when an explicit, known role that is not
|
|
22
|
+
* name-from-content is present; an unrecognized role token falls back to
|
|
23
|
+
* the implicit role.
|
|
24
|
+
*/
|
|
25
|
+
|
|
5
26
|
const id = 'link-name-present';
|
|
6
27
|
|
|
7
28
|
const meta = {
|
|
@@ -38,7 +59,7 @@ function runInPage(ctx) {
|
|
|
38
59
|
let applicableCount = 0;
|
|
39
60
|
|
|
40
61
|
function getConservativeSubtreeText(container) {
|
|
41
|
-
// "Name from content"
|
|
62
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
42
63
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
43
64
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
44
65
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -139,39 +160,34 @@ function runInPage(ctx) {
|
|
|
139
160
|
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
140
161
|
: null;
|
|
141
162
|
|
|
142
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
143
|
-
const html = helpers.getOuterHtmlSnippet
|
|
144
|
-
? helpers.getOuterHtmlSnippet(el)
|
|
145
|
-
: el.outerHTML || '';
|
|
146
163
|
const tag = (el.tagName || '').toLowerCase();
|
|
147
164
|
|
|
148
|
-
occurrences.push(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
refs: { accessibleName: nameInfo || null }
|
|
165
|
+
occurrences.push(
|
|
166
|
+
helpers.reportOccurrence(el, {
|
|
167
|
+
// Human fallbacks (allowed)
|
|
168
|
+
summary: 'This link has no accessible name.',
|
|
169
|
+
hint: 'Provide link text or an accessible-name mechanism (for example aria-label) so assistive technologies can identify the link.',
|
|
170
|
+
|
|
171
|
+
// Validator requires these keys to exist in the English dictionary
|
|
172
|
+
i18n: {
|
|
173
|
+
summaryKey: 'linkNamePresent_summary_fail',
|
|
174
|
+
hintKey: 'linkNamePresent_hint_fail',
|
|
175
|
+
params: { element: tag }
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
data: {
|
|
179
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
180
|
+
details: {
|
|
181
|
+
reasonCode: 'name_missing',
|
|
182
|
+
metrics: {
|
|
183
|
+
programmaticNameLength: programmaticName.trim().length,
|
|
184
|
+
contentNameLength: contentName.trim().length
|
|
185
|
+
},
|
|
186
|
+
refs: { accessibleName: nameInfo || null }
|
|
187
|
+
}
|
|
172
188
|
}
|
|
173
|
-
}
|
|
174
|
-
|
|
189
|
+
})
|
|
190
|
+
);
|
|
175
191
|
}
|
|
176
192
|
}
|
|
177
193
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Applies to <ul>/<ol> elements that have at least one direct element
|
|
13
13
|
* child.
|
|
14
14
|
* @expectation
|
|
15
|
-
* Every direct element child is <li>, <script>, or <template
|
|
15
|
+
* Every direct element child is <li>, <script>, or <template>. UNLESS it
|
|
16
16
|
* has an explicit `role` attribute, in which case the explicit role wins
|
|
17
17
|
* over the tag entirely: a child is valid iff that role is "listitem"
|
|
18
18
|
* (so `<li role="presentation">`/`<li role="menuitem">` are invalid
|
|
@@ -20,19 +20,19 @@
|
|
|
20
20
|
* given `role="listitem"` is valid). A wrapper <div> used for styling
|
|
21
21
|
* (no role at all) still breaks list semantics the same as before.
|
|
22
22
|
* @implementation-notes
|
|
23
|
-
* - Checked via el.children, which already excludes text/comment nodes
|
|
23
|
+
* - Checked via el.children, which already excludes text/comment nodes,
|
|
24
24
|
* no whitespace-node filtering needed.
|
|
25
25
|
* - Distinct, atomic decision from listitem-parent-valid (the
|
|
26
26
|
* inverse relationship: does a given <li> have a valid parent).
|
|
27
27
|
* - Direct children that are not exposed to the accessibility tree (e.g.
|
|
28
28
|
* display:none, [hidden], aria-hidden="true") are excluded from
|
|
29
|
-
* consideration entirely
|
|
29
|
+
* consideration entirely. An element not reachable by assistive
|
|
30
30
|
* technology can't break the list semantics a screen reader announces.
|
|
31
31
|
* Common cases: a stray `<input type="hidden">` as a direct <ul> child
|
|
32
32
|
* (UA-stylesheet display:none by spec), or `<span style="display:none">`
|
|
33
33
|
* hydration markers interleaved with real `<li>`s.
|
|
34
34
|
* - Explicit-role-overrides-tag: if a child has an explicit role, only
|
|
35
|
-
* `['listitem']` is consulted
|
|
35
|
+
* `['listitem']` is consulted, the tag name is never checked. Only
|
|
36
36
|
* without an explicit role does the tag name matter. Catches cases a
|
|
37
37
|
* tag-only check misses: `<li role="none">` hosting a list's own
|
|
38
38
|
* visually-hidden label, `<li role="menuitem">` menu items, or a real
|
|
@@ -71,7 +71,7 @@ const meta = {
|
|
|
71
71
|
function runInPage(ctx) {
|
|
72
72
|
const { helpers, rule } = ctx;
|
|
73
73
|
|
|
74
|
-
// Declared inside runInPage
|
|
74
|
+
// Declared inside runInPage, see scripts/build-core.js header
|
|
75
75
|
// ("runInPage MUST be self-contained").
|
|
76
76
|
const ALLOWED_CHILD_TAGS = new Set(['li', 'script', 'template']);
|
|
77
77
|
|
|
@@ -111,7 +111,7 @@ function runInPage(ctx) {
|
|
|
111
111
|
const roleAttr = child.getAttribute ? String(child.getAttribute('role') || '').trim() : '';
|
|
112
112
|
const explicitRole = roleAttr ? (roleAttr.split(/\s+/)[0] || '').toLowerCase() : '';
|
|
113
113
|
|
|
114
|
-
// An explicit role always wins over the tag
|
|
114
|
+
// An explicit role always wins over the tag, see header comment.
|
|
115
115
|
const valid = explicitRole ? explicitRole === 'listitem' : ALLOWED_CHILD_TAGS.has(tag);
|
|
116
116
|
|
|
117
117
|
if (!valid) invalidTags.push(tag);
|
|
@@ -122,23 +122,21 @@ function runInPage(ctx) {
|
|
|
122
122
|
const dedupedInvalidTags = [...new Set(invalidTags)];
|
|
123
123
|
|
|
124
124
|
const tag = el.tagName.toLowerCase();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
});
|
|
125
|
+
|
|
126
|
+
occurrences.push(
|
|
127
|
+
helpers.reportOccurrence(el, {
|
|
128
|
+
summary: 'This list contains a direct child that is not a list item.',
|
|
129
|
+
hint: 'Only use <li> (or <script>/<template>) as direct children of <ul>/<ol>; move other markup inside an <li>.',
|
|
130
|
+
i18n: {
|
|
131
|
+
summaryKey: 'listChildrenValid_summary_fail',
|
|
132
|
+
hintKey: 'listChildrenValid_hint_fail',
|
|
133
|
+
params: { element: tag, invalidChildren: dedupedInvalidTags.join(', ') }
|
|
134
|
+
},
|
|
135
|
+
data: {
|
|
136
|
+
details: { reasonCode: 'LIST_INVALID_CHILD', element: tag, invalidChildren: invalidTags }
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
);
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
if (applicableCount === 0) {
|
|
@@ -2,11 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check listbox-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="listbox" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="listbox" (the attribute must name
|
|
13
|
+
* that role alone, not a fallback list) that are included in the
|
|
14
|
+
* accessibility tree. An element with the matching implicit role but no
|
|
15
|
+
* role attribute is out of scope.
|
|
16
|
+
* @expectation
|
|
17
|
+
* The element has a non-empty accessible name from aria-label, from an
|
|
18
|
+
* aria-labelledby that resolves to non-empty text, or from title.
|
|
19
|
+
* role="listbox" is name-from-author-only, so subtree text is never
|
|
20
|
+
* accepted: text sitting inside a custom listbox widget is not reliably
|
|
21
|
+
* exposed as its name. On a labelable element (<select multiple
|
|
22
|
+
* role="listbox">) an associated <label> counts as well.
|
|
23
|
+
*/
|
|
24
|
+
|
|
5
25
|
const id = 'listbox-name-present';
|
|
6
26
|
|
|
7
27
|
const meta = {
|
|
8
|
-
title: '
|
|
9
|
-
description: 'Checks that elements expose a non-empty accessible name.',
|
|
28
|
+
title: 'Listboxes have an accessible name',
|
|
29
|
+
description: 'Checks that elements with role="listbox" expose a non-empty accessible name.',
|
|
10
30
|
i18n: {
|
|
11
31
|
titleKey: 'listboxNamePresent_title',
|
|
12
32
|
descriptionKey: 'listboxNamePresent_description'
|
|
@@ -51,7 +71,7 @@ function runInPage(ctx) {
|
|
|
51
71
|
}
|
|
52
72
|
|
|
53
73
|
function getConservativeSubtreeText(document, container) {
|
|
54
|
-
// "Name from content"
|
|
74
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
55
75
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
56
76
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
57
77
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -68,7 +88,7 @@ function runInPage(ctx) {
|
|
|
68
88
|
|
|
69
89
|
// A <label> contributes a name via its own aria-label/aria-labelledby
|
|
70
90
|
// (checked first, same ARIA-over-content precedence any element's
|
|
71
|
-
// accessible name gives
|
|
91
|
+
// accessible name gives, e.g. <label aria-label="Search"><svg
|
|
72
92
|
// aria-hidden="true">...</svg></label> names its control "Search" even
|
|
73
93
|
// though the label's only child content is aria-hidden) or, failing
|
|
74
94
|
// that, its rendered content (getConservativeSubtreeText).
|
|
@@ -83,10 +103,10 @@ function runInPage(ctx) {
|
|
|
83
103
|
if (content) return content;
|
|
84
104
|
// Final fallback per the general accname text-alternative algorithm,
|
|
85
105
|
// which applies to any element being asked for its name regardless of
|
|
86
|
-
// why (own aria-label, an aria-labelledby reference, or
|
|
106
|
+
// why (own aria-label, an aria-labelledby reference, or, here, native
|
|
87
107
|
// <label for> association): title, when nothing else yields a name.
|
|
88
108
|
// Purely additive (only fills in a name where there was none before),
|
|
89
|
-
// so it carries no false-positive risk
|
|
109
|
+
// so it carries no false-positive risk, see dialog-name-present.js's
|
|
90
110
|
// identical <iframe>-title-fallback fix for the concrete real-world
|
|
91
111
|
// trigger this same accname step covers elsewhere.
|
|
92
112
|
return getAttr(lab, 'title');
|
|
@@ -96,7 +116,7 @@ function runInPage(ctx) {
|
|
|
96
116
|
const raw = getAttr(el, 'aria-labelledby');
|
|
97
117
|
if (!raw) return '';
|
|
98
118
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
99
|
-
// name-from-content of the referenced element
|
|
119
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
100
120
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
101
121
|
// aria-labelledby target's only name source is its title attribute,
|
|
102
122
|
// which name-from-content alone can never see).
|
|
@@ -227,24 +247,22 @@ function runInPage(ctx) {
|
|
|
227
247
|
}
|
|
228
248
|
})()
|
|
229
249
|
: null;
|
|
230
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
231
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
232
250
|
|
|
233
|
-
occurrences.push(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
251
|
+
occurrences.push(
|
|
252
|
+
helpers.reportOccurrence(el, {
|
|
253
|
+
summary: 'This element has no accessible name.',
|
|
254
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this listbox's accessible name.",
|
|
255
|
+
i18n: {
|
|
256
|
+
summaryKey: 'listboxNamePresent_summary_fail',
|
|
257
|
+
hintKey: 'listboxNamePresent_hint_fail',
|
|
258
|
+
params: { controlType: 'listbox' }
|
|
259
|
+
},
|
|
260
|
+
data: {
|
|
261
|
+
details: { reasonCode: 'name_missing', controlType: 'listbox', methodTried: res.method },
|
|
262
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
);
|
|
248
266
|
}
|
|
249
267
|
|
|
250
268
|
if (applicableCount === 0) {
|
|
@@ -24,16 +24,16 @@
|
|
|
24
24
|
* e.g. aria-helpers.js getExplicitRole).
|
|
25
25
|
* - An explicit role on the parent WINS over its tag name, in either
|
|
26
26
|
* direction: a <ul role="menu"> no longer exposes role "list" (its own
|
|
27
|
-
* native role is fully replaced by the explicit one
|
|
27
|
+
* native role is fully replaced by the explicit one, the same "any
|
|
28
28
|
* explicit role overrides the element's native role" ARIA principle
|
|
29
29
|
* applied elsewhere in this engine), so an <li> inside it is invalid
|
|
30
30
|
* despite the <ul> tag (e.g. `<ul role="menu"><li>...`). Conversely
|
|
31
31
|
* role="presentation"/"none" on the parent is still a valid
|
|
32
|
-
* (list-semantics-suppressing) parent
|
|
32
|
+
* (list-semantics-suppressing) parent, the accepted parent roles are
|
|
33
33
|
* presentation, none, and list.
|
|
34
34
|
* - The SAME "explicit role wins" principle applies to the <li> ELEMENT
|
|
35
35
|
* ITSELF: an <li role="tab">/role="menuitem">/role="presentation"> etc.
|
|
36
|
-
* is exposed to AT with that role, never "listitem"
|
|
36
|
+
* is exposed to AT with that role, never "listitem". The whole point
|
|
37
37
|
* of this check (list items need a valid list-container parent) doesn't
|
|
38
38
|
* apply when the element isn't claiming listitem semantics in the first
|
|
39
39
|
* place. Any `<li>` with an explicit `role` attribute is excluded from
|
|
@@ -108,7 +108,7 @@ function runInPage(ctx) {
|
|
|
108
108
|
let valid;
|
|
109
109
|
if (explicitRole) {
|
|
110
110
|
// An explicit role always wins over the tag's native role, in either
|
|
111
|
-
// direction
|
|
111
|
+
// direction, see the header comment.
|
|
112
112
|
valid = explicitRole === 'list' || explicitRole === 'presentation' || explicitRole === 'none';
|
|
113
113
|
} else {
|
|
114
114
|
valid = parentTag === 'ul' || parentTag === 'ol';
|
|
@@ -116,23 +116,20 @@ function runInPage(ctx) {
|
|
|
116
116
|
|
|
117
117
|
if (valid) continue;
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
details: { reasonCode: 'LISTITEM_INVALID_PARENT', parentElement: parentTag }
|
|
134
|
-
}
|
|
135
|
-
});
|
|
119
|
+
occurrences.push(
|
|
120
|
+
helpers.reportOccurrence(el, {
|
|
121
|
+
summary: 'This list item is not contained by a list container.',
|
|
122
|
+
hint: 'Place this <li> inside a <ul>/<ol>, or give its parent role="list".',
|
|
123
|
+
i18n: {
|
|
124
|
+
summaryKey: 'listitemParentValid_summary_fail',
|
|
125
|
+
hintKey: 'listitemParentValid_hint_fail',
|
|
126
|
+
params: { parentElement: parentTag }
|
|
127
|
+
},
|
|
128
|
+
data: {
|
|
129
|
+
details: { reasonCode: 'LISTITEM_INVALID_PARENT', parentElement: parentTag }
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
);
|
|
136
133
|
}
|
|
137
134
|
|
|
138
135
|
if (applicableCount === 0) {
|
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check menuitem-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Menu items must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="menuitem", role="menuitemcheckbox" or
|
|
13
|
+
* role="menuitemradio" (the attribute must name one of those roles alone,
|
|
14
|
+
* not a fallback list) that are included in the accessibility tree.
|
|
15
|
+
* @expectation
|
|
16
|
+
* The element has a non-empty accessible name from aria-label, from an
|
|
17
|
+
* aria-labelledby that resolves to non-empty text, from title, or, all
|
|
18
|
+
* three roles being name-from-content, from its own subtree text, where a
|
|
19
|
+
* descendant's own name (an <img alt>, aria-label or title) counts as that
|
|
20
|
+
* descendant's contribution rather than only its text nodes.
|
|
21
|
+
*/
|
|
22
|
+
|
|
5
23
|
const id = 'menuitem-name-present';
|
|
6
24
|
|
|
7
25
|
const meta = {
|
|
@@ -52,7 +70,7 @@ function runInPage(ctx) {
|
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
function getConservativeSubtreeText(document, container) {
|
|
55
|
-
// "Name from content"
|
|
73
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
56
74
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
57
75
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
58
76
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -71,7 +89,7 @@ function runInPage(ctx) {
|
|
|
71
89
|
const raw = getAttr(el, 'aria-labelledby');
|
|
72
90
|
if (!raw) return '';
|
|
73
91
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
74
|
-
// name-from-content of the referenced element
|
|
92
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
75
93
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
76
94
|
// aria-labelledby target's only name source is its title attribute,
|
|
77
95
|
// which name-from-content alone can never see).
|
|
@@ -150,24 +168,22 @@ function runInPage(ctx) {
|
|
|
150
168
|
}
|
|
151
169
|
})()
|
|
152
170
|
: null;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
+
|
|
172
|
+
occurrences.push(
|
|
173
|
+
helpers.reportOccurrence(el, {
|
|
174
|
+
summary: 'This menu item has no accessible name.',
|
|
175
|
+
hint: 'Provide visible text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
|
|
176
|
+
i18n: {
|
|
177
|
+
summaryKey: 'menuitemNamePresent_summary_fail',
|
|
178
|
+
hintKey: 'menuitemNamePresent_hint_fail',
|
|
179
|
+
params: { role }
|
|
180
|
+
},
|
|
181
|
+
data: {
|
|
182
|
+
details: { reasonCode: 'name_missing', controlType: role, methodTried: res.method },
|
|
183
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
);
|
|
171
187
|
}
|
|
172
188
|
|
|
173
189
|
if (applicableCount === 0) {
|
|
@@ -9,20 +9,24 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 2.2.4, 3.2.5
|
|
11
11
|
* @applicability
|
|
12
|
-
* Applies to
|
|
13
|
-
* content attribute
|
|
12
|
+
* Applies to the first <meta http-equiv="refresh"> element, in document
|
|
13
|
+
* order, with a valid content attribute, per HTML's shared declarative
|
|
14
|
+
* refresh steps, a document only ever acts on its first valid meta
|
|
15
|
+
* refresh, so a later one (valid or not) is inert and out of scope.
|
|
14
16
|
* @expectation
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* Running the shared declarative refresh steps against that element's
|
|
18
|
+
* content value results in a delay of exactly 0. An immediate (delay=0)
|
|
19
|
+
* redirect still passes at AAA, same as the A-level rule. There is
|
|
20
|
+
* nothing for a user to be interrupted mid-read by when nothing is
|
|
21
|
+
* displayed first. Any positive delay fails, with none of the A-level
|
|
22
|
+
* rule's >20-hour exemption: at AAA, WCAG 2.2.4 (Interruptions) and
|
|
23
|
+
* 3.2.5 (Change on Request) require that a *timed* automatic context
|
|
24
|
+
* change happen only at the user's request, regardless of how long the
|
|
25
|
+
* timer is.
|
|
23
26
|
* @implementation-notes
|
|
24
|
-
* - Distinct, atomic decision from meta-refresh-timing-absent:
|
|
25
|
-
*
|
|
27
|
+
* - Distinct, atomic decision from meta-refresh-timing-absent: that
|
|
28
|
+
* rule's >20-hour exemption for a positive delay is this rule's fail
|
|
29
|
+
* case (no exemption at AAA), but delay=0 passes in both rules.
|
|
26
30
|
* - A <meta> nested inside <noscript> is excluded: it only ever takes
|
|
27
31
|
* effect when scripting is disabled, which is never the case for any
|
|
28
32
|
* context capable of running accessibility tooling in the first place
|
|
@@ -35,7 +39,7 @@ const id = 'meta-refresh-no-exceptions';
|
|
|
35
39
|
const meta = {
|
|
36
40
|
title: 'Page must not use a meta refresh at all (AAA)',
|
|
37
41
|
description:
|
|
38
|
-
'Checks that <meta http-equiv="refresh"> is not present at all, regardless of delay
|
|
42
|
+
'Checks that <meta http-equiv="refresh"> is not present at all, regardless of delay. This is the stricter AAA-level counterpart of the A-level positive-delay-only check.',
|
|
39
43
|
i18n: {
|
|
40
44
|
titleKey: 'metaRefreshNoExceptions_title',
|
|
41
45
|
descriptionKey: 'metaRefreshNoExceptions_description'
|
|
@@ -122,28 +126,32 @@ function runInPage(ctx) {
|
|
|
122
126
|
if (el.closest && el.closest('noscript')) continue; // never applies with scripting enabled
|
|
123
127
|
const raw = String(el.getAttribute('content') || '').trim();
|
|
124
128
|
if (!raw) continue;
|
|
125
|
-
|
|
129
|
+
const delay = parseRefreshDelay(raw);
|
|
130
|
+
if (delay === null) continue;
|
|
126
131
|
|
|
132
|
+
// Only the first valid meta refresh in document order is ever acted
|
|
133
|
+
// on by a browser -- any later one is inert and stops being evaluated
|
|
134
|
+
// entirely once a winner has been found.
|
|
127
135
|
applicableCount += 1;
|
|
128
136
|
|
|
129
|
-
|
|
130
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
137
|
+
if (delay === 0) break; // an immediate redirect is not a timed interruption
|
|
131
138
|
|
|
132
|
-
occurrences.push(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
'
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
139
|
+
occurrences.push(
|
|
140
|
+
helpers.reportOccurrence(el, {
|
|
141
|
+
summary:
|
|
142
|
+
'This page uses a meta refresh, which is an automatic context change not initiated by the user.',
|
|
143
|
+
hint: 'Remove the meta refresh; trigger the redirect/refresh only in response to a user action instead.',
|
|
144
|
+
i18n: {
|
|
145
|
+
summaryKey: 'metaRefreshNoExceptions_summary_fail',
|
|
146
|
+
hintKey: 'metaRefreshNoExceptions_hint_fail',
|
|
147
|
+
params: {}
|
|
148
|
+
},
|
|
149
|
+
data: {
|
|
150
|
+
details: { reasonCode: 'META_REFRESH_PRESENT' }
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
break;
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
if (applicableCount === 0) {
|
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 2.2.1
|
|
11
11
|
* @applicability
|
|
12
|
-
* Applies to <meta http-equiv="refresh">
|
|
13
|
-
* content attribute
|
|
12
|
+
* Applies to the first <meta http-equiv="refresh"> element, in document
|
|
13
|
+
* order, whose content attribute has a parseable leading delay value.
|
|
14
|
+
* Per HTML's shared declarative refresh steps, a document only ever
|
|
15
|
+
* acts on its first valid meta refresh; any later one (valid or not)
|
|
16
|
+
* is inert markup a browser never processes, so it is not evaluated.
|
|
14
17
|
* @expectation
|
|
15
18
|
* The delay is 0 (an immediate redirect, which users cannot be caught by
|
|
16
19
|
* mid-read), or exceeds 20 hours. Any other positive delay refreshes or
|
|
@@ -19,13 +22,13 @@
|
|
|
19
22
|
* be possible.
|
|
20
23
|
* @implementation-notes
|
|
21
24
|
* - An unparseable content value (no leading numeric delay) is not
|
|
22
|
-
* flagged
|
|
25
|
+
* flagged, this rule only reports a clearly-detected timed refresh,
|
|
23
26
|
* matching this engine's no-false-positives policy.
|
|
24
27
|
* - WCAG 2.2.1 Exception 3 exempts time limits longer than 20 hours (the
|
|
25
28
|
* rationale being that a delay this long gives users enough real-world
|
|
26
29
|
* time to act, so "adjustable" ceases to be a meaningful requirement).
|
|
27
30
|
* A delay over 72000 seconds is therefore not flagged.
|
|
28
|
-
* - A <meta> nested inside <noscript> is excluded
|
|
31
|
+
* - A <meta> nested inside <noscript> is excluded, see
|
|
29
32
|
* meta-refresh-no-exceptions's header comment for why.
|
|
30
33
|
*/
|
|
31
34
|
|
|
@@ -109,35 +112,36 @@ function runInPage(ctx) {
|
|
|
109
112
|
|
|
110
113
|
for (const el of nodes) {
|
|
111
114
|
if (!el || !el.getAttribute) continue;
|
|
112
|
-
if (el.closest && el.closest('noscript')) continue; // never applies with scripting enabled
|
|
115
|
+
if (el.closest && el.closest('noscript')) continue; // never applies with scripting enabled, see meta-refresh-no-exceptions.js's header comment
|
|
113
116
|
const raw = String(el.getAttribute('content') || '').trim();
|
|
114
117
|
if (!raw) continue;
|
|
115
118
|
|
|
116
119
|
const delay = parseRefreshDelay(raw);
|
|
117
120
|
if (delay === null) continue;
|
|
118
121
|
|
|
122
|
+
// Only the first valid meta refresh in document order is ever acted
|
|
123
|
+
// on by a browser -- any later one is inert and stops being evaluated
|
|
124
|
+
// entirely once a winner has been found.
|
|
119
125
|
applicableCount += 1;
|
|
120
126
|
|
|
121
|
-
if (!(delay > 0))
|
|
122
|
-
if (delay > EXEMPT_DELAY_SECONDS)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
});
|
|
127
|
+
if (!(delay > 0)) break;
|
|
128
|
+
if (delay > EXEMPT_DELAY_SECONDS) break; // WCAG 2.2.1 Exception 3 (>20 hours)
|
|
129
|
+
|
|
130
|
+
occurrences.push(
|
|
131
|
+
helpers.reportOccurrence(el, {
|
|
132
|
+
summary: 'This page refreshes itself automatically after a delay.',
|
|
133
|
+
hint: 'Remove the timed meta refresh, or provide a way for users to turn it off, extend it, or pause it before it triggers.',
|
|
134
|
+
i18n: {
|
|
135
|
+
summaryKey: 'metaRefreshTimingAbsent_summary_fail',
|
|
136
|
+
hintKey: 'metaRefreshTimingAbsent_hint_fail',
|
|
137
|
+
params: { delay: String(delay) }
|
|
138
|
+
},
|
|
139
|
+
data: {
|
|
140
|
+
details: { reasonCode: 'META_REFRESH_DELAYED', delay }
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
break;
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
if (applicableCount === 0) {
|