@surea11y/core 1.5.0 → 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 +193 -149
- package/README.md +27 -6
- package/docs/ACT_RULE_MAPPING.md +243 -0
- package/docs/API_STABILITY.md +2 -2
- package/docs/BINDING_AUTHORS_GUIDE.md +3 -3
- package/docs/DESIGN_CHALLENGES.md +301 -0
- package/docs/ENGINE_OPTIONS.md +16 -4
- package/docs/I18N.md +4 -4
- package/docs/INTEGRATION.md +1 -1
- package/docs/LIMITATIONS.md +6 -4
- package/docs/REPORT.md +1 -1
- package/docs/RULE_AUTHORING.md +53 -25
- package/docs/RULE_CATALOG.md +1878 -169
- package/docs/RULE_TAXONOMY.md +2 -2
- package/docs/TROUBLESHOOTING.md +2 -2
- package/docs/WCAG_CONFORMANCE.md +25 -9
- package/package.json +3 -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 +68 -10
- package/src/checks/automatic/aria-allowed-role.js +2 -2
- package/src/checks/automatic/aria-braille-equivalent.js +3 -3
- package/src/checks/automatic/aria-conditional-attr.js +5 -5
- package/src/checks/automatic/aria-deprecated-role.js +1 -1
- package/src/checks/automatic/aria-hidden-body.js +2 -2
- package/src/checks/automatic/aria-hidden-focus.js +5 -5
- package/src/checks/automatic/aria-prohibited-attr.js +18 -18
- package/src/checks/automatic/aria-prohibited-children.js +130 -37
- package/src/checks/automatic/aria-required-attr.js +60 -12
- package/src/checks/automatic/aria-required-children.js +21 -14
- package/src/checks/automatic/aria-required-parent.js +61 -9
- package/src/checks/automatic/aria-role-name-present.js +36 -22
- package/src/checks/automatic/aria-valid-attr-value.js +15 -12
- package/src/checks/automatic/aria-valid-attr.js +1 -1
- package/src/checks/automatic/autocomplete-valid.js +2 -2
- package/src/checks/automatic/binary-control-name-present.js +27 -5
- package/src/checks/automatic/button-name-present.js +92 -6
- package/src/checks/automatic/combobox-name-present.js +26 -6
- 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 +96 -19
- package/src/checks/automatic/definition-list-children-valid.js +7 -8
- package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
- package/src/checks/automatic/dialog-name-present.js +20 -2
- package/src/checks/automatic/duplicate-id-aria.js +5 -3
- 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 +1 -1
- package/src/checks/automatic/iframe-focusable-content.js +63 -7
- package/src/checks/automatic/iframe-name-present.js +37 -3
- package/src/checks/automatic/iframe-title-unique.js +1 -1
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +172 -18
- package/src/checks/automatic/link-in-text-block.js +10 -10
- package/src/checks/automatic/link-name-present.js +22 -1
- package/src/checks/automatic/list-children-valid.js +6 -6
- package/src/checks/automatic/listbox-name-present.js +28 -8
- package/src/checks/automatic/listitem-parent-valid.js +4 -4
- package/src/checks/automatic/menuitem-name-present.js +20 -2
- package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
- package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
- package/src/checks/automatic/meter-name-present.js +23 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
- package/src/checks/automatic/option-name-present.js +23 -4
- 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 +23 -4
- package/src/checks/automatic/role-img-alt-present.js +64 -16
- package/src/checks/automatic/searchbox-name-present.js +28 -8
- package/src/checks/automatic/server-side-image-map-absent.js +1 -1
- package/src/checks/automatic/slider-name-present.js +27 -6
- package/src/checks/automatic/spinbutton-name-present.js +28 -8
- package/src/checks/automatic/summary-name-present.js +18 -2
- 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 +21 -2
- package/src/checks/automatic/table-headers-attr-valid.js +43 -8
- package/src/checks/automatic/table-th-has-data-cells.js +61 -5
- package/src/checks/automatic/target-size-minimum.js +71 -53
- package/src/checks/automatic/td-has-header.js +5 -5
- package/src/checks/automatic/textbox-name-present.js +28 -8
- package/src/checks/automatic/tooltip-name-present.js +21 -2
- package/src/checks/automatic/treeitem-name-present.js +23 -4
- package/src/checks/automatic/valid-lang.js +92 -7
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +3 -3
- 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 +5 -5
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
- 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 +58 -11
- package/src/checks/manual/empty-table-header-manual.js +8 -8
- 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 +2 -2
- package/src/checks/manual/heading-order-manual.js +3 -3
- 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 +4 -4
- 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 +4 -4
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
- package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
- package/src/checks/manual/landmark-one-main-manual.js +6 -6
- package/src/checks/manual/landmark-unique-manual.js +9 -9
- package/src/checks/manual/link-name-quality-manual.js +161 -32
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +2 -2
- package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
- 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 +6 -6
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
- package/src/checks/manual/region-manual.js +19 -19
- package/src/checks/manual/scope-attr-valid-manual.js +2 -2
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +5 -5
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +2 -2
- package/src/checks/manual/table-duplicate-name-manual.js +2 -2
- 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 +8965 -1647
- package/src/report.js +2 -2
- package/surea11y.browser.js +3768 -611
- package/surea11y.i18n.de.js +1 -1
- package/surea11y.i18n.es.js +1 -1
- package/surea11y.i18n.fr.js +1 -1
- package/bin/surea11y-core.js +0 -20
|
@@ -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).
|
|
@@ -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,10 +126,16 @@ 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
|
|
|
137
|
+
if (delay === 0) break; // an immediate redirect is not a timed interruption
|
|
138
|
+
|
|
129
139
|
occurrences.push(
|
|
130
140
|
helpers.reportOccurrence(el, {
|
|
131
141
|
summary:
|
|
@@ -141,6 +151,7 @@ function runInPage(ctx) {
|
|
|
141
151
|
}
|
|
142
152
|
})
|
|
143
153
|
);
|
|
154
|
+
break;
|
|
144
155
|
}
|
|
145
156
|
|
|
146
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,17 +112,20 @@ 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)
|
|
127
|
+
if (!(delay > 0)) break;
|
|
128
|
+
if (delay > EXEMPT_DELAY_SECONDS) break; // WCAG 2.2.1 Exception 3 (>20 hours)
|
|
123
129
|
|
|
124
130
|
occurrences.push(
|
|
125
131
|
helpers.reportOccurrence(el, {
|
|
@@ -135,6 +141,7 @@ function runInPage(ctx) {
|
|
|
135
141
|
}
|
|
136
142
|
})
|
|
137
143
|
);
|
|
144
|
+
break;
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
if (applicableCount === 0) {
|
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check meter-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="meter" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 1.1.1
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="meter" (the attribute must name that
|
|
13
|
+
* role alone, not a fallback list) that are included in the accessibility
|
|
14
|
+
* tree. An element with the matching implicit role but no role attribute
|
|
15
|
+
* 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="meter" is name-from-author-only, so subtree text is never
|
|
20
|
+
* accepted: text sitting inside a custom meter widget is not reliably
|
|
21
|
+
* exposed as its name.
|
|
22
|
+
*/
|
|
23
|
+
|
|
5
24
|
const id = 'meter-name-present';
|
|
6
25
|
|
|
7
26
|
const meta = {
|
|
@@ -54,7 +73,7 @@ function runInPage(ctx) {
|
|
|
54
73
|
const raw = getAttr(el, 'aria-labelledby');
|
|
55
74
|
if (!raw) return '';
|
|
56
75
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
57
|
-
// name-from-content of the referenced element
|
|
76
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
58
77
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
59
78
|
// aria-labelledby target's only name source is its title attribute,
|
|
60
79
|
// which name-from-content alone can never see).
|
|
@@ -106,8 +125,8 @@ function runInPage(ctx) {
|
|
|
106
125
|
if (title) return { ok: true, method: 'title' };
|
|
107
126
|
|
|
108
127
|
// role="meter" is name-from-author-only per WAI-ARIA: aria-label,
|
|
109
|
-
// aria-labelledby, or title
|
|
110
|
-
// It must NOT fall back to subtree content
|
|
128
|
+
// aria-labelledby, or title, no content-based naming method at all.
|
|
129
|
+
// It must NOT fall back to subtree content, visible text near/inside a
|
|
111
130
|
// custom meter widget is not reliably exposed as its accessible name.
|
|
112
131
|
return { ok: false, method: 'none' };
|
|
113
132
|
}
|
|
@@ -137,7 +156,7 @@ function runInPage(ctx) {
|
|
|
137
156
|
occurrences.push(
|
|
138
157
|
helpers.reportOccurrence(el, {
|
|
139
158
|
summary: 'This meter has no accessible name.',
|
|
140
|
-
hint: "Provide aria-label, aria-labelledby, or a title attribute
|
|
159
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this meter's accessible name.",
|
|
141
160
|
i18n: {
|
|
142
161
|
summaryKey: 'meterNamePresent_summary_fail',
|
|
143
162
|
hintKey: 'meterNamePresent_hint_fail',
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
* ARIA widget role: button, link, checkbox, radio, switch, tab, textbox,
|
|
15
15
|
* combobox, listbox, menuitem, menuitemcheckbox, menuitemradio, option,
|
|
16
16
|
* slider, spinbutton, searchbox, treeitem). The container is applicable
|
|
17
|
-
* regardless of whether it is itself focusable
|
|
17
|
+
* regardless of whether it is itself focusable, focusability is only
|
|
18
18
|
* used to decide whether a *descendant* nests an interactive control.
|
|
19
19
|
* @expectation
|
|
20
20
|
* The element does not contain, as a descendant, another *operable*
|
|
21
21
|
* interactive control (e.g. a <button> wrapping a <select>, or a link
|
|
22
22
|
* containing a checkbox). Nested interactive controls are not reliably
|
|
23
|
-
* announced or operable via assistive technology
|
|
23
|
+
* announced or operable via assistive technology, activating the outer
|
|
24
24
|
* control and the inner one become ambiguous, and some AT only exposes
|
|
25
25
|
* one of the two.
|
|
26
26
|
* @implementation-notes
|
|
@@ -89,7 +89,7 @@ const meta = {
|
|
|
89
89
|
function runInPage(ctx) {
|
|
90
90
|
const { helpers, rule } = ctx;
|
|
91
91
|
|
|
92
|
-
// Declared inside runInPage
|
|
92
|
+
// Declared inside runInPage, see scripts/build-core.js header
|
|
93
93
|
// ("runInPage MUST be self-contained").
|
|
94
94
|
const INTERACTIVE_SELECTOR = [
|
|
95
95
|
'a[href]',
|
|
@@ -228,8 +228,8 @@ function runInPage(ctx) {
|
|
|
228
228
|
// A composite-owned child (option in a listbox/combobox, tab in a
|
|
229
229
|
// tablist, ...) is not a nested interactive control: its container
|
|
230
230
|
// owns it and drives its focus (roving tabindex or
|
|
231
|
-
// aria-activedescendant). Treat it as an attribution boundary
|
|
232
|
-
// count it, and do not descend past it. Any control
|
|
231
|
+
// aria-activedescendant). Treat it as an attribution boundary, do not
|
|
232
|
+
// count it, and do not descend past it. Any control nested
|
|
233
233
|
// inside it is attributed to the child itself (examined as its own
|
|
234
234
|
// container in the main loop), keeping the report at the nearest
|
|
235
235
|
// interactive ancestor rather than bubbling up to the composite.
|
|
@@ -2,11 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check option-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="option" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="option" (the attribute must name that
|
|
13
|
+
* role alone, not a fallback list) that are included in the accessibility
|
|
14
|
+
* tree. An element with the matching implicit role but no role attribute
|
|
15
|
+
* 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, from title, or,
|
|
19
|
+
* role="option" being name-from-content, from its own subtree text, where
|
|
20
|
+
* a descendant's own name (an <img alt>, aria-label or title) counts as
|
|
21
|
+
* that descendant's contribution rather than only its text nodes.
|
|
22
|
+
*/
|
|
23
|
+
|
|
5
24
|
const id = 'option-name-present';
|
|
6
25
|
|
|
7
26
|
const meta = {
|
|
8
|
-
title: '
|
|
9
|
-
description: 'Checks that elements expose a non-empty accessible name.',
|
|
27
|
+
title: 'Options have an accessible name',
|
|
28
|
+
description: 'Checks that elements with role="option" expose a non-empty accessible name.',
|
|
10
29
|
i18n: {
|
|
11
30
|
titleKey: 'optionNamePresent_title',
|
|
12
31
|
descriptionKey: 'optionNamePresent_description'
|
|
@@ -51,7 +70,7 @@ function runInPage(ctx) {
|
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
function getConservativeSubtreeText(document, container) {
|
|
54
|
-
// "Name from content"
|
|
73
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
55
74
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
56
75
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
57
76
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -70,7 +89,7 @@ function runInPage(ctx) {
|
|
|
70
89
|
const raw = getAttr(el, 'aria-labelledby');
|
|
71
90
|
if (!raw) return '';
|
|
72
91
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
73
|
-
// name-from-content of the referenced element
|
|
92
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
74
93
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
75
94
|
// aria-labelledby target's only name source is its title attribute,
|
|
76
95
|
// which name-from-content alone can never see).
|
|
@@ -2,11 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check page-title-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary The document must have a non-empty title
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 2.4.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to a run over a whole document. A run narrowed by
|
|
13
|
+
* contextSelector, or by engineOptions.fragment, is notApplicable: whether
|
|
14
|
+
* the page has a title is not a property any subtree can answer.
|
|
15
|
+
* @expectation
|
|
16
|
+
* The document has a <title> element, and document.title with whitespace
|
|
17
|
+
* collapsed is non-empty. The element is looked for anywhere in the
|
|
18
|
+
* document, not only inside <head>: a <title> the parser leaves outside
|
|
19
|
+
* <head> is still the document title in every browser. Whether that title
|
|
20
|
+
* describes the page is page-title-patterns' question.
|
|
21
|
+
*/
|
|
22
|
+
|
|
5
23
|
const id = 'page-title-present';
|
|
6
24
|
|
|
7
25
|
const meta = {
|
|
8
|
-
title: 'Page
|
|
9
|
-
description: 'Checks that the
|
|
26
|
+
title: 'Page has a non-empty title',
|
|
27
|
+
description: 'Checks that the page includes a non-empty <title> element.',
|
|
10
28
|
i18n: {
|
|
11
29
|
titleKey: 'pageTitlePresent_title',
|
|
12
30
|
descriptionKey: 'pageTitlePresent_description'
|
|
@@ -47,7 +65,7 @@ function runInPage(ctx) {
|
|
|
47
65
|
// Not restricted to `head > title`: per HTML parsing, a <title> encountered
|
|
48
66
|
// after <head> has closed is not re-parented into <head>, yet the browser
|
|
49
67
|
// (and document.title, used below) still recognizes it as the document's
|
|
50
|
-
// title
|
|
68
|
+
// title, matching that here avoids a false "missing title" report for a
|
|
51
69
|
// <title> that legitimately ended up outside <head>.
|
|
52
70
|
const titleEl = document.querySelector('title');
|
|
53
71
|
const titleText = (document.title || '').replace(/\s+/g, ' ').trim();
|