@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,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check combobox-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="combobox" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="combobox" (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="combobox" is name-from-author-only, so subtree text is never
|
|
20
|
+
* accepted: text sitting inside a custom combobox widget is not reliably
|
|
21
|
+
* exposed as its name. On a labelable element (<input role="combobox">) an
|
|
22
|
+
* associated <label> counts as well.
|
|
23
|
+
*/
|
|
24
|
+
|
|
5
25
|
const id = 'combobox-name-present';
|
|
6
26
|
|
|
7
27
|
const meta = {
|
|
@@ -66,7 +86,7 @@ function runInPage(ctx) {
|
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
function getConservativeSubtreeText(document, container) {
|
|
69
|
-
// "Name from content"
|
|
89
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
70
90
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
71
91
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
72
92
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -83,7 +103,7 @@ function runInPage(ctx) {
|
|
|
83
103
|
|
|
84
104
|
// A <label> contributes a name via its own aria-label/aria-labelledby
|
|
85
105
|
// (checked first, same ARIA-over-content precedence any element's
|
|
86
|
-
// accessible name gives
|
|
106
|
+
// accessible name gives, e.g. <label aria-label="Search"><svg
|
|
87
107
|
// aria-hidden="true">...</svg></label> names its control "Search" even
|
|
88
108
|
// though the label's only child content is aria-hidden) or, failing
|
|
89
109
|
// that, its rendered content (getConservativeSubtreeText).
|
|
@@ -98,10 +118,10 @@ function runInPage(ctx) {
|
|
|
98
118
|
if (content) return content;
|
|
99
119
|
// Final fallback per the general accname text-alternative algorithm,
|
|
100
120
|
// which applies to any element being asked for its name regardless of
|
|
101
|
-
// why (own aria-label, an aria-labelledby reference, or
|
|
121
|
+
// why (own aria-label, an aria-labelledby reference, or, here, native
|
|
102
122
|
// <label for> association): title, when nothing else yields a name.
|
|
103
123
|
// Purely additive (only fills in a name where there was none before),
|
|
104
|
-
// so it carries no false-positive risk
|
|
124
|
+
// so it carries no false-positive risk, see dialog-name-present.js's
|
|
105
125
|
// identical <iframe>-title-fallback fix for the concrete real-world
|
|
106
126
|
// trigger this same accname step covers elsewhere.
|
|
107
127
|
return getAttr(lab, 'title');
|
|
@@ -111,7 +131,7 @@ function runInPage(ctx) {
|
|
|
111
131
|
const raw = getAttr(el, 'aria-labelledby');
|
|
112
132
|
if (!raw) return '';
|
|
113
133
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
114
|
-
// name-from-content of the referenced element
|
|
134
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
115
135
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
116
136
|
// aria-labelledby target's only name source is its title attribute,
|
|
117
137
|
// which name-from-content alone can never see).
|
|
@@ -235,7 +255,7 @@ function runInPage(ctx) {
|
|
|
235
255
|
occurrences.push(
|
|
236
256
|
helpers.reportOccurrence(el, {
|
|
237
257
|
summary: 'This combobox has no accessible name.',
|
|
238
|
-
hint: "Provide aria-label, aria-labelledby, or a title attribute
|
|
258
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this combobox's accessible name.",
|
|
239
259
|
i18n: {
|
|
240
260
|
summaryKey: 'comboboxNamePresent_summary_fail',
|
|
241
261
|
hintKey: 'comboboxNamePresent_hint_fail',
|
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check contrast-computable
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Color contrast must be computable from CSS for rendered text
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 1.4.3
|
|
11
|
+
* @sc 1.4.6
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to every visible text node in scope, plus the label of <input
|
|
14
|
+
* type="button">/[type="submit"]/[type="reset"], which is rendered from
|
|
15
|
+
* the value attribute and so is invisible to a text-node walk. Text counts
|
|
16
|
+
* only when its element is DOM-visible under the run's visibility mode, is
|
|
17
|
+
* not clipped out of sight by the sr-only technique (clip or clip-path),
|
|
18
|
+
* and belongs neither to a disabled control nor to the label of one,
|
|
19
|
+
* WCAG's inactive-user-interface-component exception. Subtrees excluded
|
|
20
|
+
* via engineOptions.excludeSelectors are skipped, and open shadow roots
|
|
21
|
+
* are walked as roots in their own right.
|
|
22
|
+
* @expectation
|
|
23
|
+
* Both sides of the contrast calculation can be established from CSS for
|
|
24
|
+
* every applicable text node: an effective background resolving to an
|
|
25
|
+
* opaque color, and a parsable foreground color. Where either cannot be,
|
|
26
|
+
* a background image or gradient, mix-blend-mode, a filter or
|
|
27
|
+
* backdrop-filter, a text-shadow (which may add contrast this engine has
|
|
28
|
+
* no glyph-rendering model to account for), ancestor opacity, a root
|
|
29
|
+
* background that never becomes opaque, or a color that does not parse,
|
|
30
|
+
* the result is cantTell naming the blocker. This rule is the one that
|
|
31
|
+
* reports that uncertainty, which is what lets contrast-minimum and
|
|
32
|
+
* contrast-enhanced stay silent on the same text instead of guessing at
|
|
33
|
+
* a ratio.
|
|
34
|
+
*/
|
|
35
|
+
|
|
5
36
|
const id = 'contrast-computable';
|
|
6
37
|
|
|
7
38
|
const meta = {
|
|
@@ -118,6 +149,7 @@ function runInPage(ctx) {
|
|
|
118
149
|
else summaryKey = 'contrastComputable_cantTell_filterOrBackdropFilter';
|
|
119
150
|
} else if (rc === 'BACKGROUND_NOT_OPAQUE_AT_ROOT')
|
|
120
151
|
summaryKey = 'contrastComputable_cantTell_rootNotOpaque';
|
|
152
|
+
else if (rc === 'TEXT_SHADOW') summaryKey = 'contrastComputable_cantTell_textShadow';
|
|
121
153
|
|
|
122
154
|
const details = Object.assign(
|
|
123
155
|
{ reasonCode: rc },
|
|
@@ -146,6 +178,16 @@ function runInPage(ctx) {
|
|
|
146
178
|
: {}
|
|
147
179
|
)
|
|
148
180
|
},
|
|
181
|
+
uncertainty: {
|
|
182
|
+
code: 'not-computable',
|
|
183
|
+
needed: 'A contrast ratio for this text, which this page composition blocks.',
|
|
184
|
+
evidence: {
|
|
185
|
+
reasonCode: rc,
|
|
186
|
+
blockerProperty: details.blockerProperty || null,
|
|
187
|
+
blockerValue: details.blockerValue || null,
|
|
188
|
+
backgroundAlpha: details.backgroundAlpha === undefined ? null : details.backgroundAlpha
|
|
189
|
+
}
|
|
190
|
+
},
|
|
149
191
|
data: { details }
|
|
150
192
|
};
|
|
151
193
|
|
|
@@ -2,10 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check contrast-enhanced
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Text must meet the WCAG AAA contrast ratio for its size
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 1.4.6
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to the visible text contrast-computable applies to, see that
|
|
13
|
+
* rule for the eligibility gates, narrowed to text whose background and
|
|
14
|
+
* foreground are actually computable. Eligible text that is not computable
|
|
15
|
+
* leaves this rule notApplicable rather than cantTell: reporting that
|
|
16
|
+
* uncertainty belongs to contrast-computable, so the two never report the
|
|
17
|
+
* same text twice.
|
|
18
|
+
* @expectation
|
|
19
|
+
* Every computable text node reaches the ratio SC 1.4.6 requires for its
|
|
20
|
+
* size: 4.5:1 for large text, 7:1 for everything else. Text is large at
|
|
21
|
+
* 24px or more, or at 14pt (about 18.667px) or more when the computed font
|
|
22
|
+
* weight is 700 or higher.
|
|
23
|
+
*/
|
|
24
|
+
|
|
5
25
|
const id = 'contrast-enhanced';
|
|
6
26
|
|
|
7
27
|
const meta = {
|
|
8
|
-
title: 'Text
|
|
28
|
+
title: 'Text meets enhanced color contrast (AAA)',
|
|
9
29
|
description:
|
|
10
30
|
'Checks that visible text has a contrast ratio of at least 7:1 (normal) or 4.5:1 (large), when contrast is computable from CSS.',
|
|
11
31
|
i18n: {
|
|
@@ -196,6 +216,17 @@ function runInPage(ctx) {
|
|
|
196
216
|
|
|
197
217
|
const det = details && typeof details === 'object' ? details : { reasonCode: 'UNKNOWN' };
|
|
198
218
|
|
|
219
|
+
// The background is the only input this rule can fail to resolve; every other
|
|
220
|
+
// reason code here describes a ratio it did compute.
|
|
221
|
+
const uncertainty =
|
|
222
|
+
det.reasonCode === 'BACKGROUND_NOT_COMPUTABLE'
|
|
223
|
+
? {
|
|
224
|
+
code: 'not-computable',
|
|
225
|
+
needed: 'The effective background colour behind this text.',
|
|
226
|
+
evidence: { reasonCode: det.reasonCode, foreground: det.fg || null }
|
|
227
|
+
}
|
|
228
|
+
: null;
|
|
229
|
+
|
|
199
230
|
const occBase = {
|
|
200
231
|
selector: '',
|
|
201
232
|
html: '',
|
|
@@ -206,6 +237,7 @@ function runInPage(ctx) {
|
|
|
206
237
|
hintKey: '',
|
|
207
238
|
params: params && typeof params === 'object' ? params : {}
|
|
208
239
|
},
|
|
240
|
+
...(uncertainty ? { uncertainty } : {}),
|
|
209
241
|
data: { details: det }
|
|
210
242
|
};
|
|
211
243
|
|
|
@@ -2,10 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check contrast-minimum
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Text must meet the WCAG AA contrast ratio for its size
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 1.4.3
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to the visible text contrast-computable applies to, see that
|
|
13
|
+
* rule for the eligibility gates, narrowed to text whose background and
|
|
14
|
+
* foreground are actually computable. Eligible text that is not computable
|
|
15
|
+
* leaves this rule notApplicable rather than cantTell: reporting that
|
|
16
|
+
* uncertainty belongs to contrast-computable, so the two never report the
|
|
17
|
+
* same text twice.
|
|
18
|
+
* @expectation
|
|
19
|
+
* Every computable text node reaches the ratio SC 1.4.3 requires for its
|
|
20
|
+
* size: 3:1 for large text, 4.5:1 for everything else. Text is large at
|
|
21
|
+
* 24px or more, or at 14pt (about 18.667px) or more when the computed font
|
|
22
|
+
* weight is 700 or higher.
|
|
23
|
+
*/
|
|
24
|
+
|
|
5
25
|
const id = 'contrast-minimum';
|
|
6
26
|
|
|
7
27
|
const meta = {
|
|
8
|
-
title: 'Text
|
|
28
|
+
title: 'Text meets minimum color contrast (AA)',
|
|
9
29
|
description:
|
|
10
30
|
'Checks that visible text has a contrast ratio of at least 4.5:1 (normal) or 3:1 (large), when contrast is computable from CSS.',
|
|
11
31
|
i18n: {
|
|
@@ -193,6 +213,17 @@ function runInPage(ctx) {
|
|
|
193
213
|
|
|
194
214
|
const det = details && typeof details === 'object' ? details : { reasonCode: 'UNKNOWN' };
|
|
195
215
|
|
|
216
|
+
// The background is the only input this rule can fail to resolve; every other
|
|
217
|
+
// reason code here describes a ratio it did compute.
|
|
218
|
+
const uncertainty =
|
|
219
|
+
det.reasonCode === 'BACKGROUND_NOT_COMPUTABLE'
|
|
220
|
+
? {
|
|
221
|
+
code: 'not-computable',
|
|
222
|
+
needed: 'The effective background colour behind this text.',
|
|
223
|
+
evidence: { reasonCode: det.reasonCode, foreground: det.fg || null }
|
|
224
|
+
}
|
|
225
|
+
: null;
|
|
226
|
+
|
|
196
227
|
const occBase = {
|
|
197
228
|
selector: '',
|
|
198
229
|
html: '',
|
|
@@ -203,6 +234,7 @@ function runInPage(ctx) {
|
|
|
203
234
|
hintKey: '',
|
|
204
235
|
params: params && typeof params === 'object' ? params : {}
|
|
205
236
|
},
|
|
237
|
+
...(uncertainty ? { uncertainty } : {}),
|
|
206
238
|
data: { details: det }
|
|
207
239
|
};
|
|
208
240
|
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.3.4
|
|
11
11
|
* @applicability
|
|
12
|
-
* Any accessible (same-document, non-cross-origin) stylesheet
|
|
12
|
+
* Any accessible (same-document, non-cross-origin) stylesheet, inline
|
|
13
13
|
* `<style>` blocks and same-origin `<link>` stylesheets already loaded
|
|
14
14
|
* into `document.styleSheets`.
|
|
15
15
|
* @expectation
|
|
16
16
|
* No `@media (orientation: portrait)` or `@media (orientation:
|
|
17
17
|
* landscape)` block sets a `transform`/`-webkit-transform`/`rotate`
|
|
18
|
-
* rotation of approximately 90 degrees (mod 180, i.e. ~90 or ~270)
|
|
18
|
+
* rotation of approximately 90 degrees (mod 180, i.e. ~90 or ~270),
|
|
19
19
|
* the well-known technique for visually forcing one orientation
|
|
20
20
|
* regardless of the device's actual orientation, which defeats WCAG
|
|
21
21
|
* 1.3.4's requirement that content not restrict its view to a single
|
|
@@ -27,14 +27,15 @@
|
|
|
27
27
|
* lock. Compute the actual rotation angle and only flag ~90/~270 degrees,
|
|
28
28
|
* excluding ~0/~180 (a no-op or a flip, neither of which changes
|
|
29
29
|
* portrait<->landscape).
|
|
30
|
-
* -
|
|
31
|
-
* `
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
30
|
+
* - `rotate`/`rotateZ`, the standalone CSS `rotate` property, and
|
|
31
|
+
* `rotate3d(x, y, z, angle)`/`matrix()`/`matrix3d()` are all parsed for
|
|
32
|
+
* degrees. The matrix forms only yield an angle when they resolve to a
|
|
33
|
+
* pure rotation about the Z axis (no scale, skew, translation, or
|
|
34
|
+
* rotation combined with another axis). Anything else contributes 0,
|
|
35
|
+
* same as an unrecognized value, rather than guessing at an angle a
|
|
36
|
+
* general 3D matrix doesn't uniquely have.
|
|
36
37
|
* - Cross-origin stylesheets throw on `.cssRules` access (browser
|
|
37
|
-
* security model) and are skipped
|
|
38
|
+
* security model) and are skipped, same class of limitation as any
|
|
38
39
|
* check that can only see same-origin/inspectable content (compare
|
|
39
40
|
* `iframe-focusable-content`).
|
|
40
41
|
* - Not per-element: this is a whole-document/whole-stylesheet concern,
|
|
@@ -107,25 +108,98 @@ function runInPage(ctx) {
|
|
|
107
108
|
return value; // deg
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
function parseNumberList(argsStr) {
|
|
112
|
+
return trim(argsStr)
|
|
113
|
+
.split(',')
|
|
114
|
+
.map((s) => parseFloat(s.trim()));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A 2D linear map (a, b, c, d) -- the first four arguments of
|
|
118
|
+
// `matrix(a, b, c, d, e, f)`, or the top-left of `matrix3d`'s 4x4 --
|
|
119
|
+
// is a pure rotation only when its columns are unit length and
|
|
120
|
+
// orthogonal (no scale/skew/reflection): a = cos(theta), b = sin(theta),
|
|
121
|
+
// c = -sin(theta), d = cos(theta). Returns degrees, or null when the
|
|
122
|
+
// map isn't a pure rotation.
|
|
123
|
+
function decomposeMatrix2dRotation(a, b, c, d) {
|
|
124
|
+
const EPS = 1e-3;
|
|
125
|
+
if (Math.abs(a - d) > EPS || Math.abs(b + c) > EPS) return null;
|
|
126
|
+
if (Math.abs(a * a + b * b - 1) > EPS) return null;
|
|
127
|
+
if (Math.abs(a * d - b * c - 1) > EPS) return null;
|
|
128
|
+
return (Math.atan2(b, a) * 180) / Math.PI;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The 16 column-major values of `matrix3d(...)` represent a pure Z-axis
|
|
132
|
+
// rotation only when the 3rd/4th columns still match the identity (no
|
|
133
|
+
// translation, perspective, or rotation around another axis) and the
|
|
134
|
+
// top-left 2x2 block is a pure 2D rotation.
|
|
135
|
+
function decomposeMatrix3dZRotation(v) {
|
|
136
|
+
if (v.length !== 16 || !v.every(Number.isFinite)) return null;
|
|
137
|
+
const EPS = 1e-3;
|
|
138
|
+
const near = (x, t) => Math.abs(x - t) < EPS;
|
|
139
|
+
if (!(near(v[2], 0) && near(v[3], 0) && near(v[6], 0) && near(v[7], 0))) return null;
|
|
140
|
+
if (!(near(v[8], 0) && near(v[9], 0) && near(v[10], 1) && near(v[11], 0))) return null;
|
|
141
|
+
if (!(near(v[12], 0) && near(v[13], 0) && near(v[14], 0) && near(v[15], 1))) return null;
|
|
142
|
+
return decomposeMatrix2dRotation(v[0], v[1], v[4], v[5]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Sums the degrees of every rotate()/rotateZ()/rotate3d()/matrix()/
|
|
146
|
+
// matrix3d() function found in a transform value (a real transform can
|
|
147
|
+
// legitimately compose more than one, e.g. "translate(-50%)
|
|
148
|
+
// rotate(90deg)"). The matrix forms only contribute when they decompose
|
|
149
|
+
// to a pure Z rotation; anything else contributes 0, same as no match.
|
|
113
150
|
function rotateDegreesFromTransform(t) {
|
|
114
151
|
if (!t) return 0;
|
|
115
152
|
let total = 0;
|
|
116
|
-
const re = /rotate(?:Z)?\s*\(([^)]*)\)/gi;
|
|
117
153
|
let m;
|
|
118
|
-
|
|
154
|
+
|
|
155
|
+
const rotateRe = /rotate(?:Z)?\s*\(([^)]*)\)/gi;
|
|
156
|
+
while ((m = rotateRe.exec(t)) !== null) {
|
|
119
157
|
total += angleToDegrees(m[1]);
|
|
120
158
|
}
|
|
159
|
+
|
|
160
|
+
const rotate3dRe = /rotate3d\s*\(([^)]*)\)/gi;
|
|
161
|
+
while ((m = rotate3dRe.exec(t)) !== null) {
|
|
162
|
+
const parts = m[1].split(',');
|
|
163
|
+
if (parts.length !== 4) continue;
|
|
164
|
+
const x = parseFloat(parts[0]);
|
|
165
|
+
const y = parseFloat(parts[1]);
|
|
166
|
+
const z = parseFloat(parts[2]);
|
|
167
|
+
const deg = angleToDegrees(parts[3]);
|
|
168
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z) || !deg) continue;
|
|
169
|
+
const EPS = 1e-3;
|
|
170
|
+
if (Math.abs(x) < EPS && Math.abs(y) < EPS && Math.abs(Math.abs(z) - 1) < EPS) {
|
|
171
|
+
total += z < 0 ? -deg : deg;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const matrixRe = /matrix\s*\(([^)]*)\)/gi;
|
|
176
|
+
while ((m = matrixRe.exec(t)) !== null) {
|
|
177
|
+
const vals = parseNumberList(m[1]);
|
|
178
|
+
if (vals.length === 6 && vals.every(Number.isFinite)) {
|
|
179
|
+
const deg = decomposeMatrix2dRotation(vals[0], vals[1], vals[2], vals[3]);
|
|
180
|
+
if (deg != null) total += deg;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const matrix3dRe = /matrix3d\s*\(([^)]*)\)/gi;
|
|
185
|
+
while ((m = matrix3dRe.exec(t)) !== null) {
|
|
186
|
+
const deg = decomposeMatrix3dZRotation(parseNumberList(m[1]));
|
|
187
|
+
if (deg != null) total += deg;
|
|
188
|
+
}
|
|
189
|
+
|
|
121
190
|
return total;
|
|
122
191
|
}
|
|
123
192
|
|
|
124
193
|
// Whole-page-orientation-lock detection (see @implementation-notes): a
|
|
125
194
|
// rotation near 0 or 180 degrees (mod 180) is a no-op or a flip, neither
|
|
126
195
|
// of which changes portrait<->landscape, so it's NOT a lock; only a
|
|
127
|
-
// rotation near 90 or 270 degrees
|
|
128
|
-
//
|
|
196
|
+
// rotation near 90 or 270 degrees is. "Near" is a tolerance window, not
|
|
197
|
+
// exact equality: a `rad`/`grad`/`turn` value converts to a 90-degree
|
|
198
|
+
// rotation with floating-point remainder (e.g. `1.5708rad` is
|
|
199
|
+
// 90.0000210...deg, never exactly 90), and ACT's own failed examples
|
|
200
|
+
// include a -inexact 92.5deg, both must still register as a
|
|
201
|
+
// lock, which exact-modulo-equality (`% 90 === 0`) never does.
|
|
202
|
+
const LOCK_TOLERANCE_DEG = 5;
|
|
129
203
|
function isLockingRotation(styleDecl) {
|
|
130
204
|
if (!styleDecl) return false;
|
|
131
205
|
const transformVal =
|
|
@@ -141,9 +215,12 @@ function runInPage(ctx) {
|
|
|
141
215
|
|
|
142
216
|
if (!degrees) return false;
|
|
143
217
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
218
|
+
// Normalize into [0, 180): a rotation and its mirror (rotation + 180)
|
|
219
|
+
// swap the same two axes, so only the position within one half-turn
|
|
220
|
+
// matters. A value near the 90-degree midpoint of that range is near
|
|
221
|
+
// 90 OR 270 in the original full-turn range.
|
|
222
|
+
const normalized = ((degrees % 180) + 180) % 180;
|
|
223
|
+
return Math.abs(normalized - 90) <= LOCK_TOLERANCE_DEG;
|
|
147
224
|
}
|
|
148
225
|
|
|
149
226
|
function isOrientationMedia(mediaText) {
|
|
@@ -163,6 +240,7 @@ function runInPage(ctx) {
|
|
|
163
240
|
|
|
164
241
|
const findings = [];
|
|
165
242
|
let sheetCount = 0;
|
|
243
|
+
let unreadableSheetCount = 0;
|
|
166
244
|
|
|
167
245
|
try {
|
|
168
246
|
const sheets = document.styleSheets || [];
|
|
@@ -171,7 +249,10 @@ function runInPage(ctx) {
|
|
|
171
249
|
try {
|
|
172
250
|
rules = sheet && sheet.cssRules ? sheet.cssRules : null;
|
|
173
251
|
} catch {
|
|
174
|
-
|
|
252
|
+
// Cross-origin, not inspectable. Counted, since a lock could be
|
|
253
|
+
// declared there and a `pass` would claim more than was checked.
|
|
254
|
+
unreadableSheetCount += 1;
|
|
255
|
+
continue;
|
|
175
256
|
}
|
|
176
257
|
if (!rules) continue;
|
|
177
258
|
sheetCount += 1;
|
|
@@ -187,15 +268,48 @@ function runInPage(ctx) {
|
|
|
187
268
|
// no-throw: treat as no accessible stylesheets
|
|
188
269
|
}
|
|
189
270
|
|
|
190
|
-
|
|
191
|
-
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
192
|
-
}
|
|
271
|
+
const scanTarget = document.documentElement || document.body || null;
|
|
193
272
|
|
|
273
|
+
// A lock found in a readable sheet is still a lock, so `fail` outranks the
|
|
274
|
+
// uncertainty below.
|
|
194
275
|
if (!findings.length) {
|
|
276
|
+
if (unreadableSheetCount > 0) {
|
|
277
|
+
return {
|
|
278
|
+
ruleId: rule.ruleId,
|
|
279
|
+
outcome: 'cantTell',
|
|
280
|
+
severity: rule.defaultSeverity || 'serious',
|
|
281
|
+
confidence: 'low',
|
|
282
|
+
occurrences: [
|
|
283
|
+
helpers.reportOccurrence(scanTarget, {
|
|
284
|
+
summary: `${unreadableSheetCount} stylesheet(s) could not be read, so whether this page locks its orientation could not be determined.`,
|
|
285
|
+
hint: 'Cross-origin stylesheets are not inspectable from the page. Check any third-party CSS for an orientation media query containing a rotate() transform, or re-run the scan with those stylesheets served same-origin.',
|
|
286
|
+
i18n: {
|
|
287
|
+
summaryKey: 'cssOrientationLock_summary_cantTell_unreadableSheets',
|
|
288
|
+
hintKey: 'cssOrientationLock_hint_cantTell_unreadableSheets',
|
|
289
|
+
params: { count: String(unreadableSheetCount) }
|
|
290
|
+
},
|
|
291
|
+
uncertainty: {
|
|
292
|
+
code: 'not-computable',
|
|
293
|
+
needed: 'The contents of the stylesheets this scan could not read.',
|
|
294
|
+
evidence: { unreadableSheetCount, reasonCode: 'STYLESHEETS_NOT_READABLE' }
|
|
295
|
+
},
|
|
296
|
+
data: {
|
|
297
|
+
details: {
|
|
298
|
+
reasonCode: 'STYLESHEETS_NOT_READABLE',
|
|
299
|
+
unreadableSheetCount
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
]
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (sheetCount === 0) {
|
|
307
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
308
|
+
}
|
|
195
309
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
196
310
|
}
|
|
197
311
|
|
|
198
|
-
const target =
|
|
312
|
+
const target = scanTarget;
|
|
199
313
|
const occurrences = findings.map((f) =>
|
|
200
314
|
helpers.reportOccurrence(target, {
|
|
201
315
|
summary: f.selectorText
|
|
@@ -16,17 +16,16 @@
|
|
|
16
16
|
* level of wrapping div is allowed, matching how authors commonly group
|
|
17
17
|
* dt/dd pairs). If the flattened set contains any <dt> or <dd> at all, it
|
|
18
18
|
* must contain BOTH (an unbalanced dt-without-dd or dd-without-dt is
|
|
19
|
-
* invalid)
|
|
19
|
+
* invalid). A flattened set with neither is vacuously fine, not a
|
|
20
20
|
* violation (see implementation-notes). Any other direct or wrapped child
|
|
21
21
|
* breaks the description-list semantics assistive technologies rely on.
|
|
22
22
|
* @implementation-notes
|
|
23
|
-
* - Only one level of <div> wrapping is flattened
|
|
23
|
+
* - Only one level of <div> wrapping is flattened, a <div> nested inside
|
|
24
24
|
* another wrapping <div> is not flattened further and its contents are
|
|
25
25
|
* reported invalid.
|
|
26
26
|
* - The dt/dd pairing is only required "when not empty". A flattened set
|
|
27
|
-
* with NEITHER dt nor dd
|
|
28
|
-
*
|
|
29
|
-
* <style> content, or a genuinely childless <dl> — is not flagged; only
|
|
27
|
+
* with NEITHER dt nor dd, whether from an empty wrapping <div>, only
|
|
28
|
+
* <script>/<template>/<style> content, or a childless <dl>, is not flagged; only
|
|
30
29
|
* an unbalanced dt/dd pairing is a real structural problem.
|
|
31
30
|
* - Distinct, atomic decision from dlitem-parent-valid (the
|
|
32
31
|
* inverse relationship: does a given <dt>/<dd> have a valid parent).
|
|
@@ -64,7 +63,7 @@ const meta = {
|
|
|
64
63
|
function runInPage(ctx) {
|
|
65
64
|
const { helpers, rule } = ctx;
|
|
66
65
|
|
|
67
|
-
// Declared inside runInPage
|
|
66
|
+
// Declared inside runInPage, see scripts/build-core.js header
|
|
68
67
|
// ("runInPage MUST be self-contained").
|
|
69
68
|
const PASSTHROUGH_TAGS = new Set(['dt', 'dd', 'script', 'template', 'style']);
|
|
70
69
|
|
|
@@ -109,8 +108,8 @@ function runInPage(ctx) {
|
|
|
109
108
|
}
|
|
110
109
|
const dedupedInvalidTags = [...new Set(invalidTags)];
|
|
111
110
|
|
|
112
|
-
// The dt/dd pairing is only required "when not empty"
|
|
113
|
-
// NEITHER dt nor dd (whether
|
|
111
|
+
// The dt/dd pairing is only required "when not empty", a <dl> with
|
|
112
|
+
// NEITHER dt nor dd (whether childless after flattening, only
|
|
114
113
|
// passthrough script/template/style content, or an empty wrapping div)
|
|
115
114
|
// is vacuously fine, not a violation. Only an UNBALANCED pairing (dt
|
|
116
115
|
// present without any dd, or vice versa) is a real structural problem.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* or hide it.
|
|
16
16
|
* @expectation
|
|
17
17
|
* Neither element is present. Since their movement can never be paused,
|
|
18
|
-
* stopped, or hidden by the user, presence is itself the violation
|
|
18
|
+
* stopped, or hidden by the user, presence is itself the violation. This
|
|
19
19
|
* rule has no partial-pass case (it reports only when the element is
|
|
20
20
|
* found).
|
|
21
21
|
* @implementation-notes
|
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check dialog-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="dialog"/"alertdialog" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="dialog" or role="alertdialog" (the
|
|
13
|
+
* attribute must name one of those roles alone, not a fallback list) that
|
|
14
|
+
* are included in the accessibility tree. A native <dialog> without an
|
|
15
|
+
* explicit role 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. Both
|
|
19
|
+
* roles are name-from-author-only, so the heading or body text inside the
|
|
20
|
+
* dialog is not accepted as its name unless aria-labelledby points at it.
|
|
21
|
+
*/
|
|
22
|
+
|
|
5
23
|
const id = 'dialog-name-present';
|
|
6
24
|
|
|
7
25
|
const meta = {
|
|
@@ -54,13 +72,13 @@ function runInPage(ctx) {
|
|
|
54
72
|
function resolveAriaLabelledbyText(document, el, maxRefs) {
|
|
55
73
|
const raw = getAttr(el, 'aria-labelledby');
|
|
56
74
|
if (!raw) return '';
|
|
57
|
-
// Resolve via the shared getTextFromIdRefs helper
|
|
75
|
+
// Resolve via the shared getTextFromIdRefs helper, computes each
|
|
58
76
|
// referenced element's own ACCESSIBLE NAME (aria-label, then
|
|
59
77
|
// aria-labelledby, then a value-like name, then content, then title),
|
|
60
78
|
// not just its content text. This matters when aria-labelledby points
|
|
61
79
|
// at an <iframe title="..."> (e.g. BBC News' cookie-consent dialog,
|
|
62
80
|
// pointing at an `<iframe title="SP Consent Message">`), whose only
|
|
63
|
-
// name source is its title attribute
|
|
81
|
+
// name source is its title attribute, an iframe's content is
|
|
64
82
|
// opaque/cross-origin per HTML-AAM, so "name from content" is always
|
|
65
83
|
// empty. Computing only name-from-content of the referenced node would
|
|
66
84
|
// silently miss the title fallback and report no accessible name at
|
|
@@ -20,8 +20,10 @@
|
|
|
20
20
|
* still computed. Whether that element is the intended target depends on
|
|
21
21
|
* author intent, which markup does not carry, so the outcome is cantTell.
|
|
22
22
|
* @implementation-notes
|
|
23
|
-
* - Scoped
|
|
24
|
-
*
|
|
23
|
+
* - Scoped to ids referenced by ARIA. The page-wide check
|
|
24
|
+
* lives in its own rule, `duplicate-id`, mapped to the WCAG 2.0/2.1 SC
|
|
25
|
+
* 4.1.1 that WCAG 2.2 removed; the two overlap on referenced ids and
|
|
26
|
+
* answer different questions there (see that rule's header).
|
|
25
27
|
* - Document-wide by design: id uniqueness and ARIA id references are a
|
|
26
28
|
* whole-document property, not scoped to a sub-root. Reported occurrences are
|
|
27
29
|
* limited to the scanned scope.
|
|
@@ -59,7 +61,7 @@ const meta = {
|
|
|
59
61
|
function runInPage(ctx) {
|
|
60
62
|
const { document, helpers, rule } = ctx;
|
|
61
63
|
|
|
62
|
-
// Declared inside runInPage
|
|
64
|
+
// Declared inside runInPage, see scripts/build-core.js header
|
|
63
65
|
// ("runInPage MUST be self-contained").
|
|
64
66
|
const IDREF_ATTRS = [
|
|
65
67
|
'aria-labelledby',
|
|
@@ -126,6 +128,11 @@ function runInPage(ctx) {
|
|
|
126
128
|
hintKey: 'duplicateIdAria_hint_cantTell',
|
|
127
129
|
params: { id: refId, duplicateCount: String(els.length) }
|
|
128
130
|
},
|
|
131
|
+
uncertainty: {
|
|
132
|
+
code: 'judgement-required',
|
|
133
|
+
needed: 'Whether the first element carrying this id is the intended target.',
|
|
134
|
+
evidence: { id: refId, duplicateCount: els.length, resolvesTo: 'first' }
|
|
135
|
+
},
|
|
129
136
|
data: {
|
|
130
137
|
details: {
|
|
131
138
|
reasonCode: 'DUPLICATE_ID_ARIA_REFERENCED',
|