@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
|
@@ -23,11 +23,10 @@ const meta = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
function runInPage(ctx) {
|
|
26
|
-
const { document,
|
|
26
|
+
const { document, helpers, rule } = ctx;
|
|
27
27
|
const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
|
|
28
28
|
? helpers.getEligibilityInfo
|
|
29
29
|
: null;
|
|
30
|
-
const safeRoot = root || document;
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
function normalizeWs(s) {
|
|
@@ -91,7 +90,7 @@ function runInPage(ctx) {
|
|
|
91
90
|
let applicableCount = 0;
|
|
92
91
|
|
|
93
92
|
const selector = '[role="tooltip"]';
|
|
94
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
93
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
95
94
|
|
|
96
95
|
|
|
97
96
|
function evaluate(el) {
|
|
@@ -23,11 +23,10 @@ const meta = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
function runInPage(ctx) {
|
|
26
|
-
const { document,
|
|
26
|
+
const { document, helpers, rule } = ctx;
|
|
27
27
|
const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
|
|
28
28
|
? helpers.getEligibilityInfo
|
|
29
29
|
: null;
|
|
30
|
-
const safeRoot = root || document;
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
function normalizeWs(s) {
|
|
@@ -91,7 +90,7 @@ function runInPage(ctx) {
|
|
|
91
90
|
let applicableCount = 0;
|
|
92
91
|
|
|
93
92
|
const selector = "[role=\"treeitem\"]";
|
|
94
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
93
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
95
94
|
|
|
96
95
|
|
|
97
96
|
function hasName(el) {
|
|
@@ -47,12 +47,11 @@ const meta = {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
function runInPage(ctx) {
|
|
50
|
-
const { document,
|
|
51
|
-
const safeRoot = root || document;
|
|
50
|
+
const { document, helpers, rule } = ctx;
|
|
52
51
|
|
|
53
52
|
const BCP47_RE = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
|
|
54
53
|
|
|
55
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[lang]'
|
|
54
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[lang]') : helpers.queryAll('[lang]');
|
|
56
55
|
|
|
57
56
|
const occurrences = [];
|
|
58
57
|
let applicableCount = 0;
|
|
@@ -41,13 +41,7 @@ const meta = {
|
|
|
41
41
|
|
|
42
42
|
function runInPage(ctx) {
|
|
43
43
|
const { document, helpers, rule } = ctx;
|
|
44
|
-
|
|
45
|
-
let nodes = [];
|
|
46
|
-
try {
|
|
47
|
-
nodes = document.querySelectorAll('[accesskey]');
|
|
48
|
-
} catch {
|
|
49
|
-
nodes = [];
|
|
50
|
-
}
|
|
44
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[accesskey]') : helpers.queryAll('[accesskey]');
|
|
51
45
|
|
|
52
46
|
const groups = new Map(); // normalized key -> elements[]
|
|
53
47
|
for (const el of nodes) {
|
|
@@ -71,14 +71,13 @@ const meta = {
|
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
function runInPage(ctx) {
|
|
74
|
-
const { document,
|
|
75
|
-
const safeRoot = root || document;
|
|
74
|
+
const { document, helpers, rule } = ctx;
|
|
76
75
|
|
|
77
76
|
function trim(v) { return (v == null ? '' : String(v)).trim(); }
|
|
78
77
|
|
|
79
78
|
const nodes = helpers.queryAllSmart
|
|
80
|
-
? helpers.queryAllSmart('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]'
|
|
81
|
-
: helpers.queryAll('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]'
|
|
79
|
+
? helpers.queryAllSmart('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]')
|
|
80
|
+
: helpers.queryAll('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]');
|
|
82
81
|
|
|
83
82
|
const occurrences = [];
|
|
84
83
|
let applicableCount = 0;
|
|
@@ -46,13 +46,12 @@ const meta = {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
function runInPage(ctx) {
|
|
49
|
-
const { document,
|
|
50
|
-
const safeRoot = root || document;
|
|
49
|
+
const { document, helpers, rule } = ctx;
|
|
51
50
|
|
|
52
51
|
const FOCUSABLE_DESCENDANT_SELECTOR =
|
|
53
52
|
'a[href], button, input, select, textarea, [tabindex], iframe, [contenteditable]:not([contenteditable="false"])';
|
|
54
53
|
|
|
55
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role="text"]'
|
|
54
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role="text"]') : helpers.queryAll('[role="text"]');
|
|
56
55
|
|
|
57
56
|
const occurrences = [];
|
|
58
57
|
let applicableCount = 0;
|
|
@@ -112,12 +112,7 @@ function runInPage(ctx) {
|
|
|
112
112
|
return normalizeWs(el.getAttribute && el.getAttribute('title'));
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
nodes = document.querySelectorAll('h1, h2, h3, h4, h5, h6, [role]');
|
|
118
|
-
} catch {
|
|
119
|
-
nodes = [];
|
|
120
|
-
}
|
|
115
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('h1, h2, h3, h4, h5, h6, [role]') : helpers.queryAll('h1, h2, h3, h4, h5, h6, [role]');
|
|
121
116
|
|
|
122
117
|
const occurrences = [];
|
|
123
118
|
let applicableCount = 0;
|
|
@@ -94,15 +94,11 @@ function runInPage(ctx) {
|
|
|
94
94
|
return '';
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
nodes = document.querySelectorAll('th:not([role]), [role="columnheader"], [role="rowheader"]');
|
|
103
|
-
} catch {
|
|
104
|
-
nodes = [];
|
|
105
|
-
}
|
|
97
|
+
// Matches a widely-used reference engine's own empty-table-header selector exactly: a <th> with
|
|
98
|
+
// no conflicting explicit role, plus any element carrying an explicit
|
|
99
|
+
// columnheader/rowheader role (native or not).
|
|
100
|
+
const selector = 'th:not([role]), [role="columnheader"], [role="rowheader"]';
|
|
101
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
106
102
|
|
|
107
103
|
const occurrences = [];
|
|
108
104
|
let applicableCount = 0;
|
|
@@ -48,8 +48,7 @@ const meta = {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
function runInPage(ctx) {
|
|
51
|
-
const { document,
|
|
52
|
-
const safeRoot = root || document;
|
|
51
|
+
const { document, helpers, rule } = ctx;
|
|
53
52
|
|
|
54
53
|
const NON_INTERACTIVE_ROLES = new Set([
|
|
55
54
|
'generic', 'group', 'text', 'presentation', 'none', 'img', 'heading',
|
|
@@ -59,7 +58,7 @@ function runInPage(ctx) {
|
|
|
59
58
|
'definition', 'paragraph', 'caption', 'status', 'alert', 'log', 'tooltip'
|
|
60
59
|
]);
|
|
61
60
|
|
|
62
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[tabindex][role]'
|
|
61
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[tabindex][role]') : helpers.queryAll('[tabindex][role]');
|
|
63
62
|
|
|
64
63
|
const occurrences = [];
|
|
65
64
|
let applicableCount = 0;
|
|
@@ -72,12 +72,7 @@ function runInPage(ctx) {
|
|
|
72
72
|
return m ? parseInt(m[1], 10) : 0;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
nodes = document.querySelectorAll('h1, h2, h3, h4, h5, h6, [role]');
|
|
78
|
-
} catch {
|
|
79
|
-
nodes = [];
|
|
80
|
-
}
|
|
75
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('h1, h2, h3, h4, h5, h6, [role]') : helpers.queryAll('h1, h2, h3, h4, h5, h6, [role]');
|
|
81
76
|
|
|
82
77
|
const headings = [];
|
|
83
78
|
const seen = new Set();
|
|
@@ -52,14 +52,13 @@ const meta = {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
function runInPage(ctx) {
|
|
55
|
-
const { document,
|
|
56
|
-
const safeRoot = root || document;
|
|
55
|
+
const { document, helpers, rule } = ctx;
|
|
57
56
|
|
|
58
57
|
function normName(s) {
|
|
59
58
|
return (s == null ? '' : String(s)).replace(/\s+/g, ' ').trim().toLowerCase();
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('a[href]'
|
|
61
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('a[href]') : helpers.queryAll('a[href]');
|
|
63
62
|
|
|
64
63
|
const groups = new Map(); // normName -> [{ el, href }]
|
|
65
64
|
let applicableCount = 0;
|
|
@@ -47,8 +47,7 @@ const meta = {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
function runInPage(ctx) {
|
|
50
|
-
const { document,
|
|
51
|
-
const safeRoot = root || document;
|
|
50
|
+
const { document, helpers, rule } = ctx;
|
|
52
51
|
|
|
53
52
|
function normalizeWs(s) {
|
|
54
53
|
return String(s || '').replace(/\s+/g, ' ').trim();
|
|
@@ -67,7 +66,7 @@ function runInPage(ctx) {
|
|
|
67
66
|
return normalizeWs(text);
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('img[alt]'
|
|
69
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('img[alt]') : helpers.queryAll('img[alt]');
|
|
71
70
|
|
|
72
71
|
const occurrences = [];
|
|
73
72
|
let applicableCount = 0;
|
|
@@ -45,11 +45,10 @@ const meta = {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
function runInPage(ctx) {
|
|
48
|
-
const { document,
|
|
49
|
-
const safeRoot = root || document;
|
|
48
|
+
const { document, helpers, rule } = ctx;
|
|
50
49
|
|
|
51
50
|
const selector = 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
|
|
52
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
51
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
53
52
|
|
|
54
53
|
const labelsByFor = new Map();
|
|
55
54
|
const allLabels = document.getElementsByTagName ? document.getElementsByTagName('label') : [];
|
|
@@ -52,7 +52,7 @@ const meta = {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
function runInPage(ctx) {
|
|
55
|
-
const { document, helpers, rule } = ctx;
|
|
55
|
+
const { document, root, helpers, rule } = ctx;
|
|
56
56
|
|
|
57
57
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
58
58
|
// ("runInPage MUST be self-contained").
|
|
@@ -81,25 +81,37 @@ function runInPage(ctx) {
|
|
|
81
81
|
return raw.split(/\s+/)[0].toLowerCase();
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
// Delegates to the shared helpers.hasLandmarkScopingAncestor for the
|
|
85
|
+
// question "does this element sit inside a sectioning-content/<main>
|
|
86
|
+
// ancestor that suppresses its conditional implicit role" — role-aware
|
|
87
|
+
// (an ancestor's bare TAG only counts when it carries no role attribute
|
|
88
|
+
// at all; an explicit role="dialog"-style override no longer suppresses)
|
|
89
|
+
// rather than a local tag-only copy. See that function's header comment
|
|
90
|
+
// in src/core/aria-helpers.js for the full algorithm and the real page
|
|
91
|
+
// (handsontable.com's docs-assistant side panel, an
|
|
92
|
+
// <aside role="dialog"> containing its own <header>) that surfaced this
|
|
93
|
+
// rule's own former tag-only copy as a false negative.
|
|
94
|
+
function hasSectioningAncestor(el, includeMain) {
|
|
95
|
+
return helpers && typeof helpers.hasLandmarkScopingAncestor === 'function'
|
|
96
|
+
? helpers.hasLandmarkScopingAncestor(el, { includeMain })
|
|
97
|
+
: false;
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
function getImplicitLandmarkRole(el) {
|
|
97
101
|
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
98
|
-
if (tag === 'header') return
|
|
99
|
-
if (tag === 'footer') return
|
|
102
|
+
if (tag === 'header') return hasSectioningAncestor(el, true) ? '' : 'banner';
|
|
103
|
+
if (tag === 'footer') return hasSectioningAncestor(el, true) ? '' : 'contentinfo';
|
|
100
104
|
if (tag === 'main') return 'main';
|
|
101
105
|
if (tag === 'nav') return 'navigation';
|
|
102
|
-
if (tag === 'aside')
|
|
106
|
+
if (tag === 'aside') {
|
|
107
|
+
// A named <aside> is never suppressed, even when nested — matches
|
|
108
|
+
// landmark-unique's own verified-against-reference-engine precedent
|
|
109
|
+
// (that engine's real `aside` implicit-role function keeps
|
|
110
|
+
// "complementary" when the element has an accessible name, even
|
|
111
|
+
// inside sectioning content); propagated here for consistency.
|
|
112
|
+
if (!hasSectioningAncestor(el, false)) return 'complementary';
|
|
113
|
+
return getAccessibleLandmarkName(el) ? 'complementary' : '';
|
|
114
|
+
}
|
|
103
115
|
if (tag === 'section') return getAccessibleLandmarkName(el) ? 'region' : '';
|
|
104
116
|
if (tag === 'form') return getAccessibleLandmarkName(el) ? 'form' : '';
|
|
105
117
|
return '';
|
|
@@ -115,9 +127,13 @@ function runInPage(ctx) {
|
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
function hasLandmarkAncestor(el) {
|
|
130
|
+
const scopeRoots = Array.isArray(root) ? root : (root ? [root] : []);
|
|
118
131
|
let p = el.parentElement;
|
|
119
132
|
while (p) {
|
|
120
133
|
if (getLandmarkRole(p)) return true;
|
|
134
|
+
// Don't climb past the scanned scope -- see aria-helpers.js's
|
|
135
|
+
// hasLandmarkScopingAncestor for the same fix and rationale.
|
|
136
|
+
if (scopeRoots.includes(p)) break;
|
|
121
137
|
p = p.parentElement;
|
|
122
138
|
}
|
|
123
139
|
return false;
|
|
@@ -43,7 +43,7 @@ const meta = {
|
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
function runInPage(ctx) {
|
|
46
|
-
const { document, helpers, rule } = ctx;
|
|
46
|
+
const { document, root, helpers, rule } = ctx;
|
|
47
47
|
|
|
48
48
|
function normalizeWs(s) {
|
|
49
49
|
return String(s || '').replace(/\s+/g, ' ').trim();
|
|
@@ -70,25 +70,37 @@ function runInPage(ctx) {
|
|
|
70
70
|
return raw.split(/\s+/)[0].toLowerCase();
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
73
|
+
// Delegates to the shared helpers.hasLandmarkScopingAncestor for the
|
|
74
|
+
// question "does this element sit inside a sectioning-content/<main>
|
|
75
|
+
// ancestor that suppresses its conditional implicit role" — role-aware
|
|
76
|
+
// (an ancestor's bare TAG only counts when it carries no role attribute
|
|
77
|
+
// at all; an explicit role="dialog"-style override no longer suppresses)
|
|
78
|
+
// rather than a local tag-only copy. See that function's header comment
|
|
79
|
+
// in src/core/aria-helpers.js for the full algorithm and the real page
|
|
80
|
+
// (handsontable.com's docs-assistant side panel, an
|
|
81
|
+
// <aside role="dialog"> containing its own <header>) that surfaced this
|
|
82
|
+
// rule's own former tag-only copy as a false negative.
|
|
83
|
+
function hasSectioningAncestor(el, includeMain) {
|
|
84
|
+
return helpers && typeof helpers.hasLandmarkScopingAncestor === 'function'
|
|
85
|
+
? helpers.hasLandmarkScopingAncestor(el, { includeMain })
|
|
86
|
+
: false;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
function getImplicitLandmarkRole(el) {
|
|
86
90
|
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
87
|
-
if (tag === 'header') return
|
|
88
|
-
if (tag === 'footer') return
|
|
91
|
+
if (tag === 'header') return hasSectioningAncestor(el, true) ? '' : 'banner';
|
|
92
|
+
if (tag === 'footer') return hasSectioningAncestor(el, true) ? '' : 'contentinfo';
|
|
89
93
|
if (tag === 'main') return 'main';
|
|
90
94
|
if (tag === 'nav') return 'navigation';
|
|
91
|
-
if (tag === 'aside')
|
|
95
|
+
if (tag === 'aside') {
|
|
96
|
+
// A named <aside> is never suppressed, even when nested — matches
|
|
97
|
+
// landmark-unique's own verified-against-reference-engine precedent
|
|
98
|
+
// (that engine's real `aside` implicit-role function keeps
|
|
99
|
+
// "complementary" when the element has an accessible name, even
|
|
100
|
+
// inside sectioning content); propagated here for consistency.
|
|
101
|
+
if (!hasSectioningAncestor(el, false)) return 'complementary';
|
|
102
|
+
return getAccessibleLandmarkName(el) ? 'complementary' : '';
|
|
103
|
+
}
|
|
92
104
|
if (tag === 'section') return getAccessibleLandmarkName(el) ? 'region' : '';
|
|
93
105
|
if (tag === 'form') return getAccessibleLandmarkName(el) ? 'form' : '';
|
|
94
106
|
return '';
|
|
@@ -104,9 +116,13 @@ function runInPage(ctx) {
|
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
function hasLandmarkAncestor(el) {
|
|
119
|
+
const scopeRoots = Array.isArray(root) ? root : (root ? [root] : []);
|
|
107
120
|
let p = el.parentElement;
|
|
108
121
|
while (p) {
|
|
109
122
|
if (getLandmarkRole(p)) return true;
|
|
123
|
+
// Don't climb past the scanned scope -- see aria-helpers.js's
|
|
124
|
+
// hasLandmarkScopingAncestor for the same fix and rationale.
|
|
125
|
+
if (scopeRoots.includes(p)) break;
|
|
110
126
|
p = p.parentElement;
|
|
111
127
|
}
|
|
112
128
|
return false;
|
|
@@ -41,7 +41,7 @@ const meta = {
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
function runInPage(ctx) {
|
|
44
|
-
const { document, helpers, rule } = ctx;
|
|
44
|
+
const { document, root, helpers, rule } = ctx;
|
|
45
45
|
|
|
46
46
|
function normalizeWs(s) {
|
|
47
47
|
return String(s || '').replace(/\s+/g, ' ').trim();
|
|
@@ -68,25 +68,37 @@ function runInPage(ctx) {
|
|
|
68
68
|
return raw.split(/\s+/)[0].toLowerCase();
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
// Delegates to the shared helpers.hasLandmarkScopingAncestor for the
|
|
72
|
+
// question "does this element sit inside a sectioning-content/<main>
|
|
73
|
+
// ancestor that suppresses its conditional implicit role" — role-aware
|
|
74
|
+
// (an ancestor's bare TAG only counts when it carries no role attribute
|
|
75
|
+
// at all; an explicit role="dialog"-style override no longer suppresses)
|
|
76
|
+
// rather than a local tag-only copy. See that function's header comment
|
|
77
|
+
// in src/core/aria-helpers.js for the full algorithm and the real page
|
|
78
|
+
// (handsontable.com's docs-assistant side panel, an
|
|
79
|
+
// <aside role="dialog"> containing its own <header>) that surfaced this
|
|
80
|
+
// rule's own former tag-only copy as a false negative.
|
|
81
|
+
function hasSectioningAncestor(el, includeMain) {
|
|
82
|
+
return helpers && typeof helpers.hasLandmarkScopingAncestor === 'function'
|
|
83
|
+
? helpers.hasLandmarkScopingAncestor(el, { includeMain })
|
|
84
|
+
: false;
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
function getImplicitLandmarkRole(el) {
|
|
84
88
|
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
85
|
-
if (tag === 'header') return
|
|
86
|
-
if (tag === 'footer') return
|
|
89
|
+
if (tag === 'header') return hasSectioningAncestor(el, true) ? '' : 'banner';
|
|
90
|
+
if (tag === 'footer') return hasSectioningAncestor(el, true) ? '' : 'contentinfo';
|
|
87
91
|
if (tag === 'main') return 'main';
|
|
88
92
|
if (tag === 'nav') return 'navigation';
|
|
89
|
-
if (tag === 'aside')
|
|
93
|
+
if (tag === 'aside') {
|
|
94
|
+
// A named <aside> is never suppressed, even when nested — matches
|
|
95
|
+
// landmark-unique's own verified-against-reference-engine precedent
|
|
96
|
+
// (that engine's real `aside` implicit-role function keeps
|
|
97
|
+
// "complementary" when the element has an accessible name, even
|
|
98
|
+
// inside sectioning content); propagated here for consistency.
|
|
99
|
+
if (!hasSectioningAncestor(el, false)) return 'complementary';
|
|
100
|
+
return getAccessibleLandmarkName(el) ? 'complementary' : '';
|
|
101
|
+
}
|
|
90
102
|
if (tag === 'section') return getAccessibleLandmarkName(el) ? 'region' : '';
|
|
91
103
|
if (tag === 'form') return getAccessibleLandmarkName(el) ? 'form' : '';
|
|
92
104
|
return '';
|
|
@@ -102,9 +114,13 @@ function runInPage(ctx) {
|
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
function hasLandmarkAncestor(el) {
|
|
117
|
+
const scopeRoots = Array.isArray(root) ? root : (root ? [root] : []);
|
|
105
118
|
let p = el.parentElement;
|
|
106
119
|
while (p) {
|
|
107
120
|
if (getLandmarkRole(p)) return true;
|
|
121
|
+
// Don't climb past the scanned scope -- see aria-helpers.js's
|
|
122
|
+
// hasLandmarkScopingAncestor for the same fix and rationale.
|
|
123
|
+
if (scopeRoots.includes(p)) break;
|
|
108
124
|
p = p.parentElement;
|
|
109
125
|
}
|
|
110
126
|
return false;
|
|
@@ -79,25 +79,37 @@ function runInPage(ctx) {
|
|
|
79
79
|
return raw.split(/\s+/)[0].toLowerCase();
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
// Delegates to the shared helpers.hasLandmarkScopingAncestor for the
|
|
83
|
+
// question "does this element sit inside a sectioning-content/<main>
|
|
84
|
+
// ancestor that suppresses its conditional implicit role" — role-aware
|
|
85
|
+
// (an ancestor's bare TAG only counts when it carries no role attribute
|
|
86
|
+
// at all; an explicit role="dialog"-style override no longer suppresses)
|
|
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 and the real page
|
|
89
|
+
// (handsontable.com's docs-assistant side panel, an
|
|
90
|
+
// <aside role="dialog"> containing its own <header>) that surfaced this
|
|
91
|
+
// rule's own former tag-only copy as a false negative.
|
|
92
|
+
function hasSectioningAncestor(el, includeMain) {
|
|
93
|
+
return helpers && typeof helpers.hasLandmarkScopingAncestor === 'function'
|
|
94
|
+
? helpers.hasLandmarkScopingAncestor(el, { includeMain })
|
|
95
|
+
: false;
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
function getImplicitLandmarkRole(el) {
|
|
95
99
|
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
96
|
-
if (tag === 'header') return
|
|
97
|
-
if (tag === 'footer') return
|
|
100
|
+
if (tag === 'header') return hasSectioningAncestor(el, true) ? '' : 'banner';
|
|
101
|
+
if (tag === 'footer') return hasSectioningAncestor(el, true) ? '' : 'contentinfo';
|
|
98
102
|
if (tag === 'main') return 'main';
|
|
99
103
|
if (tag === 'nav') return 'navigation';
|
|
100
|
-
if (tag === 'aside')
|
|
104
|
+
if (tag === 'aside') {
|
|
105
|
+
// A named <aside> is never suppressed, even when nested — matches
|
|
106
|
+
// landmark-unique's own verified-against-reference-engine precedent
|
|
107
|
+
// (that engine's real `aside` implicit-role function keeps
|
|
108
|
+
// "complementary" when the element has an accessible name, even
|
|
109
|
+
// inside sectioning content); propagated here for consistency.
|
|
110
|
+
if (!hasSectioningAncestor(el, false)) return 'complementary';
|
|
111
|
+
return getAccessibleLandmarkName(el) ? 'complementary' : '';
|
|
112
|
+
}
|
|
101
113
|
if (tag === 'section') return getAccessibleLandmarkName(el) ? 'region' : '';
|
|
102
114
|
if (tag === 'form') return getAccessibleLandmarkName(el) ? 'form' : '';
|
|
103
115
|
return '';
|
|
@@ -71,25 +71,37 @@ function runInPage(ctx) {
|
|
|
71
71
|
return raw.split(/\s+/)[0].toLowerCase();
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
// Delegates to the shared helpers.hasLandmarkScopingAncestor for the
|
|
75
|
+
// question "does this element sit inside a sectioning-content/<main>
|
|
76
|
+
// ancestor that suppresses its conditional implicit role" — role-aware
|
|
77
|
+
// (an ancestor's bare TAG only counts when it carries no role attribute
|
|
78
|
+
// at all; an explicit role="dialog"-style override no longer suppresses)
|
|
79
|
+
// rather than a local tag-only copy. See that function's header comment
|
|
80
|
+
// in src/core/aria-helpers.js for the full algorithm and the real page
|
|
81
|
+
// (handsontable.com's docs-assistant side panel, an
|
|
82
|
+
// <aside role="dialog"> containing its own <header>) that surfaced this
|
|
83
|
+
// rule's own former tag-only copy as a false negative.
|
|
84
|
+
function hasSectioningAncestor(el, includeMain) {
|
|
85
|
+
return helpers && typeof helpers.hasLandmarkScopingAncestor === 'function'
|
|
86
|
+
? helpers.hasLandmarkScopingAncestor(el, { includeMain })
|
|
87
|
+
: false;
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
function getImplicitLandmarkRole(el) {
|
|
87
91
|
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
88
|
-
if (tag === 'header') return
|
|
89
|
-
if (tag === 'footer') return
|
|
92
|
+
if (tag === 'header') return hasSectioningAncestor(el, true) ? '' : 'banner';
|
|
93
|
+
if (tag === 'footer') return hasSectioningAncestor(el, true) ? '' : 'contentinfo';
|
|
90
94
|
if (tag === 'main') return 'main';
|
|
91
95
|
if (tag === 'nav') return 'navigation';
|
|
92
|
-
if (tag === 'aside')
|
|
96
|
+
if (tag === 'aside') {
|
|
97
|
+
// A named <aside> is never suppressed, even when nested — matches
|
|
98
|
+
// landmark-unique's own verified-against-reference-engine precedent
|
|
99
|
+
// (that engine's real `aside` implicit-role function keeps
|
|
100
|
+
// "complementary" when the element has an accessible name, even
|
|
101
|
+
// inside sectioning content); propagated here for consistency.
|
|
102
|
+
if (!hasSectioningAncestor(el, false)) return 'complementary';
|
|
103
|
+
return getAccessibleLandmarkName(el) ? 'complementary' : '';
|
|
104
|
+
}
|
|
93
105
|
if (tag === 'section') return getAccessibleLandmarkName(el) ? 'region' : '';
|
|
94
106
|
if (tag === 'form') return getAccessibleLandmarkName(el) ? 'form' : '';
|
|
95
107
|
return '';
|
|
@@ -61,6 +61,14 @@ const meta = {
|
|
|
61
61
|
coverage: {}
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
// This check is inherently whole-document (does the PAGE have this
|
|
65
|
+
// property?), not evaluable per-subtree -- notApplicable when contextSelector
|
|
66
|
+
// scoped this run narrower than the whole document, or when
|
|
67
|
+
// engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
|
|
68
|
+
function applicability(ctx) {
|
|
69
|
+
return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
|
|
70
|
+
}
|
|
71
|
+
|
|
64
72
|
function runInPage(ctx) {
|
|
65
73
|
const { document, helpers, rule } = ctx;
|
|
66
74
|
|
|
@@ -148,4 +156,4 @@ function runInPage(ctx) {
|
|
|
148
156
|
};
|
|
149
157
|
}
|
|
150
158
|
|
|
151
|
-
module.exports = { id, meta, runInPage };
|
|
159
|
+
module.exports = { id, meta, runInPage, applicability };
|