@surea11y/core 1.2.0 → 1.4.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 +81 -7
- package/LICENSE +373 -21
- package/README.md +175 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +27 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +19 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +63 -18
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +63 -31
- package/src/checks/automatic/aria-allowed-attr.js +204 -80
- package/src/checks/automatic/aria-allowed-role.js +23 -7
- package/src/checks/automatic/aria-braille-equivalent.js +34 -10
- package/src/checks/automatic/aria-conditional-attr.js +32 -14
- package/src/checks/automatic/aria-deprecated-role.js +26 -11
- package/src/checks/automatic/aria-hidden-body.js +48 -23
- package/src/checks/automatic/aria-hidden-focus.js +420 -66
- package/src/checks/automatic/aria-prohibited-attr.js +327 -60
- package/src/checks/automatic/aria-prohibited-children.js +111 -103
- package/src/checks/automatic/aria-required-attr.js +29 -15
- package/src/checks/automatic/aria-required-children.js +44 -24
- package/src/checks/automatic/aria-required-parent.js +64 -35
- package/src/checks/automatic/aria-role-name-present.js +49 -21
- package/src/checks/automatic/aria-roles-valid.js +24 -12
- package/src/checks/automatic/aria-valid-attr-value.js +46 -22
- package/src/checks/automatic/aria-valid-attr.js +19 -5
- package/src/checks/automatic/autocomplete-valid.js +76 -16
- package/src/checks/automatic/avoid-inline-spacing.js +23 -8
- package/src/checks/automatic/binary-control-name-present.js +62 -50
- package/src/checks/automatic/button-name-present.js +54 -24
- package/src/checks/automatic/bypass-blocks-present.js +51 -32
- package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
- package/src/checks/automatic/combobox-name-present.js +40 -45
- package/src/checks/automatic/contrast-computable.js +363 -341
- package/src/checks/automatic/contrast-enhanced.js +489 -466
- package/src/checks/automatic/contrast-minimum.js +488 -465
- package/src/checks/automatic/css-orientation-lock.js +51 -35
- package/src/checks/automatic/definition-list-children-valid.js +46 -25
- package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
- package/src/checks/automatic/dialog-name-present.js +47 -85
- package/src/checks/automatic/dlitem-parent-valid.js +25 -8
- package/src/checks/automatic/duplicate-id-aria.js +28 -9
- package/src/checks/automatic/embed-text-alternative-present.js +88 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
- package/src/checks/automatic/form-control-single-label.js +50 -14
- package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
- package/src/checks/automatic/iframe-focusable-content.js +265 -22
- package/src/checks/automatic/iframe-name-present.js +33 -9
- package/src/checks/automatic/iframe-title-unique.js +32 -9
- package/src/checks/automatic/img-alt-present.js +54 -52
- package/src/checks/automatic/input-image-alt-present.js +141 -112
- package/src/checks/automatic/label-in-name.js +65 -41
- package/src/checks/automatic/language-page-present.js +111 -109
- package/src/checks/automatic/link-in-text-block.js +61 -19
- package/src/checks/automatic/link-name-present.js +47 -14
- package/src/checks/automatic/list-children-valid.js +40 -33
- package/src/checks/automatic/listbox-name-present.js +41 -19
- package/src/checks/automatic/listitem-parent-valid.js +48 -13
- package/src/checks/automatic/menuitem-name-present.js +41 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
- package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
- package/src/checks/automatic/meter-name-present.js +40 -36
- package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
- package/src/checks/automatic/object-text-alternative-present.js +93 -39
- package/src/checks/automatic/option-name-present.js +40 -21
- package/src/checks/automatic/page-title-present.js +19 -6
- package/src/checks/automatic/progressbar-name-present.js +49 -44
- package/src/checks/automatic/role-img-alt-present.js +211 -159
- package/src/checks/automatic/searchbox-name-present.js +41 -19
- package/src/checks/automatic/server-side-image-map-absent.js +27 -11
- package/src/checks/automatic/slider-name-present.js +42 -47
- package/src/checks/automatic/spinbutton-name-present.js +41 -19
- package/src/checks/automatic/summary-name-present.js +39 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
- package/src/checks/automatic/svg-text-alternative-present.js +262 -230
- package/src/checks/automatic/tab-name-present.js +39 -60
- package/src/checks/automatic/table-headers-attr-valid.js +27 -10
- package/src/checks/automatic/table-th-has-data-cells.js +24 -8
- package/src/checks/automatic/target-size-minimum.js +123 -48
- package/src/checks/automatic/td-has-header.js +53 -12
- package/src/checks/automatic/textbox-name-present.js +41 -19
- package/src/checks/automatic/tooltip-name-present.js +39 -18
- package/src/checks/automatic/treeitem-name-present.js +40 -21
- package/src/checks/automatic/valid-lang.js +22 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
- package/src/checks/manual/accesskeys-manual.js +17 -6
- package/src/checks/manual/area-alt-decorative-manual.js +194 -193
- package/src/checks/manual/area-alt-quality-manual.js +184 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
- package/src/checks/manual/aria-text-manual.js +20 -11
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
- package/src/checks/manual/css-hidden-focus.js +375 -169
- package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
- package/src/checks/manual/empty-heading-manual.js +41 -24
- package/src/checks/manual/empty-table-header-manual.js +69 -31
- package/src/checks/manual/focus-order-semantics-manual.js +60 -13
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
- package/src/checks/manual/heading-order-manual.js +50 -8
- package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
- package/src/checks/manual/image-redundant-alt-manual.js +38 -8
- package/src/checks/manual/img-alt-decorative-manual.js +133 -96
- package/src/checks/manual/img-alt-quality-manual.js +178 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
- package/src/checks/manual/label-title-only-manual.js +44 -28
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
- package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
- package/src/checks/manual/landmark-one-main-manual.js +38 -43
- package/src/checks/manual/landmark-unique-manual.js +78 -67
- package/src/checks/manual/link-name-quality-manual.js +45 -12
- package/src/checks/manual/media-transcript-present-manual.js +37 -22
- package/src/checks/manual/meta-viewport-large-manual.js +19 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
- package/src/checks/manual/p-as-heading-manual.js +24 -7
- package/src/checks/manual/page-has-heading-one-manual.js +42 -32
- package/src/checks/manual/page-title-patterns-manual.js +80 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
- package/src/checks/manual/region-manual.js +244 -60
- package/src/checks/manual/scope-attr-valid-manual.js +13 -4
- package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
- package/src/checks/manual/skip-link-manual.js +42 -18
- package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
- package/src/checks/manual/tabindex-manual.js +13 -4
- package/src/checks/manual/table-duplicate-name-manual.js +22 -11
- package/src/checks/manual/table-fake-caption-manual.js +48 -10
- package/src/checks/manual/video-caption-manual.js +17 -4
- package/src/checks/manual-review.js +58 -12
- package/src/core.js +41705 -29650
- package/src/index.js +2 -0
- package/src/report.js +109 -47
- package/src/sarif.js +190 -0
- package/surea11y.browser.js +37774 -0
- package/bin/core.js +0 -348
- package/docs/CLI.md +0 -75
- package/src/catalogs/composites.wcag.js +0 -490
- 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 -970
- package/src/core/contrast-helpers.js +0 -1147
- package/src/core/dom-helpers.js +0 -4235
- package/src/core/dom-runner.js +0 -671
- package/src/core/frame-messaging.js +0 -210
- package/src/core/frame-scan.js +0 -178
- package/src/core/rollup-composites.js +0 -135
- package/src/core/rule-meta.js +0 -159
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -923
- package/src/i18n/fr.js +0 -844
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -55
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -21,188 +23,370 @@
|
|
|
21
23
|
const id = 'css-hidden-focus';
|
|
22
24
|
|
|
23
25
|
const meta = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
coverage: {
|
|
42
|
-
facetsBySc: {
|
|
43
|
-
'2.4.7': ['css-hidden-focusable']
|
|
44
|
-
}
|
|
26
|
+
title: 'Focusable elements must not be visually hidden',
|
|
27
|
+
description:
|
|
28
|
+
'Checks that keyboard-focusable elements are not visually hidden by CSS techniques that can leave them in the tab order.',
|
|
29
|
+
i18n: {
|
|
30
|
+
titleKey: 'cssHidden_focus_title',
|
|
31
|
+
descriptionKey: 'cssHidden_focus_description'
|
|
32
|
+
},
|
|
33
|
+
helpUrl: null,
|
|
34
|
+
tags: ['wcag2aa', 'wcag247', 'navigation', 'focus', 'css', 'atomic', 'manual'],
|
|
35
|
+
wcagSc: ['2.4.7'],
|
|
36
|
+
normativeMappings: [
|
|
37
|
+
{
|
|
38
|
+
standard: 'WCAG',
|
|
39
|
+
version: '2.2',
|
|
40
|
+
requirement: '2.4.7',
|
|
41
|
+
title: 'Focus Visible',
|
|
42
|
+
conformanceLevel: 'AA'
|
|
45
43
|
}
|
|
44
|
+
],
|
|
45
|
+
defaultSeverity: 'serious',
|
|
46
|
+
category: 'operable',
|
|
47
|
+
type: 'manual',
|
|
48
|
+
defaultConfidence: 'low',
|
|
49
|
+
coverage: {
|
|
50
|
+
facetsBySc: {
|
|
51
|
+
'2.4.7': ['css-hidden-focusable']
|
|
52
|
+
}
|
|
53
|
+
}
|
|
46
54
|
};
|
|
47
55
|
|
|
48
56
|
function runInPage(ctx) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return [];
|
|
57
|
+
const { document, root, helpers, rule } = ctx;
|
|
58
|
+
const safeRoot = root || document;
|
|
59
|
+
|
|
60
|
+
const queryAllSmart =
|
|
61
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
62
|
+
const getFocusableInfo =
|
|
63
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
64
|
+
const isDomVisibleEligible =
|
|
65
|
+
helpers && typeof helpers.isDomVisibleEligible === 'function'
|
|
66
|
+
? helpers.isDomVisibleEligible
|
|
67
|
+
: null;
|
|
68
|
+
const reportOccurrence =
|
|
69
|
+
helpers && typeof helpers.reportOccurrence === 'function' ? helpers.reportOccurrence : null;
|
|
70
|
+
|
|
71
|
+
const trim = (v) => (v == null ? '' : String(v)).trim();
|
|
72
|
+
const lower = (v) => trim(v).toLowerCase();
|
|
73
|
+
|
|
74
|
+
function qAll(sel) {
|
|
75
|
+
try {
|
|
76
|
+
if (queryAllSmart) {
|
|
77
|
+
const r = queryAllSmart(sel);
|
|
78
|
+
if (Array.isArray(r)) return r;
|
|
79
|
+
return Array.from(r || []);
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// fall through
|
|
76
83
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
} catch {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
84
|
+
try {
|
|
85
|
+
if (safeRoot && typeof safeRoot.querySelectorAll === 'function')
|
|
86
|
+
return Array.from(safeRoot.querySelectorAll(sel));
|
|
87
|
+
} catch {
|
|
88
|
+
// fall through
|
|
85
89
|
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
function getComputedStyleSafe(el) {
|
|
94
|
+
try {
|
|
95
|
+
const w =
|
|
96
|
+
document && document.defaultView
|
|
97
|
+
? document.defaultView
|
|
98
|
+
: typeof window !== 'undefined'
|
|
99
|
+
? window
|
|
100
|
+
: null;
|
|
101
|
+
return w && w.getComputedStyle ? w.getComputedStyle(el) : null;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
100
106
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
// Returns deterministic "visually hidden but can remain focusable" hints.
|
|
108
|
+
function getVisibilityHints(el) {
|
|
109
|
+
const out = [];
|
|
110
|
+
if (!el) return out;
|
|
111
|
+
const cs = getComputedStyleSafe(el);
|
|
112
|
+
|
|
113
|
+
// opacity:0
|
|
114
|
+
try {
|
|
115
|
+
const rawOp = cs && cs.opacity != null ? String(cs.opacity).trim() : '';
|
|
116
|
+
const op = rawOp ? Number.parseFloat(rawOp) : 1;
|
|
117
|
+
if (Number.isFinite(op) && op <= 0.0001) out.push('opacityZero');
|
|
118
|
+
} catch {}
|
|
119
|
+
|
|
120
|
+
// clip / clip-path
|
|
121
|
+
try {
|
|
122
|
+
const clip = cs && cs.clip != null ? String(cs.clip).trim() : '';
|
|
123
|
+
const clipPath = cs && cs.clipPath != null ? String(cs.clipPath).trim() : '';
|
|
124
|
+
const clipLow = (clip || '').toLowerCase();
|
|
125
|
+
const clipPathLow = (clipPath || '').toLowerCase();
|
|
126
|
+
|
|
127
|
+
if (clipLow && clipLow !== 'auto') {
|
|
128
|
+
if (clipLow.indexOf('rect(') !== -1 && clipLow.replace(/\s+/g, '').indexOf('rect(0') !== -1)
|
|
129
|
+
out.push('clipped');
|
|
130
|
+
}
|
|
131
|
+
if (clipPathLow && clipPathLow !== 'none') {
|
|
132
|
+
if (
|
|
133
|
+
clipPathLow.indexOf('inset(') !== -1 &&
|
|
134
|
+
(clipPathLow.indexOf('100%') !== -1 || clipPathLow.indexOf('50%') !== -1)
|
|
135
|
+
) {
|
|
136
|
+
out.push('clipped');
|
|
117
137
|
}
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
|
|
141
|
+
// zero-size + overflow hidden/clip
|
|
142
|
+
try {
|
|
143
|
+
const wv = cs && cs.width != null ? String(cs.width).trim() : '';
|
|
144
|
+
const hv = cs && cs.height != null ? String(cs.height).trim() : '';
|
|
145
|
+
const ov = cs && cs.overflow != null ? String(cs.overflow).trim().toLowerCase() : '';
|
|
146
|
+
const isZeroW = wv === '0px' || wv === '0';
|
|
147
|
+
const isZeroH = hv === '0px' || hv === '0';
|
|
148
|
+
const hidesOverflow = ov === 'hidden' || ov === 'clip';
|
|
149
|
+
if ((isZeroW || isZeroH) && hidesOverflow) out.push('zeroSizeOverflowHidden');
|
|
150
|
+
} catch {}
|
|
151
|
+
|
|
152
|
+
// off-screen heuristic (absolute/fixed + left/top <= -5000 OR text-indent <= -5000)
|
|
153
|
+
try {
|
|
154
|
+
const pos = cs && cs.position != null ? String(cs.position).trim().toLowerCase() : '';
|
|
155
|
+
const left = cs && cs.left != null ? String(cs.left).trim().toLowerCase() : '';
|
|
156
|
+
const top = cs && cs.top != null ? String(cs.top).trim().toLowerCase() : '';
|
|
157
|
+
const ti = cs && cs.textIndent != null ? String(cs.textIndent).trim().toLowerCase() : '';
|
|
158
|
+
|
|
159
|
+
const parsePx = (s) => {
|
|
160
|
+
if (!s || s === 'auto') return null;
|
|
161
|
+
const m = String(s).match(/-?\d+(\.\d+)?/);
|
|
162
|
+
if (!m) return null;
|
|
163
|
+
const n = Number.parseFloat(m[0]);
|
|
164
|
+
return Number.isFinite(n) ? n : null;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const l = parsePx(left);
|
|
168
|
+
const t = parsePx(top);
|
|
169
|
+
const ind = parsePx(ti);
|
|
170
|
+
|
|
171
|
+
if (pos === 'absolute' || pos === 'fixed') {
|
|
172
|
+
if ((l != null && l <= -5000) || (t != null && t <= -5000)) out.push('offscreen');
|
|
173
|
+
}
|
|
174
|
+
if (ind != null && ind <= -5000) out.push('offscreen');
|
|
175
|
+
} catch {}
|
|
176
|
+
|
|
177
|
+
// Dedup
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
const uniq = [];
|
|
180
|
+
for (const k of out) {
|
|
181
|
+
const kk = String(k);
|
|
182
|
+
if (!seen.has(kk)) {
|
|
183
|
+
seen.add(kk);
|
|
184
|
+
uniq.push(kk);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return uniq;
|
|
188
|
+
}
|
|
118
189
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if ((isZeroW || isZeroH) && hidesOverflow) out.push('zeroSizeOverflowHidden');
|
|
128
|
-
} catch {
|
|
129
|
-
}
|
|
190
|
+
function getFocusableInfoSafe(el) {
|
|
191
|
+
if (!getFocusableInfo) return null;
|
|
192
|
+
try {
|
|
193
|
+
return getFocusableInfo(el, ctx);
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
130
198
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (!s || s === 'auto') return null;
|
|
140
|
-
const m = String(s).match(/-?\d+(\.\d+)?/);
|
|
141
|
-
if (!m) return null;
|
|
142
|
-
const n = Number.parseFloat(m[0]);
|
|
143
|
-
return Number.isFinite(n) ? n : null;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const l = parsePx(left);
|
|
147
|
-
const t = parsePx(top);
|
|
148
|
-
const ind = parsePx(ti);
|
|
149
|
-
|
|
150
|
-
if (pos === 'absolute' || pos === 'fixed') {
|
|
151
|
-
if ((l != null && l <= -5000) || (t != null && t <= -5000)) out.push('offscreen');
|
|
152
|
-
}
|
|
153
|
-
if (ind != null && ind <= -5000) out.push('offscreen');
|
|
154
|
-
} catch {
|
|
155
|
-
}
|
|
199
|
+
function getDeepActiveElement(docRef) {
|
|
200
|
+
let cur = docRef && docRef.activeElement ? docRef.activeElement : null;
|
|
201
|
+
let guard = 0;
|
|
202
|
+
while (cur && cur.shadowRoot && cur.shadowRoot.activeElement && guard++ < 20) {
|
|
203
|
+
cur = cur.shadowRoot.activeElement;
|
|
204
|
+
}
|
|
205
|
+
return cur;
|
|
206
|
+
}
|
|
156
207
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return
|
|
208
|
+
function focusElementSafe(el) {
|
|
209
|
+
if (!el || typeof el.focus !== 'function') return false;
|
|
210
|
+
try {
|
|
211
|
+
el.focus({ preventScroll: true });
|
|
212
|
+
return true;
|
|
213
|
+
} catch {
|
|
214
|
+
try {
|
|
215
|
+
el.focus();
|
|
216
|
+
return true;
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
168
220
|
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function runFocusObservationWindow(docRef, fn) {
|
|
224
|
+
const w =
|
|
225
|
+
docRef && docRef.defaultView
|
|
226
|
+
? docRef.defaultView
|
|
227
|
+
: typeof window !== 'undefined'
|
|
228
|
+
? window
|
|
229
|
+
: null;
|
|
230
|
+
if (!w || typeof fn !== 'function') return;
|
|
231
|
+
|
|
232
|
+
const originalSetTimeout = typeof w.setTimeout === 'function' ? w.setTimeout.bind(w) : null;
|
|
233
|
+
const originalRequestAnimationFrame =
|
|
234
|
+
typeof w.requestAnimationFrame === 'function' ? w.requestAnimationFrame.bind(w) : null;
|
|
235
|
+
const originalQueueMicrotask =
|
|
236
|
+
typeof w.queueMicrotask === 'function' ? w.queueMicrotask.bind(w) : null;
|
|
237
|
+
|
|
238
|
+
const queuedMicrotasks = [];
|
|
239
|
+
const queuedRaf = [];
|
|
240
|
+
const queuedTimers = [];
|
|
241
|
+
let fakeTimerId = 1;
|
|
242
|
+
|
|
243
|
+
const patchedSetTimeout = function (cb, delay) {
|
|
244
|
+
const d = Number.isFinite(Number(delay)) ? Number(delay) : 0;
|
|
245
|
+
if (typeof cb === 'function' && d <= 200) {
|
|
246
|
+
const args = [];
|
|
247
|
+
for (let i = 2; i < arguments.length; i++) args.push(arguments[i]);
|
|
248
|
+
queuedTimers.push({ delay: d, cb: () => cb.apply(w, args) });
|
|
249
|
+
return fakeTimerId++;
|
|
250
|
+
}
|
|
251
|
+
if (originalSetTimeout) return originalSetTimeout.apply(w, arguments);
|
|
252
|
+
return fakeTimerId++;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const patchedRaf = function (cb) {
|
|
256
|
+
if (typeof cb === 'function') {
|
|
257
|
+
queuedRaf.push(cb);
|
|
258
|
+
return fakeTimerId++;
|
|
259
|
+
}
|
|
260
|
+
if (originalRequestAnimationFrame) return originalRequestAnimationFrame.apply(w, arguments);
|
|
261
|
+
return fakeTimerId++;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const patchedQueueMicrotask = function (cb) {
|
|
265
|
+
if (typeof cb === 'function') queuedMicrotasks.push(cb);
|
|
266
|
+
};
|
|
169
267
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
268
|
+
try {
|
|
269
|
+
if (originalSetTimeout) w.setTimeout = patchedSetTimeout;
|
|
270
|
+
if (originalRequestAnimationFrame) w.requestAnimationFrame = patchedRaf;
|
|
271
|
+
if (originalQueueMicrotask) w.queueMicrotask = patchedQueueMicrotask;
|
|
272
|
+
fn();
|
|
273
|
+
|
|
274
|
+
let guard = 0;
|
|
275
|
+
while (
|
|
276
|
+
(queuedMicrotasks.length || queuedRaf.length || queuedTimers.length) &&
|
|
277
|
+
guard++ < 100
|
|
278
|
+
) {
|
|
279
|
+
while (queuedMicrotasks.length) {
|
|
280
|
+
const mt = queuedMicrotasks.shift();
|
|
281
|
+
try {
|
|
282
|
+
mt();
|
|
283
|
+
} catch {}
|
|
284
|
+
}
|
|
285
|
+
while (queuedRaf.length) {
|
|
286
|
+
const rf = queuedRaf.shift();
|
|
287
|
+
try {
|
|
288
|
+
rf(16);
|
|
289
|
+
} catch {}
|
|
176
290
|
}
|
|
291
|
+
if (queuedTimers.length) {
|
|
292
|
+
queuedTimers.sort((a, b) => a.delay - b.delay);
|
|
293
|
+
const tt = queuedTimers.shift();
|
|
294
|
+
try {
|
|
295
|
+
tt.cb();
|
|
296
|
+
} catch {}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} finally {
|
|
300
|
+
if (originalSetTimeout) w.setTimeout = originalSetTimeout;
|
|
301
|
+
if (originalRequestAnimationFrame) w.requestAnimationFrame = originalRequestAnimationFrame;
|
|
302
|
+
if (originalQueueMicrotask) w.queueMicrotask = originalQueueMicrotask;
|
|
177
303
|
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function probeImmediateFocusRedirect(candidate) {
|
|
307
|
+
if (!candidate || typeof candidate.addEventListener !== 'function') return null;
|
|
308
|
+
|
|
309
|
+
let focusedByEvent = false;
|
|
310
|
+
const onFocusCapture = () => {
|
|
311
|
+
focusedByEvent = true;
|
|
312
|
+
};
|
|
313
|
+
try {
|
|
314
|
+
candidate.addEventListener('focus', onFocusCapture, true);
|
|
315
|
+
} catch {}
|
|
316
|
+
|
|
317
|
+
const before = getDeepActiveElement(document);
|
|
318
|
+
let focused = false;
|
|
319
|
+
runFocusObservationWindow(document, () => {
|
|
320
|
+
focused = focusElementSafe(candidate);
|
|
321
|
+
});
|
|
178
322
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
323
|
+
try {
|
|
324
|
+
candidate.removeEventListener('focus', onFocusCapture, true);
|
|
325
|
+
} catch {}
|
|
326
|
+
if (!focused || !focusedByEvent) return null;
|
|
327
|
+
|
|
328
|
+
const after = getDeepActiveElement(document);
|
|
329
|
+
|
|
330
|
+
if (before && before !== after) {
|
|
331
|
+
focusElementSafe(before);
|
|
182
332
|
}
|
|
183
333
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
334
|
+
if (!after || after === candidate) return null;
|
|
335
|
+
const redirectedTag = (() => {
|
|
336
|
+
try {
|
|
337
|
+
return lower(after.tagName || '');
|
|
338
|
+
} catch {
|
|
339
|
+
return '';
|
|
340
|
+
}
|
|
341
|
+
})();
|
|
342
|
+
const redirectedId = (() => {
|
|
343
|
+
try {
|
|
344
|
+
return trim(after.getAttribute && after.getAttribute('id'));
|
|
345
|
+
} catch {
|
|
346
|
+
return '';
|
|
347
|
+
}
|
|
348
|
+
})();
|
|
349
|
+
return {
|
|
350
|
+
redirected: true,
|
|
351
|
+
redirectedToTag: redirectedTag || null,
|
|
352
|
+
redirectedToId: redirectedId || null
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function isTabbable(el, info) {
|
|
357
|
+
const f = info || getFocusableInfoSafe(el);
|
|
358
|
+
return !!(f && f.focusable && f.tabbable);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Exclude elements that are not rendered / not in the visual rendering tree.
|
|
362
|
+
function isRendered(el) {
|
|
363
|
+
if (!isDomVisibleEligible) return true;
|
|
364
|
+
try {
|
|
365
|
+
const vis = isDomVisibleEligible(el, ctx, {
|
|
366
|
+
visibilityMode: 'styleOnly',
|
|
367
|
+
disableGeometry: true
|
|
368
|
+
});
|
|
369
|
+
if (vis && vis.eligible === false) {
|
|
370
|
+
const rs = Array.isArray(vis.reasons) ? vis.reasons : [];
|
|
371
|
+
// We *include* opacityZero in this rule. Everything else is treated as not rendered for this purpose.
|
|
372
|
+
const nonOpacity = rs.filter((r) => String(r) !== 'opacityZero');
|
|
373
|
+
if (nonOpacity.length) return false;
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
// If in doubt, keep deterministic behavior and consider it rendered.
|
|
199
377
|
}
|
|
200
|
-
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
const candidates = qAll(
|
|
381
|
+
'a[href],area[href],button,input,select,textarea,summary,[tabindex],[contenteditable]'
|
|
382
|
+
);
|
|
201
383
|
if (!candidates.length) {
|
|
202
384
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
203
385
|
}
|
|
204
386
|
|
|
205
387
|
const occurrences = [];
|
|
388
|
+
const maxRuntimeProbeCount = 3;
|
|
389
|
+
let runtimeProbeCount = 0;
|
|
206
390
|
|
|
207
391
|
for (const el of candidates) {
|
|
208
392
|
if (!el || !el.getAttribute) continue;
|
|
@@ -216,30 +400,52 @@ function runInPage(ctx) {
|
|
|
216
400
|
const hints = getVisibilityHints(el);
|
|
217
401
|
if (!hints.length) continue; // <-- applicability gate
|
|
218
402
|
|
|
219
|
-
const tagName = (() => {
|
|
403
|
+
const tagName = (() => {
|
|
404
|
+
try {
|
|
405
|
+
return lower(el.tagName || '');
|
|
406
|
+
} catch {
|
|
407
|
+
return '';
|
|
408
|
+
}
|
|
409
|
+
})();
|
|
220
410
|
|
|
221
411
|
const hintOrder = ['opacityZero', 'offscreen', 'clipped', 'zeroSizeOverflowHidden'];
|
|
222
412
|
const hintsArr = [];
|
|
223
413
|
for (const k of hintOrder) if (hints.includes(k)) hintsArr.push(k);
|
|
224
414
|
|
|
415
|
+
const runtimeProbe =
|
|
416
|
+
runtimeProbeCount < maxRuntimeProbeCount ? probeImmediateFocusRedirect(el) : null;
|
|
417
|
+
if (runtimeProbeCount < maxRuntimeProbeCount) runtimeProbeCount += 1;
|
|
418
|
+
const downgradedToRedirectReview = !!(runtimeProbe && runtimeProbe.redirected);
|
|
419
|
+
|
|
225
420
|
const baseOccurrence = {
|
|
226
|
-
summary:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
421
|
+
summary: downgradedToRedirectReview
|
|
422
|
+
? `Focusable ${tagName} appears visually hidden but focus moved immediately to another element. Verify sentinel/focus-trap behavior.`
|
|
423
|
+
: `Focusable ${tagName} appears visually hidden (${hintsArr.join(',')}). Verify it becomes visible on keyboard focus.`,
|
|
424
|
+
hint: downgradedToRedirectReview
|
|
425
|
+
? 'Verify this is an intentional focus sentinel/focus-trap handoff and that keyboard users never remain on visually hidden focus targets.'
|
|
426
|
+
: 'Manually tab to the element and confirm a visible focus indicator and that the element is visible when focused. If it remains hidden while focused, fix CSS/JS so it becomes visible or is removed from the tab order until visible.',
|
|
427
|
+
i18n: downgradedToRedirectReview
|
|
428
|
+
? null
|
|
429
|
+
: {
|
|
430
|
+
summaryKey: 'cssHidden_focus_summary_cantTell',
|
|
431
|
+
hintKey: 'cssHidden_focus_hint_cantTell',
|
|
432
|
+
params: { element: tagName, visibilityHints: hintsArr.join(',') }
|
|
433
|
+
},
|
|
233
434
|
data: {
|
|
234
435
|
details: {
|
|
235
|
-
reasonCode:
|
|
236
|
-
|
|
436
|
+
reasonCode: downgradedToRedirectReview
|
|
437
|
+
? 'cssHiddenTabbable_runtimeRedirect_needsReview'
|
|
438
|
+
: 'cssHiddenTabbable_needsFocusStateVerification',
|
|
439
|
+
metrics: { visibilityHints: hintsArr.slice(0) },
|
|
440
|
+
runtimeProbe: runtimeProbe || null
|
|
237
441
|
}
|
|
238
442
|
}
|
|
239
443
|
};
|
|
240
444
|
|
|
241
445
|
occurrences.push(
|
|
242
|
-
|
|
446
|
+
reportOccurrence
|
|
447
|
+
? reportOccurrence(el, baseOccurrence)
|
|
448
|
+
: { __node: el, selector: '', html: '', ...baseOccurrence }
|
|
243
449
|
);
|
|
244
450
|
}
|
|
245
451
|
|
|
@@ -256,4 +462,4 @@ function runInPage(ctx) {
|
|
|
256
462
|
};
|
|
257
463
|
}
|
|
258
464
|
|
|
259
|
-
module.exports = {id, meta, runInPage};
|
|
465
|
+
module.exports = { id, meta, runInPage };
|