@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.
- package/CHANGELOG.md +240 -149
- package/README.md +51 -44
- package/docs/ACT_RULE_MAPPING.md +245 -0
- package/docs/API_STABILITY.md +53 -5
- package/docs/BINDING_AUTHORS_GUIDE.md +106 -4
- package/docs/DESIGN_CHALLENGES.md +367 -0
- package/docs/EARL.md +100 -0
- package/docs/ENGINE_OPTIONS.md +42 -4
- package/docs/I18N.md +4 -4
- package/docs/INTEGRATION.md +4 -2
- package/docs/LIMITATIONS.md +9 -5
- package/docs/OUTPUT_SCHEMA.md +44 -6
- package/docs/POLICY.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_AUTHORING.md +63 -36
- package/docs/RULE_CATALOG.md +1928 -169
- package/docs/RULE_HELPERS.md +333 -0
- package/docs/RULE_TAXONOMY.md +27 -6
- package/docs/SARIF.md +21 -2
- package/docs/TROUBLESHOOTING.md +2 -2
- package/docs/WCAG_CONFORMANCE.md +34 -10
- package/package.json +11 -9
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +74 -10
- package/src/checks/automatic/aria-allowed-role.js +34 -25
- package/src/checks/automatic/aria-braille-equivalent.js +21 -13
- package/src/checks/automatic/aria-conditional-attr.js +22 -15
- package/src/checks/automatic/aria-deprecated-role.js +13 -1
- package/src/checks/automatic/aria-hidden-body.js +3 -3
- package/src/checks/automatic/aria-hidden-focus.js +5 -5
- package/src/checks/automatic/aria-prohibited-attr.js +23 -18
- package/src/checks/automatic/aria-prohibited-children.js +136 -43
- package/src/checks/automatic/aria-required-attr.js +119 -24
- package/src/checks/automatic/aria-required-children.js +54 -30
- package/src/checks/automatic/aria-required-parent.js +93 -15
- package/src/checks/automatic/aria-role-name-present.js +37 -23
- package/src/checks/automatic/aria-roles-valid.js +52 -21
- package/src/checks/automatic/aria-valid-attr-value.js +89 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -10
- package/src/checks/automatic/autocomplete-valid.js +2 -2
- package/src/checks/automatic/avoid-inline-spacing.js +133 -6
- package/src/checks/automatic/binary-control-name-present.js +27 -5
- package/src/checks/automatic/button-name-present.js +92 -6
- package/src/checks/automatic/combobox-name-present.js +26 -6
- package/src/checks/automatic/contrast-computable.js +42 -0
- package/src/checks/automatic/contrast-enhanced.js +33 -1
- package/src/checks/automatic/contrast-minimum.js +33 -1
- package/src/checks/automatic/css-orientation-lock.js +138 -24
- package/src/checks/automatic/definition-list-children-valid.js +7 -8
- package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
- package/src/checks/automatic/dialog-name-present.js +20 -2
- package/src/checks/automatic/duplicate-id-aria.js +10 -3
- package/src/checks/automatic/duplicate-id.js +203 -0
- package/src/checks/automatic/embed-text-alternative-present.js +2 -2
- package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
- package/src/checks/automatic/form-control-single-label.js +10 -1
- package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
- package/src/checks/automatic/iframe-focusable-content.js +68 -7
- package/src/checks/automatic/iframe-name-present.js +37 -3
- package/src/checks/automatic/iframe-title-unique.js +1 -1
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +204 -68
- package/src/checks/automatic/link-in-text-block.js +285 -29
- package/src/checks/automatic/link-name-present.js +22 -1
- package/src/checks/automatic/list-children-valid.js +6 -6
- package/src/checks/automatic/listbox-name-present.js +28 -8
- package/src/checks/automatic/listitem-parent-valid.js +4 -4
- package/src/checks/automatic/menuitem-name-present.js +20 -2
- package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
- package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
- package/src/checks/automatic/meter-name-present.js +23 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
- package/src/checks/automatic/option-name-present.js +23 -4
- package/src/checks/automatic/page-title-present.js +21 -3
- package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
- package/src/checks/automatic/progressbar-name-present.js +23 -4
- package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +64 -16
- package/src/checks/automatic/searchbox-name-present.js +28 -8
- package/src/checks/automatic/server-side-image-map-absent.js +1 -1
- package/src/checks/automatic/slider-name-present.js +27 -6
- package/src/checks/automatic/spinbutton-name-present.js +28 -8
- package/src/checks/automatic/summary-name-present.js +18 -2
- package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
- package/src/checks/automatic/svg-text-alternative-present.js +13 -10
- package/src/checks/automatic/tab-name-present.js +21 -2
- package/src/checks/automatic/table-headers-attr-valid.js +43 -8
- package/src/checks/automatic/table-th-has-data-cells.js +61 -5
- package/src/checks/automatic/target-size-minimum.js +155 -58
- package/src/checks/automatic/td-has-header.js +24 -23
- package/src/checks/automatic/textbox-name-present.js +28 -8
- package/src/checks/automatic/tooltip-name-present.js +21 -2
- package/src/checks/automatic/treeitem-name-present.js +23 -4
- package/src/checks/automatic/valid-lang.js +92 -7
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +3 -3
- package/src/checks/manual/area-alt-decorative-manual.js +7 -0
- package/src/checks/manual/area-alt-quality-manual.js +6 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +5 -5
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/empty-heading-manual.js +58 -11
- package/src/checks/manual/empty-table-header-manual.js +8 -8
- package/src/checks/manual/focus-order-semantics-manual.js +15 -15
- package/src/checks/manual/form-control-label-quality-manual.js +563 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -2
- package/src/checks/manual/heading-order-manual.js +3 -3
- package/src/checks/manual/heading-quality-manual.js +338 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
- package/src/checks/manual/image-redundant-alt-manual.js +4 -4
- package/src/checks/manual/img-alt-decorative-manual.js +211 -52
- package/src/checks/manual/img-alt-quality-manual.js +7 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
- package/src/checks/manual/label-title-only-manual.js +4 -4
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
- package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
- package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
- package/src/checks/manual/landmark-one-main-manual.js +6 -6
- package/src/checks/manual/landmark-unique-manual.js +9 -9
- package/src/checks/manual/link-name-quality-manual.js +161 -32
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +2 -2
- package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
- package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
- package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/p-as-heading-manual.js +4 -4
- package/src/checks/manual/page-has-heading-one-manual.js +6 -6
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/password-paste-enabled-manual.js +255 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
- package/src/checks/manual/region-manual.js +19 -19
- package/src/checks/manual/scope-attr-valid-manual.js +2 -2
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +5 -5
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +2 -2
- package/src/checks/manual/table-duplicate-name-manual.js +2 -2
- package/src/checks/manual/table-fake-caption-manual.js +2 -2
- package/src/checks/manual/video-caption-manual.js +3 -3
- package/src/checks/manual-review.js +17 -1
- package/src/core.js +8880 -41883
- package/src/earl.js +144 -0
- package/src/report.js +2 -2
- package/src/sarif.js +22 -2
- package/surea11y.browser.js +10 -37882
- package/surea11y.i18n.de.js +2 -21
- package/surea11y.i18n.es.js +2 -21
- package/surea11y.i18n.fr.js +2 -21
- 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
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
131
|
-
// === 'contentinfo'
|
|
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
|
|
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
|
|
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
|
|
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
|
|
27
|
-
* it
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
26
|
-
* duplicated here, since a page can
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|