@surea11y/core 1.4.1 → 1.6.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 +212 -128
- package/README.md +46 -9
- package/docs/ACT_RULE_MAPPING.md +243 -0
- package/docs/API_STABILITY.md +4 -4
- package/docs/BINDING_AUTHORS_GUIDE.md +3 -3
- package/docs/DESIGN_CHALLENGES.md +301 -0
- package/docs/ENGINE_OPTIONS.md +30 -14
- package/docs/I18N.md +176 -20
- package/docs/INTEGRATION.md +29 -7
- package/docs/LIMITATIONS.md +6 -4
- package/docs/OUTPUT_SCHEMA.md +13 -3
- package/docs/REPORT.md +3 -1
- package/docs/RULE_AUTHORING.md +104 -23
- package/docs/RULE_CATALOG.md +1878 -169
- package/docs/RULE_TAXONOMY.md +2 -2
- package/docs/TROUBLESHOOTING.md +4 -4
- package/docs/WCAG_CONFORMANCE.md +25 -9
- package/package.json +8 -7
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +95 -40
- package/src/checks/automatic/aria-allowed-role.js +16 -18
- package/src/checks/automatic/aria-braille-equivalent.js +19 -21
- package/src/checks/automatic/aria-conditional-attr.js +22 -24
- package/src/checks/automatic/aria-deprecated-role.js +63 -50
- package/src/checks/automatic/aria-hidden-body.js +4 -11
- package/src/checks/automatic/aria-hidden-focus.js +104 -23
- package/src/checks/automatic/aria-prohibited-attr.js +71 -72
- package/src/checks/automatic/aria-prohibited-children.js +154 -61
- package/src/checks/automatic/aria-required-attr.js +74 -29
- package/src/checks/automatic/aria-required-children.js +38 -34
- package/src/checks/automatic/aria-required-parent.js +78 -29
- package/src/checks/automatic/aria-role-name-present.js +36 -22
- package/src/checks/automatic/aria-roles-valid.js +37 -23
- package/src/checks/automatic/aria-valid-attr-value.js +33 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -18
- package/src/checks/automatic/autocomplete-valid.js +17 -19
- package/src/checks/automatic/avoid-inline-spacing.js +14 -16
- package/src/checks/automatic/binary-control-name-present.js +46 -26
- package/src/checks/automatic/button-name-present.js +115 -34
- package/src/checks/automatic/combobox-name-present.js +40 -22
- package/src/checks/automatic/contrast-computable.js +32 -0
- package/src/checks/automatic/contrast-enhanced.js +21 -1
- package/src/checks/automatic/contrast-minimum.js +21 -1
- package/src/checks/automatic/css-orientation-lock.js +118 -41
- package/src/checks/automatic/definition-list-children-valid.js +25 -29
- package/src/checks/automatic/deprecated-elements-not-used.js +15 -17
- package/src/checks/automatic/dialog-name-present.js +36 -20
- package/src/checks/automatic/dlitem-parent-valid.js +15 -17
- package/src/checks/automatic/duplicate-id-aria.js +50 -40
- package/src/checks/automatic/duplicate-id.js +198 -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 +39 -41
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
- package/src/checks/automatic/iframe-focusable-content.js +92 -38
- package/src/checks/automatic/iframe-name-present.js +53 -21
- package/src/checks/automatic/iframe-title-unique.js +19 -24
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +198 -49
- package/src/checks/automatic/link-in-text-block.js +29 -31
- package/src/checks/automatic/link-name-present.js +47 -31
- package/src/checks/automatic/list-children-valid.js +21 -23
- package/src/checks/automatic/listbox-name-present.js +42 -24
- package/src/checks/automatic/listitem-parent-valid.js +18 -21
- package/src/checks/automatic/menuitem-name-present.js +36 -20
- package/src/checks/automatic/meta-refresh-no-exceptions.js +39 -31
- package/src/checks/automatic/meta-refresh-timing-absent.js +29 -25
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
- package/src/checks/automatic/meter-name-present.js +38 -21
- package/src/checks/automatic/nested-interactive-controls-absent.js +22 -24
- package/src/checks/automatic/option-name-present.js +39 -22
- 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 +41 -24
- package/src/checks/automatic/role-img-alt-present.js +64 -16
- package/src/checks/automatic/searchbox-name-present.js +46 -24
- package/src/checks/automatic/server-side-image-map-absent.js +16 -19
- package/src/checks/automatic/slider-name-present.js +42 -23
- package/src/checks/automatic/spinbutton-name-present.js +46 -24
- package/src/checks/automatic/summary-name-present.js +34 -20
- 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 +37 -20
- package/src/checks/automatic/table-headers-attr-valid.js +57 -24
- package/src/checks/automatic/table-th-has-data-cells.js +76 -24
- package/src/checks/automatic/target-size-minimum.js +172 -131
- package/src/checks/automatic/td-has-header.js +20 -25
- package/src/checks/automatic/textbox-name-present.js +42 -24
- package/src/checks/automatic/tooltip-name-present.js +37 -20
- package/src/checks/automatic/treeitem-name-present.js +39 -22
- package/src/checks/automatic/valid-lang.js +107 -24
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +21 -22
- 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 +23 -26
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +48 -38
- 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 +73 -28
- package/src/checks/manual/empty-table-header-manual.js +33 -36
- package/src/checks/manual/focus-order-semantics-manual.js +15 -15
- package/src/checks/manual/form-control-label-quality-manual.js +453 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +4 -11
- package/src/checks/manual/heading-order-manual.js +20 -25
- 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 +18 -21
- 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 +19 -21
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +21 -24
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +23 -26
- package/src/checks/manual/landmark-main-is-top-level-manual.js +20 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +8 -13
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +8 -13
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +4 -9
- package/src/checks/manual/landmark-one-main-manual.js +8 -15
- package/src/checks/manual/landmark-unique-manual.js +31 -36
- package/src/checks/manual/link-name-quality-manual.js +162 -35
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +16 -19
- package/src/checks/manual/mouse-only-event-handlers-manual.js +26 -28
- 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 +8 -15
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/presentation-role-conflict-manual.js +74 -46
- package/src/checks/manual/region-manual.js +32 -25
- package/src/checks/manual/scope-attr-valid-manual.js +16 -19
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +44 -52
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +16 -19
- package/src/checks/manual/table-duplicate-name-manual.js +16 -19
- 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 +13086 -5169
- package/src/report.js +16 -2
- package/surea11y.browser.js +5665 -4219
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
- package/bin/surea11y-core.js +0 -20
|
@@ -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';
|
|
@@ -192,23 +192,20 @@ function runInPage(ctx) {
|
|
|
192
192
|
for (const el of mains) {
|
|
193
193
|
if (!hasLandmarkAncestor(el)) continue;
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
details: { reasonCode: 'LANDMARK_MAIN_NOT_TOP_LEVEL' }
|
|
210
|
-
}
|
|
211
|
-
});
|
|
195
|
+
occurrences.push(
|
|
196
|
+
helpers.reportOccurrence(el, {
|
|
197
|
+
summary: 'This main landmark is nested inside another landmark region.',
|
|
198
|
+
hint: 'Move the main landmark (<main>/role="main") so it is not contained by another landmark; main should be a top-level region of the page.',
|
|
199
|
+
i18n: {
|
|
200
|
+
summaryKey: 'landmarkMainIsTopLevel_summary_cantTell',
|
|
201
|
+
hintKey: 'landmarkMainIsTopLevel_hint_cantTell',
|
|
202
|
+
params: {}
|
|
203
|
+
},
|
|
204
|
+
data: {
|
|
205
|
+
details: { reasonCode: 'LANDMARK_MAIN_NOT_TOP_LEVEL' }
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
);
|
|
212
209
|
}
|
|
213
210
|
|
|
214
211
|
if (occurrences.length) {
|
|
@@ -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) {
|
|
@@ -171,12 +171,7 @@ function runInPage(ctx) {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
const occurrences = banners.map((el) => {
|
|
174
|
-
|
|
175
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
selector: stableSelector,
|
|
179
|
-
html,
|
|
174
|
+
return helpers.reportOccurrence(el, {
|
|
180
175
|
summary: 'This page has more than one banner landmark.',
|
|
181
176
|
hint: 'Keep only one banner landmark (header/role="banner") per page.',
|
|
182
177
|
i18n: {
|
|
@@ -187,7 +182,7 @@ function runInPage(ctx) {
|
|
|
187
182
|
data: {
|
|
188
183
|
details: { reasonCode: 'LANDMARK_DUPLICATE_BANNER', count: banners.length }
|
|
189
184
|
}
|
|
190
|
-
};
|
|
185
|
+
});
|
|
191
186
|
});
|
|
192
187
|
|
|
193
188
|
return {
|
|
@@ -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';
|
|
@@ -163,12 +163,7 @@ function runInPage(ctx) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
const occurrences = contentinfos.map((el) => {
|
|
166
|
-
|
|
167
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
selector: stableSelector,
|
|
171
|
-
html,
|
|
166
|
+
return helpers.reportOccurrence(el, {
|
|
172
167
|
summary: 'This page has more than one contentinfo landmark.',
|
|
173
168
|
hint: 'Keep only one contentinfo landmark (footer/role="contentinfo") per page.',
|
|
174
169
|
i18n: {
|
|
@@ -179,7 +174,7 @@ function runInPage(ctx) {
|
|
|
179
174
|
data: {
|
|
180
175
|
details: { reasonCode: 'LANDMARK_DUPLICATE_CONTENTINFO', count: contentinfos.length }
|
|
181
176
|
}
|
|
182
|
-
};
|
|
177
|
+
});
|
|
183
178
|
});
|
|
184
179
|
|
|
185
180
|
return {
|
|
@@ -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.
|
|
@@ -108,12 +108,7 @@ function runInPage(ctx) {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
const occurrences = mains.map((el) => {
|
|
111
|
-
|
|
112
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
selector: stableSelector,
|
|
116
|
-
html,
|
|
111
|
+
return helpers.reportOccurrence(el, {
|
|
117
112
|
summary: 'This page has more than one main landmark.',
|
|
118
113
|
hint: 'Keep only one main landmark (<main>/role="main") per page.',
|
|
119
114
|
i18n: {
|
|
@@ -124,7 +119,7 @@ function runInPage(ctx) {
|
|
|
124
119
|
data: {
|
|
125
120
|
details: { reasonCode: 'LANDMARK_DUPLICATE_MAIN', count: mains.length }
|
|
126
121
|
}
|
|
127
|
-
};
|
|
122
|
+
});
|
|
128
123
|
});
|
|
129
124
|
|
|
130
125
|
return {
|
|
@@ -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/
|
|
@@ -123,19 +123,12 @@ function runInPage(ctx) {
|
|
|
123
123
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
|
|
127
|
-
const html = helpers.getOuterHtmlSnippet
|
|
128
|
-
? helpers.getOuterHtmlSnippet(body)
|
|
129
|
-
: (body.outerHTML || '').slice(0, 200);
|
|
130
|
-
|
|
131
126
|
return {
|
|
132
127
|
ruleId: rule.ruleId,
|
|
133
128
|
outcome: 'cantTell',
|
|
134
129
|
severity: rule.defaultSeverity || 'minor',
|
|
135
130
|
occurrences: [
|
|
136
|
-
{
|
|
137
|
-
selector: stableSelector,
|
|
138
|
-
html,
|
|
131
|
+
helpers.reportOccurrence(body, {
|
|
139
132
|
summary: 'This page has no main landmark.',
|
|
140
133
|
hint: 'Add a main landmark (<main> or role="main") around the page\'s primary content.',
|
|
141
134
|
i18n: {
|
|
@@ -146,7 +139,7 @@ function runInPage(ctx) {
|
|
|
146
139
|
data: {
|
|
147
140
|
details: { reasonCode: 'LANDMARK_MAIN_MISSING' }
|
|
148
141
|
}
|
|
149
|
-
}
|
|
142
|
+
})
|
|
150
143
|
]
|
|
151
144
|
};
|
|
152
145
|
}
|
|
@@ -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
|
|
@@ -219,34 +219,29 @@ function runInPage(ctx) {
|
|
|
219
219
|
if (group.length <= 1) continue;
|
|
220
220
|
|
|
221
221
|
for (const { el } of group) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
details: {
|
|
243
|
-
reasonCode: 'LANDMARK_NOT_UNIQUE',
|
|
244
|
-
role,
|
|
245
|
-
name: normalizedName,
|
|
246
|
-
groupSize: group.length
|
|
222
|
+
occurrences.push(
|
|
223
|
+
helpers.reportOccurrence(el, {
|
|
224
|
+
summary: normalizedName
|
|
225
|
+
? `This ${role} landmark shares its accessible name with another ${role} landmark.`
|
|
226
|
+
: `This ${role} landmark has no accessible name, and more than one unnamed ${role} landmark exists on this page.`,
|
|
227
|
+
hint: `Give each ${role} landmark a distinct name via aria-label or aria-labelledby.`,
|
|
228
|
+
i18n: {
|
|
229
|
+
summaryKey: normalizedName
|
|
230
|
+
? 'landmarkUnique_summary_cantTell_duplicateName'
|
|
231
|
+
: 'landmarkUnique_summary_cantTell_bothUnnamed',
|
|
232
|
+
hintKey: 'landmarkUnique_hint_cantTell',
|
|
233
|
+
params: { role }
|
|
234
|
+
},
|
|
235
|
+
data: {
|
|
236
|
+
details: {
|
|
237
|
+
reasonCode: 'LANDMARK_NOT_UNIQUE',
|
|
238
|
+
role,
|
|
239
|
+
name: normalizedName,
|
|
240
|
+
groupSize: group.length
|
|
241
|
+
}
|
|
247
242
|
}
|
|
248
|
-
}
|
|
249
|
-
|
|
243
|
+
})
|
|
244
|
+
);
|
|
250
245
|
}
|
|
251
246
|
}
|
|
252
247
|
}
|