@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
@@ -0,0 +1,231 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
3
+ 'use strict';
4
+
5
+ /**
6
+ * @check landmark-complementary-is-top-level
7
+ * @atomic true
8
+ * @summary The complementary landmark must not be nested inside another landmark
9
+ * @standard Best Practices (no formal WCAG Success Criterion)
10
+ * @applicability
11
+ * Applies whenever the page contains at least one element carrying the
12
+ * complementary role: explicit role="complementary", or an <aside> that
13
+ * keeps its implicit role (see implementation notes on when it does not).
14
+ * @expectation
15
+ * No complementary candidate has an ancestor that is itself a landmark
16
+ * region. Complementary content supports the main content of the page and
17
+ * sits beside it; nested inside another landmark it is a section of that
18
+ * landmark instead, which is not what landmark navigation announces.
19
+ * @implementation-notes
20
+ * - Not WCAG-normative, authored as an advisory, cantTell-capped
21
+ * `type: 'manual'` rule, matching its three siblings
22
+ * (`landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`,
23
+ * `landmark-main-is-top-level`). Landmark detection and the
24
+ * ancestor walk are identical to theirs; only the role being looked for
25
+ * differs.
26
+ * - An unnamed <aside> inside sectioning content has no complementary role
27
+ * per HTML-AAM, so it is not a candidate at all: reporting it would name a
28
+ * landmark that does not exist. A *named* one keeps the role wherever it
29
+ * sits, which is exactly the case worth review -- an <aside aria-label>
30
+ * inside <main> really is a complementary landmark nested in another
31
+ * landmark. `landmark-unique` and the sibling top-level rules already
32
+ * resolve <aside> this way, through the same shared helper.
33
+ */
34
+
35
+ const id = 'landmark-complementary-is-top-level';
36
+
37
+ const meta = {
38
+ title: 'Complementary landmark must be top-level',
39
+ description:
40
+ 'Checks that the complementary landmark (role="complementary" or an <aside> that keeps its implicit role) is not nested inside another landmark region.',
41
+ i18n: {
42
+ titleKey: 'landmarkComplementaryIsTopLevel_title',
43
+ descriptionKey: 'landmarkComplementaryIsTopLevel_description'
44
+ },
45
+ helpUrl: null,
46
+ tags: ['best-practice', 'landmarks', 'structure', 'atomic', 'manual'],
47
+ wcagSc: [],
48
+ normativeMappings: [],
49
+ defaultSeverity: 'minor',
50
+ category: 'operable',
51
+ type: 'manual',
52
+ defaultConfidence: 'medium',
53
+ coverage: {}
54
+ };
55
+
56
+ function runInPage(ctx) {
57
+ const { document, root, helpers, rule } = ctx;
58
+
59
+ // Declared inside runInPage; see scripts/build-core.js header
60
+ // ("runInPage MUST be self-contained").
61
+ function normalizeWs(s) {
62
+ return String(s || '')
63
+ .replace(/\s+/g, ' ')
64
+ .trim();
65
+ }
66
+
67
+ // Delegates to the shared helpers.getLandmarkNameInfo (aria-label -> aria-labelledby, via the
68
+ // target's own accessible name, not raw textContent -> title attribute fallback) rather than a
69
+ // local copy -- see that function's header comment in src/core/dom-helpers.js. Sharing it keeps
70
+ // the title-attribute fallback consistent across the landmark rules.
71
+ function getAccessibleLandmarkName(el) {
72
+ try {
73
+ if (helpers && typeof helpers.getLandmarkNameInfo === 'function') {
74
+ const info = helpers.getLandmarkNameInfo(el, ctx);
75
+ if (info && info.present && info.value) return normalizeWs(info.value);
76
+ }
77
+ } catch {}
78
+ return '';
79
+ }
80
+
81
+ function getExplicitRoleToken(el) {
82
+ const raw = normalizeWs(el.getAttribute && el.getAttribute('role'));
83
+ if (!raw) return '';
84
+ return raw.split(/\s+/)[0].toLowerCase();
85
+ }
86
+
87
+ // Delegates to the shared helpers.hasLandmarkScopingAncestor for the
88
+ // question "does this element sit inside a sectioning-content/<main>
89
+ // ancestor that suppresses its conditional implicit role": role-aware
90
+ // (an ancestor's bare TAG only counts when it carries no role attribute
91
+ // at all; an explicit role="dialog"-style override no longer suppresses)
92
+ // rather than a local tag-only copy. See that function's header comment
93
+ // in src/core/aria-helpers.js for the full algorithm.
94
+ function hasSectioningAncestor(el, includeMain) {
95
+ return helpers && typeof helpers.hasLandmarkScopingAncestor === 'function'
96
+ ? helpers.hasLandmarkScopingAncestor(el, { includeMain })
97
+ : false;
98
+ }
99
+
100
+ function getImplicitLandmarkRole(el) {
101
+ const tag = el.tagName ? el.tagName.toLowerCase() : '';
102
+ if (tag === 'header') return hasSectioningAncestor(el, true) ? '' : 'banner';
103
+ if (tag === 'footer') return hasSectioningAncestor(el, true) ? '' : 'contentinfo';
104
+ if (tag === 'main') return 'main';
105
+ if (tag === 'nav') return 'navigation';
106
+ if (tag === 'aside') {
107
+ // A named <aside> is never suppressed, even when nested. It keeps
108
+ // "complementary" when it has an accessible name, even inside
109
+ // sectioning content. Matches landmark-unique's precedent.
110
+ if (!hasSectioningAncestor(el, false)) return 'complementary';
111
+ return getAccessibleLandmarkName(el) ? 'complementary' : '';
112
+ }
113
+ if (tag === 'section') return getAccessibleLandmarkName(el) ? 'region' : '';
114
+ if (tag === 'form') return getAccessibleLandmarkName(el) ? 'form' : '';
115
+ return '';
116
+ }
117
+
118
+ const LANDMARK_ROLES = new Set([
119
+ 'banner',
120
+ 'contentinfo',
121
+ 'main',
122
+ 'navigation',
123
+ 'complementary',
124
+ 'region',
125
+ 'form',
126
+ 'search'
127
+ ]);
128
+
129
+ function getLandmarkRole(el) {
130
+ if (!el || !el.getAttribute) return '';
131
+ const explicit = getExplicitRoleToken(el);
132
+ if (explicit) return LANDMARK_ROLES.has(explicit) ? explicit : '';
133
+ return getImplicitLandmarkRole(el);
134
+ }
135
+
136
+ // A candidate must actually carry the complementary role. An <aside> that
137
+ // HTML-AAM strips the role from is not a complementary landmark at all, so
138
+ // flagging it would report a landmark that does not exist.
139
+ function isComplementaryCandidate(el) {
140
+ return getLandmarkRole(el) === 'complementary';
141
+ }
142
+
143
+ function hasLandmarkAncestor(el) {
144
+ const scopeRoots = Array.isArray(root) ? root : root ? [root] : [];
145
+ let p = el.parentElement;
146
+ while (p) {
147
+ if (getLandmarkRole(p)) return true;
148
+ // Don't climb past the scanned scope -- see aria-helpers.js's
149
+ // hasLandmarkScopingAncestor for the same fix and rationale.
150
+ if (scopeRoots.includes(p)) break;
151
+ p = p.parentElement;
152
+ }
153
+ return false;
154
+ }
155
+
156
+ // queryAllSmart (shadow-DOM-aware) instead of plain document.querySelectorAll -- see
157
+ // landmark-unique-manual.js's header comment. A third-party shadow-DOM-hosted
158
+ // widget's own landmark is invisible to a light-DOM-only query.
159
+ let nodes;
160
+ try {
161
+ nodes =
162
+ helpers && typeof helpers.queryAllSmart === 'function'
163
+ ? helpers.queryAllSmart('header, footer, main, nav, aside, section, form, [role]')
164
+ : document.querySelectorAll('header, footer, main, nav, aside, section, form, [role]');
165
+ } catch {
166
+ nodes = [];
167
+ }
168
+
169
+ const complementaries = [];
170
+ const seen = new Set();
171
+ for (const el of nodes) {
172
+ if (!el || seen.has(el)) continue;
173
+ seen.add(el);
174
+ if (!isComplementaryCandidate(el)) continue;
175
+
176
+ // An aria-hidden candidate is removed from the accessibility tree
177
+ // entirely, so it is not part of the landmark structure assistive
178
+ // technology users navigate and there is no real landmark to call
179
+ // nested. queryAllSmart's default hidden-content policy only excludes
180
+ // "hard" CSS-based hiding (display:none, etc.), not the softer
181
+ // aria-hidden exclusion, so this needs its own check.
182
+ if (helpers && typeof helpers.isAccTreeEligible === 'function') {
183
+ const elig = (() => {
184
+ try {
185
+ return helpers.isAccTreeEligible(el, ctx);
186
+ } catch {
187
+ return { eligible: true, reasons: [] };
188
+ }
189
+ })();
190
+ if (elig && elig.eligible === false) continue;
191
+ }
192
+
193
+ complementaries.push(el);
194
+ }
195
+
196
+ if (complementaries.length === 0) {
197
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
198
+ }
199
+
200
+ const occurrences = [];
201
+ for (const el of complementaries) {
202
+ if (!hasLandmarkAncestor(el)) continue;
203
+
204
+ occurrences.push(
205
+ helpers.reportOccurrence(el, {
206
+ summary: 'This complementary landmark is nested inside another landmark region.',
207
+ hint: 'Move the complementary landmark (<aside>/role="complementary") so it is not contained by another landmark; complementary content belongs beside the main content, not inside another region.',
208
+ i18n: {
209
+ summaryKey: 'landmarkComplementaryIsTopLevel_summary_cantTell',
210
+ hintKey: 'landmarkComplementaryIsTopLevel_hint_cantTell',
211
+ params: {}
212
+ },
213
+ data: {
214
+ details: { reasonCode: 'LANDMARK_COMPLEMENTARY_NOT_TOP_LEVEL' }
215
+ }
216
+ })
217
+ );
218
+ }
219
+
220
+ if (occurrences.length) {
221
+ return {
222
+ ruleId: rule.ruleId,
223
+ outcome: 'cantTell',
224
+ severity: rule.defaultSeverity || 'minor',
225
+ occurrences
226
+ };
227
+ }
228
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
229
+ }
230
+
231
+ module.exports = { id, meta, runInPage };
@@ -6,25 +6,25 @@
6
6
  * @check landmark-contentinfo-is-top-level
7
7
  * @atomic true
8
8
  * @summary The contentinfo 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 contentinfo
12
12
  * candidate: explicit role="contentinfo", OR a <footer> with NO role
13
13
  * attribute at all, regardless of nesting (see implementation notes on
14
- * why candidate selection is deliberately unconditional).
14
+ * why candidate selection is unconditional on purpose).
15
15
  * @expectation
16
16
  * No contentinfo candidate has an ancestor that is itself any landmark
17
17
  * region. A contentinfo nested inside another landmark is not a
18
18
  * top-level, whole-page footer region and confuses landmark-based
19
19
  * navigation for assistive technology users.
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 (this rule mirrors
24
24
  * its structure with contentinfo/footer in place of banner/header).
25
25
  * - Candidate selection (`isContentinfoCandidate`) requires the element to
26
26
  * really carry the contentinfo role, via the suppression-aware
27
- * `getLandmarkRole` — same reasoning as landmark-banner-is-top-level.
27
+ * `getLandmarkRole`, same reasoning as landmark-banner-is-top-level.
28
28
  */
29
29
 
30
30
  const id = 'landmark-contentinfo-is-top-level';
@@ -79,7 +79,7 @@ function runInPage(ctx) {
79
79
 
80
80
  // Delegates to the shared helpers.hasLandmarkScopingAncestor for the
81
81
  // question "does this element sit inside a sectioning-content/<main>
82
- // ancestor that suppresses its conditional implicit role" — role-aware
82
+ // ancestor that suppresses its conditional implicit role": role-aware
83
83
  // (an ancestor's bare TAG only counts when it carries no role attribute
84
84
  // at all; an explicit role="dialog"-style override no longer suppresses)
85
85
  // rather than a local tag-only copy. See that function's header comment
@@ -98,7 +98,7 @@ function runInPage(ctx) {
98
98
  if (tag === 'main') return 'main';
99
99
  if (tag === 'nav') return 'navigation';
100
100
  if (tag === 'aside') {
101
- // A named <aside> is never suppressed, even when nested — it keeps
101
+ // A named <aside> is never suppressed, even when nested. It keeps
102
102
  // "complementary" when it has an accessible name, even inside
103
103
  // sectioning content. Matches landmark-unique's precedent.
104
104
  if (!hasSectioningAncestor(el, false)) return 'complementary';
@@ -127,12 +127,12 @@ function runInPage(ctx) {
127
127
  return getImplicitLandmarkRole(el);
128
128
  }
129
129
 
130
- // Candidate selection is deliberately NOT the same as getLandmarkRole()
131
- // === 'contentinfo' — see the header comment above. A <footer> is
130
+ // Candidate selection is NOT the same as getLandmarkRole()
131
+ // === 'contentinfo'; see the header comment above. A <footer> is
132
132
  // a candidate purely by tag + absence of any role attribute, independent
133
133
  // of whether sectioning-ancestor nesting would currently suppress its
134
134
  // implicit role; an explicit role="contentinfo" is always a candidate too.
135
- // A candidate must actually have the contentinfo role — a <footer> inside
135
+ // A candidate must actually have the contentinfo role: a <footer> inside
136
136
  // article/aside/main/nav/section is not one, so flagging it as nested
137
137
  // would report a landmark that does not exist.
138
138
  function isContentinfoCandidate(el) {
@@ -6,7 +6,7 @@
6
6
  * @check landmark-main-is-top-level
7
7
  * @atomic true
8
8
  * @summary The main 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 main landmark
12
12
  * (explicit role="main", or an implicit <main> element).
@@ -16,15 +16,15 @@
16
16
  * whole-page main content area and confuses landmark-based navigation
17
17
  * for assistive technology users.
18
18
  * @implementation-notes
19
- * - Not WCAG-normative — authored as an advisory, cantTell-capped
19
+ * - Not WCAG-normative, authored as an advisory, cantTell-capped
20
20
  * `type: 'manual'` rule; see landmark-banner-is-top-level's
21
21
  * header comment for the shared rationale/precedent (this rule mirrors
22
22
  * its structure with main in place of banner/header).
23
23
  * - Unlike landmark-banner-is-top-level/landmark-contentinfo-is-top-level
24
24
  * (see that file's header comment), candidate selection here doesn't
25
25
  * need to be unconditional: `<main>`'s implicit role is unconditional
26
- * per HTML-AAM — unlike `<header>`/`<footer>`, nesting never suppresses
27
- * it — so `getImplicitLandmarkRole`'s `main` branch is never subject to
26
+ * per HTML-AAM. Unlike `<header>`/`<footer>`, nesting never suppresses
27
+ * it, so `getImplicitLandmarkRole`'s `main` branch is never subject to
28
28
  * the self-defeating candidate-selection problem those two rules guard
29
29
  * against.
30
30
  */
@@ -81,7 +81,7 @@ function runInPage(ctx) {
81
81
 
82
82
  // Delegates to the shared helpers.hasLandmarkScopingAncestor for the
83
83
  // question "does this element sit inside a sectioning-content/<main>
84
- // ancestor that suppresses its conditional implicit role" — role-aware
84
+ // ancestor that suppresses its conditional implicit role": role-aware
85
85
  // (an ancestor's bare TAG only counts when it carries no role attribute
86
86
  // at all; an explicit role="dialog"-style override no longer suppresses)
87
87
  // rather than a local tag-only copy. See that function's header comment
@@ -100,7 +100,7 @@ function runInPage(ctx) {
100
100
  if (tag === 'main') return 'main';
101
101
  if (tag === 'nav') return 'navigation';
102
102
  if (tag === 'aside') {
103
- // A named <aside> is never suppressed, even when nested — it keeps
103
+ // A named <aside> is never suppressed, even when nested. It keeps
104
104
  // "complementary" when it has an accessible name, even inside
105
105
  // sectioning content. Matches landmark-unique's precedent.
106
106
  if (!hasSectioningAncestor(el, false)) return 'complementary';
@@ -6,19 +6,19 @@
6
6
  * @check landmark-no-duplicate-banner
7
7
  * @atomic true
8
8
  * @summary A page must not have more than one banner 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 landmark
12
- * (explicit role="banner", or an implicit, non-nested <header> — see
12
+ * (explicit role="banner", or an implicit, non-nested <header>; see
13
13
  * landmark-banner-is-top-level's implementation notes for the
14
14
  * shared landmark-detection model).
15
15
  * @expectation
16
16
  * At most one banner landmark exists on the page. Per WAI-ARIA
17
17
  * Authoring Practices, the banner landmark represents site-oriented
18
- * content that identifies the page as a whole — having more than one
18
+ * content that identifies the page as a whole, so having more than one
19
19
  * is ambiguous for assistive technology users navigating by landmark.
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
  * - Flags every banner instance (not just the "extra" ones) when more
@@ -81,11 +81,11 @@ function runInPage(ctx) {
81
81
 
82
82
  // Delegates to the shared helpers.hasLandmarkScopingAncestor for the
83
83
  // question "does this element sit inside a sectioning-content/<main>
84
- // ancestor that suppresses its conditional implicit role" — role-aware
84
+ // ancestor that suppresses its conditional implicit role": role-aware
85
85
  // (an ancestor's bare TAG only counts when it carries no role attribute
86
86
  // at all; an explicit role="dialog"-style override no longer suppresses)
87
87
  // rather than a local tag-only copy. See that function's header comment
88
- // in src/core/aria-helpers.js for the full algorithm — e.g. an
88
+ // in src/core/aria-helpers.js for the full algorithm, e.g. an
89
89
  // <aside role="dialog"> containing its own <header>, where the <header>
90
90
  // keeps its banner role.
91
91
  function hasSectioningAncestor(el, includeMain) {
@@ -6,18 +6,18 @@
6
6
  * @check landmark-no-duplicate-contentinfo
7
7
  * @atomic true
8
8
  * @summary A page must not have more than one contentinfo 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 contentinfo landmark
12
12
  * (explicit role="contentinfo", or an implicit, non-nested <footer>).
13
13
  * @expectation
14
- * At most one contentinfo landmark exists on the page — mirrors
14
+ * At most one contentinfo landmark exists on the page, mirroring
15
15
  * landmark-no-duplicate-banner's rationale for contentinfo.
16
16
  * @implementation-notes
17
- * - Not WCAG-normative — authored as an advisory, cantTell-capped
17
+ * - Not WCAG-normative, authored as an advisory, cantTell-capped
18
18
  * `type: 'manual'` rule; see landmark-banner-is-top-level's
19
19
  * header comment for the shared rationale/precedent.
20
- * - Only landmarks actually exposed to assistive technology can collide —
20
+ * - Only landmarks actually exposed to assistive technology can collide,
21
21
  * same as the sibling banner/main rules, avoiding hidden-duplicate false
22
22
  * positives.
23
23
  */
@@ -74,7 +74,7 @@ function runInPage(ctx) {
74
74
 
75
75
  // Delegates to the shared helpers.hasLandmarkScopingAncestor for the
76
76
  // question "does this element sit inside a sectioning-content/<main>
77
- // ancestor that suppresses its conditional implicit role" — role-aware
77
+ // ancestor that suppresses its conditional implicit role": role-aware
78
78
  // (an ancestor's bare TAG only counts when it carries no role attribute
79
79
  // at all; an explicit role="dialog"-style override no longer suppresses)
80
80
  // rather than a local tag-only copy. See that function's header comment
@@ -93,7 +93,7 @@ function runInPage(ctx) {
93
93
  if (tag === 'main') return 'main';
94
94
  if (tag === 'nav') return 'navigation';
95
95
  if (tag === 'aside') {
96
- // A named <aside> is never suppressed, even when nested — it keeps
96
+ // A named <aside> is never suppressed, even when nested. It keeps
97
97
  // "complementary" when it has an accessible name, even inside
98
98
  // sectioning content. Matches landmark-unique's precedent.
99
99
  if (!hasSectioningAncestor(el, false)) return 'complementary';
@@ -6,7 +6,7 @@
6
6
  * @check landmark-no-duplicate-main
7
7
  * @atomic true
8
8
  * @summary A page must not have more than one main 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 main landmark
12
12
  * (explicit role="main", or an implicit <main>).
@@ -15,7 +15,7 @@
15
15
  * decision from landmark-one-main (that rule flags zero
16
16
  * mains too; this one only flags more than one).
17
17
  * @implementation-notes
18
- * - Not WCAG-normative — authored as an advisory, cantTell-capped
18
+ * - Not WCAG-normative, authored as an advisory, cantTell-capped
19
19
  * `type: 'manual'` rule; see landmark-banner-is-top-level's
20
20
  * header comment for the shared rationale/precedent.
21
21
  * - Only landmarks actually exposed to assistive technology can collide.
@@ -6,24 +6,24 @@
6
6
  * @check landmark-one-main
7
7
  * @atomic true
8
8
  * @summary The page should have a main 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
- * Always applicable to any HTML document with a <body> element —
11
+ * Always applicable to any HTML document with a <body> element:
12
12
  * "does the page have a main landmark" is a whole-page concern,
13
13
  * matching bypass-blocks-present's pattern of evaluating the
14
14
  * document directly.
15
15
  * @expectation
16
16
  * At least one main landmark (role="main" or <main>), exposed to
17
- * assistive technology, exists on the page — a page with none gives
17
+ * assistive technology, exists on the page. A page with none gives
18
18
  * AT users no landmark to jump straight to for the primary content.
19
19
  * @implementation-notes
20
- * - Not WCAG-normative — authored as an advisory, cantTell-capped
20
+ * - Not WCAG-normative, authored as an advisory, cantTell-capped
21
21
  * `type: 'manual'` rule; see landmark-banner-is-top-level's
22
22
  * header comment for the shared rationale/precedent.
23
23
  * - Presence-only: a plain descendant-exists test. It does NOT flag more
24
24
  * than one main. "More than one main" is a separate rule,
25
- * `landmark-no-duplicate-main`, already implemented — deliberately not
26
- * duplicated here, since a page can genuinely have two visible `<main>`
25
+ * `landmark-no-duplicate-main`, already implemented and not
26
+ * duplicated here, since a page can legitimately have two visible `<main>`
27
27
  * elements, which is out of scope for "does a main landmark exist," not
28
28
  * a violation this rule should report.
29
29
  * - Filters candidates through `isAccTreeEligible` (hidden/aria-hidden/
@@ -6,20 +6,20 @@
6
6
  * @check landmark-unique
7
7
  * @atomic true
8
8
  * @summary Landmarks sharing the same role must have unique accessible names
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 two or more landmark regions on the page share the
12
12
  * same landmark role (banner, contentinfo, main, navigation,
13
- * complementary, region, form, or search — see implementation notes
13
+ * complementary, region, form, or search; see implementation notes
14
14
  * for the detection model).
15
15
  * @expectation
16
16
  * Among landmarks sharing a role, each has a distinct accessible name
17
- * (via aria-label/aria-labelledby — landmarks are not named from
17
+ * (via aria-label/aria-labelledby; landmarks are not named from
18
18
  * content). Two same-role landmarks with the same name (including two
19
19
  * both left unnamed) are indistinguishable to assistive technology
20
20
  * users navigating by landmark.
21
21
  * @implementation-notes
22
- * - Not WCAG-normative — authored as an advisory, cantTell-capped
22
+ * - Not WCAG-normative, authored as an advisory, cantTell-capped
23
23
  * `type: 'manual'` rule; see landmark-banner-is-top-level's
24
24
  * header comment for the shared rationale/precedent and the landmark-
25
25
  * detection model (HTML-AAM implicit-role mapping + explicit role
@@ -80,18 +80,18 @@ function runInPage(ctx) {
80
80
 
81
81
  // Delegates to the shared helpers.hasLandmarkScopingAncestor (role-aware:
82
82
  // an ancestor's bare TAG only counts when it carries no role attribute at
83
- // all; an explicit role="dialog"-style override no longer suppresses —
83
+ // all; an explicit role="dialog"-style override no longer suppresses;
84
84
  // see that function's header comment in src/core/aria-helpers.js), using
85
85
  // two distinct ancestor scopes rather than one shared list: <header>/
86
86
  // <footer> use "sectioning content PLUS <main>" (includeMain: true) to
87
87
  // decide banner/contentinfo suppression, but <aside> uses PLAIN
88
- // sectioning content only — NOT main (includeMain: false) — to decide
88
+ // sectioning content only, not main (includeMain: false), to decide
89
89
  // complementary suppression. A single shared sectioning-ancestors set
90
90
  // that includes 'main' is correct for header/footer but wrong for aside:
91
91
  // e.g. two unnamed <aside> elements that are direct children of <main>
92
92
  // would have their implicit "complementary" role incorrectly suppressed,
93
93
  // hiding a real duplicate-landmark violation. The role-aware half matters
94
- // too: e.g. an <aside role="dialog"> containing its own <header> —
94
+ // too: take an <aside role="dialog"> containing its own <header>.
95
95
  // role="dialog" isn't one of the four scoping roles, so the nested
96
96
  // <header> keeps "banner" per spec, but a tag-only (non-role-aware)
97
97
  // check would unconditionally suppress it just because the ancestor TAG
@@ -110,7 +110,7 @@ function runInPage(ctx) {
110
110
  if (tag === 'nav') return 'navigation';
111
111
  if (tag === 'aside') {
112
112
  // An <aside> is suppressed by a sectioning-content ancestor ONLY
113
- // when it also has no accessible name — a named <aside> is never
113
+ // when it also has no accessible name. A named <aside> is never
114
114
  // suppressed, even when nested.
115
115
  if (!hasSectioningAncestor(el, false)) return 'complementary';
116
116
  return getAccessibleLandmarkName(el) ? 'complementary' : '';
@@ -137,7 +137,7 @@ function runInPage(ctx) {
137
137
  if (explicit) {
138
138
  if (!LANDMARK_ROLES.has(explicit)) return '';
139
139
  // <form>/<section> only count as landmarks when they have an
140
- // accessible name — a property of the ELEMENT, not of how the role
140
+ // accessible name, a property of the ELEMENT, not of how the role
141
141
  // got there. This applies whether the role is implicit (already
142
142
  // handled in getImplicitLandmarkRole below) or explicit. Per the W3C
143
143
  // ARIA-in-HTML spec ("a form is not exposed as a landmark region