@surea11y/core 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +240 -149
  2. package/README.md +51 -44
  3. package/docs/ACT_RULE_MAPPING.md +245 -0
  4. package/docs/API_STABILITY.md +53 -5
  5. package/docs/BINDING_AUTHORS_GUIDE.md +106 -4
  6. package/docs/DESIGN_CHALLENGES.md +367 -0
  7. package/docs/EARL.md +100 -0
  8. package/docs/ENGINE_OPTIONS.md +42 -4
  9. package/docs/I18N.md +4 -4
  10. package/docs/INTEGRATION.md +4 -2
  11. package/docs/LIMITATIONS.md +9 -5
  12. package/docs/OUTPUT_SCHEMA.md +44 -6
  13. package/docs/POLICY.md +1 -1
  14. package/docs/REPORT.md +1 -1
  15. package/docs/RULE_AUTHORING.md +63 -36
  16. package/docs/RULE_CATALOG.md +1928 -169
  17. package/docs/RULE_HELPERS.md +333 -0
  18. package/docs/RULE_TAXONOMY.md +27 -6
  19. package/docs/SARIF.md +21 -2
  20. package/docs/TROUBLESHOOTING.md +2 -2
  21. package/docs/WCAG_CONFORMANCE.md +34 -10
  22. package/package.json +11 -9
  23. package/src/baseline.js +3 -3
  24. package/src/checks/automatic/area-alt-present.js +2 -2
  25. package/src/checks/automatic/aria-allowed-attr.js +74 -10
  26. package/src/checks/automatic/aria-allowed-role.js +34 -25
  27. package/src/checks/automatic/aria-braille-equivalent.js +21 -13
  28. package/src/checks/automatic/aria-conditional-attr.js +22 -15
  29. package/src/checks/automatic/aria-deprecated-role.js +13 -1
  30. package/src/checks/automatic/aria-hidden-body.js +3 -3
  31. package/src/checks/automatic/aria-hidden-focus.js +5 -5
  32. package/src/checks/automatic/aria-prohibited-attr.js +23 -18
  33. package/src/checks/automatic/aria-prohibited-children.js +136 -43
  34. package/src/checks/automatic/aria-required-attr.js +119 -24
  35. package/src/checks/automatic/aria-required-children.js +54 -30
  36. package/src/checks/automatic/aria-required-parent.js +93 -15
  37. package/src/checks/automatic/aria-role-name-present.js +37 -23
  38. package/src/checks/automatic/aria-roles-valid.js +52 -21
  39. package/src/checks/automatic/aria-valid-attr-value.js +89 -33
  40. package/src/checks/automatic/aria-valid-attr.js +15 -10
  41. package/src/checks/automatic/autocomplete-valid.js +2 -2
  42. package/src/checks/automatic/avoid-inline-spacing.js +133 -6
  43. package/src/checks/automatic/binary-control-name-present.js +27 -5
  44. package/src/checks/automatic/button-name-present.js +92 -6
  45. package/src/checks/automatic/combobox-name-present.js +26 -6
  46. package/src/checks/automatic/contrast-computable.js +42 -0
  47. package/src/checks/automatic/contrast-enhanced.js +33 -1
  48. package/src/checks/automatic/contrast-minimum.js +33 -1
  49. package/src/checks/automatic/css-orientation-lock.js +138 -24
  50. package/src/checks/automatic/definition-list-children-valid.js +7 -8
  51. package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
  52. package/src/checks/automatic/dialog-name-present.js +20 -2
  53. package/src/checks/automatic/duplicate-id-aria.js +10 -3
  54. package/src/checks/automatic/duplicate-id.js +203 -0
  55. package/src/checks/automatic/embed-text-alternative-present.js +2 -2
  56. package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
  57. package/src/checks/automatic/form-control-single-label.js +10 -1
  58. package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
  59. package/src/checks/automatic/iframe-focusable-content.js +68 -7
  60. package/src/checks/automatic/iframe-name-present.js +37 -3
  61. package/src/checks/automatic/iframe-title-unique.js +1 -1
  62. package/src/checks/automatic/img-alt-present.js +12 -4
  63. package/src/checks/automatic/label-in-name.js +204 -68
  64. package/src/checks/automatic/link-in-text-block.js +285 -29
  65. package/src/checks/automatic/link-name-present.js +22 -1
  66. package/src/checks/automatic/list-children-valid.js +6 -6
  67. package/src/checks/automatic/listbox-name-present.js +28 -8
  68. package/src/checks/automatic/listitem-parent-valid.js +4 -4
  69. package/src/checks/automatic/menuitem-name-present.js +20 -2
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
  72. package/src/checks/automatic/meter-name-present.js +23 -4
  73. package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
  74. package/src/checks/automatic/option-name-present.js +23 -4
  75. package/src/checks/automatic/page-title-present.js +21 -3
  76. package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
  77. package/src/checks/automatic/progressbar-name-present.js +23 -4
  78. package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +64 -16
  79. package/src/checks/automatic/searchbox-name-present.js +28 -8
  80. package/src/checks/automatic/server-side-image-map-absent.js +1 -1
  81. package/src/checks/automatic/slider-name-present.js +27 -6
  82. package/src/checks/automatic/spinbutton-name-present.js +28 -8
  83. package/src/checks/automatic/summary-name-present.js +18 -2
  84. package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
  85. package/src/checks/automatic/svg-text-alternative-present.js +13 -10
  86. package/src/checks/automatic/tab-name-present.js +21 -2
  87. package/src/checks/automatic/table-headers-attr-valid.js +43 -8
  88. package/src/checks/automatic/table-th-has-data-cells.js +61 -5
  89. package/src/checks/automatic/target-size-minimum.js +155 -58
  90. package/src/checks/automatic/td-has-header.js +24 -23
  91. package/src/checks/automatic/textbox-name-present.js +28 -8
  92. package/src/checks/automatic/tooltip-name-present.js +21 -2
  93. package/src/checks/automatic/treeitem-name-present.js +23 -4
  94. package/src/checks/automatic/valid-lang.js +92 -7
  95. package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
  96. package/src/checks/manual/accesskeys-manual.js +3 -3
  97. package/src/checks/manual/area-alt-decorative-manual.js +7 -0
  98. package/src/checks/manual/area-alt-quality-manual.js +6 -0
  99. package/src/checks/manual/aria-checked-state-mismatch-manual.js +5 -5
  100. package/src/checks/manual/aria-text-manual.js +4 -4
  101. package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
  103. package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
  105. package/src/checks/manual/empty-heading-manual.js +58 -11
  106. package/src/checks/manual/empty-table-header-manual.js +8 -8
  107. package/src/checks/manual/focus-order-semantics-manual.js +15 -15
  108. package/src/checks/manual/form-control-label-quality-manual.js +563 -0
  109. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -2
  110. package/src/checks/manual/heading-order-manual.js +3 -3
  111. package/src/checks/manual/heading-quality-manual.js +338 -0
  112. package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
  113. package/src/checks/manual/image-redundant-alt-manual.js +4 -4
  114. package/src/checks/manual/img-alt-decorative-manual.js +211 -52
  115. package/src/checks/manual/img-alt-quality-manual.js +7 -0
  116. package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
  117. package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
  118. package/src/checks/manual/label-title-only-manual.js +4 -4
  119. package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
  120. package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
  121. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
  122. package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
  123. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
  124. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
  125. package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
  126. package/src/checks/manual/landmark-one-main-manual.js +6 -6
  127. package/src/checks/manual/landmark-unique-manual.js +9 -9
  128. package/src/checks/manual/link-name-quality-manual.js +161 -32
  129. package/src/checks/manual/media-transcript-present-manual.js +2 -3
  130. package/src/checks/manual/meta-viewport-large-manual.js +2 -2
  131. package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
  132. package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
  133. package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
  134. package/src/checks/manual/p-as-heading-manual.js +4 -4
  135. package/src/checks/manual/page-has-heading-one-manual.js +6 -6
  136. package/src/checks/manual/page-title-patterns-manual.js +26 -3
  137. package/src/checks/manual/password-paste-enabled-manual.js +255 -0
  138. package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
  139. package/src/checks/manual/region-manual.js +19 -19
  140. package/src/checks/manual/scope-attr-valid-manual.js +2 -2
  141. package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
  142. package/src/checks/manual/skip-link-manual.js +5 -5
  143. package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
  144. package/src/checks/manual/tabindex-manual.js +2 -2
  145. package/src/checks/manual/table-duplicate-name-manual.js +2 -2
  146. package/src/checks/manual/table-fake-caption-manual.js +2 -2
  147. package/src/checks/manual/video-caption-manual.js +3 -3
  148. package/src/checks/manual-review.js +17 -1
  149. package/src/core.js +8880 -41883
  150. package/src/earl.js +144 -0
  151. package/src/report.js +2 -2
  152. package/src/sarif.js +22 -2
  153. package/surea11y.browser.js +10 -37882
  154. package/surea11y.i18n.de.js +2 -21
  155. package/surea11y.i18n.es.js +2 -21
  156. package/surea11y.i18n.fr.js +2 -21
  157. package/bin/surea11y-core.js +0 -20
@@ -5,20 +5,34 @@
5
5
  /**
6
6
  * @check img-alt-decorative
7
7
  * @atomic true
8
- * @summary Manual review: text alternative appropriateness (WCAG 1.1.1)
8
+ * @summary Manual review: an image/canvas/svg excluded from the accessibility tree must be purely decorative
9
9
  * @standard WCAG 2.2
10
10
  * @sc 1.1.1
11
11
  * @type manual
12
+ * @applicability
13
+ * Applies to visible <img>, <canvas> or <svg> elements excluded from the
14
+ * accessibility tree by any of: an aria-hidden ancestor-or-self, an
15
+ * explicit role="none"/"presentation" not overridden by focusability, an
16
+ * <img alt=""> (the native decorative marker, same focusability
17
+ * override), an unlabeled <svg> whose implicit role is graphics-document
18
+ * (no img/graphics-symbol role restatement, aria-name, <title>/<desc>, or
19
+ * focusability), or an unlabeled <canvas> with no explicit role at all.
20
+ * Per ACT e88epe, an element is skipped entirely when any ancestor
21
+ * already has an author-supplied name (aria-label, aria-labelledby,
22
+ * title, or an associated <label>), that ancestor's name is what
23
+ * matters, not this element's exclusion (the common real case: an
24
+ * icon-only button already named via aria-label).
12
25
  * @expectation
13
- * Human review is required to confirm that the provided text alternative is accurate and appropriate.
26
+ * Human review is required to confirm the excluded element is purely
27
+ * decorative and conveys no information a user would otherwise miss.
14
28
  */
15
29
 
16
30
  const id = 'img-alt-decorative';
17
31
 
18
32
  const meta = {
19
- title: '<img> with alt="" must be decorative (manual review)',
33
+ title: 'Excluded <img>/<canvas>/<svg> must be decorative (manual review)',
20
34
  description:
21
- 'Flags <img> elements with empty alt for human review that they are purely decorative.',
35
+ 'Flags <img>, <canvas> and <svg> elements excluded from the accessibility tree (aria-hidden, role="none"/"presentation", empty alt, or an unlabeled svg/canvas) for human review that they are purely decorative.',
22
36
  i18n: {
23
37
  titleKey: 'img_altDecorative_title',
24
38
  descriptionKey: 'img_altDecorative_description'
@@ -65,56 +79,207 @@ function runInPage(ctx) {
65
79
  }
66
80
  };
67
81
 
68
- const isAccTreeEligible =
69
- helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
82
+ const isIncludedInAccessibilityTree =
83
+ helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
84
+ ? helpers.isIncludedInAccessibilityTree
85
+ : helpers && typeof helpers.isAccTreeEligible === 'function'
86
+ ? helpers.isAccTreeEligible
87
+ : null;
70
88
 
71
89
  const getFocusableInfo =
72
90
  helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
73
91
 
74
- function isRolePresentationExcluded(el) {
75
- const role = (() => {
92
+ const getAccessibleNameInfo =
93
+ helpers && typeof helpers.getAccessibleNameInfo === 'function'
94
+ ? helpers.getAccessibleNameInfo
95
+ : null;
96
+
97
+ const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
98
+
99
+ function trim(v) {
100
+ try {
101
+ return (v == null ? '' : String(v)).trim();
102
+ } catch {
103
+ return '';
104
+ }
105
+ }
106
+
107
+ function getExplicitRole(el) {
108
+ if (ariaHelpers && typeof ariaHelpers.getExplicitRole === 'function') {
76
109
  try {
77
- return String(el.getAttribute('role') || '')
78
- .trim()
79
- .toLowerCase();
110
+ return ariaHelpers.getExplicitRole(el) || '';
80
111
  } catch {
81
112
  return '';
82
113
  }
83
- })();
84
- if (role !== 'presentation' && role !== 'none') return false;
114
+ }
115
+ try {
116
+ const raw = trim(el.getAttribute('role'));
117
+ return raw ? raw.split(/\s+/)[0].toLowerCase() : '';
118
+ } catch {
119
+ return '';
120
+ }
121
+ }
85
122
 
86
- // Exclude only when NOT focusable (mirrors img-alt-present policy)
87
- let focusable;
123
+ function isFocusable(el) {
88
124
  if (getFocusableInfo) {
89
- const fi = (() => {
90
- try {
91
- return getFocusableInfo(el, ctx);
92
- } catch {
93
- return null;
94
- }
95
- })();
96
- focusable = !!(fi && fi.focusable);
97
- } else {
125
+ try {
126
+ const fi = getFocusableInfo(el, ctx);
127
+ return !!(fi && fi.focusable);
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+ try {
98
133
  const tabindex = el.getAttribute('tabindex');
99
- focusable =
134
+ return (
100
135
  tabindex != null &&
101
136
  String(tabindex).trim() !== '' &&
102
- !Number.isNaN(Number(String(tabindex).trim()));
137
+ !Number.isNaN(Number(String(tabindex).trim()))
138
+ );
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+
144
+ function isDomVisible(el) {
145
+ if (!el) return false;
146
+ if (helpers && helpers.isDomVisibleEligible)
147
+ return !!helpers.isDomVisibleEligible(el, ctx, { targetSet: 'dom' }).eligible;
148
+ if (helpers && helpers.getEligibilityInfo)
149
+ return !!helpers.getEligibilityInfo(el, ctx, { targetSet: 'dom' }).eligible;
150
+ return true;
151
+ }
152
+
153
+ // Same offscreen-hint pattern as avoid-inline-spacing.js.
154
+ function isOffscreen(el) {
155
+ if (!helpers || typeof helpers.getVisibilityHintsInfo !== 'function') return false;
156
+ try {
157
+ const info = helpers.getVisibilityHintsInfo(el, ctx, {});
158
+ return !!(info && Array.isArray(info.hints) && info.hints.indexOf('offscreen') !== -1);
159
+ } catch {
160
+ return false;
103
161
  }
104
- return !focusable;
162
+ }
163
+
164
+ function hasAriaNamingAttr(el) {
165
+ try {
166
+ return el.getAttribute('aria-label') != null || el.getAttribute('aria-labelledby') != null;
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+
172
+ // Same first-child <title>/<desc> convention as svg-text-alternative-present.js.
173
+ function hasNonEmptyFirstChildTitleOrDesc(svg) {
174
+ try {
175
+ const first = svg.firstElementChild;
176
+ const tn = first ? (first.localName || first.tagName || '').toLowerCase() : '';
177
+ if (tn === 'title' || tn === 'desc') return !!trim(first.textContent);
178
+ } catch {
179
+ // ignore
180
+ }
181
+ return false;
182
+ }
183
+
184
+ // General exclusion: aria-hidden, inert, or otherwise not included per the
185
+ // shared eligibility model. Visibility is checked separately beforehand,
186
+ // so a display:none/hidden element never reaches this path.
187
+ function isGenerallyExcluded(el) {
188
+ if (!isIncludedInAccessibilityTree) return false;
189
+ try {
190
+ const r = isIncludedInAccessibilityTree(el, ctx);
191
+ if (typeof r === 'boolean') return !r;
192
+ return !(r && r.eligible !== false);
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+
198
+ // Presentational exclusion: explicit role="none"/"presentation", or (img
199
+ // only) the native alt="" marker. Both are overridden by focusability, per
200
+ // ARIA conflict resolution (a focusable element is never presentational).
201
+ function isPresentationallyExcluded(el, tag) {
202
+ const role = getExplicitRole(el);
203
+ let presentational = role === 'presentation' || role === 'none';
204
+ if (!presentational && tag === 'img') {
205
+ const alt = el.getAttribute('alt');
206
+ presentational = alt != null && trim(alt) === '';
207
+ }
208
+ if (!presentational) return false;
209
+ return !isFocusable(el);
210
+ }
211
+
212
+ // ACT e88epe's "ignored svg": an implicit graphics-document role (no
213
+ // explicit role, or role explicitly restated as graphics-document) with
214
+ // no accessible name and not focusable. An svg explicitly given role="img"
215
+ // /"graphics-symbol", an aria-name, a <title>/<desc>, or a tab stop is
216
+ // "included", a naming question for svg-text-alternative-present, not
217
+ // this rule's "is it decorative" question.
218
+ function isIgnoredSvg(el) {
219
+ const role = getExplicitRole(el);
220
+ if (role && role !== 'graphics-document') return false;
221
+ if (hasAriaNamingAttr(el)) return false;
222
+ if (hasNonEmptyFirstChildTitleOrDesc(el)) return false;
223
+ if (isFocusable(el)) return false;
224
+ return true;
225
+ }
226
+
227
+ // ACT e88epe's "ignored canvas": no explicit role at all and no
228
+ // accessible name. Canvas fallback content as a naming mechanism is not
229
+ // modeled here (a separate, narrower question than this rule needs to
230
+ // settle; see docs/DESIGN_CHALLENGES.md).
231
+ function isIgnoredCanvas(el) {
232
+ const role = getExplicitRole(el);
233
+ if (role) return false;
234
+ if (hasAriaNamingAttr(el)) return false;
235
+ return true;
236
+ }
237
+
238
+ const AUTHOR_NAME_MECHANISMS = new Set(['aria-label', 'aria-labelledby', 'title', 'label']);
239
+
240
+ // ACT e88epe's own exception: never applies under an ancestor already
241
+ // named by the author. The common real case is an icon-only button
242
+ // (<button aria-label="Close"><svg>...</svg></button>) that already has a
243
+ // correct name from the button itself. Whether the svg "is decorative"
244
+ // is moot there, and flagging it would just be noise on ordinary icon usage.
245
+ function hasAncestorNamedFromAuthor(el) {
246
+ if (!getAccessibleNameInfo) return false;
247
+ const getComposedParent =
248
+ helpers && typeof helpers.composedParent === 'function'
249
+ ? helpers.composedParent
250
+ : (n) => (n && n.parentElement ? n.parentElement : null);
251
+
252
+ let cur = getComposedParent(el);
253
+ let guard = 0;
254
+ while (cur && guard++ < 200) {
255
+ if (cur.nodeType === 1) {
256
+ try {
257
+ const info = getAccessibleNameInfo(cur, ctx, { maxRefs: 8 });
258
+ if (
259
+ info &&
260
+ info.present &&
261
+ trim(info.value) &&
262
+ AUTHOR_NAME_MECHANISMS.has(info.mechanism)
263
+ )
264
+ return true;
265
+ } catch {
266
+ // ignore
267
+ }
268
+ }
269
+ cur = getComposedParent(cur);
270
+ }
271
+ return false;
105
272
  }
106
273
 
107
274
  const els = (() => {
108
- // Only likely candidates:
109
- // - alt="" (exact)
110
- // - alt that starts/ends with space (to catch whitespace-only like " ")
111
- const sel = 'img[alt=""], img[alt^=" "], img[alt$=" "]';
275
+ const sel = 'img, canvas, svg';
112
276
  try {
113
277
  return Array.from((queryAllSmart ? queryAllSmart(sel) : queryAll(sel)) || []);
114
278
  } catch {
115
279
  return queryAll(sel);
116
280
  }
117
281
  })();
282
+
118
283
  const uniqueEls = [];
119
284
  const seen = new Set();
120
285
  for (const el of els) {
@@ -124,7 +289,7 @@ function runInPage(ctx) {
124
289
  }
125
290
  }
126
291
 
127
- if (!els.length) {
292
+ if (!uniqueEls.length) {
128
293
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
129
294
  }
130
295
 
@@ -132,39 +297,33 @@ function runInPage(ctx) {
132
297
  let applicableCount = 0;
133
298
 
134
299
  for (const el of uniqueEls) {
135
- if (!el || !el.getAttribute) continue;
300
+ if (!el || !el.tagName) continue;
301
+ const tag = el.tagName.toLowerCase();
136
302
 
137
- if (isAccTreeEligible) {
138
- const elig = (() => {
139
- try {
140
- return isAccTreeEligible(el, ctx);
141
- } catch {
142
- return { eligible: true, reasons: [] };
143
- }
144
- })();
145
- if (elig && elig.eligible === false) continue;
146
- }
303
+ if (!isDomVisible(el)) continue;
304
+ if (isOffscreen(el)) continue;
147
305
 
148
- if (isRolePresentationExcluded(el)) continue;
306
+ let excluded = isGenerallyExcluded(el);
307
+ if (!excluded) excluded = isPresentationallyExcluded(el, tag);
308
+ if (!excluded && tag === 'svg') excluded = isIgnoredSvg(el);
309
+ if (!excluded && tag === 'canvas') excluded = isIgnoredCanvas(el);
149
310
 
150
- // Rule-specific applicability (only elements that already have a text alternative mechanism)
151
- const rawAlt = el.getAttribute('alt');
152
- if (rawAlt == null) continue;
153
- if (String(rawAlt).trim() !== '') continue;
311
+ if (!excluded) continue;
312
+ if (hasAncestorNamedFromAuthor(el)) continue;
154
313
 
155
314
  applicableCount += 1;
156
315
 
157
316
  const baseOccurrence = {
158
- summary: 'Review whether <img> is decorative (alt="").',
159
- hint: 'Confirm the image is purely decorative. If it conveys information or function, provide meaningful alt text.',
317
+ summary: `Review whether this <${tag}> is decorative.`,
318
+ hint: 'Confirm the element is purely decorative. If it conveys information or function, give it a real text alternative (or an accessible name) instead of excluding it.',
160
319
  i18n: {
161
320
  summaryKey: 'img_altDecorative_summary_cantTell',
162
321
  hintKey: 'img_altDecorative_hint_cantTell',
163
- params: { element: 'img' }
322
+ params: { element: tag }
164
323
  },
165
324
  data: {
166
325
  visibilityFilter: { targetSet: 'acc', accEligible: null, reasons: [] },
167
- details: null
326
+ details: { reasonCode: 'EXCLUDED_ELEMENT_REVIEW', element: tag }
168
327
  }
169
328
  };
170
329
 
@@ -9,6 +9,13 @@
9
9
  * @standard WCAG 2.2
10
10
  * @sc 1.1.1
11
11
  * @type manual
12
+ * @applicability
13
+ * Applies to <img> elements whose alt attribute is present and non-empty.
14
+ * The element must be included in the accessibility tree, and
15
+ * role="presentation"/"none" takes it out of scope unless it is focusable,
16
+ * which restores its role. An <img> with no alt at all is
17
+ * img-alt-present's failure, and one with alt="" is img-alt-decorative's
18
+ * review.
12
19
  * @expectation
13
20
  * Human review is required to confirm that the provided text alternative is accurate and appropriate.
14
21
  */
@@ -9,6 +9,14 @@
9
9
  * @standard WCAG 2.2
10
10
  * @sc 1.1.1
11
11
  * @type manual
12
+ * @applicability
13
+ * Applies to <input type="image"> elements whose alt attribute is present
14
+ * but empty once trimmed, and which still carry a name from another
15
+ * source: an ARIA name resolving to non-empty text, or a title. Without
16
+ * that other name there is nothing to weigh the empty alt against, and the
17
+ * control is input-image-alt-present's failure instead. The element must
18
+ * be included in the accessibility tree, and role="presentation"/"none"
19
+ * takes it out of scope unless it is focusable, which restores its role.
12
20
  * @expectation
13
21
  * Human review is required to confirm that the provided text alternative is accurate and appropriate.
14
22
  */
@@ -9,6 +9,11 @@
9
9
  * @standard WCAG 2.2
10
10
  * @sc 1.1.1
11
11
  * @type manual
12
+ * @applicability
13
+ * Applies to <input type="image"> elements whose alt attribute is present
14
+ * and non-empty: an image button whose alt is its label. The element must
15
+ * be included in the accessibility tree, and role="presentation"/"none"
16
+ * takes it out of scope unless it is focusable, which restores its role.
12
17
  * @expectation
13
18
  * Human review is required to confirm that the provided text alternative is accurate and appropriate.
14
19
  */
@@ -6,19 +6,19 @@
6
6
  * @check label-title-only
7
7
  * @atomic true
8
8
  * @summary Form controls should not rely on the title attribute as their only label
9
- * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion)
10
10
  * @applicability
11
11
  * Applies to labelable form controls (input, excluding
12
12
  * hidden/submit/reset/button/image; select; textarea) that have a
13
13
  * non-empty title attribute.
14
14
  * @expectation
15
- * The control also has a real label — a wrapping/associated <label>,
16
- * aria-label, or aria-labelledby — rather than depending on the title
15
+ * The control also has a real label (a wrapping/associated <label>,
16
+ * aria-label, or aria-labelledby), rather than depending on the title
17
17
  * attribute alone. A title-only tooltip is not reliably exposed by all
18
18
  * assistive technology and is not visible at all until hover/focus,
19
19
  * unlike a persistent visible label.
20
20
  * @implementation-notes
21
- * - Not WCAG-normative — authored as an advisory, cantTell-capped
21
+ * - Not WCAG-normative, authored as an advisory, cantTell-capped
22
22
  * `type: 'manual'` rule; see landmark-banner-is-top-level's
23
23
  * header comment for the shared rationale/precedent.
24
24
  * - Distinct from form-control-programmatic-label-present (that
@@ -6,20 +6,20 @@
6
6
  * @check landmark-banner-is-top-level
7
7
  * @atomic true
8
8
  * @summary The banner landmark must not be nested inside another landmark
9
- * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion)
10
10
  * @applicability
11
11
  * Applies whenever the page contains at least one banner candidate:
12
12
  * explicit role="banner", OR a <header> with NO role attribute at all,
13
13
  * regardless of nesting (see implementation notes on why candidate
14
- * selection is deliberately unconditional).
14
+ * selection is unconditional on purpose).
15
15
  * @expectation
16
16
  * No banner candidate has an ancestor that is itself any landmark
17
17
  * region. A banner nested inside another landmark is not a top-level,
18
18
  * whole-page banner and confuses landmark-based navigation for
19
19
  * assistive technology users.
20
20
  * @implementation-notes
21
- * - Not WCAG-normative — authored as an advisory, cantTell-capped
22
- * `type: 'manual'` rule per ROADMAP.md Tier 1b and the design doc's policy model
21
+ * - Not WCAG-normative, authored as an advisory, cantTell-capped
22
+ * `type: 'manual'` rule per the design doc's policy model
23
23
  * ("Advisory / best-practice rules may exist, but must not produce
24
24
  * `fail`"). Matches the existing `page-title-patterns-manual.js`
25
25
  * precedent: deterministic DOM analysis, no human required, but
@@ -61,7 +61,7 @@ const meta = {
61
61
  function runInPage(ctx) {
62
62
  const { document, root, helpers, rule } = ctx;
63
63
 
64
- // Declared inside runInPage — see scripts/build-core.js header
64
+ // Declared inside runInPage; see scripts/build-core.js header
65
65
  // ("runInPage MUST be self-contained").
66
66
  function normalizeWs(s) {
67
67
  return String(s || '')
@@ -91,7 +91,7 @@ function runInPage(ctx) {
91
91
 
92
92
  // Delegates to the shared helpers.hasLandmarkScopingAncestor for the
93
93
  // question "does this element sit inside a sectioning-content/<main>
94
- // ancestor that suppresses its conditional implicit role" — role-aware
94
+ // ancestor that suppresses its conditional implicit role": role-aware
95
95
  // (an ancestor's bare TAG only counts when it carries no role attribute
96
96
  // at all; an explicit role="dialog"-style override no longer suppresses)
97
97
  // rather than a local tag-only copy. See that function's header comment
@@ -110,7 +110,7 @@ function runInPage(ctx) {
110
110
  if (tag === 'main') return 'main';
111
111
  if (tag === 'nav') return 'navigation';
112
112
  if (tag === 'aside') {
113
- // A named <aside> is never suppressed, even when nested — it keeps
113
+ // A named <aside> is never suppressed, even when nested. It keeps
114
114
  // "complementary" when it has an accessible name, even inside
115
115
  // sectioning content. Matches landmark-unique's precedent.
116
116
  if (!hasSectioningAncestor(el, false)) return 'complementary';