@surea11y/core 1.2.0 → 1.3.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 +37 -7
- package/LICENSE +373 -21
- package/README.md +67 -1
- package/bin/core.js +144 -19
- package/docs/API_STABILITY.md +1 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +54 -1
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/INTEGRATION.md +18 -0
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +42 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +55 -16
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +42 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +30 -8
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +109 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
- package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +17 -6
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -7
- package/src/checks/manual/empty-table-header-manual.js +17 -6
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -7
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
- package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +26 -20
- package/src/checks/manual/landmark-unique-manual.js +41 -15
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +16 -5
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +31 -22
- package/src/checks/manual/page-title-patterns-manual.js +78 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +241 -48
- package/src/checks/manual/scope-attr-valid-manual.js +10 -3
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +10 -3
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1127 -886
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4175 -3917
- package/src/core/dom-runner.js +720 -604
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +58 -41
- package/src/core.js +36552 -29111
- package/src/i18n/en.js +1194 -889
- package/src/i18n/fr.js +1136 -795
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +63 -43
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -10,10 +10,16 @@ const meta = {
|
|
|
10
10
|
descriptionKey: 'tabNamePresent_description'
|
|
11
11
|
},
|
|
12
12
|
helpUrl: null,
|
|
13
|
-
tags: ['wcag2a','wcag412','navigation','atomic','automatic','name','tab'],
|
|
13
|
+
tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name', 'tab'],
|
|
14
14
|
wcagSc: ['4.1.2'],
|
|
15
15
|
normativeMappings: [
|
|
16
|
-
{
|
|
16
|
+
{
|
|
17
|
+
standard: 'WCAG',
|
|
18
|
+
version: '2.2',
|
|
19
|
+
requirement: '4.1.2',
|
|
20
|
+
title: 'Name, Role, Value',
|
|
21
|
+
conformanceLevel: 'A'
|
|
22
|
+
}
|
|
17
23
|
],
|
|
18
24
|
defaultSeverity: 'serious',
|
|
19
25
|
category: 'robust',
|
|
@@ -24,53 +30,22 @@ const meta = {
|
|
|
24
30
|
|
|
25
31
|
function runInPage(ctx) {
|
|
26
32
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
33
|
+
const getEligibilityInfo =
|
|
34
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
35
|
|
|
32
36
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
37
|
+
return String(s || '')
|
|
38
|
+
.replace(/\s+/g, ' ')
|
|
39
|
+
.trim();
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
function getAttr(el, name) {
|
|
37
43
|
try {
|
|
38
44
|
if (!el || !el.getAttribute) return '';
|
|
39
45
|
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;
|
|
46
|
+
} catch {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
74
49
|
}
|
|
75
50
|
|
|
76
51
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -85,7 +60,7 @@ function runInPage(ctx) {
|
|
|
85
60
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
86
61
|
return info && info.present ? info.value : '';
|
|
87
62
|
}
|
|
88
|
-
const t =
|
|
63
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
89
64
|
return t.replace(/\s+/g, ' ').trim();
|
|
90
65
|
}
|
|
91
66
|
|
|
@@ -106,17 +81,9 @@ function runInPage(ctx) {
|
|
|
106
81
|
return '';
|
|
107
82
|
}
|
|
108
83
|
|
|
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
84
|
function isEligibleAcc(helpers, el, ctx) {
|
|
119
|
-
const fn =
|
|
85
|
+
const fn =
|
|
86
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
120
87
|
if (!fn) return true;
|
|
121
88
|
try {
|
|
122
89
|
const r = fn(el, ctx);
|
|
@@ -127,13 +94,13 @@ function runInPage(ctx) {
|
|
|
127
94
|
}
|
|
128
95
|
}
|
|
129
96
|
|
|
130
|
-
|
|
131
97
|
const occurrences = [];
|
|
132
98
|
let applicableCount = 0;
|
|
133
99
|
|
|
134
100
|
const selector = '[role="tab"]';
|
|
135
|
-
const nodes = helpers.queryAllSmart
|
|
136
|
-
|
|
101
|
+
const nodes = helpers.queryAllSmart
|
|
102
|
+
? helpers.queryAllSmart(selector)
|
|
103
|
+
: helpers.queryAll(selector);
|
|
137
104
|
|
|
138
105
|
function evaluate(el) {
|
|
139
106
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -164,10 +131,16 @@ function runInPage(ctx) {
|
|
|
164
131
|
if (res.ok) continue;
|
|
165
132
|
|
|
166
133
|
const eligInfo = getEligibilityInfo
|
|
167
|
-
|
|
168
|
-
|
|
134
|
+
? (() => {
|
|
135
|
+
try {
|
|
136
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
})()
|
|
141
|
+
: null;
|
|
169
142
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
170
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
143
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
171
144
|
|
|
172
145
|
occurrences.push({
|
|
173
146
|
selector: stableSelector,
|
|
@@ -186,12 +159,16 @@ function runInPage(ctx) {
|
|
|
186
159
|
});
|
|
187
160
|
}
|
|
188
161
|
|
|
189
|
-
|
|
190
162
|
if (applicableCount === 0) {
|
|
191
163
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
192
164
|
}
|
|
193
165
|
if (occurrences.length) {
|
|
194
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
ruleId: rule.ruleId,
|
|
168
|
+
outcome: 'fail',
|
|
169
|
+
severity: rule.defaultSeverity || 'minor',
|
|
170
|
+
occurrences
|
|
171
|
+
};
|
|
195
172
|
}
|
|
196
173
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
197
174
|
}
|
|
@@ -25,7 +25,8 @@ const id = 'table-headers-attr-valid';
|
|
|
25
25
|
|
|
26
26
|
const meta = {
|
|
27
27
|
title: 'Table cell "headers" attribute must reference valid header cells',
|
|
28
|
-
description:
|
|
28
|
+
description:
|
|
29
|
+
'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
30
|
i18n: {
|
|
30
31
|
titleKey: 'tableHeadersAttrValid_title',
|
|
31
32
|
descriptionKey: 'tableHeadersAttrValid_description'
|
|
@@ -34,7 +35,13 @@ const meta = {
|
|
|
34
35
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'table'],
|
|
35
36
|
wcagSc: ['1.3.1'],
|
|
36
37
|
normativeMappings: [
|
|
37
|
-
{
|
|
38
|
+
{
|
|
39
|
+
standard: 'WCAG',
|
|
40
|
+
version: '2.2',
|
|
41
|
+
requirement: '1.3.1',
|
|
42
|
+
title: 'Info and Relationships',
|
|
43
|
+
conformanceLevel: 'A'
|
|
44
|
+
}
|
|
38
45
|
],
|
|
39
46
|
defaultSeverity: 'serious',
|
|
40
47
|
category: 'perceivable',
|
|
@@ -46,7 +53,9 @@ const meta = {
|
|
|
46
53
|
function runInPage(ctx) {
|
|
47
54
|
const { document, helpers, rule } = ctx;
|
|
48
55
|
|
|
49
|
-
const nodes = helpers.queryAllSmart
|
|
56
|
+
const nodes = helpers.queryAllSmart
|
|
57
|
+
? helpers.queryAllSmart('td[headers], th[headers]')
|
|
58
|
+
: helpers.queryAll('td[headers], th[headers]');
|
|
50
59
|
|
|
51
60
|
const occurrences = [];
|
|
52
61
|
let applicableCount = 0;
|
|
@@ -65,7 +74,7 @@ function runInPage(ctx) {
|
|
|
65
74
|
const invalid = [];
|
|
66
75
|
|
|
67
76
|
for (const headerId of ids) {
|
|
68
|
-
let ref
|
|
77
|
+
let ref;
|
|
69
78
|
try {
|
|
70
79
|
ref = document.getElementById(headerId);
|
|
71
80
|
} catch {
|
|
@@ -91,9 +100,11 @@ function runInPage(ctx) {
|
|
|
91
100
|
|
|
92
101
|
if (!invalid.length) continue;
|
|
93
102
|
|
|
103
|
+
const dedupedInvalidIds = [...new Set(invalid.map((i) => i.id))];
|
|
104
|
+
|
|
94
105
|
const tag = el.tagName.toLowerCase();
|
|
95
106
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
96
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
107
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
97
108
|
|
|
98
109
|
occurrences.push({
|
|
99
110
|
selector: stableSelector,
|
|
@@ -103,7 +114,7 @@ function runInPage(ctx) {
|
|
|
103
114
|
i18n: {
|
|
104
115
|
summaryKey: 'tableHeadersAttrValid_summary_fail',
|
|
105
116
|
hintKey: 'tableHeadersAttrValid_hint_fail',
|
|
106
|
-
params: { element: tag, invalidIds:
|
|
117
|
+
params: { element: tag, invalidIds: dedupedInvalidIds.join(', ') }
|
|
107
118
|
},
|
|
108
119
|
data: {
|
|
109
120
|
details: { reasonCode: 'TABLE_HEADERS_ATTR_INVALID', element: tag, invalid }
|
|
@@ -115,9 +126,14 @@ function runInPage(ctx) {
|
|
|
115
126
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
116
127
|
}
|
|
117
128
|
if (occurrences.length) {
|
|
118
|
-
return {
|
|
129
|
+
return {
|
|
130
|
+
ruleId: rule.ruleId,
|
|
131
|
+
outcome: 'fail',
|
|
132
|
+
severity: rule.defaultSeverity || 'serious',
|
|
133
|
+
occurrences
|
|
134
|
+
};
|
|
119
135
|
}
|
|
120
136
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
121
137
|
}
|
|
122
138
|
|
|
123
|
-
module.exports = { id, meta, runInPage };
|
|
139
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -37,7 +37,8 @@ const id = 'table-th-has-data-cells';
|
|
|
37
37
|
|
|
38
38
|
const meta = {
|
|
39
39
|
title: '<th> elements must describe at least one data cell',
|
|
40
|
-
description:
|
|
40
|
+
description:
|
|
41
|
+
'Checks that a table containing <th> elements also contains at least one <td> data cell for those headers to describe.',
|
|
41
42
|
i18n: {
|
|
42
43
|
titleKey: 'tableThHasDataCells_title',
|
|
43
44
|
descriptionKey: 'tableThHasDataCells_description'
|
|
@@ -46,7 +47,13 @@ const meta = {
|
|
|
46
47
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'table'],
|
|
47
48
|
wcagSc: ['1.3.1'],
|
|
48
49
|
normativeMappings: [
|
|
49
|
-
{
|
|
50
|
+
{
|
|
51
|
+
standard: 'WCAG',
|
|
52
|
+
version: '2.2',
|
|
53
|
+
requirement: '1.3.1',
|
|
54
|
+
title: 'Info and Relationships',
|
|
55
|
+
conformanceLevel: 'A'
|
|
56
|
+
}
|
|
50
57
|
],
|
|
51
58
|
defaultSeverity: 'moderate',
|
|
52
59
|
category: 'perceivable',
|
|
@@ -56,7 +63,7 @@ const meta = {
|
|
|
56
63
|
};
|
|
57
64
|
|
|
58
65
|
function runInPage(ctx) {
|
|
59
|
-
const {
|
|
66
|
+
const { helpers, rule } = ctx;
|
|
60
67
|
|
|
61
68
|
const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
|
|
62
69
|
|
|
@@ -66,7 +73,7 @@ function runInPage(ctx) {
|
|
|
66
73
|
for (const table of tables) {
|
|
67
74
|
if (!table || !table.querySelectorAll) continue;
|
|
68
75
|
|
|
69
|
-
let ths
|
|
76
|
+
let ths;
|
|
70
77
|
try {
|
|
71
78
|
ths = table.querySelectorAll('th');
|
|
72
79
|
} catch {
|
|
@@ -76,7 +83,7 @@ function runInPage(ctx) {
|
|
|
76
83
|
|
|
77
84
|
applicableCount += 1;
|
|
78
85
|
|
|
79
|
-
let hasDataCell
|
|
86
|
+
let hasDataCell;
|
|
80
87
|
try {
|
|
81
88
|
hasDataCell = table.querySelectorAll('td').length > 0;
|
|
82
89
|
} catch {
|
|
@@ -87,7 +94,9 @@ function runInPage(ctx) {
|
|
|
87
94
|
for (const th of ths) {
|
|
88
95
|
if (!th) continue;
|
|
89
96
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(th) : 'html';
|
|
90
|
-
const html = helpers.getOuterHtmlSnippet
|
|
97
|
+
const html = helpers.getOuterHtmlSnippet
|
|
98
|
+
? helpers.getOuterHtmlSnippet(th)
|
|
99
|
+
: th.outerHTML || '';
|
|
91
100
|
|
|
92
101
|
occurrences.push({
|
|
93
102
|
selector: stableSelector,
|
|
@@ -110,9 +119,14 @@ function runInPage(ctx) {
|
|
|
110
119
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
111
120
|
}
|
|
112
121
|
if (occurrences.length) {
|
|
113
|
-
return {
|
|
122
|
+
return {
|
|
123
|
+
ruleId: rule.ruleId,
|
|
124
|
+
outcome: 'fail',
|
|
125
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
126
|
+
occurrences
|
|
127
|
+
};
|
|
114
128
|
}
|
|
115
129
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
116
130
|
}
|
|
117
131
|
|
|
118
|
-
module.exports = { id, meta, runInPage };
|
|
132
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -60,13 +60,23 @@ const id = 'target-size-minimum';
|
|
|
60
60
|
const meta = {
|
|
61
61
|
title: 'Pointer targets meet minimum size (AA)',
|
|
62
62
|
description:
|
|
63
|
-
|
|
63
|
+
'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
64
|
i18n: {
|
|
65
65
|
titleKey: 'targetSizeMinimum_title',
|
|
66
66
|
descriptionKey: 'targetSizeMinimum_description'
|
|
67
67
|
},
|
|
68
68
|
helpUrl: null,
|
|
69
|
-
tags: [
|
|
69
|
+
tags: [
|
|
70
|
+
'wcag22aa',
|
|
71
|
+
'wcag258',
|
|
72
|
+
'navigation',
|
|
73
|
+
'operable',
|
|
74
|
+
'pointer',
|
|
75
|
+
'target-size',
|
|
76
|
+
'atomic',
|
|
77
|
+
'automatic',
|
|
78
|
+
'dom'
|
|
79
|
+
],
|
|
70
80
|
wcagSc: ['2.5.8'],
|
|
71
81
|
normativeMappings: [
|
|
72
82
|
{
|
|
@@ -115,8 +125,10 @@ function runInPage(ctx) {
|
|
|
115
125
|
// with its own .querySelectorAll to call directly.
|
|
116
126
|
function qsa(sel) {
|
|
117
127
|
try {
|
|
118
|
-
if (helpers && typeof helpers.queryAllSmart === 'function')
|
|
119
|
-
|
|
128
|
+
if (helpers && typeof helpers.queryAllSmart === 'function')
|
|
129
|
+
return Array.from(helpers.queryAllSmart(sel) || []);
|
|
130
|
+
if (helpers && typeof helpers.queryAll === 'function')
|
|
131
|
+
return Array.from(helpers.queryAll(sel) || []);
|
|
120
132
|
return [];
|
|
121
133
|
} catch {
|
|
122
134
|
return [];
|
|
@@ -135,10 +147,11 @@ function runInPage(ctx) {
|
|
|
135
147
|
|
|
136
148
|
function htmlSnippet(el) {
|
|
137
149
|
try {
|
|
138
|
-
if (helpers && typeof helpers.getOuterHtmlSnippet === 'function')
|
|
150
|
+
if (helpers && typeof helpers.getOuterHtmlSnippet === 'function')
|
|
151
|
+
return helpers.getOuterHtmlSnippet(el);
|
|
139
152
|
} catch {}
|
|
140
153
|
try {
|
|
141
|
-
return
|
|
154
|
+
return el && el.outerHTML ? String(el.outerHTML) : '';
|
|
142
155
|
} catch {}
|
|
143
156
|
return '';
|
|
144
157
|
}
|
|
@@ -153,8 +166,14 @@ function runInPage(ctx) {
|
|
|
153
166
|
if (!el || el.nodeType !== 1) return false;
|
|
154
167
|
|
|
155
168
|
const tag = (el.tagName || '').toLowerCase();
|
|
156
|
-
const role =
|
|
157
|
-
|
|
169
|
+
const role =
|
|
170
|
+
(el.getAttribute &&
|
|
171
|
+
String(el.getAttribute('role') || '')
|
|
172
|
+
.trim()
|
|
173
|
+
.toLowerCase()) ||
|
|
174
|
+
'';
|
|
175
|
+
const isLinkLike =
|
|
176
|
+
(tag === 'a' && el.getAttribute && el.getAttribute('href')) || role === 'link';
|
|
158
177
|
if (!isLinkLike) return false;
|
|
159
178
|
|
|
160
179
|
const cs = getStyle(el);
|
|
@@ -164,11 +183,11 @@ function runInPage(ctx) {
|
|
|
164
183
|
// Inline exception is for inline text runs; many design systems use inline-block for links,
|
|
165
184
|
// so treat inline-block variants as eligible for this exception.
|
|
166
185
|
const isInline =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
186
|
+
display === 'inline' ||
|
|
187
|
+
display === 'inline-block' ||
|
|
188
|
+
display === 'inline-flex' ||
|
|
189
|
+
display === 'inline-grid' ||
|
|
190
|
+
display === 'inline-table';
|
|
172
191
|
|
|
173
192
|
if (!isInline) return false;
|
|
174
193
|
|
|
@@ -182,7 +201,10 @@ function runInPage(ctx) {
|
|
|
182
201
|
// Fallback: sometimes links are inside spans within a paragraph-like region
|
|
183
202
|
const inlineContainer = closest(el, 'span, em, strong, small, label');
|
|
184
203
|
if (inlineContainer) {
|
|
185
|
-
const outerTextBlock = closest(
|
|
204
|
+
const outerTextBlock = closest(
|
|
205
|
+
inlineContainer,
|
|
206
|
+
'p, li, dd, dt, blockquote, figcaption, caption, td, th'
|
|
207
|
+
);
|
|
186
208
|
if (outerTextBlock) return true;
|
|
187
209
|
}
|
|
188
210
|
|
|
@@ -234,7 +256,7 @@ function runInPage(ctx) {
|
|
|
234
256
|
try {
|
|
235
257
|
if (det.hasAttribute('open')) return false;
|
|
236
258
|
|
|
237
|
-
const tag =
|
|
259
|
+
const tag = el && el.tagName ? String(el.tagName).toLowerCase() : '';
|
|
238
260
|
// summary remains operable even when <details> is closed
|
|
239
261
|
if (tag === 'summary') return false;
|
|
240
262
|
|
|
@@ -256,9 +278,9 @@ function runInPage(ctx) {
|
|
|
256
278
|
|
|
257
279
|
function getStyle(el) {
|
|
258
280
|
try {
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
|
|
281
|
+
return document && document.defaultView && document.defaultView.getComputedStyle
|
|
282
|
+
? document.defaultView.getComputedStyle(el)
|
|
283
|
+
: null;
|
|
262
284
|
} catch {
|
|
263
285
|
return null;
|
|
264
286
|
}
|
|
@@ -288,7 +310,11 @@ function runInPage(ctx) {
|
|
|
288
310
|
|
|
289
311
|
// aria-disabled elements are typically treated as not operable
|
|
290
312
|
try {
|
|
291
|
-
const ad =
|
|
313
|
+
const ad =
|
|
314
|
+
el.getAttribute &&
|
|
315
|
+
String(el.getAttribute('aria-disabled') || '')
|
|
316
|
+
.trim()
|
|
317
|
+
.toLowerCase();
|
|
292
318
|
if (ad === 'true') return false;
|
|
293
319
|
} catch {}
|
|
294
320
|
|
|
@@ -321,7 +347,8 @@ function runInPage(ctx) {
|
|
|
321
347
|
|
|
322
348
|
function elementFromPoint(x, y) {
|
|
323
349
|
try {
|
|
324
|
-
if (document && typeof document.elementFromPoint === 'function')
|
|
350
|
+
if (document && typeof document.elementFromPoint === 'function')
|
|
351
|
+
return document.elementFromPoint(x, y);
|
|
325
352
|
} catch {}
|
|
326
353
|
return null;
|
|
327
354
|
}
|
|
@@ -347,7 +374,8 @@ function runInPage(ctx) {
|
|
|
347
374
|
}
|
|
348
375
|
}
|
|
349
376
|
|
|
350
|
-
const CANDIDATE_SELECTOR =
|
|
377
|
+
const CANDIDATE_SELECTOR =
|
|
378
|
+
'button, summary, a[href], area[href], input, select, textarea, [role="button"], [role="link"]';
|
|
351
379
|
|
|
352
380
|
// --- candidate collection ---
|
|
353
381
|
const candidates = qsa(CANDIDATE_SELECTOR);
|
|
@@ -481,14 +509,20 @@ function runInPage(ctx) {
|
|
|
481
509
|
const tag = (el.tagName || '').toLowerCase();
|
|
482
510
|
if (tag !== 'input') return false;
|
|
483
511
|
|
|
484
|
-
const type =
|
|
512
|
+
const type =
|
|
513
|
+
(el.getAttribute &&
|
|
514
|
+
String(el.getAttribute('type') || '')
|
|
515
|
+
.trim()
|
|
516
|
+
.toLowerCase()) ||
|
|
517
|
+
'';
|
|
485
518
|
if (type !== 'checkbox' && type !== 'radio') return false;
|
|
486
519
|
|
|
487
520
|
const cs = getStyle(el);
|
|
488
521
|
let appearance = '';
|
|
489
522
|
try {
|
|
490
523
|
if (cs && typeof cs.getPropertyValue === 'function') {
|
|
491
|
-
appearance =
|
|
524
|
+
appearance =
|
|
525
|
+
cs.getPropertyValue('appearance') || cs.getPropertyValue('-webkit-appearance') || '';
|
|
492
526
|
} else if (cs) {
|
|
493
527
|
appearance = cs.appearance || cs.webkitAppearance || '';
|
|
494
528
|
}
|
|
@@ -528,8 +562,8 @@ function runInPage(ctx) {
|
|
|
528
562
|
}
|
|
529
563
|
}
|
|
530
564
|
|
|
531
|
-
const
|
|
532
|
-
|
|
565
|
+
const failOccurrences = [];
|
|
566
|
+
const cantTellOccurrences = [];
|
|
533
567
|
|
|
534
568
|
for (const it of undersized) {
|
|
535
569
|
// Inline-text exception: do not fail purely on size/spacing for inline links in text.
|
|
@@ -548,18 +582,64 @@ function runInPage(ctx) {
|
|
|
548
582
|
const info = hasSpacingConflict(it);
|
|
549
583
|
|
|
550
584
|
if (!info.conflict && info.confident === false) {
|
|
551
|
-
// Ambiguous perimeter-sampling result near the decision threshold
|
|
552
|
-
|
|
585
|
+
// Ambiguous perimeter-sampling result near the decision threshold —
|
|
586
|
+
// previously recorded only as a page-level boolean with no per-target
|
|
587
|
+
// occurrence at all, so this specific target was unrecoverable from
|
|
588
|
+
// the result once any other target on the page had a confident
|
|
589
|
+
// fail (see helpers.resolveTieredOutcome's header comment). Now
|
|
590
|
+
// reported as its own cantTell-tier occurrence instead.
|
|
591
|
+
cantTellOccurrences.push({
|
|
592
|
+
selector: buildSelector(it.el),
|
|
593
|
+
html: htmlSnippet(it.el),
|
|
594
|
+
summary:
|
|
595
|
+
'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.',
|
|
596
|
+
hint: 'Manually verify the effective spacing between this target and its neighbor; increase target size or spacing if the overlap is real.',
|
|
597
|
+
i18n: {
|
|
598
|
+
summaryKey: 'targetSizeMinimum_summary_cantTell_ambiguousSpacing',
|
|
599
|
+
hintKey: 'targetSizeMinimum_hint_cantTell_ambiguousSpacing',
|
|
600
|
+
params: {}
|
|
601
|
+
},
|
|
602
|
+
data: {
|
|
603
|
+
details: {
|
|
604
|
+
measured: { width: it.rect.width, height: it.rect.height },
|
|
605
|
+
reasonCode: 'undersized-ambiguous-spacing',
|
|
606
|
+
conflictHitCount: info.hitCount,
|
|
607
|
+
conflictWith: info.conflictEl ? buildSelector(info.conflictEl) : null
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
});
|
|
553
611
|
continue;
|
|
554
612
|
}
|
|
555
613
|
|
|
556
614
|
if (info.conflict) {
|
|
557
615
|
if (isPlausiblyEssentialOrEquivalent(it.el)) {
|
|
558
|
-
|
|
616
|
+
// Confident spacing conflict, but the target may be exempt as part
|
|
617
|
+
// of an essential graphic/image-map region — same "previously
|
|
618
|
+
// unrecoverable" gap as above, now reported instead of dropped.
|
|
619
|
+
cantTellOccurrences.push({
|
|
620
|
+
selector: buildSelector(it.el),
|
|
621
|
+
html: htmlSnippet(it.el),
|
|
622
|
+
summary:
|
|
623
|
+
'Target is too small and too close to another target, but may be exempt as part of an essential graphic or image-map region.',
|
|
624
|
+
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.',
|
|
625
|
+
i18n: {
|
|
626
|
+
summaryKey: 'targetSizeMinimum_summary_cantTell_plausiblyEssential',
|
|
627
|
+
hintKey: 'targetSizeMinimum_hint_cantTell_plausiblyEssential',
|
|
628
|
+
params: {}
|
|
629
|
+
},
|
|
630
|
+
data: {
|
|
631
|
+
details: {
|
|
632
|
+
measured: { width: it.rect.width, height: it.rect.height },
|
|
633
|
+
reasonCode: 'undersized-plausibly-essential',
|
|
634
|
+
conflictHitCount: info.hitCount,
|
|
635
|
+
conflictWith: info.conflictEl ? buildSelector(info.conflictEl) : null
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
});
|
|
559
639
|
continue;
|
|
560
640
|
}
|
|
561
641
|
|
|
562
|
-
|
|
642
|
+
failOccurrences.push({
|
|
563
643
|
selector: buildSelector(it.el),
|
|
564
644
|
html: htmlSnippet(it.el),
|
|
565
645
|
summary: 'Target is too small and too close to another target.',
|
|
@@ -581,25 +661,15 @@ function runInPage(ctx) {
|
|
|
581
661
|
}
|
|
582
662
|
}
|
|
583
663
|
|
|
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: [] };
|
|
664
|
+
// See helpers.resolveTieredOutcome's own header comment (src/core/dom-helpers.js):
|
|
665
|
+
// a fail-tier finding never silently discards cantTell-tier findings from
|
|
666
|
+
// the same run — both are returned together when the outcome is 'fail'.
|
|
667
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
668
|
+
failOccurrences,
|
|
669
|
+
cantTellOccurrences,
|
|
670
|
+
(rule && rule.defaultSeverity) || 'minor'
|
|
671
|
+
);
|
|
672
|
+
return { ruleId: RULE_ID, ...resolved };
|
|
603
673
|
}
|
|
604
674
|
|
|
605
675
|
module.exports = { id, meta, runInPage };
|