@surea11y/core 1.3.0 → 1.4.1
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 +87 -2
- package/README.md +109 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +26 -0
- package/docs/ARIA_DEPRECATION.md +95 -0
- package/docs/CI_INTEGRATIONS.md +7 -7
- package/docs/ENGINE_OPTIONS.md +1 -1
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +1 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +6 -6
- package/package.json +52 -16
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +4 -6
- package/src/checks/automatic/aria-allowed-attr.js +663 -134
- package/src/checks/automatic/aria-allowed-role.js +2 -0
- package/src/checks/automatic/aria-braille-equivalent.js +2 -0
- package/src/checks/automatic/aria-conditional-attr.js +8 -7
- package/src/checks/automatic/aria-deprecated-role.js +107 -38
- package/src/checks/automatic/aria-hidden-body.js +6 -4
- package/src/checks/automatic/aria-hidden-focus.js +12 -13
- package/src/checks/automatic/aria-prohibited-attr.js +98 -105
- package/src/checks/automatic/aria-prohibited-children.js +56 -87
- package/src/checks/automatic/aria-required-attr.js +6 -7
- package/src/checks/automatic/aria-required-children.js +7 -10
- package/src/checks/automatic/aria-required-parent.js +20 -25
- package/src/checks/automatic/aria-role-name-present.js +2 -0
- package/src/checks/automatic/aria-roles-valid.js +33 -6
- package/src/checks/automatic/aria-valid-attr-value.js +18 -15
- package/src/checks/automatic/aria-valid-attr.js +2 -0
- package/src/checks/automatic/autocomplete-valid.js +39 -1
- package/src/checks/automatic/avoid-inline-spacing.js +181 -20
- package/src/checks/automatic/binary-control-name-present.js +13 -3
- package/src/checks/automatic/button-name-present.js +54 -21
- package/src/checks/automatic/canvas-text-alternative-present.js +17 -8
- package/src/checks/automatic/combobox-name-present.js +10 -1
- package/src/checks/automatic/contrast-computable.js +2 -0
- package/src/checks/automatic/contrast-enhanced.js +2 -0
- package/src/checks/automatic/contrast-minimum.js +2 -0
- package/src/checks/automatic/css-orientation-lock.js +21 -27
- package/src/checks/automatic/definition-list-children-valid.js +6 -6
- package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
- package/src/checks/automatic/dialog-name-present.js +18 -11
- package/src/checks/automatic/dlitem-parent-valid.js +2 -0
- package/src/checks/automatic/duplicate-id-aria.js +4 -3
- package/src/checks/automatic/embed-text-alternative-present.js +2 -0
- package/src/checks/automatic/form-control-programmatic-label-present.js +50 -4
- package/src/checks/automatic/form-control-single-label.js +110 -43
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
- package/src/checks/automatic/iframe-focusable-content.js +246 -18
- package/src/checks/automatic/iframe-name-present.js +2 -0
- package/src/checks/automatic/iframe-title-unique.js +3 -1
- package/src/checks/automatic/img-alt-present.js +23 -18
- package/src/checks/automatic/input-image-alt-present.js +101 -52
- package/src/checks/automatic/label-in-name.js +97 -27
- package/src/checks/automatic/language-page-present.js +7 -1
- package/src/checks/automatic/link-in-text-block.js +2 -0
- package/src/checks/automatic/link-name-present.js +52 -17
- package/src/checks/automatic/list-children-valid.js +14 -24
- package/src/checks/automatic/listbox-name-present.js +10 -1
- package/src/checks/automatic/listitem-parent-valid.js +30 -7
- package/src/checks/automatic/menuitem-name-present.js +10 -1
- package/src/checks/automatic/meta-refresh-no-exceptions.js +33 -4
- package/src/checks/automatic/meta-refresh-timing-absent.js +32 -4
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +39 -15
- package/src/checks/automatic/meter-name-present.js +12 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +177 -25
- package/src/checks/automatic/object-text-alternative-present.js +16 -7
- package/src/checks/automatic/option-name-present.js +10 -1
- package/src/checks/automatic/page-title-present.js +2 -0
- package/src/checks/automatic/progressbar-name-present.js +16 -11
- package/src/checks/automatic/role-img-alt-present.js +4 -4
- package/src/checks/automatic/searchbox-name-present.js +10 -1
- package/src/checks/automatic/server-side-image-map-absent.js +4 -3
- package/src/checks/automatic/slider-name-present.js +13 -2
- package/src/checks/automatic/spinbutton-name-present.js +10 -1
- package/src/checks/automatic/summary-name-present.js +10 -1
- package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
- package/src/checks/automatic/svg-text-alternative-present.js +17 -5
- package/src/checks/automatic/tab-name-present.js +10 -1
- package/src/checks/automatic/table-headers-attr-valid.js +3 -2
- package/src/checks/automatic/table-th-has-data-cells.js +69 -6
- package/src/checks/automatic/target-size-minimum.js +5 -0
- package/src/checks/automatic/td-has-header.js +24 -1
- package/src/checks/automatic/textbox-name-present.js +10 -1
- package/src/checks/automatic/tooltip-name-present.js +10 -1
- package/src/checks/automatic/treeitem-name-present.js +10 -1
- package/src/checks/automatic/valid-lang.js +18 -3
- package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
- package/src/checks/manual/accesskeys-manual.js +3 -1
- package/src/checks/manual/area-alt-decorative-manual.js +2 -0
- package/src/checks/manual/area-alt-quality-manual.js +2 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
- package/src/checks/manual/aria-text-manual.js +6 -5
- package/src/checks/manual/bypass-blocks-present-manual.js +279 -0
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/css-hidden-focus.js +184 -9
- package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
- package/src/checks/manual/empty-heading-manual.js +17 -17
- package/src/checks/manual/empty-table-header-manual.js +52 -25
- package/src/checks/manual/focus-order-semantics-manual.js +16 -4
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
- package/src/checks/manual/heading-order-manual.js +28 -1
- package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
- package/src/checks/manual/image-redundant-alt-manual.js +21 -1
- package/src/checks/manual/img-alt-decorative-manual.js +2 -0
- package/src/checks/manual/img-alt-quality-manual.js +2 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +26 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
- package/src/checks/manual/label-title-only-manual.js +29 -22
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +51 -55
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +44 -31
- package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
- package/src/checks/manual/landmark-one-main-manual.js +12 -23
- package/src/checks/manual/landmark-unique-manual.js +37 -52
- package/src/checks/manual/link-name-quality-manual.js +2 -0
- package/src/checks/manual/media-transcript-present-manual.js +2 -0
- package/src/checks/manual/meta-viewport-large-manual.js +3 -1
- package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
- package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
- package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/p-as-heading-manual.js +2 -0
- package/src/checks/manual/page-has-heading-one-manual.js +12 -11
- package/src/checks/manual/page-title-patterns-manual.js +2 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
- package/src/checks/manual/region-manual.js +27 -36
- package/src/checks/manual/scope-attr-valid-manual.js +3 -1
- package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
- package/src/checks/manual/skip-link-manual.js +7 -6
- package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/tabindex-manual.js +3 -1
- package/src/checks/manual/table-duplicate-name-manual.js +5 -4
- package/src/checks/manual/table-fake-caption-manual.js +24 -3
- package/src/checks/manual/video-caption-manual.js +2 -0
- package/src/checks/manual-review.js +2 -0
- package/src/core.js +11820 -3317
- package/src/index.js +2 -0
- package/src/report.js +51 -9
- package/src/sarif.js +20 -5
- package/surea11y.browser.js +4943 -1388
- package/bin/core.js +0 -473
- package/docs/CLI.md +0 -128
- package/src/catalogs/composites.wcag.js +0 -454
- package/src/checks/automatic/bypass-blocks-present.js +0 -215
- package/src/checks/rules-and-tags.full.csv +0 -19
- package/src/checks/rules-and-tags.full.json +0 -259
- package/src/core/aria-helpers.js +0 -1211
- package/src/core/contrast-helpers.js +0 -1302
- package/src/core/dom-helpers.js +0 -4493
- package/src/core/dom-runner.js +0 -787
- package/src/core/frame-messaging.js +0 -261
- package/src/core/frame-scan.js +0 -190
- package/src/core/rollup-composites.js +0 -127
- package/src/core/rule-meta.js +0 -176
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -1228
- package/src/i18n/fr.js +0 -1185
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -59
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
|
@@ -1,34 +1,40 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @check avoid-inline-spacing
|
|
5
7
|
* @atomic true
|
|
6
|
-
* @summary Inline style must not force text
|
|
8
|
+
* @summary Inline style must not force text spacing below the WCAG metric
|
|
7
9
|
* @standard WCAG 2.2
|
|
8
10
|
* @sc 1.4.12
|
|
9
11
|
* @applicability
|
|
10
|
-
* Applies to
|
|
11
|
-
* line-height, letter-spacing
|
|
12
|
+
* Applies to a rendered element with visible text of its own whose style
|
|
13
|
+
* attribute declares line-height, letter-spacing or word-spacing as
|
|
14
|
+
* `!important` with a real value. A CSS-wide keyword (inherit, initial,
|
|
15
|
+
* unset, revert) specifies no spacing of its own and is out of scope.
|
|
12
16
|
* @expectation
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* defeats that override (inline `!important` wins over nearly every
|
|
18
|
-
* other origin, including user stylesheets applied the normal way).
|
|
17
|
+
* Each such declaration already meets WCAG 1.4.12's own metric for that
|
|
18
|
+
* property, as a multiple of the font size: line-height at least 1.5,
|
|
19
|
+
* letter-spacing at least 0.12, word-spacing at least 0.16. A forced value
|
|
20
|
+
* that already satisfies the criterion leaves the user nothing to override.
|
|
19
21
|
* @implementation-notes
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* - Computed style resolves the cascade and the units; the declared value is
|
|
23
|
+
* only a fallback for environments that do not lay the document out. Spacing
|
|
24
|
+
* that resolves to neither is left unreported rather than failed.
|
|
25
|
+
* - Within one declaration block, importance outranks order, so the effective
|
|
26
|
+
* declaration is the last `!important` one for that property.
|
|
27
|
+
* - ACT 78fd32/24afc2/9e45ec additionally require the text to contain a soft
|
|
28
|
+
* wrap break, which cannot be determined without layout. Not implemented, so
|
|
29
|
+
* a forced value on text that never wraps is still reported.
|
|
24
30
|
*/
|
|
25
31
|
|
|
26
32
|
const id = 'avoid-inline-spacing';
|
|
27
33
|
|
|
28
34
|
const meta = {
|
|
29
|
-
title: 'Inline style must not force text spacing
|
|
35
|
+
title: 'Inline style must not force text spacing below the WCAG metric',
|
|
30
36
|
description:
|
|
31
|
-
'Checks that inline style
|
|
37
|
+
'Checks that where inline style forces line-height, letter-spacing or word-spacing with !important, the value already meets WCAG 1.4.12, so the user has nothing left to override.',
|
|
32
38
|
i18n: {
|
|
33
39
|
titleKey: 'avoidInlineSpacing_title',
|
|
34
40
|
descriptionKey: 'avoidInlineSpacing_description'
|
|
@@ -56,6 +62,20 @@ function runInPage(ctx) {
|
|
|
56
62
|
const { helpers, rule } = ctx;
|
|
57
63
|
|
|
58
64
|
const SPACING_PROPS = ['line-height', 'letter-spacing', 'word-spacing'];
|
|
65
|
+
// WCAG 1.4.12's own metrics, as multiples of the font size.
|
|
66
|
+
const MIN_RATIO = { 'line-height': 1.5, 'letter-spacing': 0.12, 'word-spacing': 0.16 };
|
|
67
|
+
// Only these two take the value from the parent, leaving this declaration
|
|
68
|
+
// specifying no spacing of its own. `initial` and `revert` resolve to a
|
|
69
|
+
// concrete value (`normal` for all three properties), so they stay in scope.
|
|
70
|
+
const INHERITED_KEYWORDS = ['inherit', 'unset'];
|
|
71
|
+
// No user agent's `normal` line height reaches 1.5, so a forced `normal`
|
|
72
|
+
// always falls short of the metric.
|
|
73
|
+
const NORMAL_LINE_HEIGHT_RATIO = 1.2;
|
|
74
|
+
const CAMEL = {
|
|
75
|
+
'line-height': 'lineHeight',
|
|
76
|
+
'letter-spacing': 'letterSpacing',
|
|
77
|
+
'word-spacing': 'wordSpacing'
|
|
78
|
+
};
|
|
59
79
|
|
|
60
80
|
const nodes = helpers.queryAllSmart
|
|
61
81
|
? helpers.queryAllSmart('[style]')
|
|
@@ -64,6 +84,136 @@ function runInPage(ctx) {
|
|
|
64
84
|
const occurrences = [];
|
|
65
85
|
let applicableCount = 0;
|
|
66
86
|
|
|
87
|
+
// Within one declaration block, importance wins over order, so the last
|
|
88
|
+
// important declaration is the one that takes effect. Passed Example 5 of ACT
|
|
89
|
+
// 78fd32 turns on this and Passed Example 6 on the non-important half.
|
|
90
|
+
function effectiveDeclaration(raw, prop) {
|
|
91
|
+
const re = new RegExp('(?:^|;)\\s*' + prop + '\\s*:\\s*([^;]*)', 'gi');
|
|
92
|
+
let chosen = null;
|
|
93
|
+
let m;
|
|
94
|
+
while ((m = re.exec(raw))) {
|
|
95
|
+
const value = String(m[1] || '').trim();
|
|
96
|
+
const important = /!\s*important\s*$/i.test(value);
|
|
97
|
+
const clean = value.replace(/!\s*important\s*$/i, '').trim();
|
|
98
|
+
if (!clean) continue;
|
|
99
|
+
if (!chosen || important || !chosen.important) {
|
|
100
|
+
if (chosen && chosen.important && !important) continue;
|
|
101
|
+
chosen = { value: clean, important };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return chosen;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function hasVisibleTextChild(el) {
|
|
108
|
+
let kids;
|
|
109
|
+
try {
|
|
110
|
+
kids = el.childNodes ? Array.from(el.childNodes) : [];
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return kids.some((n) => n && n.nodeType === 3 && String(n.nodeValue || '').trim() !== '');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isRendered(el) {
|
|
118
|
+
if (helpers.isDomVisibleEligible) {
|
|
119
|
+
try {
|
|
120
|
+
return !!helpers.isDomVisibleEligible(el, ctx, { targetSet: 'dom' }).eligible;
|
|
121
|
+
} catch {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const px = (v) => {
|
|
129
|
+
const n = parseFloat(v);
|
|
130
|
+
return Number.isFinite(n) && /px\s*$/.test(String(v)) ? n : null;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
function computedStyleOf(el) {
|
|
134
|
+
if (helpers && typeof helpers.computedStyle === 'function') {
|
|
135
|
+
try {
|
|
136
|
+
const cs = helpers.computedStyle(el);
|
|
137
|
+
if (cs) return cs;
|
|
138
|
+
} catch {
|
|
139
|
+
// fall through to the realm's own view
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const view = el.ownerDocument && el.ownerDocument.defaultView;
|
|
144
|
+
if (view && typeof view.getComputedStyle === 'function') return view.getComputedStyle(el);
|
|
145
|
+
} catch {
|
|
146
|
+
// no computed style available
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ACT scopes these rules to text visible on screen, and text pushed far off
|
|
152
|
+
// canvas is the one hidden shape the shared eligibility check keeps eligible.
|
|
153
|
+
function isOffScreen(el) {
|
|
154
|
+
if (!helpers.getVisibilityHintsInfo) return false;
|
|
155
|
+
try {
|
|
156
|
+
const info = helpers.getVisibilityHintsInfo(el, ctx, {});
|
|
157
|
+
return !!(info && Array.isArray(info.hints) && info.hints.indexOf('offscreen') !== -1);
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// A realm that does not lay the document out reports font-size as the CSS
|
|
164
|
+
// absolute-size keyword rather than a length. Resolving those keeps px-valued
|
|
165
|
+
// spacing checkable there, and cannot manufacture a failure: a font size
|
|
166
|
+
// larger than assumed only makes a px ratio smaller.
|
|
167
|
+
const ABSOLUTE_FONT_SIZES = {
|
|
168
|
+
'xx-small': 9,
|
|
169
|
+
'x-small': 10,
|
|
170
|
+
small: 13,
|
|
171
|
+
medium: 16,
|
|
172
|
+
large: 18,
|
|
173
|
+
'x-large': 24,
|
|
174
|
+
'xx-large': 32
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
function fontSizeOf(cs) {
|
|
178
|
+
if (!cs) return null;
|
|
179
|
+
const direct = px(cs.fontSize);
|
|
180
|
+
if (direct !== null) return direct;
|
|
181
|
+
const keyword = String(cs.fontSize || '')
|
|
182
|
+
.trim()
|
|
183
|
+
.toLowerCase();
|
|
184
|
+
return ABSOLUTE_FONT_SIZES[keyword] || null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The spacing as a multiple of the font size, or null when it cannot be
|
|
189
|
+
* resolved. Computed style is preferred because it already applies the
|
|
190
|
+
* cascade and unit resolution; the declared value is only a fallback for
|
|
191
|
+
* environments that do not lay the document out.
|
|
192
|
+
*/
|
|
193
|
+
function spacingRatio(el, prop, declared) {
|
|
194
|
+
const cs = computedStyleOf(el);
|
|
195
|
+
const fontSize = fontSizeOf(cs);
|
|
196
|
+
if (cs && fontSize) {
|
|
197
|
+
const used = px(cs[CAMEL[prop]]);
|
|
198
|
+
if (used !== null) return used / fontSize;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const v = String(declared || '')
|
|
202
|
+
.trim()
|
|
203
|
+
.toLowerCase();
|
|
204
|
+
// `normal`, and the keywords that resolve to it, add nothing for the two
|
|
205
|
+
// spacing properties and stay under the metric for line height.
|
|
206
|
+
if (v === 'normal' || v === 'initial' || v === 'revert' || v === 'revert-layer') {
|
|
207
|
+
return prop === 'line-height' ? NORMAL_LINE_HEIGHT_RATIO : 0;
|
|
208
|
+
}
|
|
209
|
+
if (/^[0-9.]+$/.test(v)) return parseFloat(v);
|
|
210
|
+
if (/em$/.test(v)) return parseFloat(v);
|
|
211
|
+
if (/%$/.test(v)) return parseFloat(v) / 100;
|
|
212
|
+
const asPx = px(v);
|
|
213
|
+
if (asPx !== null && fontSize) return asPx / fontSize;
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
67
217
|
for (const el of nodes) {
|
|
68
218
|
if (!el || !el.getAttribute) continue;
|
|
69
219
|
const raw = String(el.getAttribute('style') || '');
|
|
@@ -73,14 +223,25 @@ function runInPage(ctx) {
|
|
|
73
223
|
const hasAnySpacingProp = SPACING_PROPS.some((p) => lower.includes(p));
|
|
74
224
|
if (!hasAnySpacingProp) continue;
|
|
75
225
|
|
|
76
|
-
|
|
226
|
+
// The rule is about text the user needs to re-space, so an element with no
|
|
227
|
+
// text of its own, or none that renders, is not a target.
|
|
228
|
+
if (!hasVisibleTextChild(el) || !isRendered(el) || isOffScreen(el)) continue;
|
|
77
229
|
|
|
78
230
|
const flagged = [];
|
|
231
|
+
let inScope = false;
|
|
79
232
|
for (const prop of SPACING_PROPS) {
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
233
|
+
const decl = effectiveDeclaration(raw, prop);
|
|
234
|
+
if (!decl || !decl.important) continue;
|
|
235
|
+
if (INHERITED_KEYWORDS.indexOf(decl.value.toLowerCase()) !== -1) continue;
|
|
236
|
+
inScope = true;
|
|
237
|
+
const ratio = spacingRatio(el, prop, decl.value);
|
|
238
|
+
// Unresolvable spacing is left alone: this engine reserves fail for
|
|
239
|
+
// high-confidence violations.
|
|
240
|
+
if (ratio === null) continue;
|
|
241
|
+
if (ratio < MIN_RATIO[prop]) flagged.push(prop);
|
|
82
242
|
}
|
|
83
243
|
|
|
244
|
+
if (inScope) applicableCount += 1;
|
|
84
245
|
if (!flagged.length) continue;
|
|
85
246
|
|
|
86
247
|
const tag = el.tagName.toLowerCase();
|
|
@@ -90,8 +251,8 @@ function runInPage(ctx) {
|
|
|
90
251
|
occurrences.push({
|
|
91
252
|
selector: stableSelector,
|
|
92
253
|
html,
|
|
93
|
-
summary: `This element's inline style forces ${flagged.join(', ')} with !important
|
|
94
|
-
hint: 'Remove !important from line-height/letter-spacing/word-spacing in inline styles
|
|
254
|
+
summary: `This element's inline style forces ${flagged.join(', ')} with !important below the WCAG text-spacing metric, so the user cannot raise it.`,
|
|
255
|
+
hint: 'Remove !important from line-height/letter-spacing/word-spacing in inline styles, or set a value that already meets the metric (line-height 1.5, letter-spacing 0.12em, word-spacing 0.16em).',
|
|
95
256
|
i18n: {
|
|
96
257
|
summaryKey: 'avoidInlineSpacing_summary_fail',
|
|
97
258
|
hintKey: 'avoidInlineSpacing_hint_fail',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'binary-control-name-present';
|
|
@@ -135,9 +137,16 @@ function runInPage(ctx) {
|
|
|
135
137
|
return '';
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
141
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
142
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
138
143
|
function isEligibleAcc(helpers, el, ctx) {
|
|
139
144
|
const fn =
|
|
140
|
-
helpers && typeof helpers.
|
|
145
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
146
|
+
? helpers.isIncludedInAccessibilityTree
|
|
147
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
148
|
+
? helpers.isAccTreeEligible
|
|
149
|
+
: null;
|
|
141
150
|
if (!fn) return true;
|
|
142
151
|
try {
|
|
143
152
|
const r = fn(el, ctx);
|
|
@@ -151,8 +160,9 @@ function runInPage(ctx) {
|
|
|
151
160
|
const occurrences = [];
|
|
152
161
|
let applicableCount = 0;
|
|
153
162
|
|
|
154
|
-
|
|
155
|
-
|
|
163
|
+
// Native checkbox/radio without an explicit role belongs to
|
|
164
|
+
// form-control-programmatic-label-present.
|
|
165
|
+
const selector = '[role="checkbox"], [role="radio"], [role="switch"]';
|
|
156
166
|
const nodes = helpers.queryAllSmart
|
|
157
167
|
? helpers.queryAllSmart(selector)
|
|
158
168
|
: helpers.queryAll(selector);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
// NOTE: Repo ruleId contract requires ENGINE_TAG prefix in the rule id.
|
|
@@ -47,9 +49,8 @@ function runInPage(ctx) {
|
|
|
47
49
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
48
50
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
49
51
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
50
|
-
// (
|
|
51
|
-
// "<
|
|
52
|
-
// icon-button pattern).
|
|
52
|
+
// (covers the common "<a><img alt='...'></a>" logo-link /
|
|
53
|
+
// "<button><img alt='...'></button>" icon-button pattern).
|
|
53
54
|
if (helpers.getContentNameInfo) {
|
|
54
55
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
55
56
|
return info && info.present ? info.value : '';
|
|
@@ -77,7 +78,14 @@ function runInPage(ctx) {
|
|
|
77
78
|
|
|
78
79
|
for (const el of nodes) {
|
|
79
80
|
// isAccTreeEligible returns { eligible, reasons }, not a boolean.
|
|
80
|
-
|
|
81
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
82
|
+
// (ACT c487ae), which excludes focusable aria-hidden content;
|
|
83
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
84
|
+
const eligResult = helpers.isIncludedInAccessibilityTree
|
|
85
|
+
? helpers.isIncludedInAccessibilityTree(el, ctx)
|
|
86
|
+
: helpers.isAccTreeEligible
|
|
87
|
+
? helpers.isAccTreeEligible(el, ctx)
|
|
88
|
+
: true;
|
|
81
89
|
const eligible =
|
|
82
90
|
typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
|
|
83
91
|
if (!eligible) continue;
|
|
@@ -103,25 +111,50 @@ function runInPage(ctx) {
|
|
|
103
111
|
inputValueName = getInputButtonValueName(el);
|
|
104
112
|
}
|
|
105
113
|
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// name-from-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
114
|
+
// ARIA 1.2 "Name From: author, contents". Every other known role is
|
|
115
|
+
// name-from-author-only: <button role="combobox">List</button> exposes a
|
|
116
|
+
// value, not a label. An unknown role falls back to the implicit role.
|
|
117
|
+
// <generated:aria-name-from-content>
|
|
118
|
+
const NAME_FROM_CONTENT_ROLES = [
|
|
119
|
+
'button',
|
|
120
|
+
'cell',
|
|
121
|
+
'checkbox',
|
|
122
|
+
'columnheader',
|
|
123
|
+
'doc-backlink',
|
|
124
|
+
'doc-biblioref',
|
|
125
|
+
'doc-glossref',
|
|
126
|
+
'doc-noteref',
|
|
127
|
+
'graphics-object',
|
|
128
|
+
'gridcell',
|
|
129
|
+
'heading',
|
|
130
|
+
'link',
|
|
131
|
+
'menuitem',
|
|
132
|
+
'menuitemcheckbox',
|
|
133
|
+
'menuitemradio',
|
|
134
|
+
'option',
|
|
135
|
+
'radio',
|
|
136
|
+
'row',
|
|
137
|
+
'rowgroup',
|
|
138
|
+
'rowheader',
|
|
139
|
+
'switch',
|
|
140
|
+
'tab',
|
|
141
|
+
'tooltip',
|
|
142
|
+
'treeitem'
|
|
122
143
|
];
|
|
144
|
+
// </generated:aria-name-from-content>
|
|
145
|
+
const isKnownRoleToken =
|
|
146
|
+
helpers && helpers.aria && typeof helpers.aria.isKnownRole === 'function'
|
|
147
|
+
? (() => {
|
|
148
|
+
try {
|
|
149
|
+
return !!helpers.aria.isKnownRole(roleNorm);
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
})()
|
|
154
|
+
: false;
|
|
123
155
|
const isContentNameCandidate =
|
|
124
|
-
(tag === 'button' || role === 'button') &&
|
|
156
|
+
(tag === 'button' || role === 'button') &&
|
|
157
|
+
(!roleNorm || !isKnownRoleToken || NAME_FROM_CONTENT_ROLES.includes(roleNorm));
|
|
125
158
|
const contentName =
|
|
126
159
|
!trustedProgrammaticName && !inputValueName && isContentNameCandidate
|
|
127
160
|
? getConservativeSubtreeText(el)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -7,9 +9,8 @@
|
|
|
7
9
|
* @standard WCAG 2.2
|
|
8
10
|
* @sc 1.1.1
|
|
9
11
|
* @applicability
|
|
10
|
-
* Applies to <canvas> elements
|
|
11
|
-
*
|
|
12
|
-
* if they are focusable (tabbable) or referenced by IDREF relationships (per engine eligibility checks).
|
|
12
|
+
* Applies to <canvas> elements included in the accessibility tree.
|
|
13
|
+
* Hidden elements are excluded whether or not they are focusable.
|
|
13
14
|
* @expectation
|
|
14
15
|
* Each applicable <canvas> provides a text alternative via fallback content or an accessible name.
|
|
15
16
|
*/
|
|
@@ -69,8 +70,15 @@ function runInPage(ctx) {
|
|
|
69
70
|
const getEligibilityInfo =
|
|
70
71
|
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
// aria-hidden removes the canvas from the accessibility tree, so a text
|
|
74
|
+
// alternative on it cannot reach anyone; aria-hidden-focus reports a
|
|
75
|
+
// focusable one. Matches the other non-text-content rules.
|
|
76
|
+
const isEligibleHelper =
|
|
77
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
78
|
+
? helpers.isIncludedInAccessibilityTree
|
|
79
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
80
|
+
? helpers.isAccTreeEligible
|
|
81
|
+
: null;
|
|
74
82
|
|
|
75
83
|
const getTextAlternativeInfo =
|
|
76
84
|
helpers && typeof helpers.getTextAlternativeInfo === 'function'
|
|
@@ -95,15 +103,16 @@ function runInPage(ctx) {
|
|
|
95
103
|
for (const el of canvases) {
|
|
96
104
|
if (!el) continue;
|
|
97
105
|
|
|
98
|
-
// Applicability:
|
|
99
|
-
if (
|
|
106
|
+
// Applicability: included in the accessibility tree.
|
|
107
|
+
if (isEligibleHelper) {
|
|
100
108
|
const elig = (() => {
|
|
101
109
|
try {
|
|
102
|
-
return
|
|
110
|
+
return isEligibleHelper(el, ctx);
|
|
103
111
|
} catch {
|
|
104
112
|
return { eligible: true, reasons: [] };
|
|
105
113
|
}
|
|
106
114
|
})();
|
|
115
|
+
if (elig === false) continue;
|
|
107
116
|
if (elig && elig.eligible === false) continue;
|
|
108
117
|
}
|
|
109
118
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'combobox-name-present';
|
|
@@ -122,9 +124,16 @@ function runInPage(ctx) {
|
|
|
122
124
|
return '';
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
128
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
129
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
125
130
|
function isEligibleAcc(helpers, el, ctx) {
|
|
126
131
|
const fn =
|
|
127
|
-
helpers && typeof helpers.
|
|
132
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
133
|
+
? helpers.isIncludedInAccessibilityTree
|
|
134
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
135
|
+
? helpers.isAccTreeEligible
|
|
136
|
+
: null;
|
|
128
137
|
if (!fn) return true;
|
|
129
138
|
try {
|
|
130
139
|
const r = fn(el, ctx);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -20,25 +22,17 @@
|
|
|
20
22
|
* display orientation unless that orientation is essential.
|
|
21
23
|
* @implementation-notes
|
|
22
24
|
* - The rotation DEGREE is what makes this the exploit signature, not
|
|
23
|
-
* merely the presence of a `rotate()` function
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* version of this rule flagged the bare presence of "rotate(" in an
|
|
29
|
-
* orientation-media-query rule regardless of angle — a real, confirmed
|
|
30
|
-
* false positive found via the live-DOM cross-engine corpus 2026-07-23
|
|
31
|
-
* on a real page (Times of India) whose `@media (orientation:portrait)`
|
|
32
|
-
* block only rotated a small decorative arrow icon 45 degrees, nothing
|
|
33
|
-
* resembling a page-orientation lock.
|
|
25
|
+
* merely the presence of a `rotate()` function: a small decorative icon
|
|
26
|
+
* rotated 45 degrees inside an orientation media query is not a page
|
|
27
|
+
* lock. Compute the actual rotation angle and only flag ~90/~270 degrees,
|
|
28
|
+
* excluding ~0/~180 (a no-op or a flip, neither of which changes
|
|
29
|
+
* portrait<->landscape).
|
|
34
30
|
* - Only `rotate`/`rotateZ` (transform functions) and the standalone CSS
|
|
35
|
-
* `rotate` property are parsed for degrees
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* value" work already deferred elsewhere in this engine, e.g.
|
|
41
|
-
* `table-th-has-data-cells`'s narrower positional-header algorithm).
|
|
31
|
+
* `rotate` property are parsed for degrees, from the two sources
|
|
32
|
+
* `style.transform`/`style.rotate`; `matrix()`/`matrix3d()`/`rotate3d()`
|
|
33
|
+
* are not decomposed into an equivalent angle (deliberately deferred as
|
|
34
|
+
* the same class of higher-complexity/lower-value work deferred elsewhere,
|
|
35
|
+
* e.g. `table-th-has-data-cells`'s narrower positional-header algorithm).
|
|
42
36
|
* - Cross-origin stylesheets throw on `.cssRules` access (browser
|
|
43
37
|
* security model) and are skipped — same class of limitation as any
|
|
44
38
|
* check that can only see same-origin/inspectable content (compare
|
|
@@ -98,9 +92,9 @@ function runInPage(ctx) {
|
|
|
98
92
|
}
|
|
99
93
|
|
|
100
94
|
// Converts a rotate()/rotateZ() angle argument (with unit) to degrees.
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
95
|
+
// Only deg/grad/rad/turn are recognized; a unitless or unrecognized value
|
|
96
|
+
// contributes 0 (ignored, not treated as a lock -- an unparseable angle
|
|
97
|
+
// isn't a confirmed exploit).
|
|
104
98
|
function angleToDegrees(raw) {
|
|
105
99
|
const m = /(-?[\d.]+)\s*(deg|grad|rad|turn)/i.exec(raw);
|
|
106
100
|
if (!m) return 0;
|
|
@@ -127,17 +121,17 @@ function runInPage(ctx) {
|
|
|
127
121
|
return total;
|
|
128
122
|
}
|
|
129
123
|
|
|
130
|
-
// Whole-page-orientation-lock detection
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
124
|
+
// Whole-page-orientation-lock detection (see @implementation-notes): a
|
|
125
|
+
// rotation near 0 or 180 degrees (mod 180) is a no-op or a flip, neither
|
|
126
|
+
// of which changes portrait<->landscape, so it's NOT a lock; only a
|
|
127
|
+
// rotation near 90 or 270 degrees (mod 90, once the 0/180 case is
|
|
128
|
+
// excluded) is.
|
|
135
129
|
function isLockingRotation(styleDecl) {
|
|
136
130
|
if (!styleDecl) return false;
|
|
137
131
|
const transformVal =
|
|
138
132
|
trim(styleDecl.transform) ||
|
|
139
133
|
trim(styleDecl.getPropertyValue ? styleDecl.getPropertyValue('-webkit-transform') : '');
|
|
140
|
-
// The standalone CSS `rotate` property (distinct from `transform: rotate()`)
|
|
134
|
+
// The standalone CSS `rotate` property (distinct from `transform: rotate()`).
|
|
141
135
|
const rotatePropVal = trim(
|
|
142
136
|
styleDecl.getPropertyValue ? styleDecl.getPropertyValue('rotate') : ''
|
|
143
137
|
);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -18,12 +20,11 @@
|
|
|
18
20
|
* violation (see implementation-notes). Any other direct or wrapped child
|
|
19
21
|
* breaks the description-list semantics assistive technologies rely on.
|
|
20
22
|
* @implementation-notes
|
|
21
|
-
* - Only one level of <div> wrapping is flattened
|
|
22
|
-
* reference engine's definition-list algorithm — a <div> nested inside
|
|
23
|
+
* - Only one level of <div> wrapping is flattened — a <div> nested inside
|
|
23
24
|
* another wrapping <div> is not flattened further and its contents are
|
|
24
25
|
* reported invalid.
|
|
25
|
-
* -
|
|
26
|
-
*
|
|
26
|
+
* - The dt/dd pairing is only required "when not empty". A flattened set
|
|
27
|
+
* with NEITHER dt nor dd
|
|
27
28
|
* — whether from an empty wrapping <div>, only <script>/<template>/
|
|
28
29
|
* <style> content, or a genuinely childless <dl> — is not flagged; only
|
|
29
30
|
* an unbalanced dt/dd pairing is a real structural problem.
|
|
@@ -108,8 +109,7 @@ function runInPage(ctx) {
|
|
|
108
109
|
}
|
|
109
110
|
const dedupedInvalidTags = [...new Set(invalidTags)];
|
|
110
111
|
|
|
111
|
-
//
|
|
112
|
-
// the dt/dd pairing is only required "when not empty" — a <dl> with
|
|
112
|
+
// The dt/dd pairing is only required "when not empty" — a <dl> with
|
|
113
113
|
// NEITHER dt nor dd (whether genuinely childless after flattening, only
|
|
114
114
|
// passthrough script/template/style content, or an empty wrapping div)
|
|
115
115
|
// is vacuously fine, not a violation. Only an UNBALANCED pairing (dt
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -14,8 +16,8 @@
|
|
|
14
16
|
* @expectation
|
|
15
17
|
* Neither element is present. Since their movement can never be paused,
|
|
16
18
|
* stopped, or hidden by the user, presence is itself the violation — this
|
|
17
|
-
* rule has no partial-pass case
|
|
18
|
-
*
|
|
19
|
+
* rule has no partial-pass case (it reports only when the element is
|
|
20
|
+
* found).
|
|
19
21
|
* @implementation-notes
|
|
20
22
|
* - Not rule-gated on isAccTreeEligible: presence in markup is itself the
|
|
21
23
|
* violation, independent of visibility (moving/blinking content inside a
|