@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
|
const id = 'tab-name-present';
|
|
@@ -10,10 +12,16 @@ const meta = {
|
|
|
10
12
|
descriptionKey: 'tabNamePresent_description'
|
|
11
13
|
},
|
|
12
14
|
helpUrl: null,
|
|
13
|
-
tags: ['wcag2a','wcag412','navigation','atomic','automatic','name','tab'],
|
|
15
|
+
tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name', 'tab'],
|
|
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,53 +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 {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
function hasAttr(el, name) {
|
|
44
|
-
try { return !!(el && el.hasAttribute && el.hasAttribute(name)); } catch { return false; }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isExplicitProgrammatic(el) {
|
|
48
|
-
// Programmatic name mechanisms we treat as authoritative (presence-only):
|
|
49
|
-
// - aria-label (non-empty)
|
|
50
|
-
// - aria-labelledby (non-empty)
|
|
51
|
-
// - title (non-empty) [weak but allowed for presence]
|
|
52
|
-
const al = getAttr(el, 'aria-label');
|
|
53
|
-
if (al) return true;
|
|
54
|
-
const alb = getAttr(el, 'aria-labelledby');
|
|
55
|
-
if (alb) return true;
|
|
56
|
-
const t = getAttr(el, 'title');
|
|
57
|
-
if (t) return true;
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function buildLabelForMap(doc) {
|
|
62
|
-
const map = new Map(); // id -> label element (first)
|
|
63
|
-
try {
|
|
64
|
-
const labels = doc && doc.getElementsByTagName ? doc.getElementsByTagName('label') : [];
|
|
65
|
-
for (let i = 0; i < labels.length; i += 1) {
|
|
66
|
-
const lab = labels[i];
|
|
67
|
-
if (!lab || !lab.getAttribute) continue;
|
|
68
|
-
const f = normalizeWs(lab.getAttribute('for'));
|
|
69
|
-
if (!f) continue;
|
|
70
|
-
if (!map.has(f)) map.set(f, lab);
|
|
71
|
-
}
|
|
72
|
-
} catch {}
|
|
73
|
-
return map;
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
74
51
|
}
|
|
75
52
|
|
|
76
53
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -85,7 +62,7 @@ function runInPage(ctx) {
|
|
|
85
62
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
86
63
|
return info && info.present ? info.value : '';
|
|
87
64
|
}
|
|
88
|
-
const t =
|
|
65
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
89
66
|
return t.replace(/\s+/g, ' ').trim();
|
|
90
67
|
}
|
|
91
68
|
|
|
@@ -106,17 +83,9 @@ function runInPage(ctx) {
|
|
|
106
83
|
return '';
|
|
107
84
|
}
|
|
108
85
|
|
|
109
|
-
function getInputValueName(el) {
|
|
110
|
-
try {
|
|
111
|
-
const type = getAttr(el, 'type').toLowerCase();
|
|
112
|
-
if (type !== 'button' && type !== 'submit' && type !== 'reset') return '';
|
|
113
|
-
const v = getAttr(el, 'value');
|
|
114
|
-
return v;
|
|
115
|
-
} catch { return ''; }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
86
|
function isEligibleAcc(helpers, el, ctx) {
|
|
119
|
-
const fn =
|
|
87
|
+
const fn =
|
|
88
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
120
89
|
if (!fn) return true;
|
|
121
90
|
try {
|
|
122
91
|
const r = fn(el, ctx);
|
|
@@ -127,13 +96,13 @@ function runInPage(ctx) {
|
|
|
127
96
|
}
|
|
128
97
|
}
|
|
129
98
|
|
|
130
|
-
|
|
131
99
|
const occurrences = [];
|
|
132
100
|
let applicableCount = 0;
|
|
133
101
|
|
|
134
102
|
const selector = '[role="tab"]';
|
|
135
|
-
const nodes = helpers.queryAllSmart
|
|
136
|
-
|
|
103
|
+
const nodes = helpers.queryAllSmart
|
|
104
|
+
? helpers.queryAllSmart(selector)
|
|
105
|
+
: helpers.queryAll(selector);
|
|
137
106
|
|
|
138
107
|
function evaluate(el) {
|
|
139
108
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -164,10 +133,16 @@ function runInPage(ctx) {
|
|
|
164
133
|
if (res.ok) continue;
|
|
165
134
|
|
|
166
135
|
const eligInfo = getEligibilityInfo
|
|
167
|
-
|
|
168
|
-
|
|
136
|
+
? (() => {
|
|
137
|
+
try {
|
|
138
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
})()
|
|
143
|
+
: null;
|
|
169
144
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
170
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
145
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
171
146
|
|
|
172
147
|
occurrences.push({
|
|
173
148
|
selector: stableSelector,
|
|
@@ -186,12 +161,16 @@ function runInPage(ctx) {
|
|
|
186
161
|
});
|
|
187
162
|
}
|
|
188
163
|
|
|
189
|
-
|
|
190
164
|
if (applicableCount === 0) {
|
|
191
165
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
192
166
|
}
|
|
193
167
|
if (occurrences.length) {
|
|
194
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
ruleId: rule.ruleId,
|
|
170
|
+
outcome: 'fail',
|
|
171
|
+
severity: rule.defaultSeverity || 'minor',
|
|
172
|
+
occurrences
|
|
173
|
+
};
|
|
195
174
|
}
|
|
196
175
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
197
176
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -14,8 +16,7 @@
|
|
|
14
16
|
* the referencing cell, and (d) is not the cell itself.
|
|
15
17
|
* @implementation-notes
|
|
16
18
|
* - One occurrence per offending cell (not per bad token), listing every
|
|
17
|
-
* invalid reference
|
|
18
|
-
* td-headers-attr reporting granularity.
|
|
19
|
+
* invalid reference.
|
|
19
20
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
20
21
|
* property, while engine-level hidden-subtree filtering still applies
|
|
21
22
|
* unless engineOptions.includeHiddenElements is true.
|
|
@@ -25,7 +26,8 @@ const id = 'table-headers-attr-valid';
|
|
|
25
26
|
|
|
26
27
|
const meta = {
|
|
27
28
|
title: 'Table cell "headers" attribute must reference valid header cells',
|
|
28
|
-
description:
|
|
29
|
+
description:
|
|
30
|
+
'Checks that each id in a <td>/<th> headers attribute resolves to a <th> element within the same table (not missing, not a non-th element, not itself).',
|
|
29
31
|
i18n: {
|
|
30
32
|
titleKey: 'tableHeadersAttrValid_title',
|
|
31
33
|
descriptionKey: 'tableHeadersAttrValid_description'
|
|
@@ -34,7 +36,13 @@ const meta = {
|
|
|
34
36
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'table'],
|
|
35
37
|
wcagSc: ['1.3.1'],
|
|
36
38
|
normativeMappings: [
|
|
37
|
-
{
|
|
39
|
+
{
|
|
40
|
+
standard: 'WCAG',
|
|
41
|
+
version: '2.2',
|
|
42
|
+
requirement: '1.3.1',
|
|
43
|
+
title: 'Info and Relationships',
|
|
44
|
+
conformanceLevel: 'A'
|
|
45
|
+
}
|
|
38
46
|
],
|
|
39
47
|
defaultSeverity: 'serious',
|
|
40
48
|
category: 'perceivable',
|
|
@@ -46,7 +54,9 @@ const meta = {
|
|
|
46
54
|
function runInPage(ctx) {
|
|
47
55
|
const { document, helpers, rule } = ctx;
|
|
48
56
|
|
|
49
|
-
const nodes = helpers.queryAllSmart
|
|
57
|
+
const nodes = helpers.queryAllSmart
|
|
58
|
+
? helpers.queryAllSmart('td[headers], th[headers]')
|
|
59
|
+
: helpers.queryAll('td[headers], th[headers]');
|
|
50
60
|
|
|
51
61
|
const occurrences = [];
|
|
52
62
|
let applicableCount = 0;
|
|
@@ -65,7 +75,7 @@ function runInPage(ctx) {
|
|
|
65
75
|
const invalid = [];
|
|
66
76
|
|
|
67
77
|
for (const headerId of ids) {
|
|
68
|
-
let ref
|
|
78
|
+
let ref;
|
|
69
79
|
try {
|
|
70
80
|
ref = document.getElementById(headerId);
|
|
71
81
|
} catch {
|
|
@@ -91,9 +101,11 @@ function runInPage(ctx) {
|
|
|
91
101
|
|
|
92
102
|
if (!invalid.length) continue;
|
|
93
103
|
|
|
104
|
+
const dedupedInvalidIds = [...new Set(invalid.map((i) => i.id))];
|
|
105
|
+
|
|
94
106
|
const tag = el.tagName.toLowerCase();
|
|
95
107
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
96
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
108
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
97
109
|
|
|
98
110
|
occurrences.push({
|
|
99
111
|
selector: stableSelector,
|
|
@@ -103,7 +115,7 @@ function runInPage(ctx) {
|
|
|
103
115
|
i18n: {
|
|
104
116
|
summaryKey: 'tableHeadersAttrValid_summary_fail',
|
|
105
117
|
hintKey: 'tableHeadersAttrValid_hint_fail',
|
|
106
|
-
params: { element: tag, invalidIds:
|
|
118
|
+
params: { element: tag, invalidIds: dedupedInvalidIds.join(', ') }
|
|
107
119
|
},
|
|
108
120
|
data: {
|
|
109
121
|
details: { reasonCode: 'TABLE_HEADERS_ATTR_INVALID', element: tag, invalid }
|
|
@@ -115,9 +127,14 @@ function runInPage(ctx) {
|
|
|
115
127
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
116
128
|
}
|
|
117
129
|
if (occurrences.length) {
|
|
118
|
-
return {
|
|
130
|
+
return {
|
|
131
|
+
ruleId: rule.ruleId,
|
|
132
|
+
outcome: 'fail',
|
|
133
|
+
severity: rule.defaultSeverity || 'serious',
|
|
134
|
+
occurrences
|
|
135
|
+
};
|
|
119
136
|
}
|
|
120
137
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
121
138
|
}
|
|
122
139
|
|
|
123
|
-
module.exports = { id, meta, runInPage };
|
|
140
|
+
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
|
/**
|
|
@@ -37,7 +39,8 @@ const id = 'table-th-has-data-cells';
|
|
|
37
39
|
|
|
38
40
|
const meta = {
|
|
39
41
|
title: '<th> elements must describe at least one data cell',
|
|
40
|
-
description:
|
|
42
|
+
description:
|
|
43
|
+
'Checks that a table containing <th> elements also contains at least one <td> data cell for those headers to describe.',
|
|
41
44
|
i18n: {
|
|
42
45
|
titleKey: 'tableThHasDataCells_title',
|
|
43
46
|
descriptionKey: 'tableThHasDataCells_description'
|
|
@@ -46,7 +49,13 @@ const meta = {
|
|
|
46
49
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'table'],
|
|
47
50
|
wcagSc: ['1.3.1'],
|
|
48
51
|
normativeMappings: [
|
|
49
|
-
{
|
|
52
|
+
{
|
|
53
|
+
standard: 'WCAG',
|
|
54
|
+
version: '2.2',
|
|
55
|
+
requirement: '1.3.1',
|
|
56
|
+
title: 'Info and Relationships',
|
|
57
|
+
conformanceLevel: 'A'
|
|
58
|
+
}
|
|
50
59
|
],
|
|
51
60
|
defaultSeverity: 'moderate',
|
|
52
61
|
category: 'perceivable',
|
|
@@ -56,7 +65,7 @@ const meta = {
|
|
|
56
65
|
};
|
|
57
66
|
|
|
58
67
|
function runInPage(ctx) {
|
|
59
|
-
const {
|
|
68
|
+
const { helpers, rule } = ctx;
|
|
60
69
|
|
|
61
70
|
const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
|
|
62
71
|
|
|
@@ -66,7 +75,7 @@ function runInPage(ctx) {
|
|
|
66
75
|
for (const table of tables) {
|
|
67
76
|
if (!table || !table.querySelectorAll) continue;
|
|
68
77
|
|
|
69
|
-
let ths
|
|
78
|
+
let ths;
|
|
70
79
|
try {
|
|
71
80
|
ths = table.querySelectorAll('th');
|
|
72
81
|
} catch {
|
|
@@ -76,7 +85,7 @@ function runInPage(ctx) {
|
|
|
76
85
|
|
|
77
86
|
applicableCount += 1;
|
|
78
87
|
|
|
79
|
-
let hasDataCell
|
|
88
|
+
let hasDataCell;
|
|
80
89
|
try {
|
|
81
90
|
hasDataCell = table.querySelectorAll('td').length > 0;
|
|
82
91
|
} catch {
|
|
@@ -87,7 +96,9 @@ function runInPage(ctx) {
|
|
|
87
96
|
for (const th of ths) {
|
|
88
97
|
if (!th) continue;
|
|
89
98
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(th) : 'html';
|
|
90
|
-
const html = helpers.getOuterHtmlSnippet
|
|
99
|
+
const html = helpers.getOuterHtmlSnippet
|
|
100
|
+
? helpers.getOuterHtmlSnippet(th)
|
|
101
|
+
: th.outerHTML || '';
|
|
91
102
|
|
|
92
103
|
occurrences.push({
|
|
93
104
|
selector: stableSelector,
|
|
@@ -110,9 +121,14 @@ function runInPage(ctx) {
|
|
|
110
121
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
111
122
|
}
|
|
112
123
|
if (occurrences.length) {
|
|
113
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
ruleId: rule.ruleId,
|
|
126
|
+
outcome: 'fail',
|
|
127
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
128
|
+
occurrences
|
|
129
|
+
};
|
|
114
130
|
}
|
|
115
131
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
116
132
|
}
|
|
117
133
|
|
|
118
|
-
module.exports = { id, meta, runInPage };
|
|
134
|
+
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
|
/**
|
|
@@ -60,13 +62,23 @@ const id = 'target-size-minimum';
|
|
|
60
62
|
const meta = {
|
|
61
63
|
title: 'Pointer targets meet minimum size (AA)',
|
|
62
64
|
description:
|
|
63
|
-
|
|
65
|
+
'Checks that pointer-operable targets have an effective hit region of at least 24 by 24 CSS pixels, or meet an allowed exception (e.g. sufficient spacing).',
|
|
64
66
|
i18n: {
|
|
65
67
|
titleKey: 'targetSizeMinimum_title',
|
|
66
68
|
descriptionKey: 'targetSizeMinimum_description'
|
|
67
69
|
},
|
|
68
70
|
helpUrl: null,
|
|
69
|
-
tags: [
|
|
71
|
+
tags: [
|
|
72
|
+
'wcag22aa',
|
|
73
|
+
'wcag258',
|
|
74
|
+
'navigation',
|
|
75
|
+
'operable',
|
|
76
|
+
'pointer',
|
|
77
|
+
'target-size',
|
|
78
|
+
'atomic',
|
|
79
|
+
'automatic',
|
|
80
|
+
'dom'
|
|
81
|
+
],
|
|
70
82
|
wcagSc: ['2.5.8'],
|
|
71
83
|
normativeMappings: [
|
|
72
84
|
{
|
|
@@ -115,8 +127,10 @@ function runInPage(ctx) {
|
|
|
115
127
|
// with its own .querySelectorAll to call directly.
|
|
116
128
|
function qsa(sel) {
|
|
117
129
|
try {
|
|
118
|
-
if (helpers && typeof helpers.queryAllSmart === 'function')
|
|
119
|
-
|
|
130
|
+
if (helpers && typeof helpers.queryAllSmart === 'function')
|
|
131
|
+
return Array.from(helpers.queryAllSmart(sel) || []);
|
|
132
|
+
if (helpers && typeof helpers.queryAll === 'function')
|
|
133
|
+
return Array.from(helpers.queryAll(sel) || []);
|
|
120
134
|
return [];
|
|
121
135
|
} catch {
|
|
122
136
|
return [];
|
|
@@ -135,10 +149,11 @@ function runInPage(ctx) {
|
|
|
135
149
|
|
|
136
150
|
function htmlSnippet(el) {
|
|
137
151
|
try {
|
|
138
|
-
if (helpers && typeof helpers.getOuterHtmlSnippet === 'function')
|
|
152
|
+
if (helpers && typeof helpers.getOuterHtmlSnippet === 'function')
|
|
153
|
+
return helpers.getOuterHtmlSnippet(el);
|
|
139
154
|
} catch {}
|
|
140
155
|
try {
|
|
141
|
-
return
|
|
156
|
+
return el && el.outerHTML ? String(el.outerHTML) : '';
|
|
142
157
|
} catch {}
|
|
143
158
|
return '';
|
|
144
159
|
}
|
|
@@ -153,8 +168,14 @@ function runInPage(ctx) {
|
|
|
153
168
|
if (!el || el.nodeType !== 1) return false;
|
|
154
169
|
|
|
155
170
|
const tag = (el.tagName || '').toLowerCase();
|
|
156
|
-
const role =
|
|
157
|
-
|
|
171
|
+
const role =
|
|
172
|
+
(el.getAttribute &&
|
|
173
|
+
String(el.getAttribute('role') || '')
|
|
174
|
+
.trim()
|
|
175
|
+
.toLowerCase()) ||
|
|
176
|
+
'';
|
|
177
|
+
const isLinkLike =
|
|
178
|
+
(tag === 'a' && el.getAttribute && el.getAttribute('href')) || role === 'link';
|
|
158
179
|
if (!isLinkLike) return false;
|
|
159
180
|
|
|
160
181
|
const cs = getStyle(el);
|
|
@@ -164,11 +185,11 @@ function runInPage(ctx) {
|
|
|
164
185
|
// Inline exception is for inline text runs; many design systems use inline-block for links,
|
|
165
186
|
// so treat inline-block variants as eligible for this exception.
|
|
166
187
|
const isInline =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
188
|
+
display === 'inline' ||
|
|
189
|
+
display === 'inline-block' ||
|
|
190
|
+
display === 'inline-flex' ||
|
|
191
|
+
display === 'inline-grid' ||
|
|
192
|
+
display === 'inline-table';
|
|
172
193
|
|
|
173
194
|
if (!isInline) return false;
|
|
174
195
|
|
|
@@ -182,7 +203,10 @@ function runInPage(ctx) {
|
|
|
182
203
|
// Fallback: sometimes links are inside spans within a paragraph-like region
|
|
183
204
|
const inlineContainer = closest(el, 'span, em, strong, small, label');
|
|
184
205
|
if (inlineContainer) {
|
|
185
|
-
const outerTextBlock = closest(
|
|
206
|
+
const outerTextBlock = closest(
|
|
207
|
+
inlineContainer,
|
|
208
|
+
'p, li, dd, dt, blockquote, figcaption, caption, td, th'
|
|
209
|
+
);
|
|
186
210
|
if (outerTextBlock) return true;
|
|
187
211
|
}
|
|
188
212
|
|
|
@@ -234,7 +258,7 @@ function runInPage(ctx) {
|
|
|
234
258
|
try {
|
|
235
259
|
if (det.hasAttribute('open')) return false;
|
|
236
260
|
|
|
237
|
-
const tag =
|
|
261
|
+
const tag = el && el.tagName ? String(el.tagName).toLowerCase() : '';
|
|
238
262
|
// summary remains operable even when <details> is closed
|
|
239
263
|
if (tag === 'summary') return false;
|
|
240
264
|
|
|
@@ -256,9 +280,9 @@ function runInPage(ctx) {
|
|
|
256
280
|
|
|
257
281
|
function getStyle(el) {
|
|
258
282
|
try {
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
|
|
283
|
+
return document && document.defaultView && document.defaultView.getComputedStyle
|
|
284
|
+
? document.defaultView.getComputedStyle(el)
|
|
285
|
+
: null;
|
|
262
286
|
} catch {
|
|
263
287
|
return null;
|
|
264
288
|
}
|
|
@@ -288,7 +312,11 @@ function runInPage(ctx) {
|
|
|
288
312
|
|
|
289
313
|
// aria-disabled elements are typically treated as not operable
|
|
290
314
|
try {
|
|
291
|
-
const ad =
|
|
315
|
+
const ad =
|
|
316
|
+
el.getAttribute &&
|
|
317
|
+
String(el.getAttribute('aria-disabled') || '')
|
|
318
|
+
.trim()
|
|
319
|
+
.toLowerCase();
|
|
292
320
|
if (ad === 'true') return false;
|
|
293
321
|
} catch {}
|
|
294
322
|
|
|
@@ -321,7 +349,8 @@ function runInPage(ctx) {
|
|
|
321
349
|
|
|
322
350
|
function elementFromPoint(x, y) {
|
|
323
351
|
try {
|
|
324
|
-
if (document && typeof document.elementFromPoint === 'function')
|
|
352
|
+
if (document && typeof document.elementFromPoint === 'function')
|
|
353
|
+
return document.elementFromPoint(x, y);
|
|
325
354
|
} catch {}
|
|
326
355
|
return null;
|
|
327
356
|
}
|
|
@@ -347,7 +376,8 @@ function runInPage(ctx) {
|
|
|
347
376
|
}
|
|
348
377
|
}
|
|
349
378
|
|
|
350
|
-
const CANDIDATE_SELECTOR =
|
|
379
|
+
const CANDIDATE_SELECTOR =
|
|
380
|
+
'button, summary, a[href], area[href], input, select, textarea, [role="button"], [role="link"]';
|
|
351
381
|
|
|
352
382
|
// --- candidate collection ---
|
|
353
383
|
const candidates = qsa(CANDIDATE_SELECTOR);
|
|
@@ -481,14 +511,20 @@ function runInPage(ctx) {
|
|
|
481
511
|
const tag = (el.tagName || '').toLowerCase();
|
|
482
512
|
if (tag !== 'input') return false;
|
|
483
513
|
|
|
484
|
-
const type =
|
|
514
|
+
const type =
|
|
515
|
+
(el.getAttribute &&
|
|
516
|
+
String(el.getAttribute('type') || '')
|
|
517
|
+
.trim()
|
|
518
|
+
.toLowerCase()) ||
|
|
519
|
+
'';
|
|
485
520
|
if (type !== 'checkbox' && type !== 'radio') return false;
|
|
486
521
|
|
|
487
522
|
const cs = getStyle(el);
|
|
488
523
|
let appearance = '';
|
|
489
524
|
try {
|
|
490
525
|
if (cs && typeof cs.getPropertyValue === 'function') {
|
|
491
|
-
appearance =
|
|
526
|
+
appearance =
|
|
527
|
+
cs.getPropertyValue('appearance') || cs.getPropertyValue('-webkit-appearance') || '';
|
|
492
528
|
} else if (cs) {
|
|
493
529
|
appearance = cs.appearance || cs.webkitAppearance || '';
|
|
494
530
|
}
|
|
@@ -528,8 +564,8 @@ function runInPage(ctx) {
|
|
|
528
564
|
}
|
|
529
565
|
}
|
|
530
566
|
|
|
531
|
-
const
|
|
532
|
-
|
|
567
|
+
const failOccurrences = [];
|
|
568
|
+
const cantTellOccurrences = [];
|
|
533
569
|
|
|
534
570
|
for (const it of undersized) {
|
|
535
571
|
// Inline-text exception: do not fail purely on size/spacing for inline links in text.
|
|
@@ -548,20 +584,69 @@ function runInPage(ctx) {
|
|
|
548
584
|
const info = hasSpacingConflict(it);
|
|
549
585
|
|
|
550
586
|
if (!info.conflict && info.confident === false) {
|
|
551
|
-
// Ambiguous perimeter-sampling result near the decision threshold
|
|
552
|
-
|
|
587
|
+
// Ambiguous perimeter-sampling result near the decision threshold —
|
|
588
|
+
// previously recorded only as a page-level boolean with no per-target
|
|
589
|
+
// occurrence at all, so this specific target was unrecoverable from
|
|
590
|
+
// the result once any other target on the page had a confident
|
|
591
|
+
// fail (see helpers.resolveTieredOutcome's header comment). Now
|
|
592
|
+
// reported as its own cantTell-tier occurrence instead.
|
|
593
|
+
cantTellOccurrences.push({
|
|
594
|
+
selector: buildSelector(it.el),
|
|
595
|
+
html: htmlSnippet(it.el),
|
|
596
|
+
occurrenceOutcome: 'cantTell',
|
|
597
|
+
summary:
|
|
598
|
+
'Target may be too small and too close to another target, but the overlap is near the detection threshold and could not be confidently measured.',
|
|
599
|
+
hint: 'Manually verify the effective spacing between this target and its neighbor; increase target size or spacing if the overlap is real.',
|
|
600
|
+
i18n: {
|
|
601
|
+
summaryKey: 'targetSizeMinimum_summary_cantTell_ambiguousSpacing',
|
|
602
|
+
hintKey: 'targetSizeMinimum_hint_cantTell_ambiguousSpacing',
|
|
603
|
+
params: {}
|
|
604
|
+
},
|
|
605
|
+
data: {
|
|
606
|
+
details: {
|
|
607
|
+
measured: { width: it.rect.width, height: it.rect.height },
|
|
608
|
+
reasonCode: 'undersized-ambiguous-spacing',
|
|
609
|
+
conflictHitCount: info.hitCount,
|
|
610
|
+
conflictWith: info.conflictEl ? buildSelector(info.conflictEl) : null
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
});
|
|
553
614
|
continue;
|
|
554
615
|
}
|
|
555
616
|
|
|
556
617
|
if (info.conflict) {
|
|
557
618
|
if (isPlausiblyEssentialOrEquivalent(it.el)) {
|
|
558
|
-
|
|
619
|
+
// Confident spacing conflict, but the target may be exempt as part
|
|
620
|
+
// of an essential graphic/image-map region — same "previously
|
|
621
|
+
// unrecoverable" gap as above, now reported instead of dropped.
|
|
622
|
+
cantTellOccurrences.push({
|
|
623
|
+
selector: buildSelector(it.el),
|
|
624
|
+
html: htmlSnippet(it.el),
|
|
625
|
+
occurrenceOutcome: 'cantTell',
|
|
626
|
+
summary:
|
|
627
|
+
'Target is too small and too close to another target, but may be exempt as part of an essential graphic or image-map region.',
|
|
628
|
+
hint: 'Verify whether this target’s size is genuinely essential to its function (e.g. part of an SVG/canvas/image map); if not, increase target size or spacing.',
|
|
629
|
+
i18n: {
|
|
630
|
+
summaryKey: 'targetSizeMinimum_summary_cantTell_plausiblyEssential',
|
|
631
|
+
hintKey: 'targetSizeMinimum_hint_cantTell_plausiblyEssential',
|
|
632
|
+
params: {}
|
|
633
|
+
},
|
|
634
|
+
data: {
|
|
635
|
+
details: {
|
|
636
|
+
measured: { width: it.rect.width, height: it.rect.height },
|
|
637
|
+
reasonCode: 'undersized-plausibly-essential',
|
|
638
|
+
conflictHitCount: info.hitCount,
|
|
639
|
+
conflictWith: info.conflictEl ? buildSelector(info.conflictEl) : null
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
});
|
|
559
643
|
continue;
|
|
560
644
|
}
|
|
561
645
|
|
|
562
|
-
|
|
646
|
+
failOccurrences.push({
|
|
563
647
|
selector: buildSelector(it.el),
|
|
564
648
|
html: htmlSnippet(it.el),
|
|
649
|
+
occurrenceOutcome: 'fail',
|
|
565
650
|
summary: 'Target is too small and too close to another target.',
|
|
566
651
|
hint: 'Increase target size to at least 24 by 24 CSS pixels, or add sufficient spacing.',
|
|
567
652
|
i18n: {
|
|
@@ -581,25 +666,15 @@ function runInPage(ctx) {
|
|
|
581
666
|
}
|
|
582
667
|
}
|
|
583
668
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (hasUncertainConflicts) {
|
|
594
|
-
return {
|
|
595
|
-
ruleId: RULE_ID,
|
|
596
|
-
outcome: 'cantTell',
|
|
597
|
-
severity: 'minor',
|
|
598
|
-
occurrences: []
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
return { ruleId: RULE_ID, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
669
|
+
// See helpers.resolveTieredOutcome's own header comment (src/core/dom-helpers.js):
|
|
670
|
+
// a fail-tier finding never silently discards cantTell-tier findings from
|
|
671
|
+
// the same run — both are returned together when the outcome is 'fail'.
|
|
672
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
673
|
+
failOccurrences,
|
|
674
|
+
cantTellOccurrences,
|
|
675
|
+
(rule && rule.defaultSeverity) || 'minor'
|
|
676
|
+
);
|
|
677
|
+
return { ruleId: RULE_ID, ...resolved };
|
|
603
678
|
}
|
|
604
679
|
|
|
605
680
|
module.exports = { id, meta, runInPage };
|