@surea11y/core 1.1.1 → 1.2.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 +29 -0
- package/README.md +3 -0
- package/bin/core.js +107 -3
- package/docs/API_STABILITY.md +61 -0
- package/docs/BASELINE.md +66 -0
- package/docs/CLI.md +26 -0
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/INTEGRATION.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/REPORT.md +33 -0
- package/docs/RULE_AUTHORING.md +31 -0
- package/package.json +1 -1
- package/src/baseline.js +0 -0
- package/src/checks/automatic/aria-allowed-attr.js +2 -3
- package/src/checks/automatic/aria-allowed-role.js +2 -3
- package/src/checks/automatic/aria-braille-equivalent.js +3 -4
- package/src/checks/automatic/aria-conditional-attr.js +3 -4
- package/src/checks/automatic/aria-deprecated-role.js +2 -3
- package/src/checks/automatic/aria-hidden-body.js +9 -1
- package/src/checks/automatic/aria-prohibited-attr.js +2 -3
- package/src/checks/automatic/aria-prohibited-children.js +73 -17
- package/src/checks/automatic/aria-required-attr.js +2 -3
- package/src/checks/automatic/aria-required-children.js +2 -3
- package/src/checks/automatic/aria-required-parent.js +3 -4
- package/src/checks/automatic/aria-roles-valid.js +2 -3
- package/src/checks/automatic/aria-valid-attr-value.js +2 -3
- package/src/checks/automatic/aria-valid-attr.js +2 -3
- package/src/checks/automatic/autocomplete-valid.js +2 -3
- package/src/checks/automatic/avoid-inline-spacing.js +2 -3
- package/src/checks/automatic/binary-control-name-present.js +2 -3
- package/src/checks/automatic/button-name-present.js +14 -4
- package/src/checks/automatic/bypass-blocks-present.js +44 -8
- package/src/checks/automatic/combobox-name-present.js +2 -3
- package/src/checks/automatic/css-orientation-lock.js +9 -1
- package/src/checks/automatic/definition-list-children-valid.js +2 -3
- package/src/checks/automatic/deprecated-elements-not-used.js +2 -3
- package/src/checks/automatic/dialog-name-present.js +2 -3
- package/src/checks/automatic/dlitem-parent-valid.js +2 -3
- package/src/checks/automatic/form-control-single-label.js +2 -3
- package/src/checks/automatic/html-xml-lang-mismatch.js +9 -1
- package/src/checks/automatic/iframe-focusable-content.js +2 -3
- package/src/checks/automatic/iframe-name-present.js +2 -3
- package/src/checks/automatic/iframe-title-unique.js +2 -3
- package/src/checks/automatic/label-in-name.js +7 -8
- package/src/checks/automatic/language-page-present.js +9 -1
- package/src/checks/automatic/link-in-text-block.js +2 -3
- package/src/checks/automatic/link-name-present.js +16 -4
- package/src/checks/automatic/list-children-valid.js +2 -3
- package/src/checks/automatic/listbox-name-present.js +2 -3
- package/src/checks/automatic/listitem-parent-valid.js +2 -3
- package/src/checks/automatic/menuitem-name-present.js +2 -3
- package/src/checks/automatic/meta-refresh-no-exceptions.js +9 -1
- package/src/checks/automatic/meta-refresh-timing-absent.js +9 -1
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +9 -1
- package/src/checks/automatic/meter-name-present.js +2 -3
- package/src/checks/automatic/nested-interactive-controls-absent.js +2 -3
- package/src/checks/automatic/option-name-present.js +2 -3
- package/src/checks/automatic/page-title-present.js +9 -1
- package/src/checks/automatic/progressbar-name-present.js +2 -3
- package/src/checks/automatic/searchbox-name-present.js +2 -3
- package/src/checks/automatic/server-side-image-map-absent.js +2 -3
- package/src/checks/automatic/slider-name-present.js +2 -3
- package/src/checks/automatic/spinbutton-name-present.js +2 -3
- package/src/checks/automatic/summary-name-present.js +2 -3
- package/src/checks/automatic/tab-name-present.js +2 -3
- package/src/checks/automatic/table-headers-attr-valid.js +2 -3
- package/src/checks/automatic/table-th-has-data-cells.js +2 -3
- package/src/checks/automatic/td-has-header.js +2 -3
- package/src/checks/automatic/textbox-name-present.js +2 -3
- package/src/checks/automatic/tooltip-name-present.js +2 -3
- package/src/checks/automatic/treeitem-name-present.js +2 -3
- package/src/checks/automatic/valid-lang.js +2 -3
- package/src/checks/manual/accesskeys-manual.js +1 -7
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +3 -4
- package/src/checks/manual/aria-text-manual.js +2 -3
- package/src/checks/manual/empty-heading-manual.js +1 -6
- package/src/checks/manual/empty-table-header-manual.js +5 -9
- package/src/checks/manual/focus-order-semantics-manual.js +2 -3
- package/src/checks/manual/heading-order-manual.js +1 -6
- package/src/checks/manual/identical-links-same-purpose-manual.js +2 -3
- package/src/checks/manual/image-redundant-alt-manual.js +2 -3
- package/src/checks/manual/label-title-only-manual.js +2 -3
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +30 -14
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +30 -14
- package/src/checks/manual/landmark-main-is-top-level-manual.js +30 -14
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +25 -13
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +25 -13
- package/src/checks/manual/landmark-one-main-manual.js +9 -1
- package/src/checks/manual/landmark-unique-manual.js +32 -30
- package/src/checks/manual/link-name-quality-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +9 -1
- package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -3
- package/src/checks/manual/no-autoplay-audio-manual.js +2 -3
- package/src/checks/manual/p-as-heading-manual.js +2 -3
- package/src/checks/manual/page-has-heading-one-manual.js +40 -3
- package/src/checks/manual/page-title-patterns-manual.js +9 -1
- package/src/checks/manual/presentation-role-conflict-manual.js +3 -4
- package/src/checks/manual/region-manual.js +34 -14
- package/src/checks/manual/scope-attr-valid-manual.js +2 -7
- package/src/checks/manual/scrollable-region-focusable-manual.js +2 -3
- package/src/checks/manual/skip-link-manual.js +91 -9
- package/src/checks/manual/tabindex-manual.js +2 -7
- package/src/checks/manual/table-duplicate-name-manual.js +2 -3
- package/src/checks/manual/table-fake-caption-manual.js +2 -3
- package/src/checks/manual/video-caption-manual.js +2 -3
- package/src/core/aria-helpers.js +82 -18
- package/src/core/dom-helpers.js +88 -3
- package/src/core/dom-runner.js +23 -0
- package/src/core/rule-meta.js +19 -0
- package/src/core.js +2632 -885
- package/src/i18n/en.js +6 -2
- package/src/i18n/fr.js +6 -2
- package/src/report.js +482 -0
|
@@ -75,46 +75,48 @@ function runInPage(ctx) {
|
|
|
75
75
|
return raw.split(/\s+/)[0].toLowerCase();
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// <aside
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
78
|
+
// Delegates to the shared helpers.hasLandmarkScopingAncestor (role-aware:
|
|
79
|
+
// an ancestor's bare TAG only counts when it carries no role attribute at
|
|
80
|
+
// all; an explicit role="dialog"-style override no longer suppresses —
|
|
81
|
+
// see that function's header comment in src/core/aria-helpers.js) rather
|
|
82
|
+
// than the two local tag-only Sets this file used to carry. Two distinct
|
|
83
|
+
// ancestor scopes, verified 2026-07-20 against a widely-used reference
|
|
84
|
+
// engine's own implicit-role functions directly rather than assumed from
|
|
85
|
+
// one shared list: <header>/<footer> use "sectioning content PLUS <main>"
|
|
86
|
+
// (includeMain: true) to decide banner/contentinfo suppression, but
|
|
87
|
+
// <aside> uses PLAIN sectioning content only — NOT main (includeMain:
|
|
88
|
+
// false) — to decide complementary suppression. The old single
|
|
89
|
+
// SECTIONING_ANCESTORS set (which included 'main') was correct for
|
|
90
|
+
// header/footer but wrong for aside — found via a real page: Know Your
|
|
91
|
+
// Meme's two unnamed <aside class="extra-large-only"> elements are direct
|
|
92
|
+
// children of <main>, which incorrectly suppressed their implicit
|
|
93
|
+
// "complementary" role entirely, hiding a real duplicate-landmark
|
|
94
|
+
// violation that reference engine correctly flags. The tag-only
|
|
95
|
+
// (non-role-aware) half of this bug was separately found and fixed
|
|
96
|
+
// 2026-07-30 via the cross-engine comparisons project, on
|
|
97
|
+
// handsontable.com's docs-assistant side panel: an <aside role="dialog">
|
|
98
|
+
// containing its own <header> — role="dialog" isn't one of the four
|
|
99
|
+
// scoping roles, so the nested <header> keeps "banner" per spec, but a
|
|
100
|
+
// tag-only check unconditionally suppressed it just because the ancestor
|
|
101
|
+
// TAG was <aside>.
|
|
102
|
+
function hasSectioningAncestor(el, includeMain) {
|
|
103
|
+
return helpers && typeof helpers.hasLandmarkScopingAncestor === 'function'
|
|
104
|
+
? helpers.hasLandmarkScopingAncestor(el, { includeMain })
|
|
105
|
+
: false;
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
function getImplicitLandmarkRole(el) {
|
|
105
109
|
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
106
|
-
if (tag === 'header') return
|
|
107
|
-
if (tag === 'footer') return
|
|
110
|
+
if (tag === 'header') return hasSectioningAncestor(el, true) ? '' : 'banner';
|
|
111
|
+
if (tag === 'footer') return hasSectioningAncestor(el, true) ? '' : 'contentinfo';
|
|
108
112
|
if (tag === 'main') return 'main';
|
|
109
113
|
if (tag === 'nav') return 'navigation';
|
|
110
114
|
if (tag === 'aside') {
|
|
111
115
|
// Per a widely-used reference engine's own `aside` implicit-role function: suppressed by a
|
|
112
116
|
// sectioning-content ancestor ONLY when the <aside> also has no
|
|
113
117
|
// accessible name — a named <aside> is never suppressed, even when
|
|
114
|
-
// nested.
|
|
115
|
-
|
|
116
|
-
// leaving a known partial fix in place.
|
|
117
|
-
if (!hasSectioningAncestorFrom(el, SECTIONING_ANCESTORS)) return 'complementary';
|
|
118
|
+
// nested.
|
|
119
|
+
if (!hasSectioningAncestor(el, false)) return 'complementary';
|
|
118
120
|
return getAccessibleLandmarkName(el) ? 'complementary' : '';
|
|
119
121
|
}
|
|
120
122
|
if (tag === 'section') return getAccessibleLandmarkName(el) ? 'region' : '';
|
|
@@ -60,8 +60,7 @@ const meta = {
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
function runInPage(ctx) {
|
|
63
|
-
const { document,
|
|
64
|
-
const safeRoot = root || document;
|
|
63
|
+
const { document, helpers, rule } = ctx;
|
|
65
64
|
|
|
66
65
|
const GENERIC_LINK_TEXT = new Set([
|
|
67
66
|
'click here', 'here', 'click', 'more', 'more info', 'more information',
|
|
@@ -80,7 +79,7 @@ function runInPage(ctx) {
|
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
const selector = 'a[href]';
|
|
83
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
82
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
84
83
|
|
|
85
84
|
const occurrences = [];
|
|
86
85
|
let applicableCount = 0;
|
|
@@ -43,6 +43,14 @@ const meta = {
|
|
|
43
43
|
coverage: {}
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
// This check is inherently whole-document (does the PAGE have this
|
|
47
|
+
// property?), not evaluable per-subtree -- notApplicable when contextSelector
|
|
48
|
+
// scoped this run narrower than the whole document, or when
|
|
49
|
+
// engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
|
|
50
|
+
function applicability(ctx) {
|
|
51
|
+
return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
function runInPage(ctx) {
|
|
47
55
|
const { document, helpers, rule } = ctx;
|
|
48
56
|
|
|
@@ -116,4 +124,4 @@ function runInPage(ctx) {
|
|
|
116
124
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
module.exports = { id, meta, runInPage };
|
|
127
|
+
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -67,8 +67,7 @@ const meta = {
|
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
function runInPage(ctx) {
|
|
70
|
-
const { document,
|
|
71
|
-
const safeRoot = root || document;
|
|
70
|
+
const { document, helpers, rule } = ctx;
|
|
72
71
|
|
|
73
72
|
const MOUSE_ONLY_ATTRS = [
|
|
74
73
|
'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup',
|
|
@@ -79,7 +78,7 @@ function runInPage(ctx) {
|
|
|
79
78
|
function trim(v) { return (v == null ? '' : String(v)).trim(); }
|
|
80
79
|
|
|
81
80
|
const selector = MOUSE_ONLY_ATTRS.map((a) => `[${a}]`).join(', ');
|
|
82
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
81
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
83
82
|
|
|
84
83
|
const occurrences = [];
|
|
85
84
|
let applicableCount = 0;
|
|
@@ -58,10 +58,9 @@ const meta = {
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
function runInPage(ctx) {
|
|
61
|
-
const { document,
|
|
62
|
-
const safeRoot = root || document;
|
|
61
|
+
const { document, helpers, rule } = ctx;
|
|
63
62
|
|
|
64
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('audio[autoplay], video[autoplay]'
|
|
63
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('audio[autoplay], video[autoplay]') : helpers.queryAll('audio[autoplay], video[autoplay]');
|
|
65
64
|
|
|
66
65
|
const occurrences = [];
|
|
67
66
|
let applicableCount = 0;
|
|
@@ -53,8 +53,7 @@ const meta = {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
function runInPage(ctx) {
|
|
56
|
-
const { document,
|
|
57
|
-
const safeRoot = root || document;
|
|
56
|
+
const { document, helpers, rule } = ctx;
|
|
58
57
|
|
|
59
58
|
const MAX_HEADING_LIKE_CHARS = 120;
|
|
60
59
|
const MIN_FONT_SIZE_PX = 18;
|
|
@@ -106,7 +105,7 @@ function runInPage(ctx) {
|
|
|
106
105
|
return Number.isFinite(px) ? px : 0;
|
|
107
106
|
}
|
|
108
107
|
|
|
109
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('p'
|
|
108
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('p') : helpers.queryAll('p');
|
|
110
109
|
|
|
111
110
|
const occurrences = [];
|
|
112
111
|
let applicableCount = 0;
|
|
@@ -19,6 +19,18 @@
|
|
|
19
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.
|
|
22
|
+
* - Filters candidates through `isAccTreeEligible` (hidden/aria-hidden/
|
|
23
|
+
* display:none/inert elements don't count as "the page has a heading
|
|
24
|
+
* one"), matching `landmark-one-main`'s own precedent. Found via a real
|
|
25
|
+
* page (CDC's flu page, 2026-07-30): its only `<h1>` sits inside a
|
|
26
|
+
* `display:none` ancestor — genuinely unreachable by sighted and screen
|
|
27
|
+
* reader users alike — and a raw `document.querySelectorAll` credited it
|
|
28
|
+
* anyway, reporting `notApplicable` where axe-core correctly fails. This
|
|
29
|
+
* does NOT regress purely-visually-clipped-but-AT-exposed headings (e.g.
|
|
30
|
+
* eBay's homepage `<h1>` hidden via clip-path/off-screen positioning,
|
|
31
|
+
* `visibility:visible`, no `aria-hidden`) — `isAccTreeEligible` only
|
|
32
|
+
* excludes elements actually removed from the accessibility tree, not
|
|
33
|
+
* ones merely clipped from the visual viewport.
|
|
22
34
|
*/
|
|
23
35
|
|
|
24
36
|
const id = 'page-has-heading-one';
|
|
@@ -41,6 +53,14 @@ const meta = {
|
|
|
41
53
|
coverage: {}
|
|
42
54
|
};
|
|
43
55
|
|
|
56
|
+
// This check is inherently whole-document (does the PAGE have this
|
|
57
|
+
// property?), not evaluable per-subtree -- notApplicable when contextSelector
|
|
58
|
+
// scoped this run narrower than the whole document, or when
|
|
59
|
+
// engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
|
|
60
|
+
function applicability(ctx) {
|
|
61
|
+
return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
function runInPage(ctx) {
|
|
45
65
|
const { document, helpers, rule } = ctx;
|
|
46
66
|
|
|
@@ -70,14 +90,31 @@ function runInPage(ctx) {
|
|
|
70
90
|
return tag === 'h1';
|
|
71
91
|
}
|
|
72
92
|
|
|
93
|
+
const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
94
|
+
|
|
95
|
+
function isExposedToAt(el) {
|
|
96
|
+
if (!isAccTreeEligible) return true;
|
|
97
|
+
try {
|
|
98
|
+
const r = isAccTreeEligible(el, ctx);
|
|
99
|
+
if (typeof r === 'boolean') return r;
|
|
100
|
+
return !!(r && r.eligible);
|
|
101
|
+
} catch {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// queryAllSmart (shadow-DOM-aware) instead of plain document.querySelectorAll -- see
|
|
107
|
+
// landmark-one-main-manual.js's identical precedent.
|
|
73
108
|
let nodes = [];
|
|
74
109
|
try {
|
|
75
|
-
nodes =
|
|
110
|
+
nodes = helpers && typeof helpers.queryAllSmart === 'function'
|
|
111
|
+
? helpers.queryAllSmart('h1, [role]')
|
|
112
|
+
: document.querySelectorAll('h1, [role]');
|
|
76
113
|
} catch {
|
|
77
114
|
nodes = [];
|
|
78
115
|
}
|
|
79
116
|
|
|
80
|
-
const hasH1 = Array.from(nodes).some((el) => el && isLevelOneHeading(el));
|
|
117
|
+
const hasH1 = Array.from(nodes).some((el) => el && isLevelOneHeading(el) && isExposedToAt(el));
|
|
81
118
|
|
|
82
119
|
if (hasH1) {
|
|
83
120
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
@@ -107,4 +144,4 @@ function runInPage(ctx) {
|
|
|
107
144
|
};
|
|
108
145
|
}
|
|
109
146
|
|
|
110
|
-
module.exports = { id, meta, runInPage };
|
|
147
|
+
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -24,6 +24,14 @@ const meta = {
|
|
|
24
24
|
coverage: { facetsBySc: { '2.4.2': ['page-title-patterns'] } }
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
// This check is inherently whole-document (does the PAGE have this
|
|
28
|
+
// property?), not evaluable per-subtree -- notApplicable when contextSelector
|
|
29
|
+
// scoped this run narrower than the whole document, or when
|
|
30
|
+
// engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
|
|
31
|
+
function applicability(ctx) {
|
|
32
|
+
return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
function runInPage(ctx) {
|
|
28
36
|
const { document, helpers, rule } = ctx;
|
|
29
37
|
const probes = ctx && ctx.inputs && ctx.inputs.probes && typeof ctx.inputs.probes === 'object'
|
|
@@ -259,4 +267,4 @@ function runInPage(ctx) {
|
|
|
259
267
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
260
268
|
}
|
|
261
269
|
|
|
262
|
-
module.exports = { id, meta, runInPage };
|
|
270
|
+
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -73,8 +73,7 @@ const meta = {
|
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
function runInPage(ctx) {
|
|
76
|
-
const { document,
|
|
77
|
-
const safeRoot = root || document;
|
|
76
|
+
const { document, helpers, rule } = ctx;
|
|
78
77
|
|
|
79
78
|
// The full set of ARIA attributes marked `global: true` per the WAI-ARIA
|
|
80
79
|
// spec (confirmed against a widely-used reference engine's own
|
|
@@ -91,8 +90,8 @@ function runInPage(ctx) {
|
|
|
91
90
|
const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
92
91
|
|
|
93
92
|
const nodes = helpers.queryAllSmart
|
|
94
|
-
? helpers.queryAllSmart('[role="presentation"], [role="none"], img[alt=""]'
|
|
95
|
-
: helpers.queryAll('[role="presentation"], [role="none"], img[alt=""]'
|
|
93
|
+
? helpers.queryAllSmart('[role="presentation"], [role="none"], img[alt=""]')
|
|
94
|
+
: helpers.queryAll('[role="presentation"], [role="none"], img[alt=""]');
|
|
96
95
|
|
|
97
96
|
const occurrences = [];
|
|
98
97
|
let applicableCount = 0;
|
|
@@ -46,6 +46,14 @@ const meta = {
|
|
|
46
46
|
coverage: {}
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
// This check is inherently whole-document (does the PAGE have this
|
|
50
|
+
// property?), not evaluable per-subtree -- notApplicable when contextSelector
|
|
51
|
+
// scoped this run narrower than the whole document, or when
|
|
52
|
+
// engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
|
|
53
|
+
function applicability(ctx) {
|
|
54
|
+
return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
function runInPage(ctx) {
|
|
50
58
|
const { document, helpers, rule } = ctx;
|
|
51
59
|
|
|
@@ -79,25 +87,37 @@ function runInPage(ctx) {
|
|
|
79
87
|
return raw.split(/\s+/)[0].toLowerCase();
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
// Delegates to the shared helpers.hasLandmarkScopingAncestor for the
|
|
91
|
+
// question "does this element sit inside a sectioning-content/<main>
|
|
92
|
+
// ancestor that suppresses its conditional implicit role" — role-aware
|
|
93
|
+
// (an ancestor's bare TAG only counts when it carries no role attribute
|
|
94
|
+
// at all; an explicit role="dialog"-style override no longer suppresses)
|
|
95
|
+
// rather than a local tag-only copy. See that function's header comment
|
|
96
|
+
// in src/core/aria-helpers.js for the full algorithm and the real page
|
|
97
|
+
// (handsontable.com's docs-assistant side panel, an
|
|
98
|
+
// <aside role="dialog"> containing its own <header>) that surfaced this
|
|
99
|
+
// rule's own former tag-only copy as a false negative.
|
|
100
|
+
function hasSectioningAncestor(el, includeMain) {
|
|
101
|
+
return helpers && typeof helpers.hasLandmarkScopingAncestor === 'function'
|
|
102
|
+
? helpers.hasLandmarkScopingAncestor(el, { includeMain })
|
|
103
|
+
: false;
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
function getImplicitLandmarkRole(el) {
|
|
95
107
|
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
96
|
-
if (tag === 'header') return
|
|
97
|
-
if (tag === 'footer') return
|
|
108
|
+
if (tag === 'header') return hasSectioningAncestor(el, true) ? '' : 'banner';
|
|
109
|
+
if (tag === 'footer') return hasSectioningAncestor(el, true) ? '' : 'contentinfo';
|
|
98
110
|
if (tag === 'main') return 'main';
|
|
99
111
|
if (tag === 'nav') return 'navigation';
|
|
100
|
-
if (tag === 'aside')
|
|
112
|
+
if (tag === 'aside') {
|
|
113
|
+
// A named <aside> is never suppressed, even when nested — matches
|
|
114
|
+
// landmark-unique's own verified-against-reference-engine precedent
|
|
115
|
+
// (that engine's real `aside` implicit-role function keeps
|
|
116
|
+
// "complementary" when the element has an accessible name, even
|
|
117
|
+
// inside sectioning content); propagated here for consistency.
|
|
118
|
+
if (!hasSectioningAncestor(el, false)) return 'complementary';
|
|
119
|
+
return getAccessibleLandmarkName(el) ? 'complementary' : '';
|
|
120
|
+
}
|
|
101
121
|
if (tag === 'section') return getAccessibleLandmarkName(el) ? 'region' : '';
|
|
102
122
|
if (tag === 'form') return getAccessibleLandmarkName(el) ? 'form' : '';
|
|
103
123
|
return '';
|
|
@@ -180,4 +200,4 @@ function runInPage(ctx) {
|
|
|
180
200
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
181
201
|
}
|
|
182
202
|
|
|
183
|
-
module.exports = { id, meta, runInPage };
|
|
203
|
+
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -39,16 +39,11 @@ const meta = {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
function runInPage(ctx) {
|
|
42
|
-
const {
|
|
42
|
+
const { helpers, rule } = ctx;
|
|
43
43
|
|
|
44
44
|
const VALID_SCOPES = new Set(['row', 'col', 'rowgroup', 'colgroup']);
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
nodes = document.querySelectorAll('[scope]');
|
|
49
|
-
} catch {
|
|
50
|
-
nodes = [];
|
|
51
|
-
}
|
|
46
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[scope]') : helpers.queryAll('[scope]');
|
|
52
47
|
|
|
53
48
|
const occurrences = [];
|
|
54
49
|
let applicableCount = 0;
|
|
@@ -64,8 +64,7 @@ const meta = {
|
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
function runInPage(ctx) {
|
|
67
|
-
const { document,
|
|
68
|
-
const safeRoot = root || document;
|
|
67
|
+
const { document, helpers, rule } = ctx;
|
|
69
68
|
|
|
70
69
|
function safeComputedStyle(el) {
|
|
71
70
|
try {
|
|
@@ -110,7 +109,7 @@ function runInPage(ctx) {
|
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
const CANDIDATE_SELECTOR = 'div, section, article, aside, main, nav, pre, table, blockquote, ul, ol, textarea';
|
|
113
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(CANDIDATE_SELECTOR
|
|
112
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(CANDIDATE_SELECTOR) : helpers.queryAll(CANDIDATE_SELECTOR);
|
|
114
113
|
|
|
115
114
|
const occurrences = [];
|
|
116
115
|
let applicableCount = 0;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @check skip-link
|
|
5
5
|
* @atomic true
|
|
6
|
-
* @summary A "skip" link must resolve to a real,
|
|
6
|
+
* @summary A "skip" link must resolve to a real, usable target
|
|
7
7
|
* @standard Best Practices (a widely-used reference engine's classification; no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
|
|
8
8
|
* @applicability
|
|
9
9
|
* Applies to <a href="#fragment"> elements whose accessible name
|
|
@@ -18,8 +18,11 @@
|
|
|
18
18
|
* implementation-notes) — this only widens the known-convention list.
|
|
19
19
|
* @expectation
|
|
20
20
|
* The link's fragment resolves to a real element in the document
|
|
21
|
-
* (via a matching id, or a legacy <a name="...">)
|
|
22
|
-
*
|
|
21
|
+
* (via a matching id, or a legacy <a name="...">), and that target is
|
|
22
|
+
* currently usable (not hidden from the accessibility tree; and, when
|
|
23
|
+
* browser geometry is available, not zero-area/no-rects). A skip link
|
|
24
|
+
* whose target is missing or effectively unusable does not provide a
|
|
25
|
+
* reliable bypass destination.
|
|
23
26
|
* @implementation-notes
|
|
24
27
|
* - Not WCAG-normative — authored as an advisory, cantTell-capped
|
|
25
28
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
@@ -33,8 +36,8 @@
|
|
|
33
36
|
const id = 'skip-link';
|
|
34
37
|
|
|
35
38
|
const meta = {
|
|
36
|
-
title: 'Skip link must have a resolvable target',
|
|
37
|
-
description: 'Checks that a "skip to ..." link\'s href fragment resolves to a real element in the document.',
|
|
39
|
+
title: 'Skip link must have a resolvable, usable target',
|
|
40
|
+
description: 'Checks that a "skip to ..." link\'s href fragment resolves to a real, currently usable element in the document.',
|
|
38
41
|
i18n: {
|
|
39
42
|
titleKey: 'skipLink_title',
|
|
40
43
|
descriptionKey: 'skipLink_description'
|
|
@@ -51,8 +54,7 @@ const meta = {
|
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
function runInPage(ctx) {
|
|
54
|
-
const { document,
|
|
55
|
-
const safeRoot = root || document;
|
|
57
|
+
const { document, helpers, rule } = ctx;
|
|
56
58
|
|
|
57
59
|
function normalizeWs(s) {
|
|
58
60
|
return String(s || '').replace(/\s+/g, ' ').trim();
|
|
@@ -79,7 +81,31 @@ function runInPage(ctx) {
|
|
|
79
81
|
return normalizeWs(el.textContent);
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
function hasReliableGeometrySupport() {
|
|
85
|
+
const probe = document.documentElement || document.body || null;
|
|
86
|
+
if (!probe || !probe.getClientRects || !probe.getBoundingClientRect) return false;
|
|
87
|
+
try {
|
|
88
|
+
const rects = probe.getClientRects();
|
|
89
|
+
const rectCount = rects ? rects.length : 0;
|
|
90
|
+
const r = probe.getBoundingClientRect();
|
|
91
|
+
const w = r && Number.isFinite(r.width) ? r.width : 0;
|
|
92
|
+
const h = r && Number.isFinite(r.height) ? r.height : 0;
|
|
93
|
+
return rectCount > 0 && (w > 0 || h > 0);
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toEligibility(info) {
|
|
100
|
+
return {
|
|
101
|
+
eligible: !!(info && info.eligible),
|
|
102
|
+
reasons: info && Array.isArray(info.reasons) ? info.reasons.slice(0) : []
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const geometrySupported = hasReliableGeometrySupport();
|
|
107
|
+
|
|
108
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('a[href]') : helpers.queryAll('a[href]');
|
|
83
109
|
|
|
84
110
|
const occurrences = [];
|
|
85
111
|
let applicableCount = 0;
|
|
@@ -117,7 +143,63 @@ function runInPage(ctx) {
|
|
|
117
143
|
}
|
|
118
144
|
}
|
|
119
145
|
|
|
120
|
-
if (target)
|
|
146
|
+
if (target) {
|
|
147
|
+
const accEligibility = toEligibility(
|
|
148
|
+
helpers.getEligibilityInfo
|
|
149
|
+
? helpers.getEligibilityInfo(target, ctx, { targetSet: 'acc' })
|
|
150
|
+
: (helpers.isAccTreeEligible ? helpers.isAccTreeEligible(target, ctx) : { eligible: true, reasons: [] })
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
let geometryEligibility = null;
|
|
154
|
+
let geometryReasonCode = null;
|
|
155
|
+
if (geometrySupported && helpers.isDomVisibleEligible) {
|
|
156
|
+
geometryEligibility = toEligibility(
|
|
157
|
+
helpers.isDomVisibleEligible(target, ctx, { visibilityMode: 'styleAndGeometry', ignoreOpacity: true })
|
|
158
|
+
);
|
|
159
|
+
if (!geometryEligibility.eligible && geometryEligibility.reasons.includes('noClientRects')) {
|
|
160
|
+
geometryReasonCode = 'NO_CLIENT_RECTS';
|
|
161
|
+
} else if (!geometryEligibility.eligible && geometryEligibility.reasons.includes('zeroArea')) {
|
|
162
|
+
geometryReasonCode = 'ZERO_AREA_TARGET';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const unusableByAcc = !accEligibility.eligible;
|
|
167
|
+
const unusableByGeometry = !!geometryReasonCode;
|
|
168
|
+
if (!unusableByAcc && !unusableByGeometry) continue;
|
|
169
|
+
|
|
170
|
+
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
171
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
|
|
172
|
+
|
|
173
|
+
occurrences.push({
|
|
174
|
+
selector: stableSelector,
|
|
175
|
+
html,
|
|
176
|
+
summary: 'This skip link points to a target that exists but is not currently usable.',
|
|
177
|
+
hint: 'Point this skip link to a target that is exposed and usable as a navigation destination.',
|
|
178
|
+
i18n: {
|
|
179
|
+
summaryKey: 'skipLink_summary_unusableTarget_cantTell',
|
|
180
|
+
hintKey: 'skipLink_hint_unusableTarget_cantTell',
|
|
181
|
+
params: { href }
|
|
182
|
+
},
|
|
183
|
+
data: {
|
|
184
|
+
details: {
|
|
185
|
+
reasonCode: 'SKIP_LINK_TARGET_UNUSABLE',
|
|
186
|
+
href,
|
|
187
|
+
unusableReasonCode: unusableByAcc ? 'ACC_TREE_INELIGIBLE' : geometryReasonCode,
|
|
188
|
+
targetSelector: helpers.buildSelector ? helpers.buildSelector(target) : null,
|
|
189
|
+
geometryCheckEnabled: geometrySupported
|
|
190
|
+
},
|
|
191
|
+
visibilityFilter: {
|
|
192
|
+
targetSet: 'acc',
|
|
193
|
+
accEligible: accEligibility.eligible,
|
|
194
|
+
reasons: accEligibility.reasons
|
|
195
|
+
},
|
|
196
|
+
targetGeometry: geometryEligibility
|
|
197
|
+
? { eligible: geometryEligibility.eligible, reasons: geometryEligibility.reasons }
|
|
198
|
+
: { eligible: null, reasons: [] }
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
121
203
|
|
|
122
204
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
123
205
|
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
|
|
@@ -40,14 +40,9 @@ const meta = {
|
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
function runInPage(ctx) {
|
|
43
|
-
const {
|
|
43
|
+
const { helpers, rule } = ctx;
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
nodes = document.querySelectorAll('[tabindex]');
|
|
48
|
-
} catch {
|
|
49
|
-
nodes = [];
|
|
50
|
-
}
|
|
45
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[tabindex]') : helpers.queryAll('[tabindex]');
|
|
51
46
|
|
|
52
47
|
const occurrences = [];
|
|
53
48
|
let applicableCount = 0;
|
|
@@ -43,14 +43,13 @@ const meta = {
|
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
function runInPage(ctx) {
|
|
46
|
-
const { document,
|
|
47
|
-
const safeRoot = root || document;
|
|
46
|
+
const { document, helpers, rule } = ctx;
|
|
48
47
|
|
|
49
48
|
function normalizeWs(s) {
|
|
50
49
|
return String(s || '').replace(/\s+/g, ' ').trim();
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table[summary]'
|
|
52
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table[summary]') : helpers.queryAll('table[summary]');
|
|
54
53
|
|
|
55
54
|
const occurrences = [];
|
|
56
55
|
let applicableCount = 0;
|
|
@@ -51,12 +51,11 @@ const meta = {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
function runInPage(ctx) {
|
|
54
|
-
const { document,
|
|
55
|
-
const safeRoot = root || document;
|
|
54
|
+
const { document, helpers, rule } = ctx;
|
|
56
55
|
|
|
57
56
|
function trim(v) { return (v == null ? '' : String(v)).trim(); }
|
|
58
57
|
|
|
59
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table'
|
|
58
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
|
|
60
59
|
|
|
61
60
|
const occurrences = [];
|
|
62
61
|
let applicableCount = 0;
|
|
@@ -51,10 +51,9 @@ const meta = {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
function runInPage(ctx) {
|
|
54
|
-
const { document,
|
|
55
|
-
const safeRoot = root || document;
|
|
54
|
+
const { document, helpers, rule } = ctx;
|
|
56
55
|
|
|
57
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('video'
|
|
56
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('video') : helpers.queryAll('video');
|
|
58
57
|
|
|
59
58
|
const occurrences = [];
|
|
60
59
|
let applicableCount = 0;
|