@surea11y/core 1.2.0 → 1.3.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 +37 -7
- package/LICENSE +373 -21
- package/README.md +67 -1
- package/bin/core.js +144 -19
- package/docs/API_STABILITY.md +1 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +54 -1
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/INTEGRATION.md +18 -0
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +42 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +55 -16
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +42 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +30 -8
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +109 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
- package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +17 -6
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -7
- package/src/checks/manual/empty-table-header-manual.js +17 -6
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -7
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
- package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +26 -20
- package/src/checks/manual/landmark-unique-manual.js +41 -15
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +16 -5
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +31 -22
- package/src/checks/manual/page-title-patterns-manual.js +78 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +241 -48
- package/src/checks/manual/scope-attr-valid-manual.js +10 -3
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +10 -3
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1127 -886
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4175 -3917
- package/src/core/dom-runner.js +720 -604
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +58 -41
- package/src/core.js +36552 -29111
- package/src/i18n/en.js +1194 -889
- package/src/i18n/fr.js +1136 -795
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +63 -43
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -21,183 +21,206 @@
|
|
|
21
21
|
const id = 'css-hidden-focus';
|
|
22
22
|
|
|
23
23
|
const meta = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
24
|
+
title: 'Focusable elements must not be visually hidden',
|
|
25
|
+
description:
|
|
26
|
+
'Checks that keyboard-focusable elements are not visually hidden by CSS techniques that can leave them in the tab order.',
|
|
27
|
+
i18n: {
|
|
28
|
+
titleKey: 'cssHidden_focus_title',
|
|
29
|
+
descriptionKey: 'cssHidden_focus_description'
|
|
30
|
+
},
|
|
31
|
+
helpUrl: null,
|
|
32
|
+
tags: ['wcag2aa', 'wcag247', 'navigation', 'focus', 'css', 'atomic', 'manual'],
|
|
33
|
+
wcagSc: ['2.4.7'],
|
|
34
|
+
normativeMappings: [
|
|
35
|
+
{
|
|
36
|
+
standard: 'WCAG',
|
|
37
|
+
version: '2.2',
|
|
38
|
+
requirement: '2.4.7',
|
|
39
|
+
title: 'Focus Visible',
|
|
40
|
+
conformanceLevel: 'AA'
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
defaultSeverity: 'serious',
|
|
44
|
+
category: 'operable',
|
|
45
|
+
type: 'manual',
|
|
46
|
+
defaultConfidence: 'low',
|
|
47
|
+
coverage: {
|
|
48
|
+
facetsBySc: {
|
|
49
|
+
'2.4.7': ['css-hidden-focusable']
|
|
45
50
|
}
|
|
51
|
+
}
|
|
46
52
|
};
|
|
47
53
|
|
|
48
54
|
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 [];
|
|
55
|
+
const { document, root, helpers, rule } = ctx;
|
|
56
|
+
const safeRoot = root || document;
|
|
57
|
+
|
|
58
|
+
const queryAllSmart =
|
|
59
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
60
|
+
const getFocusableInfo =
|
|
61
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
62
|
+
const isDomVisibleEligible =
|
|
63
|
+
helpers && typeof helpers.isDomVisibleEligible === 'function'
|
|
64
|
+
? helpers.isDomVisibleEligible
|
|
65
|
+
: null;
|
|
66
|
+
const reportOccurrence =
|
|
67
|
+
helpers && typeof helpers.reportOccurrence === 'function' ? helpers.reportOccurrence : null;
|
|
68
|
+
|
|
69
|
+
const trim = (v) => (v == null ? '' : String(v)).trim();
|
|
70
|
+
const lower = (v) => trim(v).toLowerCase();
|
|
71
|
+
|
|
72
|
+
function qAll(sel) {
|
|
73
|
+
try {
|
|
74
|
+
if (queryAllSmart) {
|
|
75
|
+
const r = queryAllSmart(sel);
|
|
76
|
+
if (Array.isArray(r)) return r;
|
|
77
|
+
return Array.from(r || []);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// fall through
|
|
76
81
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
} catch {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
82
|
+
try {
|
|
83
|
+
if (safeRoot && typeof safeRoot.querySelectorAll === 'function')
|
|
84
|
+
return Array.from(safeRoot.querySelectorAll(sel));
|
|
85
|
+
} catch {
|
|
86
|
+
// fall through
|
|
85
87
|
}
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
} catch {
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// clip / clip-path
|
|
102
|
-
try {
|
|
103
|
-
const clip = cs && cs.clip != null ? String(cs.clip).trim() : '';
|
|
104
|
-
const clipPath = cs && cs.clipPath != null ? String(cs.clipPath).trim() : '';
|
|
105
|
-
const clipLow = (clip || '').toLowerCase();
|
|
106
|
-
const clipPathLow = (clipPath || '').toLowerCase();
|
|
107
|
-
|
|
108
|
-
if (clipLow && clipLow !== 'auto') {
|
|
109
|
-
if (clipLow.indexOf('rect(') !== -1 && clipLow.replace(/\s+/g, '').indexOf('rect(0') !== -1) out.push('clipped');
|
|
110
|
-
}
|
|
111
|
-
if (clipPathLow && clipPathLow !== 'none') {
|
|
112
|
-
if (clipPathLow.indexOf('inset(') !== -1 && (clipPathLow.indexOf('100%') !== -1 || clipPathLow.indexOf('50%') !== -1)) {
|
|
113
|
-
out.push('clipped');
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} catch {
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// zero-size + overflow hidden/clip
|
|
120
|
-
try {
|
|
121
|
-
const wv = cs && cs.width != null ? String(cs.width).trim() : '';
|
|
122
|
-
const hv = cs && cs.height != null ? String(cs.height).trim() : '';
|
|
123
|
-
const ov = cs && cs.overflow != null ? String(cs.overflow).trim().toLowerCase() : '';
|
|
124
|
-
const isZeroW = wv === '0px' || wv === '0';
|
|
125
|
-
const isZeroH = hv === '0px' || hv === '0';
|
|
126
|
-
const hidesOverflow = ov === 'hidden' || ov === 'clip';
|
|
127
|
-
if ((isZeroW || isZeroH) && hidesOverflow) out.push('zeroSizeOverflowHidden');
|
|
128
|
-
} catch {
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// off-screen heuristic (absolute/fixed + left/top <= -5000 OR text-indent <= -5000)
|
|
132
|
-
try {
|
|
133
|
-
const pos = cs && cs.position != null ? String(cs.position).trim().toLowerCase() : '';
|
|
134
|
-
const left = cs && cs.left != null ? String(cs.left).trim().toLowerCase() : '';
|
|
135
|
-
const top = cs && cs.top != null ? String(cs.top).trim().toLowerCase() : '';
|
|
136
|
-
const ti = cs && cs.textIndent != null ? String(cs.textIndent).trim().toLowerCase() : '';
|
|
137
|
-
|
|
138
|
-
const parsePx = (s) => {
|
|
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
|
-
}
|
|
156
|
-
|
|
157
|
-
// Dedup
|
|
158
|
-
const seen = new Set();
|
|
159
|
-
const uniq = [];
|
|
160
|
-
for (const k of out) {
|
|
161
|
-
const kk = String(k);
|
|
162
|
-
if (!seen.has(kk)) {
|
|
163
|
-
seen.add(kk);
|
|
164
|
-
uniq.push(kk);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return uniq;
|
|
91
|
+
function getComputedStyleSafe(el) {
|
|
92
|
+
try {
|
|
93
|
+
const w =
|
|
94
|
+
document && document.defaultView
|
|
95
|
+
? document.defaultView
|
|
96
|
+
: typeof window !== 'undefined'
|
|
97
|
+
? window
|
|
98
|
+
: null;
|
|
99
|
+
return w && w.getComputedStyle ? w.getComputedStyle(el) : null;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
168
102
|
}
|
|
103
|
+
}
|
|
169
104
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
105
|
+
// Returns deterministic "visually hidden but can remain focusable" hints.
|
|
106
|
+
function getVisibilityHints(el) {
|
|
107
|
+
const out = [];
|
|
108
|
+
if (!el) return out;
|
|
109
|
+
const cs = getComputedStyleSafe(el);
|
|
110
|
+
|
|
111
|
+
// opacity:0
|
|
112
|
+
try {
|
|
113
|
+
const rawOp = cs && cs.opacity != null ? String(cs.opacity).trim() : '';
|
|
114
|
+
const op = rawOp ? Number.parseFloat(rawOp) : 1;
|
|
115
|
+
if (Number.isFinite(op) && op <= 0.0001) out.push('opacityZero');
|
|
116
|
+
} catch {}
|
|
117
|
+
|
|
118
|
+
// clip / clip-path
|
|
119
|
+
try {
|
|
120
|
+
const clip = cs && cs.clip != null ? String(cs.clip).trim() : '';
|
|
121
|
+
const clipPath = cs && cs.clipPath != null ? String(cs.clipPath).trim() : '';
|
|
122
|
+
const clipLow = (clip || '').toLowerCase();
|
|
123
|
+
const clipPathLow = (clipPath || '').toLowerCase();
|
|
124
|
+
|
|
125
|
+
if (clipLow && clipLow !== 'auto') {
|
|
126
|
+
if (clipLow.indexOf('rect(') !== -1 && clipLow.replace(/\s+/g, '').indexOf('rect(0') !== -1)
|
|
127
|
+
out.push('clipped');
|
|
128
|
+
}
|
|
129
|
+
if (clipPathLow && clipPathLow !== 'none') {
|
|
130
|
+
if (
|
|
131
|
+
clipPathLow.indexOf('inset(') !== -1 &&
|
|
132
|
+
(clipPathLow.indexOf('100%') !== -1 || clipPathLow.indexOf('50%') !== -1)
|
|
133
|
+
) {
|
|
134
|
+
out.push('clipped');
|
|
176
135
|
}
|
|
136
|
+
}
|
|
137
|
+
} catch {}
|
|
138
|
+
|
|
139
|
+
// zero-size + overflow hidden/clip
|
|
140
|
+
try {
|
|
141
|
+
const wv = cs && cs.width != null ? String(cs.width).trim() : '';
|
|
142
|
+
const hv = cs && cs.height != null ? String(cs.height).trim() : '';
|
|
143
|
+
const ov = cs && cs.overflow != null ? String(cs.overflow).trim().toLowerCase() : '';
|
|
144
|
+
const isZeroW = wv === '0px' || wv === '0';
|
|
145
|
+
const isZeroH = hv === '0px' || hv === '0';
|
|
146
|
+
const hidesOverflow = ov === 'hidden' || ov === 'clip';
|
|
147
|
+
if ((isZeroW || isZeroH) && hidesOverflow) out.push('zeroSizeOverflowHidden');
|
|
148
|
+
} catch {}
|
|
149
|
+
|
|
150
|
+
// off-screen heuristic (absolute/fixed + left/top <= -5000 OR text-indent <= -5000)
|
|
151
|
+
try {
|
|
152
|
+
const pos = cs && cs.position != null ? String(cs.position).trim().toLowerCase() : '';
|
|
153
|
+
const left = cs && cs.left != null ? String(cs.left).trim().toLowerCase() : '';
|
|
154
|
+
const top = cs && cs.top != null ? String(cs.top).trim().toLowerCase() : '';
|
|
155
|
+
const ti = cs && cs.textIndent != null ? String(cs.textIndent).trim().toLowerCase() : '';
|
|
156
|
+
|
|
157
|
+
const parsePx = (s) => {
|
|
158
|
+
if (!s || s === 'auto') return null;
|
|
159
|
+
const m = String(s).match(/-?\d+(\.\d+)?/);
|
|
160
|
+
if (!m) return null;
|
|
161
|
+
const n = Number.parseFloat(m[0]);
|
|
162
|
+
return Number.isFinite(n) ? n : null;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const l = parsePx(left);
|
|
166
|
+
const t = parsePx(top);
|
|
167
|
+
const ind = parsePx(ti);
|
|
168
|
+
|
|
169
|
+
if (pos === 'absolute' || pos === 'fixed') {
|
|
170
|
+
if ((l != null && l <= -5000) || (t != null && t <= -5000)) out.push('offscreen');
|
|
171
|
+
}
|
|
172
|
+
if (ind != null && ind <= -5000) out.push('offscreen');
|
|
173
|
+
} catch {}
|
|
174
|
+
|
|
175
|
+
// Dedup
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
const uniq = [];
|
|
178
|
+
for (const k of out) {
|
|
179
|
+
const kk = String(k);
|
|
180
|
+
if (!seen.has(kk)) {
|
|
181
|
+
seen.add(kk);
|
|
182
|
+
uniq.push(kk);
|
|
183
|
+
}
|
|
177
184
|
}
|
|
185
|
+
return uniq;
|
|
186
|
+
}
|
|
178
187
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
188
|
+
function getFocusableInfoSafe(el) {
|
|
189
|
+
if (!getFocusableInfo) return null;
|
|
190
|
+
try {
|
|
191
|
+
return getFocusableInfo(el, ctx);
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
182
194
|
}
|
|
195
|
+
}
|
|
183
196
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
function isTabbable(el, info) {
|
|
198
|
+
const f = info || getFocusableInfoSafe(el);
|
|
199
|
+
return !!(f && f.focusable && f.tabbable);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Exclude elements that are not rendered / not in the visual rendering tree.
|
|
203
|
+
function isRendered(el) {
|
|
204
|
+
if (!isDomVisibleEligible) return true;
|
|
205
|
+
try {
|
|
206
|
+
const vis = isDomVisibleEligible(el, ctx, {
|
|
207
|
+
visibilityMode: 'styleOnly',
|
|
208
|
+
disableGeometry: true
|
|
209
|
+
});
|
|
210
|
+
if (vis && vis.eligible === false) {
|
|
211
|
+
const rs = Array.isArray(vis.reasons) ? vis.reasons : [];
|
|
212
|
+
// We *include* opacityZero in this rule. Everything else is treated as not rendered for this purpose.
|
|
213
|
+
const nonOpacity = rs.filter((r) => String(r) !== 'opacityZero');
|
|
214
|
+
if (nonOpacity.length) return false;
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// If in doubt, keep deterministic behavior and consider it rendered.
|
|
199
218
|
}
|
|
200
|
-
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
const candidates = qAll(
|
|
222
|
+
'a[href],area[href],button,input,select,textarea,summary,[tabindex],[contenteditable]'
|
|
223
|
+
);
|
|
201
224
|
if (!candidates.length) {
|
|
202
225
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
203
226
|
}
|
|
@@ -216,7 +239,13 @@ function runInPage(ctx) {
|
|
|
216
239
|
const hints = getVisibilityHints(el);
|
|
217
240
|
if (!hints.length) continue; // <-- applicability gate
|
|
218
241
|
|
|
219
|
-
const tagName = (() => {
|
|
242
|
+
const tagName = (() => {
|
|
243
|
+
try {
|
|
244
|
+
return lower(el.tagName || '');
|
|
245
|
+
} catch {
|
|
246
|
+
return '';
|
|
247
|
+
}
|
|
248
|
+
})();
|
|
220
249
|
|
|
221
250
|
const hintOrder = ['opacityZero', 'offscreen', 'clipped', 'zeroSizeOverflowHidden'];
|
|
222
251
|
const hintsArr = [];
|
|
@@ -239,7 +268,9 @@ function runInPage(ctx) {
|
|
|
239
268
|
};
|
|
240
269
|
|
|
241
270
|
occurrences.push(
|
|
242
|
-
|
|
271
|
+
reportOccurrence
|
|
272
|
+
? reportOccurrence(el, baseOccurrence)
|
|
273
|
+
: { __node: el, selector: '', html: '', ...baseOccurrence }
|
|
243
274
|
);
|
|
244
275
|
}
|
|
245
276
|
|
|
@@ -256,4 +287,4 @@ function runInPage(ctx) {
|
|
|
256
287
|
};
|
|
257
288
|
}
|
|
258
289
|
|
|
259
|
-
module.exports = {id, meta, runInPage};
|
|
290
|
+
module.exports = { id, meta, runInPage };
|