@surea11y/core 1.5.0 → 1.7.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 +240 -149
- package/README.md +51 -44
- package/docs/ACT_RULE_MAPPING.md +245 -0
- package/docs/API_STABILITY.md +53 -5
- package/docs/BINDING_AUTHORS_GUIDE.md +106 -4
- package/docs/DESIGN_CHALLENGES.md +367 -0
- package/docs/EARL.md +100 -0
- package/docs/ENGINE_OPTIONS.md +42 -4
- package/docs/I18N.md +4 -4
- package/docs/INTEGRATION.md +4 -2
- package/docs/LIMITATIONS.md +9 -5
- package/docs/OUTPUT_SCHEMA.md +44 -6
- package/docs/POLICY.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_AUTHORING.md +63 -36
- package/docs/RULE_CATALOG.md +1928 -169
- package/docs/RULE_HELPERS.md +333 -0
- package/docs/RULE_TAXONOMY.md +27 -6
- package/docs/SARIF.md +21 -2
- package/docs/TROUBLESHOOTING.md +2 -2
- package/docs/WCAG_CONFORMANCE.md +34 -10
- package/package.json +11 -9
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +74 -10
- package/src/checks/automatic/aria-allowed-role.js +34 -25
- package/src/checks/automatic/aria-braille-equivalent.js +21 -13
- package/src/checks/automatic/aria-conditional-attr.js +22 -15
- package/src/checks/automatic/aria-deprecated-role.js +13 -1
- package/src/checks/automatic/aria-hidden-body.js +3 -3
- package/src/checks/automatic/aria-hidden-focus.js +5 -5
- package/src/checks/automatic/aria-prohibited-attr.js +23 -18
- package/src/checks/automatic/aria-prohibited-children.js +136 -43
- package/src/checks/automatic/aria-required-attr.js +119 -24
- package/src/checks/automatic/aria-required-children.js +54 -30
- package/src/checks/automatic/aria-required-parent.js +93 -15
- package/src/checks/automatic/aria-role-name-present.js +37 -23
- package/src/checks/automatic/aria-roles-valid.js +52 -21
- package/src/checks/automatic/aria-valid-attr-value.js +89 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -10
- package/src/checks/automatic/autocomplete-valid.js +2 -2
- package/src/checks/automatic/avoid-inline-spacing.js +133 -6
- 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 +42 -0
- package/src/checks/automatic/contrast-enhanced.js +33 -1
- package/src/checks/automatic/contrast-minimum.js +33 -1
- package/src/checks/automatic/css-orientation-lock.js +138 -24
- 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 +10 -3
- package/src/checks/automatic/duplicate-id.js +203 -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 +10 -1
- package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
- package/src/checks/automatic/iframe-focusable-content.js +68 -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 +204 -68
- package/src/checks/automatic/link-in-text-block.js +285 -29
- 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 → role-img-text-alternative-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 +155 -58
- package/src/checks/automatic/td-has-header.js +24 -23
- 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 +563 -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-complementary-is-top-level-manual.js +231 -0
- 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/password-paste-enabled-manual.js +255 -0
- 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 +8880 -41883
- package/src/earl.js +144 -0
- package/src/report.js +2 -2
- package/src/sarif.js +22 -2
- package/surea11y.browser.js +10 -37882
- package/surea11y.i18n.de.js +2 -21
- package/surea11y.i18n.es.js +2 -21
- package/surea11y.i18n.fr.js +2 -21
- package/bin/surea11y-core.js +0 -20
|
@@ -2,11 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check textbox-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="textbox" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="textbox" (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="textbox" is name-from-author-only, so subtree text is never
|
|
20
|
+
* accepted: text sitting inside a custom textbox widget is not reliably
|
|
21
|
+
* exposed as its name. On a labelable element (<input role="textbox">) an
|
|
22
|
+
* associated <label> counts as well.
|
|
23
|
+
*/
|
|
24
|
+
|
|
5
25
|
const id = 'textbox-name-present';
|
|
6
26
|
|
|
7
27
|
const meta = {
|
|
8
|
-
title: '
|
|
9
|
-
description: 'Checks that elements expose a non-empty accessible name.',
|
|
28
|
+
title: 'Textboxes have an accessible name',
|
|
29
|
+
description: 'Checks that elements with role="textbox" expose a non-empty accessible name.',
|
|
10
30
|
i18n: {
|
|
11
31
|
titleKey: 'textboxNamePresent_title',
|
|
12
32
|
descriptionKey: 'textboxNamePresent_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).
|
|
@@ -231,7 +251,7 @@ function runInPage(ctx) {
|
|
|
231
251
|
occurrences.push(
|
|
232
252
|
helpers.reportOccurrence(el, {
|
|
233
253
|
summary: 'This element has no accessible name.',
|
|
234
|
-
hint: "Provide aria-label, aria-labelledby, or a title attribute
|
|
254
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this textbox's accessible name.",
|
|
235
255
|
i18n: {
|
|
236
256
|
summaryKey: 'textboxNamePresent_summary_fail',
|
|
237
257
|
hintKey: 'textboxNamePresent_hint_fail',
|
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check tooltip-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="tooltip" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="tooltip" (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, from title, or,
|
|
19
|
+
* role="tooltip" being name-from-content, from its own subtree text,
|
|
20
|
+
* where a descendant's own name (an <img alt>, aria-label or title) counts
|
|
21
|
+
* as that descendant's contribution rather than only its text nodes.
|
|
22
|
+
*/
|
|
23
|
+
|
|
5
24
|
const id = 'tooltip-name-present';
|
|
6
25
|
|
|
7
26
|
const meta = {
|
|
@@ -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,30 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check treeitem-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="treeitem" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="treeitem" (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, from title, or,
|
|
19
|
+
* role="treeitem" being name-from-content, from its own subtree text,
|
|
20
|
+
* where a descendant's own name (an <img alt>, aria-label or title) counts
|
|
21
|
+
* as that descendant's contribution rather than only its text nodes.
|
|
22
|
+
*/
|
|
23
|
+
|
|
5
24
|
const id = 'treeitem-name-present';
|
|
6
25
|
|
|
7
26
|
const meta = {
|
|
8
|
-
title: '
|
|
9
|
-
description: 'Checks that elements expose a non-empty accessible name.',
|
|
27
|
+
title: 'Tree items have an accessible name',
|
|
28
|
+
description: 'Checks that elements with role="treeitem" expose a non-empty accessible name.',
|
|
10
29
|
i18n: {
|
|
11
30
|
titleKey: 'treeitemNamePresent_title',
|
|
12
31
|
descriptionKey: 'treeitemNamePresent_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).
|
|
@@ -10,11 +10,23 @@
|
|
|
10
10
|
* @sc 3.1.2
|
|
11
11
|
* @applicability
|
|
12
12
|
* Applies to any element other than the root <html> with a non-empty
|
|
13
|
-
* lang attribute
|
|
13
|
+
* lang attribute AND at least some non-whitespace "governed text" that
|
|
14
|
+
* actually inherits its language from that element, per ACT de46e4:
|
|
15
|
+
* - Descendant text/alt is governed by the nearest lang-carrying
|
|
16
|
+
* ancestor only, a nested descendant with its own non-empty lang
|
|
17
|
+
* re-scopes everything inside it, so that subtree no longer counts
|
|
18
|
+
* toward the outer element's applicability (it counts toward the
|
|
19
|
+
* nested element's own, if that one is also being evaluated).
|
|
20
|
+
* - A non-empty alt attribute on img/area/input[type=image] counts as
|
|
21
|
+
* governed text, the same as a text node.
|
|
22
|
+
* - Text (or alt) that CSS keeps out of the render tree (display:none,
|
|
23
|
+
* the hidden attribute, ...) does not count. aria-hidden and
|
|
24
|
+
* offscreen positioning do NOT exempt text, per ACT's own failed
|
|
25
|
+
* examples for both, only actual non-rendering does.
|
|
14
26
|
* @expectation
|
|
15
27
|
* The lang value matches a valid BCP47 language-tag syntax. WCAG 3.1.2
|
|
16
28
|
* (Language of Parts) requires that when a passage's language differs
|
|
17
|
-
* from the page's default, it is identified programmatically
|
|
29
|
+
* from the page's default, it is identified programmatically. An
|
|
18
30
|
* invalid tag fails to identify a real language at all.
|
|
19
31
|
* @implementation-notes
|
|
20
32
|
* - Distinct, atomic decision from html-lang-attr-present (that
|
|
@@ -22,7 +34,7 @@
|
|
|
22
34
|
* covers every other element, for SC 3.1.2.
|
|
23
35
|
* - Same minimal BCP47 *syntax* check as html-lang-attr-present (primary
|
|
24
36
|
* subtag + optional subtags), not IANA Language Subtag Registry
|
|
25
|
-
* validation
|
|
37
|
+
* validation, same documented scope limitation (syntactically
|
|
26
38
|
* well-formed but unregistered tags like "xx-ZZ" are not flagged).
|
|
27
39
|
*/
|
|
28
40
|
|
|
@@ -66,6 +78,78 @@ function runInPage(ctx) {
|
|
|
66
78
|
? helpers.isValidLanguageTag
|
|
67
79
|
: (v) => BCP47_RE.test(String(v || ''));
|
|
68
80
|
|
|
81
|
+
function isDomVisible(node) {
|
|
82
|
+
if (!node) return false;
|
|
83
|
+
if (helpers.isDomVisibleEligible)
|
|
84
|
+
return !!helpers.isDomVisibleEligible(node, ctx, { targetSet: 'dom' }).eligible;
|
|
85
|
+
if (helpers.getEligibilityInfo)
|
|
86
|
+
return !!helpers.getEligibilityInfo(node, ctx, { targetSet: 'dom' }).eligible;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasOwnNonEmptyLang(node) {
|
|
91
|
+
try {
|
|
92
|
+
const v = node.getAttribute ? node.getAttribute('lang') : null;
|
|
93
|
+
return v != null && v.trim() !== '';
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isAltBearing(node) {
|
|
100
|
+
const tag = (node.tagName || '').toLowerCase();
|
|
101
|
+
if (tag === 'img' || tag === 'area') return true;
|
|
102
|
+
if (tag !== 'input') return false;
|
|
103
|
+
try {
|
|
104
|
+
return (node.getAttribute('type') || '').toLowerCase() === 'image';
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// "Governed text": non-whitespace text (or alt) that inherits its
|
|
111
|
+
// language from `root`, per ACT de46e4, see @applicability above. Walks
|
|
112
|
+
// the flat subtree, stopping at any descendant carrying its own non-empty
|
|
113
|
+
// lang (that subtree governs itself, not `root`), and treating
|
|
114
|
+
// display:none/hidden content as absent. Bails out as soon as any
|
|
115
|
+
// qualifying text is found; a node-visit budget guards pathological
|
|
116
|
+
// markup the same way other subtree walks in this engine do.
|
|
117
|
+
const MAX_VISITS = 5000;
|
|
118
|
+
function hasGovernedText(root) {
|
|
119
|
+
let found = false;
|
|
120
|
+
let visits = 0;
|
|
121
|
+
|
|
122
|
+
function walk(node, isRoot) {
|
|
123
|
+
if (found || visits++ > MAX_VISITS) return;
|
|
124
|
+
if (!node || node.nodeType !== 1) return;
|
|
125
|
+
if (!isRoot && hasOwnNonEmptyLang(node)) return; // re-scoped to itself
|
|
126
|
+
|
|
127
|
+
if (isAltBearing(node)) {
|
|
128
|
+
const alt = node.getAttribute ? node.getAttribute('alt') : null;
|
|
129
|
+
if (alt != null && alt.trim() !== '' && isDomVisible(node)) found = true;
|
|
130
|
+
return; // alt-bearing elements have no other text to walk into
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!isDomVisible(node)) return;
|
|
134
|
+
|
|
135
|
+
const kids = node.childNodes ? Array.from(node.childNodes) : [];
|
|
136
|
+
for (const kid of kids) {
|
|
137
|
+
if (found) return;
|
|
138
|
+
if (kid.nodeType === 3) {
|
|
139
|
+
if (String(kid.nodeValue || '').trim()) {
|
|
140
|
+
found = true;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
} else if (kid.nodeType === 1) {
|
|
144
|
+
walk(kid, false);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
walk(root, true);
|
|
150
|
+
return found;
|
|
151
|
+
}
|
|
152
|
+
|
|
69
153
|
const nodes = helpers.queryAllSmart
|
|
70
154
|
? helpers.queryAllSmart('[lang]')
|
|
71
155
|
: helpers.queryAll('[lang]');
|
|
@@ -80,10 +164,11 @@ function runInPage(ctx) {
|
|
|
80
164
|
const rawAttr = el.getAttribute('lang');
|
|
81
165
|
if (rawAttr === null || rawAttr === '') continue; // ACT de46e4: empty is out of scope
|
|
82
166
|
|
|
83
|
-
// The rule applies only where text actually inherits the language
|
|
84
|
-
// element
|
|
85
|
-
//
|
|
86
|
-
|
|
167
|
+
// The rule applies only where text actually inherits the language from
|
|
168
|
+
// THIS element specifically, not merely where the subtree has any text
|
|
169
|
+
// at all, which could all belong to a nested element's own (possibly
|
|
170
|
+
// valid) lang instead. See hasGovernedText's doc comment.
|
|
171
|
+
if (!hasGovernedText(el)) continue;
|
|
87
172
|
|
|
88
173
|
applicableCount += 1;
|
|
89
174
|
|
|
@@ -87,7 +87,7 @@ function runInPage(ctx) {
|
|
|
87
87
|
|
|
88
88
|
function computeNameInfo(el) {
|
|
89
89
|
// <video> is not a labelable element (no browser computes an accessible
|
|
90
|
-
// name from <label for="...">), so only ARIA naming and title count
|
|
90
|
+
// name from <label for="...">), so only ARIA naming and title count,
|
|
91
91
|
// do not accept native <label> associations.
|
|
92
92
|
const flags = [];
|
|
93
93
|
let aria = null;
|
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
* @check accesskeys
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary accesskey values must be unique on the page
|
|
9
|
-
* @standard Best Practices (no formal WCAG Success Criterion
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
10
10
|
* @applicability
|
|
11
11
|
* Applies whenever two or more elements share the same non-empty
|
|
12
12
|
* accesskey attribute value (case-insensitive).
|
|
13
13
|
* @expectation
|
|
14
14
|
* Every accesskey value on the page is unique. Duplicate accesskeys
|
|
15
|
-
* make keyboard-shortcut activation ambiguous
|
|
15
|
+
* make keyboard-shortcut activation ambiguous: only one of the
|
|
16
16
|
* elements sharing the key can actually be reached by it, and which
|
|
17
17
|
* one is browser/platform-dependent.
|
|
18
18
|
* @implementation-notes
|
|
19
|
-
* - Not WCAG-normative
|
|
19
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
20
20
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
21
21
|
* header comment for the shared rationale/precedent.
|
|
22
22
|
*/
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.1.1
|
|
11
11
|
* @type manual
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to <area> elements whose alt attribute is present but empty once
|
|
14
|
+
* trimmed, the markup that declares a hotspot decorative. The <area> must
|
|
15
|
+
* belong to a <map> that an <img usemap> actually references, and both
|
|
16
|
+
* that <img> and the <area> itself must be included in the accessibility
|
|
17
|
+
* tree; an <area> in an unused map is out of scope.
|
|
18
|
+
* role="presentation"/"none" takes an element out unless it is focusable.
|
|
12
19
|
* @expectation
|
|
13
20
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
14
21
|
*/
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.1.1
|
|
11
11
|
* @type manual
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to <area> elements whose alt attribute is present and non-empty.
|
|
14
|
+
* The <area> must belong to a <map> that an <img usemap> actually
|
|
15
|
+
* references, and both that <img> and the <area> itself must be included
|
|
16
|
+
* in the accessibility tree; an <area> in an unused map is out of scope.
|
|
17
|
+
* role="presentation"/"none" takes an element out unless it is focusable.
|
|
12
18
|
* @expectation
|
|
13
19
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
14
20
|
*/
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
* @expectation
|
|
15
15
|
* `aria-checked` is redundant on a native checkbox/radio (the role's
|
|
16
16
|
* checked state is already exposed natively), but when an author sets
|
|
17
|
-
* it explicitly it should agree with the element's actual state
|
|
17
|
+
* it explicitly it should agree with the element's actual state,
|
|
18
18
|
* otherwise assistive technology is told something different from what
|
|
19
19
|
* a sighted user perceives.
|
|
20
20
|
* @implementation-notes
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* - Authored as `type: 'manual'` (cantTell-capped, never fail), unlike
|
|
22
|
+
* most ARIA-validity rules in this file family. This engine analyzes
|
|
23
|
+
* STATIC markup only (no script execution), so `.checked`
|
|
24
24
|
* reliably reflects the static `checked` attribute for freshly-parsed
|
|
25
25
|
* markup, but a very common, entirely legitimate real-world pattern is a
|
|
26
26
|
* JS-hydrated widget whose server-rendered HTML intentionally ships
|
|
@@ -118,7 +118,7 @@ function runInPage(ctx) {
|
|
|
118
118
|
helpers.reportOccurrence(el, {
|
|
119
119
|
summary:
|
|
120
120
|
'This element’s aria-checked value does not match its actual checked/indeterminate state.',
|
|
121
|
-
hint: 'Set aria-checked to match the element’s real state, or remove it
|
|
121
|
+
hint: 'Set aria-checked to match the element’s real state, or remove it; a native checkbox/radio already exposes this state without it.',
|
|
122
122
|
i18n: {
|
|
123
123
|
summaryKey: 'ariaCheckedStateMismatch_summary_cantTell',
|
|
124
124
|
hintKey: 'ariaCheckedStateMismatch_hint_cantTell',
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @check aria-text
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary role="text" elements should have no focusable descendants
|
|
9
|
-
* @standard Best Practices (no formal WCAG Success Criterion
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
10
10
|
* @applicability
|
|
11
11
|
* Elements with an explicit `role="text"`.
|
|
12
12
|
* @expectation
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* subtree as a single unit of plain text (e.g. text visually split
|
|
15
15
|
* across multiple `<span>`s by styling). Per the WAI-ARIA Authoring
|
|
16
16
|
* Practices, this only makes sense when that subtree contains no
|
|
17
|
-
* focusable content
|
|
17
|
+
* focusable content: a focusable descendant inside a "this is just
|
|
18
18
|
* text" region is unreachable or confusing for keyboard/AT users.
|
|
19
19
|
* @implementation-notes
|
|
20
|
-
* - Not WCAG-normative
|
|
20
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
21
21
|
* `type: 'manual'` rule, matching the Tier 1b precedent (see
|
|
22
22
|
* `landmark-unique`'s header comment for the shared rationale).
|
|
23
23
|
* - "Focusable descendant" is a presence check (link/button/form
|
|
@@ -80,7 +80,7 @@ function runInPage(ctx) {
|
|
|
80
80
|
selector: stableSelector,
|
|
81
81
|
html,
|
|
82
82
|
summary: 'This role="text" element contains a focusable descendant.',
|
|
83
|
-
hint: 'Remove role="text" (or remove the focusable descendant)
|
|
83
|
+
hint: 'Remove role="text" (or remove the focusable descendant); a "plain text" region should not contain focusable content.',
|
|
84
84
|
i18n: {
|
|
85
85
|
summaryKey: 'ariaText_summary_cantTell',
|
|
86
86
|
hintKey: 'ariaText_hint_cantTell',
|
|
@@ -9,31 +9,36 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 2.4.1
|
|
11
11
|
* @applicability
|
|
12
|
-
* Always applicable to any HTML document with a <body> element
|
|
12
|
+
* Always applicable to any HTML document with a <body> element:
|
|
13
13
|
* "bypass blocks" is a whole-page concern, matching
|
|
14
14
|
* aria-hidden-body / page-title-present's pattern of
|
|
15
15
|
* evaluating the document directly rather than a scoped root.
|
|
16
16
|
* @expectation
|
|
17
17
|
* At least one of the following recognized WCAG 2.4.1 techniques is
|
|
18
18
|
* present:
|
|
19
|
-
* (a) a main landmark (<main> or [role="main"])
|
|
19
|
+
* (a) a main landmark (<main> or [role="main"]), technique ARIA11: a
|
|
20
20
|
* screen reader user can jump straight to it, bypassing everything
|
|
21
21
|
* before it (nav, header, repeated blocks) in one step;
|
|
22
|
-
* (b) a working same-page anchor link
|
|
22
|
+
* (b) a working same-page anchor link, technique G1/G123: an
|
|
23
23
|
* <a href="#id"> (or legacy <a name="id">) whose target resolves to
|
|
24
24
|
* a real element in the link's own tree (light DOM or the same shadow
|
|
25
|
-
* root).
|
|
26
|
-
*
|
|
27
|
-
* (c) at least one heading (<h1>-<h6> or [role="heading"])
|
|
28
|
-
*
|
|
29
|
-
*
|
|
25
|
+
* root). Not required to be positioned before a <nav> or be
|
|
26
|
+
* keyboard-focus-order-first (see implementation notes);
|
|
27
|
+
* (c) at least one heading (<h1>-<h6> or [role="heading"]) that is both
|
|
28
|
+
* included in the accessibility tree AND visible (not off-screen,
|
|
29
|
+
* clipped, opacity:0, or zero-size-overflow-hidden), technique H69:
|
|
30
|
+
* heading navigation is itself a standards-recognized bypass
|
|
31
|
+
* mechanism (e.g. a screen reader's "jump by heading" command), but
|
|
32
|
+
* ACT 047fe0's own Expectation requires visibility too, since a
|
|
33
|
+
* screen-reader-only heading leaves sighted keyboard users with no
|
|
34
|
+
* equivalent way to locate the start of non-repeated content.
|
|
30
35
|
* @implementation-notes
|
|
31
36
|
* - Outcome model: this rule is `type: 'manual'` (cantTell-capped, never
|
|
32
37
|
* `fail`). When a recognized mechanism is found the page has nothing to
|
|
33
38
|
* review here → `notApplicable` (matching page-has-heading-one-manual /
|
|
34
39
|
* skip-link-manual's "nothing to flag" convention). When none is found we
|
|
35
|
-
* return `cantTell
|
|
36
|
-
* verify"
|
|
40
|
+
* return `cantTell`: "we could not detect a bypass mechanism, please
|
|
41
|
+
* verify," rather than a hard `fail`. The absence of a *detectable*
|
|
37
42
|
* mechanism is NOT high-confidence evidence that 2.4.1 is violated, for
|
|
38
43
|
* several reasons the engine cannot resolve from a single static snapshot:
|
|
39
44
|
* • Applicability itself is undecidable in-page. 2.4.1 governs blocks of
|
|
@@ -45,7 +50,7 @@
|
|
|
45
50
|
* rest of the page is routinely made `inert` or `aria-hidden="true"`,
|
|
46
51
|
* so the page's real <main>/headings are (correctly) filtered out by
|
|
47
52
|
* isAccTreeEligible for the duration of that state and only the dialog
|
|
48
|
-
* is exposed
|
|
53
|
+
* is exposed, so a snapshot taken then would see "no mechanism" though
|
|
49
54
|
* the page has one once the dialog closes. The same applies to content
|
|
50
55
|
* that is display:none until revealed by script (tabs, accordions, an
|
|
51
56
|
* unmounted SPA view).
|
|
@@ -54,23 +59,19 @@
|
|
|
54
59
|
* human review instead. No ACT rule hard-fails 2.4.1 by presence alone,
|
|
55
60
|
* for the same reason.
|
|
56
61
|
* - This rule intentionally checks presence, not position, for the
|
|
57
|
-
* same-page-anchor condition (b): a full bypass algorithm is heuristic
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* (a page whose only anchor link isn't a real skip mechanism, e.g. a
|
|
64
|
-
* "back to top" link) — never raise a spurious one.
|
|
62
|
+
* same-page-anchor condition (b): a full bypass algorithm is heuristic,
|
|
63
|
+
* and getting DOM-order / keyboard-focus-order positioning exactly right
|
|
64
|
+
* without introducing false positives is materially harder than checking
|
|
65
|
+
* presence alone. Being lenient about condition (b) can only make us
|
|
66
|
+
* *miss* a review prompt (a page whose only anchor link isn't a real
|
|
67
|
+
* skip mechanism, e.g. a "back to top" link), never raising a spurious one.
|
|
65
68
|
* - Shadow DOM: all three conditions use `helpers.queryAllSmart`, which is
|
|
66
69
|
* shadow-DOM-aware (when the run enables includeShadowDom) and applies the
|
|
67
70
|
* engine's hidden-content policy. The same-page-anchor target is resolved
|
|
68
|
-
* in the link's own root (`getRootNode()
|
|
71
|
+
* in the link's own root (`getRootNode()`: the document, or the shadow
|
|
69
72
|
* root the link lives in) before falling back to the document, so a skip
|
|
70
73
|
* link encapsulated in a web component is credited the same as one in the
|
|
71
|
-
* light DOM.
|
|
72
|
-
* `document.querySelectorAll`/`getElementById`, which never pierced shadow
|
|
73
|
-
* roots — a genuine gap now closed.)
|
|
74
|
+
* light DOM.
|
|
74
75
|
*/
|
|
75
76
|
|
|
76
77
|
const id = 'bypass-blocks-present';
|
|
@@ -227,9 +228,26 @@ function runInPage(ctx) {
|
|
|
227
228
|
return false;
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
// ACT 047fe0's own Expectation requires the heading to be visible, not
|
|
232
|
+
// only included in the accessibility tree: a screen-reader-only heading
|
|
233
|
+
// still leaves sighted keyboard users with no way to locate the start of
|
|
234
|
+
// non-repeated content. "Visible" per ACT's own glossary: making it fully
|
|
235
|
+
// transparent would change rendered pixels, which every CSS-only hiding
|
|
236
|
+
// technique (off-screen positioning, clip/clip-path, opacity:0, a
|
|
237
|
+
// zero-size overflow:hidden box) fails.
|
|
238
|
+
function isCssHidden(el) {
|
|
239
|
+
if (!helpers || typeof helpers.getVisibilityHintsInfo !== 'function') return false;
|
|
240
|
+
try {
|
|
241
|
+
const info = helpers.getVisibilityHintsInfo(el, ctx, {});
|
|
242
|
+
return !!(info && Array.isArray(info.hints) && info.hints.length > 0);
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
230
248
|
function hasHeading() {
|
|
231
249
|
for (const el of queryAll('h1, h2, h3, h4, h5, h6, [role="heading"]')) {
|
|
232
|
-
if (el && isExposedToAt(el)) return true;
|
|
250
|
+
if (el && isExposedToAt(el) && !isCssHidden(el)) return true;
|
|
233
251
|
}
|
|
234
252
|
return false;
|
|
235
253
|
}
|
|
@@ -246,8 +264,8 @@ function runInPage(ctx) {
|
|
|
246
264
|
const occurrences = [
|
|
247
265
|
helpers.reportOccurrence(body, {
|
|
248
266
|
summary:
|
|
249
|
-
'No recognized way to bypass repeated blocks of content was detected on this page
|
|
250
|
-
hint: 'Confirm the page offers a bypass mechanism: a main landmark (<main> or role="main"), a working "skip to content" link, or heading elements that assistive technology can use to jump past repeated content. (A mechanism may be temporarily hidden
|
|
267
|
+
'No recognized way to bypass repeated blocks of content was detected on this page. Verify a bypass mechanism exists.',
|
|
268
|
+
hint: 'Confirm the page offers a bypass mechanism: a main landmark (<main> or role="main"), a working "skip to content" link, or heading elements that assistive technology can use to jump past repeated content. (A mechanism may be temporarily hidden, e.g. while a modal dialog makes the page inert, or provided on a per-site basis; this needs human confirmation.)',
|
|
251
269
|
i18n: {
|
|
252
270
|
summaryKey: 'bypassBlocksPresent_summary_cantTell',
|
|
253
271
|
hintKey: 'bypassBlocksPresent_hint_cantTell',
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.1.1
|
|
11
11
|
* @type manual
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to <canvas> elements that already carry a text alternative:
|
|
14
|
+
* fallback content inside the element, an ARIA name, or a title. A
|
|
15
|
+
* <canvas> with none of those has no alternative whose quality could be
|
|
16
|
+
* judged; that's canvas-text-alternative-present's failure. The element
|
|
17
|
+
* must be included in the accessibility tree, and
|
|
18
|
+
* role="presentation"/"none" takes it out of scope unless it is focusable,
|
|
19
|
+
* which restores its role.
|
|
12
20
|
* @expectation
|
|
13
21
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
14
22
|
*/
|