@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.
Files changed (157) hide show
  1. package/CHANGELOG.md +37 -7
  2. package/LICENSE +373 -21
  3. package/README.md +67 -1
  4. package/bin/core.js +144 -19
  5. package/docs/API_STABILITY.md +1 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/CLI.md +54 -1
  9. package/docs/ENGINE_OPTIONS.md +2 -0
  10. package/docs/INTEGRATION.md +18 -0
  11. package/docs/OUTPUT_SCHEMA.md +1 -1
  12. package/docs/RULE_CATALOG.md +1 -1
  13. package/docs/SARIF.md +59 -0
  14. package/package.json +15 -4
  15. package/src/baseline.js +0 -0
  16. package/src/catalogs/composites.wcag.js +414 -450
  17. package/src/checks/automatic/area-alt-present.js +59 -25
  18. package/src/checks/automatic/aria-allowed-attr.js +193 -33
  19. package/src/checks/automatic/aria-allowed-role.js +21 -7
  20. package/src/checks/automatic/aria-braille-equivalent.js +32 -10
  21. package/src/checks/automatic/aria-conditional-attr.js +24 -7
  22. package/src/checks/automatic/aria-deprecated-role.js +22 -8
  23. package/src/checks/automatic/aria-hidden-body.js +42 -19
  24. package/src/checks/automatic/aria-hidden-focus.js +408 -53
  25. package/src/checks/automatic/aria-prohibited-attr.js +296 -22
  26. package/src/checks/automatic/aria-prohibited-children.js +55 -16
  27. package/src/checks/automatic/aria-required-attr.js +23 -8
  28. package/src/checks/automatic/aria-required-children.js +37 -14
  29. package/src/checks/automatic/aria-required-parent.js +48 -14
  30. package/src/checks/automatic/aria-role-name-present.js +47 -21
  31. package/src/checks/automatic/aria-roles-valid.js +22 -12
  32. package/src/checks/automatic/aria-valid-attr-value.js +28 -7
  33. package/src/checks/automatic/aria-valid-attr.js +17 -5
  34. package/src/checks/automatic/autocomplete-valid.js +74 -16
  35. package/src/checks/automatic/avoid-inline-spacing.js +20 -6
  36. package/src/checks/automatic/binary-control-name-present.js +60 -50
  37. package/src/checks/automatic/button-name-present.js +48 -18
  38. package/src/checks/automatic/bypass-blocks-present.js +42 -25
  39. package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
  40. package/src/checks/automatic/combobox-name-present.js +38 -45
  41. package/src/checks/automatic/contrast-computable.js +361 -341
  42. package/src/checks/automatic/contrast-enhanced.js +487 -466
  43. package/src/checks/automatic/contrast-minimum.js +486 -465
  44. package/src/checks/automatic/css-orientation-lock.js +30 -8
  45. package/src/checks/automatic/definition-list-children-valid.js +40 -19
  46. package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
  47. package/src/checks/automatic/dialog-name-present.js +37 -75
  48. package/src/checks/automatic/dlitem-parent-valid.js +23 -8
  49. package/src/checks/automatic/duplicate-id-aria.js +24 -6
  50. package/src/checks/automatic/embed-text-alternative-present.js +86 -35
  51. package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
  52. package/src/checks/automatic/form-control-single-label.js +47 -10
  53. package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
  54. package/src/checks/automatic/iframe-focusable-content.js +26 -11
  55. package/src/checks/automatic/iframe-name-present.js +31 -9
  56. package/src/checks/automatic/iframe-title-unique.js +29 -8
  57. package/src/checks/automatic/img-alt-present.js +47 -43
  58. package/src/checks/automatic/input-image-alt-present.js +143 -112
  59. package/src/checks/automatic/label-in-name.js +50 -22
  60. package/src/checks/automatic/language-page-present.js +109 -109
  61. package/src/checks/automatic/link-in-text-block.js +59 -19
  62. package/src/checks/automatic/link-name-present.js +45 -14
  63. package/src/checks/automatic/list-children-valid.js +26 -9
  64. package/src/checks/automatic/listbox-name-present.js +39 -19
  65. package/src/checks/automatic/listitem-parent-valid.js +18 -6
  66. package/src/checks/automatic/menuitem-name-present.js +39 -61
  67. package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
  68. package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
  69. package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
  70. package/src/checks/automatic/meter-name-present.js +36 -33
  71. package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
  72. package/src/checks/automatic/object-text-alternative-present.js +91 -39
  73. package/src/checks/automatic/option-name-present.js +38 -21
  74. package/src/checks/automatic/page-title-present.js +17 -6
  75. package/src/checks/automatic/progressbar-name-present.js +41 -34
  76. package/src/checks/automatic/role-img-alt-present.js +209 -157
  77. package/src/checks/automatic/searchbox-name-present.js +39 -19
  78. package/src/checks/automatic/server-side-image-map-absent.js +23 -8
  79. package/src/checks/automatic/slider-name-present.js +40 -47
  80. package/src/checks/automatic/spinbutton-name-present.js +39 -19
  81. package/src/checks/automatic/summary-name-present.js +37 -17
  82. package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
  83. package/src/checks/automatic/svg-text-alternative-present.js +246 -226
  84. package/src/checks/automatic/tab-name-present.js +37 -60
  85. package/src/checks/automatic/table-headers-attr-valid.js +24 -8
  86. package/src/checks/automatic/table-th-has-data-cells.js +22 -8
  87. package/src/checks/automatic/target-size-minimum.js +118 -48
  88. package/src/checks/automatic/td-has-header.js +29 -11
  89. package/src/checks/automatic/textbox-name-present.js +39 -19
  90. package/src/checks/automatic/tooltip-name-present.js +37 -18
  91. package/src/checks/automatic/treeitem-name-present.js +38 -21
  92. package/src/checks/automatic/valid-lang.js +20 -6
  93. package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
  94. package/src/checks/manual/accesskeys-manual.js +14 -5
  95. package/src/checks/manual/area-alt-decorative-manual.js +192 -193
  96. package/src/checks/manual/area-alt-quality-manual.js +182 -141
  97. package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
  98. package/src/checks/manual/aria-text-manual.js +14 -6
  99. package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
  100. package/src/checks/manual/css-hidden-focus.js +196 -165
  101. package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
  102. package/src/checks/manual/empty-heading-manual.js +24 -7
  103. package/src/checks/manual/empty-table-header-manual.js +17 -6
  104. package/src/checks/manual/focus-order-semantics-manual.js +45 -10
  105. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
  106. package/src/checks/manual/heading-order-manual.js +22 -7
  107. package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
  108. package/src/checks/manual/image-redundant-alt-manual.js +17 -7
  109. package/src/checks/manual/img-alt-decorative-manual.js +131 -96
  110. package/src/checks/manual/img-alt-quality-manual.js +176 -127
  111. package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
  112. package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
  113. package/src/checks/manual/label-title-only-manual.js +14 -5
  114. package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
  115. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
  116. package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
  117. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
  118. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
  119. package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
  120. package/src/checks/manual/landmark-one-main-manual.js +26 -20
  121. package/src/checks/manual/landmark-unique-manual.js +41 -15
  122. package/src/checks/manual/link-name-quality-manual.js +43 -12
  123. package/src/checks/manual/media-transcript-present-manual.js +35 -22
  124. package/src/checks/manual/meta-viewport-large-manual.js +16 -5
  125. package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
  126. package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
  127. package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
  128. package/src/checks/manual/p-as-heading-manual.js +22 -7
  129. package/src/checks/manual/page-has-heading-one-manual.js +31 -22
  130. package/src/checks/manual/page-title-patterns-manual.js +78 -50
  131. package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
  132. package/src/checks/manual/region-manual.js +241 -48
  133. package/src/checks/manual/scope-attr-valid-manual.js +10 -3
  134. package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
  135. package/src/checks/manual/skip-link-manual.js +35 -12
  136. package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
  137. package/src/checks/manual/tabindex-manual.js +10 -3
  138. package/src/checks/manual/table-duplicate-name-manual.js +17 -7
  139. package/src/checks/manual/table-fake-caption-manual.js +24 -7
  140. package/src/checks/manual/video-caption-manual.js +15 -4
  141. package/src/checks/manual-review.js +56 -12
  142. package/src/core/aria-helpers.js +1127 -886
  143. package/src/core/contrast-helpers.js +1217 -1062
  144. package/src/core/dom-helpers.js +4175 -3917
  145. package/src/core/dom-runner.js +720 -604
  146. package/src/core/frame-messaging.js +189 -138
  147. package/src/core/frame-scan.js +94 -82
  148. package/src/core/rollup-composites.js +94 -102
  149. package/src/core/rule-meta.js +58 -41
  150. package/src/core.js +36552 -29111
  151. package/src/i18n/en.js +1194 -889
  152. package/src/i18n/fr.js +1136 -795
  153. package/src/policy/contracts.js +13 -13
  154. package/src/policy/resolvePolicy.js +48 -44
  155. package/src/report.js +63 -43
  156. package/src/sarif.js +175 -0
  157. package/surea11y.browser.js +36042 -0
@@ -21,183 +21,206 @@
21
21
  const id = 'css-hidden-focus';
22
22
 
23
23
  const meta = {
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
- {standard: 'WCAG', version: '2.2', requirement: '2.4.7', title: 'Focus Visible', conformanceLevel: 'AA'}
36
- ],
37
- defaultSeverity: 'serious',
38
- category: 'operable',
39
- type: 'manual',
40
- defaultConfidence: 'low',
41
- coverage: {
42
- facetsBySc: {
43
- '2.4.7': ['css-hidden-focusable']
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
- const {document, root, helpers, rule} = ctx;
50
- const safeRoot = root || document;
51
-
52
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
53
- const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
54
- const isDomVisibleEligible = helpers && typeof helpers.isDomVisibleEligible === 'function' ? helpers.isDomVisibleEligible : null;
55
- const reportOccurrence = helpers && typeof helpers.reportOccurrence === 'function' ? helpers.reportOccurrence : null;
56
-
57
- const trim = (v) => (v == null ? '' : String(v)).trim();
58
- const lower = (v) => trim(v).toLowerCase();
59
-
60
- function qAll(sel) {
61
- try {
62
- if (queryAllSmart) {
63
- const r = queryAllSmart(sel);
64
- if (Array.isArray(r)) return r;
65
- return Array.from(r || []);
66
- }
67
- } catch {
68
- // fall through
69
- }
70
- try {
71
- if (safeRoot && typeof safeRoot.querySelectorAll === 'function') return Array.from(safeRoot.querySelectorAll(sel));
72
- } catch {
73
- // fall through
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
- function getComputedStyleSafe(el) {
79
- try {
80
- const w = (document && document.defaultView) ? document.defaultView : (typeof window !== 'undefined' ? window : null);
81
- return w && w.getComputedStyle ? w.getComputedStyle(el) : null;
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
- // Returns deterministic "visually hidden but can remain focusable" hints.
88
- function getVisibilityHints(el) {
89
- const out = [];
90
- if (!el) return out;
91
- const cs = getComputedStyleSafe(el);
92
-
93
- // opacity:0
94
- try {
95
- const rawOp = cs && cs.opacity != null ? String(cs.opacity).trim() : '';
96
- const op = rawOp ? Number.parseFloat(rawOp) : 1;
97
- if (Number.isFinite(op) && op <= 0.0001) out.push('opacityZero');
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
- function getFocusableInfoSafe(el) {
171
- if (!getFocusableInfo) return null;
172
- try {
173
- return getFocusableInfo(el, ctx);
174
- } catch {
175
- return null;
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
- function isTabbable(el, info) {
180
- const f = info || getFocusableInfoSafe(el);
181
- return !!(f && f.focusable && f.tabbable);
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
- // Exclude elements that are not rendered / not in the visual rendering tree.
185
- function isRendered(el) {
186
- if (!isDomVisibleEligible) return true;
187
- try {
188
- const vis = isDomVisibleEligible(el, ctx, {visibilityMode: 'styleOnly', disableGeometry: true});
189
- if (vis && vis.eligible === false) {
190
- const rs = Array.isArray(vis.reasons) ? vis.reasons : [];
191
- // We *include* opacityZero in this rule. Everything else is treated as not rendered for this purpose.
192
- const nonOpacity = rs.filter((r) => String(r) !== 'opacityZero');
193
- if (nonOpacity.length) return false;
194
- }
195
- } catch {
196
- // If in doubt, keep deterministic behavior and consider it rendered.
197
- }
198
- return true;
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
- const candidates = qAll('a[href],area[href],button,input,select,textarea,summary,[tabindex],[contenteditable]');
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 = (() => { try { return lower(el.tagName || ''); } catch { return ''; } })();
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
- reportOccurrence ? reportOccurrence(el, baseOccurrence) : { __node: el, selector: '', html: '', ...baseOccurrence }
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 };