@surea11y/core 1.2.0 → 1.4.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 +81 -7
- package/LICENSE +373 -21
- package/README.md +175 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +27 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +19 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +63 -18
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +63 -31
- package/src/checks/automatic/aria-allowed-attr.js +204 -80
- package/src/checks/automatic/aria-allowed-role.js +23 -7
- package/src/checks/automatic/aria-braille-equivalent.js +34 -10
- package/src/checks/automatic/aria-conditional-attr.js +32 -14
- package/src/checks/automatic/aria-deprecated-role.js +26 -11
- package/src/checks/automatic/aria-hidden-body.js +48 -23
- package/src/checks/automatic/aria-hidden-focus.js +420 -66
- package/src/checks/automatic/aria-prohibited-attr.js +327 -60
- package/src/checks/automatic/aria-prohibited-children.js +111 -103
- package/src/checks/automatic/aria-required-attr.js +29 -15
- package/src/checks/automatic/aria-required-children.js +44 -24
- package/src/checks/automatic/aria-required-parent.js +64 -35
- package/src/checks/automatic/aria-role-name-present.js +49 -21
- package/src/checks/automatic/aria-roles-valid.js +24 -12
- package/src/checks/automatic/aria-valid-attr-value.js +46 -22
- package/src/checks/automatic/aria-valid-attr.js +19 -5
- package/src/checks/automatic/autocomplete-valid.js +76 -16
- package/src/checks/automatic/avoid-inline-spacing.js +23 -8
- package/src/checks/automatic/binary-control-name-present.js +62 -50
- package/src/checks/automatic/button-name-present.js +54 -24
- package/src/checks/automatic/bypass-blocks-present.js +51 -32
- package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
- package/src/checks/automatic/combobox-name-present.js +40 -45
- package/src/checks/automatic/contrast-computable.js +363 -341
- package/src/checks/automatic/contrast-enhanced.js +489 -466
- package/src/checks/automatic/contrast-minimum.js +488 -465
- package/src/checks/automatic/css-orientation-lock.js +51 -35
- package/src/checks/automatic/definition-list-children-valid.js +46 -25
- package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
- package/src/checks/automatic/dialog-name-present.js +47 -85
- package/src/checks/automatic/dlitem-parent-valid.js +25 -8
- package/src/checks/automatic/duplicate-id-aria.js +28 -9
- package/src/checks/automatic/embed-text-alternative-present.js +88 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
- package/src/checks/automatic/form-control-single-label.js +50 -14
- package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
- package/src/checks/automatic/iframe-focusable-content.js +265 -22
- package/src/checks/automatic/iframe-name-present.js +33 -9
- package/src/checks/automatic/iframe-title-unique.js +32 -9
- package/src/checks/automatic/img-alt-present.js +54 -52
- package/src/checks/automatic/input-image-alt-present.js +141 -112
- package/src/checks/automatic/label-in-name.js +65 -41
- package/src/checks/automatic/language-page-present.js +111 -109
- package/src/checks/automatic/link-in-text-block.js +61 -19
- package/src/checks/automatic/link-name-present.js +47 -14
- package/src/checks/automatic/list-children-valid.js +40 -33
- package/src/checks/automatic/listbox-name-present.js +41 -19
- package/src/checks/automatic/listitem-parent-valid.js +48 -13
- package/src/checks/automatic/menuitem-name-present.js +41 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
- package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
- package/src/checks/automatic/meter-name-present.js +40 -36
- package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
- package/src/checks/automatic/object-text-alternative-present.js +93 -39
- package/src/checks/automatic/option-name-present.js +40 -21
- package/src/checks/automatic/page-title-present.js +19 -6
- package/src/checks/automatic/progressbar-name-present.js +49 -44
- package/src/checks/automatic/role-img-alt-present.js +211 -159
- package/src/checks/automatic/searchbox-name-present.js +41 -19
- package/src/checks/automatic/server-side-image-map-absent.js +27 -11
- package/src/checks/automatic/slider-name-present.js +42 -47
- package/src/checks/automatic/spinbutton-name-present.js +41 -19
- package/src/checks/automatic/summary-name-present.js +39 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
- package/src/checks/automatic/svg-text-alternative-present.js +262 -230
- package/src/checks/automatic/tab-name-present.js +39 -60
- package/src/checks/automatic/table-headers-attr-valid.js +27 -10
- package/src/checks/automatic/table-th-has-data-cells.js +24 -8
- package/src/checks/automatic/target-size-minimum.js +123 -48
- package/src/checks/automatic/td-has-header.js +53 -12
- package/src/checks/automatic/textbox-name-present.js +41 -19
- package/src/checks/automatic/tooltip-name-present.js +39 -18
- package/src/checks/automatic/treeitem-name-present.js +40 -21
- package/src/checks/automatic/valid-lang.js +22 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
- package/src/checks/manual/accesskeys-manual.js +17 -6
- package/src/checks/manual/area-alt-decorative-manual.js +194 -193
- package/src/checks/manual/area-alt-quality-manual.js +184 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
- package/src/checks/manual/aria-text-manual.js +20 -11
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
- package/src/checks/manual/css-hidden-focus.js +375 -169
- package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
- package/src/checks/manual/empty-heading-manual.js +41 -24
- package/src/checks/manual/empty-table-header-manual.js +69 -31
- package/src/checks/manual/focus-order-semantics-manual.js +60 -13
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
- package/src/checks/manual/heading-order-manual.js +50 -8
- package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
- package/src/checks/manual/image-redundant-alt-manual.js +38 -8
- package/src/checks/manual/img-alt-decorative-manual.js +133 -96
- package/src/checks/manual/img-alt-quality-manual.js +178 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
- package/src/checks/manual/label-title-only-manual.js +44 -28
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
- package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
- package/src/checks/manual/landmark-one-main-manual.js +38 -43
- package/src/checks/manual/landmark-unique-manual.js +78 -67
- package/src/checks/manual/link-name-quality-manual.js +45 -12
- package/src/checks/manual/media-transcript-present-manual.js +37 -22
- package/src/checks/manual/meta-viewport-large-manual.js +19 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
- package/src/checks/manual/p-as-heading-manual.js +24 -7
- package/src/checks/manual/page-has-heading-one-manual.js +42 -32
- package/src/checks/manual/page-title-patterns-manual.js +80 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
- package/src/checks/manual/region-manual.js +244 -60
- package/src/checks/manual/scope-attr-valid-manual.js +13 -4
- package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
- package/src/checks/manual/skip-link-manual.js +42 -18
- package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
- package/src/checks/manual/tabindex-manual.js +13 -4
- package/src/checks/manual/table-duplicate-name-manual.js +22 -11
- package/src/checks/manual/table-fake-caption-manual.js +48 -10
- package/src/checks/manual/video-caption-manual.js +17 -4
- package/src/checks/manual-review.js +58 -12
- package/src/core.js +41705 -29650
- package/src/index.js +2 -0
- package/src/report.js +109 -47
- package/src/sarif.js +190 -0
- package/surea11y.browser.js +37774 -0
- package/bin/core.js +0 -348
- package/docs/CLI.md +0 -75
- package/src/catalogs/composites.wcag.js +0 -490
- package/src/checks/rules-and-tags.full.csv +0 -19
- package/src/checks/rules-and-tags.full.json +0 -259
- package/src/core/aria-helpers.js +0 -970
- package/src/core/contrast-helpers.js +0 -1147
- package/src/core/dom-helpers.js +0 -4235
- package/src/core/dom-runner.js +0 -671
- package/src/core/frame-messaging.js +0 -210
- package/src/core/frame-scan.js +0 -178
- package/src/core/rollup-composites.js +0 -135
- package/src/core/rule-meta.js +0 -159
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -923
- package/src/i18n/fr.js +0 -844
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -55
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -45,7 +47,13 @@ const meta = {
|
|
|
45
47
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic'],
|
|
46
48
|
wcagSc: ['1.3.1'],
|
|
47
49
|
normativeMappings: [
|
|
48
|
-
{
|
|
50
|
+
{
|
|
51
|
+
standard: 'WCAG',
|
|
52
|
+
version: '2.2',
|
|
53
|
+
requirement: '1.3.1',
|
|
54
|
+
title: 'Info and Relationships',
|
|
55
|
+
conformanceLevel: 'A'
|
|
56
|
+
}
|
|
49
57
|
],
|
|
50
58
|
defaultSeverity: 'serious',
|
|
51
59
|
category: 'perceivable',
|
|
@@ -55,11 +63,26 @@ const meta = {
|
|
|
55
63
|
};
|
|
56
64
|
|
|
57
65
|
function runInPage(ctx) {
|
|
58
|
-
const {
|
|
66
|
+
const { helpers, rule } = ctx;
|
|
59
67
|
|
|
60
68
|
const MIN_SIZE = 4;
|
|
61
69
|
|
|
62
|
-
function trim(v) {
|
|
70
|
+
function trim(v) {
|
|
71
|
+
return (v == null ? '' : String(v)).trim();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isAccTreeEligible =
|
|
75
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
76
|
+
|
|
77
|
+
function isEligible(node) {
|
|
78
|
+
if (!isAccTreeEligible) return true;
|
|
79
|
+
try {
|
|
80
|
+
const r = isAccTreeEligible(node, ctx);
|
|
81
|
+
return !!(r && r.eligible);
|
|
82
|
+
} catch {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
63
86
|
|
|
64
87
|
const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
|
|
65
88
|
|
|
@@ -76,17 +99,23 @@ function runInPage(ctx) {
|
|
|
76
99
|
const maxCols = rowCells.reduce((m, cells) => Math.max(m, cells.length), 0);
|
|
77
100
|
if (maxCols < MIN_SIZE) continue;
|
|
78
101
|
|
|
79
|
-
const hasSpan = rowCells.some((cells) =>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
102
|
+
const hasSpan = rowCells.some((cells) =>
|
|
103
|
+
cells.some((c) => {
|
|
104
|
+
const cs = Number.parseInt(c.getAttribute('colspan') || '1', 10);
|
|
105
|
+
const rs = Number.parseInt(c.getAttribute('rowspan') || '1', 10);
|
|
106
|
+
return (Number.isFinite(cs) && cs > 1) || (Number.isFinite(rs) && rs > 1);
|
|
107
|
+
})
|
|
108
|
+
);
|
|
84
109
|
if (hasSpan) continue;
|
|
85
110
|
|
|
86
111
|
applicableCount += 1;
|
|
87
112
|
|
|
113
|
+
// An aria-hidden <th> is removed from the accessibility tree entirely
|
|
114
|
+
// -- a screen reader never announces it, so it can't actually serve as
|
|
115
|
+
// another cell's row/column header, even though it's still structurally
|
|
116
|
+
// a <th>.
|
|
88
117
|
function isHeaderCell(cell) {
|
|
89
|
-
return !!(cell && cell.tagName && cell.tagName.toLowerCase() === 'th');
|
|
118
|
+
return !!(cell && cell.tagName && cell.tagName.toLowerCase() === 'th' && isEligible(cell));
|
|
90
119
|
}
|
|
91
120
|
|
|
92
121
|
function hasColumnHeaderAbove(r, c) {
|
|
@@ -111,6 +140,10 @@ function runInPage(ctx) {
|
|
|
111
140
|
const cell = cells[c];
|
|
112
141
|
if (!cell || isHeaderCell(cell)) continue;
|
|
113
142
|
|
|
143
|
+
// An aria-hidden data cell isn't exposed to AT either, so it has
|
|
144
|
+
// no need for an accessible header association.
|
|
145
|
+
if (!isEligible(cell)) continue;
|
|
146
|
+
|
|
114
147
|
const headersAttr = trim(cell.getAttribute('headers'));
|
|
115
148
|
if (headersAttr) continue;
|
|
116
149
|
|
|
@@ -118,12 +151,15 @@ function runInPage(ctx) {
|
|
|
118
151
|
if (hasRowHeaderBefore(r, c)) continue;
|
|
119
152
|
|
|
120
153
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(cell) : 'html';
|
|
121
|
-
const html = helpers.getOuterHtmlSnippet
|
|
154
|
+
const html = helpers.getOuterHtmlSnippet
|
|
155
|
+
? helpers.getOuterHtmlSnippet(cell)
|
|
156
|
+
: cell.outerHTML || '';
|
|
122
157
|
|
|
123
158
|
occurrences.push({
|
|
124
159
|
selector: stableSelector,
|
|
125
160
|
html,
|
|
126
|
-
summary:
|
|
161
|
+
summary:
|
|
162
|
+
'This data cell has no associated header (no headers attribute, no column <th> above it, no row <th> to its left).',
|
|
127
163
|
hint: 'Add a headers attribute referencing the relevant <th> id(s), or restructure the table so this cell has an implicit row/column header.',
|
|
128
164
|
i18n: {
|
|
129
165
|
summaryKey: 'tdHasHeader_summary_fail',
|
|
@@ -142,7 +178,12 @@ function runInPage(ctx) {
|
|
|
142
178
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
143
179
|
}
|
|
144
180
|
if (occurrences.length) {
|
|
145
|
-
return {
|
|
181
|
+
return {
|
|
182
|
+
ruleId: rule.ruleId,
|
|
183
|
+
outcome: 'fail',
|
|
184
|
+
severity: rule.defaultSeverity || 'serious',
|
|
185
|
+
occurrences
|
|
186
|
+
};
|
|
146
187
|
}
|
|
147
188
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
148
189
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'textbox-name-present';
|
|
@@ -10,10 +12,16 @@ const meta = {
|
|
|
10
12
|
descriptionKey: 'textboxNamePresent_description'
|
|
11
13
|
},
|
|
12
14
|
helpUrl: null,
|
|
13
|
-
tags: [
|
|
15
|
+
tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'textbox'],
|
|
14
16
|
wcagSc: ['4.1.2'],
|
|
15
17
|
normativeMappings: [
|
|
16
|
-
{
|
|
18
|
+
{
|
|
19
|
+
standard: 'WCAG',
|
|
20
|
+
version: '2.2',
|
|
21
|
+
requirement: '4.1.2',
|
|
22
|
+
title: 'Name, Role, Value',
|
|
23
|
+
conformanceLevel: 'A'
|
|
24
|
+
}
|
|
17
25
|
],
|
|
18
26
|
defaultSeverity: 'serious',
|
|
19
27
|
category: 'robust',
|
|
@@ -24,20 +32,22 @@ const meta = {
|
|
|
24
32
|
|
|
25
33
|
function runInPage(ctx) {
|
|
26
34
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
35
|
+
const getEligibilityInfo =
|
|
36
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
37
|
|
|
32
38
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
39
|
+
return String(s || '')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
function getAttr(el, name) {
|
|
37
45
|
try {
|
|
38
46
|
if (!el || !el.getAttribute) return '';
|
|
39
47
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -52,7 +62,7 @@ function runInPage(ctx) {
|
|
|
52
62
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
63
|
return info && info.present ? info.value : '';
|
|
54
64
|
}
|
|
55
|
-
const t =
|
|
65
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
56
66
|
return t.replace(/\s+/g, ' ').trim();
|
|
57
67
|
}
|
|
58
68
|
|
|
@@ -100,7 +110,8 @@ function runInPage(ctx) {
|
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
function isEligibleAcc(helpers, el, ctx) {
|
|
103
|
-
const fn =
|
|
113
|
+
const fn =
|
|
114
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
104
115
|
if (!fn) return true;
|
|
105
116
|
try {
|
|
106
117
|
const r = fn(el, ctx);
|
|
@@ -126,12 +137,13 @@ function runInPage(ctx) {
|
|
|
126
137
|
return map;
|
|
127
138
|
}
|
|
128
139
|
|
|
129
|
-
|
|
130
140
|
const occurrences = [];
|
|
131
141
|
let applicableCount = 0;
|
|
132
142
|
|
|
133
|
-
const selector =
|
|
134
|
-
const nodes = helpers.queryAllSmart
|
|
143
|
+
const selector = '[role="textbox"]';
|
|
144
|
+
const nodes = helpers.queryAllSmart
|
|
145
|
+
? helpers.queryAllSmart(selector)
|
|
146
|
+
: helpers.queryAll(selector);
|
|
135
147
|
|
|
136
148
|
// Precompute label[for] map for textbox elements that are labelable
|
|
137
149
|
// native form controls (e.g. <input role="textbox">).
|
|
@@ -200,16 +212,22 @@ function runInPage(ctx) {
|
|
|
200
212
|
if (res.ok) continue;
|
|
201
213
|
|
|
202
214
|
const eligInfo = getEligibilityInfo
|
|
203
|
-
|
|
204
|
-
|
|
215
|
+
? (() => {
|
|
216
|
+
try {
|
|
217
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
})()
|
|
222
|
+
: null;
|
|
205
223
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
206
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
224
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
207
225
|
|
|
208
226
|
occurrences.push({
|
|
209
227
|
selector: stableSelector,
|
|
210
228
|
html,
|
|
211
229
|
summary: 'This element has no accessible name.',
|
|
212
|
-
hint:
|
|
230
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this textbox's accessible name.",
|
|
213
231
|
i18n: {
|
|
214
232
|
summaryKey: 'textboxNamePresent_summary_fail',
|
|
215
233
|
hintKey: 'textboxNamePresent_hint_fail',
|
|
@@ -222,12 +240,16 @@ function runInPage(ctx) {
|
|
|
222
240
|
});
|
|
223
241
|
}
|
|
224
242
|
|
|
225
|
-
|
|
226
243
|
if (applicableCount === 0) {
|
|
227
244
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
228
245
|
}
|
|
229
246
|
if (occurrences.length) {
|
|
230
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
ruleId: rule.ruleId,
|
|
249
|
+
outcome: 'fail',
|
|
250
|
+
severity: rule.defaultSeverity || 'minor',
|
|
251
|
+
occurrences
|
|
252
|
+
};
|
|
231
253
|
}
|
|
232
254
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
233
255
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'tooltip-name-present';
|
|
@@ -13,7 +15,13 @@ const meta = {
|
|
|
13
15
|
tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name'],
|
|
14
16
|
wcagSc: ['4.1.2'],
|
|
15
17
|
normativeMappings: [
|
|
16
|
-
{
|
|
18
|
+
{
|
|
19
|
+
standard: 'WCAG',
|
|
20
|
+
version: '2.2',
|
|
21
|
+
requirement: '4.1.2',
|
|
22
|
+
title: 'Name, Role, Value',
|
|
23
|
+
conformanceLevel: 'A'
|
|
24
|
+
}
|
|
17
25
|
],
|
|
18
26
|
defaultSeverity: 'serious',
|
|
19
27
|
category: 'robust',
|
|
@@ -24,20 +32,22 @@ const meta = {
|
|
|
24
32
|
|
|
25
33
|
function runInPage(ctx) {
|
|
26
34
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
35
|
+
const getEligibilityInfo =
|
|
36
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
37
|
|
|
32
38
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
39
|
+
return String(s || '')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
function getAttr(el, name) {
|
|
37
45
|
try {
|
|
38
46
|
if (!el || !el.getAttribute) return '';
|
|
39
47
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -52,7 +62,7 @@ function runInPage(ctx) {
|
|
|
52
62
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
63
|
return info && info.present ? info.value : '';
|
|
54
64
|
}
|
|
55
|
-
const t =
|
|
65
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
56
66
|
return t.replace(/\s+/g, ' ').trim();
|
|
57
67
|
}
|
|
58
68
|
|
|
@@ -74,7 +84,8 @@ function runInPage(ctx) {
|
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
function isEligibleAcc(helpers, el, ctx) {
|
|
77
|
-
const fn =
|
|
87
|
+
const fn =
|
|
88
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
78
89
|
if (!fn) return true;
|
|
79
90
|
try {
|
|
80
91
|
const r = fn(el, ctx);
|
|
@@ -85,13 +96,13 @@ function runInPage(ctx) {
|
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
|
|
89
99
|
const occurrences = [];
|
|
90
100
|
let applicableCount = 0;
|
|
91
101
|
|
|
92
102
|
const selector = '[role="tooltip"]';
|
|
93
|
-
const nodes = helpers.queryAllSmart
|
|
94
|
-
|
|
103
|
+
const nodes = helpers.queryAllSmart
|
|
104
|
+
? helpers.queryAllSmart(selector)
|
|
105
|
+
: helpers.queryAll(selector);
|
|
95
106
|
|
|
96
107
|
function evaluate(el) {
|
|
97
108
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -122,10 +133,16 @@ function runInPage(ctx) {
|
|
|
122
133
|
if (res.ok) continue;
|
|
123
134
|
|
|
124
135
|
const eligInfo = getEligibilityInfo
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
? (() => {
|
|
137
|
+
try {
|
|
138
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
})()
|
|
143
|
+
: null;
|
|
127
144
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
128
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
145
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
129
146
|
|
|
130
147
|
occurrences.push({
|
|
131
148
|
selector: stableSelector,
|
|
@@ -144,14 +161,18 @@ function runInPage(ctx) {
|
|
|
144
161
|
});
|
|
145
162
|
}
|
|
146
163
|
|
|
147
|
-
|
|
148
164
|
if (applicableCount === 0) {
|
|
149
165
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
150
166
|
}
|
|
151
167
|
if (occurrences.length) {
|
|
152
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
ruleId: rule.ruleId,
|
|
170
|
+
outcome: 'fail',
|
|
171
|
+
severity: rule.defaultSeverity || 'minor',
|
|
172
|
+
occurrences
|
|
173
|
+
};
|
|
153
174
|
}
|
|
154
175
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
155
176
|
}
|
|
156
177
|
|
|
157
|
-
module.exports = { id, meta, runInPage };
|
|
178
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'treeitem-name-present';
|
|
@@ -10,10 +12,16 @@ const meta = {
|
|
|
10
12
|
descriptionKey: 'treeitemNamePresent_description'
|
|
11
13
|
},
|
|
12
14
|
helpUrl: null,
|
|
13
|
-
tags: [
|
|
15
|
+
tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name', 'treeitem'],
|
|
14
16
|
wcagSc: ['4.1.2'],
|
|
15
17
|
normativeMappings: [
|
|
16
|
-
{
|
|
18
|
+
{
|
|
19
|
+
standard: 'WCAG',
|
|
20
|
+
version: '2.2',
|
|
21
|
+
requirement: '4.1.2',
|
|
22
|
+
title: 'Name, Role, Value',
|
|
23
|
+
conformanceLevel: 'A'
|
|
24
|
+
}
|
|
17
25
|
],
|
|
18
26
|
defaultSeverity: 'serious',
|
|
19
27
|
category: 'robust',
|
|
@@ -24,20 +32,22 @@ const meta = {
|
|
|
24
32
|
|
|
25
33
|
function runInPage(ctx) {
|
|
26
34
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
35
|
+
const getEligibilityInfo =
|
|
36
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
37
|
|
|
32
38
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
39
|
+
return String(s || '')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
function getAttr(el, name) {
|
|
37
45
|
try {
|
|
38
46
|
if (!el || !el.getAttribute) return '';
|
|
39
47
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -52,7 +62,7 @@ function runInPage(ctx) {
|
|
|
52
62
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
63
|
return info && info.present ? info.value : '';
|
|
54
64
|
}
|
|
55
|
-
const t =
|
|
65
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
56
66
|
return t.replace(/\s+/g, ' ').trim();
|
|
57
67
|
}
|
|
58
68
|
|
|
@@ -74,7 +84,8 @@ function runInPage(ctx) {
|
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
function isEligibleAcc(helpers, el, ctx) {
|
|
77
|
-
const fn =
|
|
87
|
+
const fn =
|
|
88
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
78
89
|
if (!fn) return true;
|
|
79
90
|
try {
|
|
80
91
|
const r = fn(el, ctx);
|
|
@@ -85,13 +96,13 @@ function runInPage(ctx) {
|
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
|
|
89
99
|
const occurrences = [];
|
|
90
100
|
let applicableCount = 0;
|
|
91
101
|
|
|
92
|
-
const selector =
|
|
93
|
-
const nodes = helpers.queryAllSmart
|
|
94
|
-
|
|
102
|
+
const selector = '[role="treeitem"]';
|
|
103
|
+
const nodes = helpers.queryAllSmart
|
|
104
|
+
? helpers.queryAllSmart(selector)
|
|
105
|
+
: helpers.queryAll(selector);
|
|
95
106
|
|
|
96
107
|
function hasName(el) {
|
|
97
108
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -103,11 +114,9 @@ function runInPage(ctx) {
|
|
|
103
114
|
const title = getAttr(el, 'title');
|
|
104
115
|
if (title) return { ok: true, method: 'title' };
|
|
105
116
|
|
|
106
|
-
|
|
107
117
|
const t = getConservativeSubtreeText(document, el);
|
|
108
118
|
if (t) return { ok: true, method: 'content' };
|
|
109
119
|
|
|
110
|
-
|
|
111
120
|
return { ok: false, method: 'none' };
|
|
112
121
|
}
|
|
113
122
|
|
|
@@ -121,10 +130,16 @@ function runInPage(ctx) {
|
|
|
121
130
|
if (res.ok) continue;
|
|
122
131
|
|
|
123
132
|
const eligInfo = getEligibilityInfo
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
? (() => {
|
|
134
|
+
try {
|
|
135
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
})()
|
|
140
|
+
: null;
|
|
126
141
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
127
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
142
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
128
143
|
|
|
129
144
|
occurrences.push({
|
|
130
145
|
selector: stableSelector,
|
|
@@ -143,12 +158,16 @@ function runInPage(ctx) {
|
|
|
143
158
|
});
|
|
144
159
|
}
|
|
145
160
|
|
|
146
|
-
|
|
147
161
|
if (applicableCount === 0) {
|
|
148
162
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
149
163
|
}
|
|
150
164
|
if (occurrences.length) {
|
|
151
|
-
return {
|
|
165
|
+
return {
|
|
166
|
+
ruleId: rule.ruleId,
|
|
167
|
+
outcome: 'fail',
|
|
168
|
+
severity: rule.defaultSeverity || 'minor',
|
|
169
|
+
occurrences
|
|
170
|
+
};
|
|
152
171
|
}
|
|
153
172
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
154
173
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -28,7 +30,8 @@ const id = 'valid-lang';
|
|
|
28
30
|
|
|
29
31
|
const meta = {
|
|
30
32
|
title: 'Element lang attribute must be syntactically valid',
|
|
31
|
-
description:
|
|
33
|
+
description:
|
|
34
|
+
'Checks that any element (other than the root <html>) with a non-empty lang attribute uses a syntactically valid language tag.',
|
|
32
35
|
i18n: {
|
|
33
36
|
titleKey: 'validLang_title',
|
|
34
37
|
descriptionKey: 'validLang_description'
|
|
@@ -37,7 +40,13 @@ const meta = {
|
|
|
37
40
|
tags: ['wcag2aa', 'wcag312', 'structure', 'language', 'atomic', 'automatic'],
|
|
38
41
|
wcagSc: ['3.1.2'],
|
|
39
42
|
normativeMappings: [
|
|
40
|
-
{
|
|
43
|
+
{
|
|
44
|
+
standard: 'WCAG',
|
|
45
|
+
version: '2.2',
|
|
46
|
+
requirement: '3.1.2',
|
|
47
|
+
title: 'Language of Parts',
|
|
48
|
+
conformanceLevel: 'AA'
|
|
49
|
+
}
|
|
41
50
|
],
|
|
42
51
|
defaultSeverity: 'moderate',
|
|
43
52
|
category: 'understandable',
|
|
@@ -47,11 +56,13 @@ const meta = {
|
|
|
47
56
|
};
|
|
48
57
|
|
|
49
58
|
function runInPage(ctx) {
|
|
50
|
-
const {
|
|
59
|
+
const { helpers, rule } = ctx;
|
|
51
60
|
|
|
52
61
|
const BCP47_RE = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
|
|
53
62
|
|
|
54
|
-
const nodes = helpers.queryAllSmart
|
|
63
|
+
const nodes = helpers.queryAllSmart
|
|
64
|
+
? helpers.queryAllSmart('[lang]')
|
|
65
|
+
: helpers.queryAll('[lang]');
|
|
55
66
|
|
|
56
67
|
const occurrences = [];
|
|
57
68
|
let applicableCount = 0;
|
|
@@ -69,7 +80,7 @@ function runInPage(ctx) {
|
|
|
69
80
|
|
|
70
81
|
const tag = el.tagName.toLowerCase();
|
|
71
82
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
72
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
83
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
73
84
|
|
|
74
85
|
occurrences.push({
|
|
75
86
|
selector: stableSelector,
|
|
@@ -91,7 +102,12 @@ function runInPage(ctx) {
|
|
|
91
102
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
92
103
|
}
|
|
93
104
|
if (occurrences.length) {
|
|
94
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
ruleId: rule.ruleId,
|
|
107
|
+
outcome: 'fail',
|
|
108
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
109
|
+
occurrences
|
|
110
|
+
};
|
|
95
111
|
}
|
|
96
112
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
97
113
|
}
|