@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
|
@@ -11,20 +11,26 @@
|
|
|
11
11
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
const id =
|
|
14
|
+
const id = 'area-alt-quality';
|
|
15
15
|
|
|
16
16
|
const meta = {
|
|
17
|
-
title:
|
|
18
|
-
description:
|
|
17
|
+
title: '<area> alt text must be appropriate (manual review)',
|
|
18
|
+
description: 'Flags <area> elements with non-empty alt text for human review of appropriateness.',
|
|
19
19
|
i18n: {
|
|
20
|
-
titleKey:
|
|
21
|
-
descriptionKey:
|
|
20
|
+
titleKey: 'area_altQuality_title',
|
|
21
|
+
descriptionKey: 'area_altQuality_description'
|
|
22
22
|
},
|
|
23
23
|
helpUrl: null,
|
|
24
|
-
tags: [
|
|
24
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'imagemap', 'manual', 'atomic'],
|
|
25
25
|
wcagSc: ['1.1.1'],
|
|
26
26
|
normativeMappings: [
|
|
27
|
-
{
|
|
27
|
+
{
|
|
28
|
+
standard: 'WCAG',
|
|
29
|
+
version: '2.2',
|
|
30
|
+
requirement: '1.1.1',
|
|
31
|
+
title: 'Non-text Content',
|
|
32
|
+
conformanceLevel: 'A'
|
|
33
|
+
}
|
|
28
34
|
],
|
|
29
35
|
defaultSeverity: 'minor',
|
|
30
36
|
category: 'perceivable',
|
|
@@ -38,167 +44,202 @@ const meta = {
|
|
|
38
44
|
};
|
|
39
45
|
|
|
40
46
|
function runInPage(ctx) {
|
|
47
|
+
const { document, root, helpers, rule } = ctx;
|
|
48
|
+
const safeRoot = root || document;
|
|
49
|
+
|
|
50
|
+
const queryAllSmart =
|
|
51
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
52
|
+
const queryAll =
|
|
53
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
54
|
+
? helpers.queryAll
|
|
55
|
+
: (sel) => {
|
|
56
|
+
try {
|
|
57
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
58
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
59
|
+
: [];
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
};
|
|
41
64
|
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
const getEligibilityInfo =
|
|
66
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
44
67
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
? helpers.queryAll
|
|
48
|
-
: (sel) => {
|
|
49
|
-
try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
|
|
50
|
-
catch { return []; }
|
|
51
|
-
};
|
|
68
|
+
const isAccTreeEligible =
|
|
69
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!node || typeof node !== 'object') return { eligible: true, reasons: [] };
|
|
65
|
-
const c = __accEligCache.get(node);
|
|
66
|
-
if (c) return c;
|
|
67
|
-
let r;
|
|
68
|
-
try { r = isAccTreeEligible(node, ctx); } catch { r = { eligible: true, reasons: [] }; }
|
|
69
|
-
r = r && typeof r === 'object' ? r : { eligible: !!r, reasons: [] };
|
|
70
|
-
__accEligCache.set(node, r);
|
|
71
|
-
return r;
|
|
71
|
+
const __accEligCache = new WeakMap();
|
|
72
|
+
function accEligibleCached(node) {
|
|
73
|
+
if (!isAccTreeEligible) return { eligible: true, reasons: [] };
|
|
74
|
+
if (!node || typeof node !== 'object') return { eligible: true, reasons: [] };
|
|
75
|
+
const c = __accEligCache.get(node);
|
|
76
|
+
if (c) return c;
|
|
77
|
+
let r;
|
|
78
|
+
try {
|
|
79
|
+
r = isAccTreeEligible(node, ctx);
|
|
80
|
+
} catch {
|
|
81
|
+
r = { eligible: true, reasons: [] };
|
|
72
82
|
}
|
|
83
|
+
r = r && typeof r === 'object' ? r : { eligible: !!r, reasons: [] };
|
|
84
|
+
__accEligCache.set(node, r);
|
|
85
|
+
return r;
|
|
86
|
+
}
|
|
73
87
|
|
|
74
|
-
// --- image-map semantics (rule-local; match automatic <area> applicability) ---
|
|
75
|
-
function normUsemap(val) {
|
|
88
|
+
// --- image-map semantics (rule-local; match automatic <area> applicability) ---
|
|
89
|
+
function normUsemap(val) {
|
|
76
90
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} catch {
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
const t = String(val || '').trim();
|
|
92
|
+
if (!t) return '';
|
|
93
|
+
return t[0] === '#' ? t.slice(1).trim().toLowerCase() : t.toLowerCase();
|
|
94
|
+
} catch {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function getMapName(mapEl) {
|
|
83
99
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
} catch {
|
|
88
|
-
|
|
100
|
+
if (!mapEl || !mapEl.getAttribute) return '';
|
|
101
|
+
const n = String(mapEl.getAttribute('name') || mapEl.getAttribute('id') || '').trim();
|
|
102
|
+
return n ? n.toLowerCase() : '';
|
|
103
|
+
} catch {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const getFocusableInfo =
|
|
109
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
110
|
+
|
|
111
|
+
function isRolePresentationExcluded(el) {
|
|
112
|
+
const role = (() => {
|
|
113
|
+
try {
|
|
114
|
+
return String(el.getAttribute('role') || '')
|
|
115
|
+
.trim()
|
|
116
|
+
.toLowerCase();
|
|
117
|
+
} catch {
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
if (role !== 'presentation' && role !== 'none') return false;
|
|
89
122
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
})();
|
|
99
|
-
if (role !== 'presentation' && role !== 'none') return false;
|
|
100
|
-
|
|
101
|
-
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
102
|
-
let focusable = false;
|
|
103
|
-
if (getFocusableInfo) {
|
|
104
|
-
const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
|
|
105
|
-
focusable = !!(fi && fi.focusable);
|
|
106
|
-
} else {
|
|
107
|
-
const tabindex = el.getAttribute('tabindex');
|
|
108
|
-
focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
|
|
123
|
+
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
124
|
+
let focusable;
|
|
125
|
+
if (getFocusableInfo) {
|
|
126
|
+
const fi = (() => {
|
|
127
|
+
try {
|
|
128
|
+
return getFocusableInfo(el, ctx);
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
109
131
|
}
|
|
110
|
-
|
|
132
|
+
})();
|
|
133
|
+
focusable = !!(fi && fi.focusable);
|
|
134
|
+
} else {
|
|
135
|
+
const tabindex = el.getAttribute('tabindex');
|
|
136
|
+
focusable =
|
|
137
|
+
tabindex != null &&
|
|
138
|
+
String(tabindex).trim() !== '' &&
|
|
139
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
111
140
|
}
|
|
141
|
+
return !focusable;
|
|
142
|
+
}
|
|
112
143
|
|
|
144
|
+
const els = (() => {
|
|
145
|
+
try {
|
|
146
|
+
return Array.from((queryAllSmart ? queryAllSmart('area') : queryAll('area')) || []);
|
|
147
|
+
} catch {
|
|
148
|
+
return queryAll('area');
|
|
149
|
+
}
|
|
150
|
+
})();
|
|
113
151
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
})();
|
|
152
|
+
if (!els.length) {
|
|
153
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
154
|
+
}
|
|
118
155
|
|
|
119
|
-
|
|
120
|
-
|
|
156
|
+
const occurrences = [];
|
|
157
|
+
let applicableCount = 0;
|
|
158
|
+
|
|
159
|
+
const __usemapIndex = new Map(); // mapName -> img (first in document order)
|
|
160
|
+
try {
|
|
161
|
+
const imgs = Array.from(document.querySelectorAll('img[usemap]'));
|
|
162
|
+
for (const img of imgs) {
|
|
163
|
+
const u = normUsemap(img.getAttribute('usemap'));
|
|
164
|
+
if (!u) continue;
|
|
165
|
+
if (!__usemapIndex.has(u)) __usemapIndex.set(u, img);
|
|
121
166
|
}
|
|
167
|
+
} catch {}
|
|
122
168
|
|
|
123
|
-
|
|
124
|
-
|
|
169
|
+
for (const el of els) {
|
|
170
|
+
if (!el || !el.getAttribute) continue;
|
|
125
171
|
|
|
126
|
-
|
|
172
|
+
// Must belong to a *used* image map (referenced by an <img usemap>). If unused, not applicable.
|
|
173
|
+
let img;
|
|
127
174
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
175
|
+
const map = el.closest && el.closest('map');
|
|
176
|
+
const mapName = map ? getMapName(map) : '';
|
|
177
|
+
img = mapName ? __usemapIndex.get(mapName) || null : null;
|
|
178
|
+
} catch {
|
|
179
|
+
img = null;
|
|
180
|
+
}
|
|
181
|
+
if (!img) continue;
|
|
135
182
|
|
|
136
|
-
|
|
137
|
-
|
|
183
|
+
// The referencing <img> must be eligible in the accessibility tree.
|
|
184
|
+
if (isAccTreeEligible) {
|
|
185
|
+
const imgElig = accEligibleCached(img);
|
|
186
|
+
if (imgElig && imgElig.eligible === false) continue;
|
|
187
|
+
}
|
|
138
188
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const mapName = map ? getMapName(map) : '';
|
|
144
|
-
img = mapName ? (__usemapIndex.get(mapName) || null) : null;
|
|
145
|
-
} catch {
|
|
146
|
-
img = null;
|
|
147
|
-
}
|
|
148
|
-
if (!img) continue;
|
|
189
|
+
if (isAccTreeEligible) {
|
|
190
|
+
const elig = accEligibleCached(el);
|
|
191
|
+
if (elig && elig.eligible === false) continue;
|
|
192
|
+
}
|
|
149
193
|
|
|
150
|
-
|
|
151
|
-
if (isAccTreeEligible) {
|
|
152
|
-
const imgElig = accEligibleCached(img);
|
|
153
|
-
if (imgElig && imgElig.eligible === false) continue;
|
|
154
|
-
}
|
|
194
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
155
195
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
196
|
+
// Rule-specific applicability (only elements that already have a text alternative mechanism)
|
|
197
|
+
let alt;
|
|
198
|
+
try {
|
|
199
|
+
alt = el.getAttribute('alt');
|
|
200
|
+
} catch {
|
|
201
|
+
alt = null;
|
|
202
|
+
}
|
|
203
|
+
if (alt === null) continue;
|
|
204
|
+
if (String(alt).trim() === '') continue; // only non-empty alt is applicable here
|
|
160
205
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// Rule-specific applicability (only elements that already have a text alternative mechanism)
|
|
164
|
-
let alt = null;
|
|
165
|
-
try { alt = el.getAttribute('alt'); } catch { alt = null; }
|
|
166
|
-
if (alt === null) continue;
|
|
167
|
-
if (String(alt).trim() === '') continue; // only non-empty alt is applicable here
|
|
168
|
-
|
|
169
|
-
applicableCount += 1;
|
|
170
|
-
|
|
171
|
-
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
172
|
-
|
|
173
|
-
let altVal = '';
|
|
174
|
-
try { altVal = String(el.getAttribute('alt') || ''); } catch { altVal = ''; }
|
|
175
|
-
|
|
176
|
-
const baseOccurrence = {
|
|
177
|
-
summary: 'Review alt text on <area> for accuracy and appropriateness.',
|
|
178
|
-
hint: 'Ensure the alt text identifies the destination/action of the image map area in context.',
|
|
179
|
-
i18n: {
|
|
180
|
-
summaryKey: 'area_altQuality_summary_cantTell',
|
|
181
|
-
hintKey: 'area_altQuality_hint_cantTell',
|
|
182
|
-
params: { element: (el.tagName || '').toLowerCase() }
|
|
183
|
-
},
|
|
184
|
-
data: {
|
|
185
|
-
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
186
|
-
details: { alt: altVal.trim() } // optional but useful for manual review
|
|
187
|
-
}
|
|
188
|
-
};
|
|
206
|
+
applicableCount += 1;
|
|
189
207
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
208
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
209
|
+
|
|
210
|
+
let altVal;
|
|
211
|
+
try {
|
|
212
|
+
altVal = String(el.getAttribute('alt') || '');
|
|
213
|
+
} catch {
|
|
214
|
+
altVal = '';
|
|
195
215
|
}
|
|
196
216
|
|
|
197
|
-
|
|
198
|
-
|
|
217
|
+
const baseOccurrence = {
|
|
218
|
+
summary: 'Review alt text on <area> for accuracy and appropriateness.',
|
|
219
|
+
hint: 'Ensure the alt text identifies the destination/action of the image map area in context.',
|
|
220
|
+
i18n: {
|
|
221
|
+
summaryKey: 'area_altQuality_summary_cantTell',
|
|
222
|
+
hintKey: 'area_altQuality_hint_cantTell',
|
|
223
|
+
params: { element: (el.tagName || '').toLowerCase() }
|
|
224
|
+
},
|
|
225
|
+
data: {
|
|
226
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
227
|
+
details: { alt: altVal.trim() } // optional but useful for manual review
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
232
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
233
|
+
} else {
|
|
234
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
199
235
|
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (applicableCount === 0) {
|
|
239
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
240
|
+
}
|
|
200
241
|
|
|
201
|
-
|
|
242
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
202
243
|
}
|
|
203
244
|
|
|
204
245
|
module.exports = { id, meta, runInPage };
|
|
@@ -52,7 +52,8 @@ const id = 'aria-checked-state-mismatch';
|
|
|
52
52
|
|
|
53
53
|
const meta = {
|
|
54
54
|
title: 'Native checkbox/radio aria-checked should match its actual state',
|
|
55
|
-
description:
|
|
55
|
+
description:
|
|
56
|
+
'Flags a native <input type="checkbox">/<input type="radio"> whose explicit aria-checked value disagrees with its actual checked/indeterminate state, for manual review.',
|
|
56
57
|
i18n: {
|
|
57
58
|
titleKey: 'ariaCheckedStateMismatch_title',
|
|
58
59
|
descriptionKey: 'ariaCheckedStateMismatch_description'
|
|
@@ -61,7 +62,13 @@ const meta = {
|
|
|
61
62
|
tags: ['wcag2a', 'wcag412', 'aria', 'forms', 'atomic', 'manual'],
|
|
62
63
|
wcagSc: ['4.1.2'],
|
|
63
64
|
normativeMappings: [
|
|
64
|
-
{
|
|
65
|
+
{
|
|
66
|
+
standard: 'WCAG',
|
|
67
|
+
version: '2.2',
|
|
68
|
+
requirement: '4.1.2',
|
|
69
|
+
title: 'Name, Role, Value',
|
|
70
|
+
conformanceLevel: 'A'
|
|
71
|
+
}
|
|
65
72
|
],
|
|
66
73
|
defaultSeverity: 'moderate',
|
|
67
74
|
category: 'robust',
|
|
@@ -71,12 +78,16 @@ const meta = {
|
|
|
71
78
|
};
|
|
72
79
|
|
|
73
80
|
function runInPage(ctx) {
|
|
74
|
-
const {
|
|
81
|
+
const { helpers, rule } = ctx;
|
|
75
82
|
|
|
76
|
-
function trim(v) {
|
|
83
|
+
function trim(v) {
|
|
84
|
+
return (v == null ? '' : String(v)).trim();
|
|
85
|
+
}
|
|
77
86
|
|
|
78
87
|
const nodes = helpers.queryAllSmart
|
|
79
|
-
? helpers.queryAllSmart(
|
|
88
|
+
? helpers.queryAllSmart(
|
|
89
|
+
'input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]'
|
|
90
|
+
)
|
|
80
91
|
: helpers.queryAll('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]');
|
|
81
92
|
|
|
82
93
|
const occurrences = [];
|
|
@@ -97,9 +108,10 @@ function runInPage(ctx) {
|
|
|
97
108
|
|
|
98
109
|
let normalizedAriaChecked;
|
|
99
110
|
if (isCheckbox) {
|
|
100
|
-
normalizedAriaChecked =
|
|
111
|
+
normalizedAriaChecked =
|
|
112
|
+
rawAriaChecked === 'mixed' || rawAriaChecked === 'true' ? rawAriaChecked : 'false';
|
|
101
113
|
} else {
|
|
102
|
-
normalizedAriaChecked =
|
|
114
|
+
normalizedAriaChecked = rawAriaChecked === 'true' ? 'true' : 'false';
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
let actualState;
|
|
@@ -112,12 +124,13 @@ function runInPage(ctx) {
|
|
|
112
124
|
if (normalizedAriaChecked === actualState) continue;
|
|
113
125
|
|
|
114
126
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
115
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
127
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
116
128
|
|
|
117
129
|
occurrences.push({
|
|
118
130
|
selector: stableSelector,
|
|
119
131
|
html,
|
|
120
|
-
summary:
|
|
132
|
+
summary:
|
|
133
|
+
'This element’s aria-checked value does not match its actual checked/indeterminate state.',
|
|
121
134
|
hint: 'Set aria-checked to match the element’s real state, or remove it — a native checkbox/radio already exposes this state without it.',
|
|
122
135
|
i18n: {
|
|
123
136
|
summaryKey: 'ariaCheckedStateMismatch_summary_cantTell',
|
|
@@ -125,7 +138,12 @@ function runInPage(ctx) {
|
|
|
125
138
|
params: { ariaChecked: normalizedAriaChecked, actualState, type }
|
|
126
139
|
},
|
|
127
140
|
data: {
|
|
128
|
-
details: {
|
|
141
|
+
details: {
|
|
142
|
+
reasonCode: 'ARIA_CHECKED_STATE_MISMATCH',
|
|
143
|
+
ariaChecked: normalizedAriaChecked,
|
|
144
|
+
actualState,
|
|
145
|
+
type
|
|
146
|
+
}
|
|
129
147
|
}
|
|
130
148
|
});
|
|
131
149
|
}
|
|
@@ -134,7 +152,12 @@ function runInPage(ctx) {
|
|
|
134
152
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
135
153
|
}
|
|
136
154
|
if (occurrences.length) {
|
|
137
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
ruleId: rule.ruleId,
|
|
157
|
+
outcome: 'cantTell',
|
|
158
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
159
|
+
occurrences
|
|
160
|
+
};
|
|
138
161
|
}
|
|
139
162
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
140
163
|
}
|
|
@@ -29,7 +29,8 @@ const id = 'aria-text';
|
|
|
29
29
|
|
|
30
30
|
const meta = {
|
|
31
31
|
title: 'role="text" elements should have no focusable descendants',
|
|
32
|
-
description:
|
|
32
|
+
description:
|
|
33
|
+
'Checks that elements with role="text" contain no focusable descendant (link, button, form control, tabindex, iframe, or contenteditable).',
|
|
33
34
|
i18n: {
|
|
34
35
|
titleKey: 'ariaText_title',
|
|
35
36
|
descriptionKey: 'ariaText_description'
|
|
@@ -46,12 +47,14 @@ const meta = {
|
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
function runInPage(ctx) {
|
|
49
|
-
const {
|
|
50
|
+
const { helpers, rule } = ctx;
|
|
50
51
|
|
|
51
52
|
const FOCUSABLE_DESCENDANT_SELECTOR =
|
|
52
53
|
'a[href], button, input, select, textarea, [tabindex], iframe, [contenteditable]:not([contenteditable="false"])';
|
|
53
54
|
|
|
54
|
-
const nodes = helpers.queryAllSmart
|
|
55
|
+
const nodes = helpers.queryAllSmart
|
|
56
|
+
? helpers.queryAllSmart('[role="text"]')
|
|
57
|
+
: helpers.queryAll('[role="text"]');
|
|
55
58
|
|
|
56
59
|
const occurrences = [];
|
|
57
60
|
let applicableCount = 0;
|
|
@@ -61,7 +64,7 @@ function runInPage(ctx) {
|
|
|
61
64
|
|
|
62
65
|
applicableCount += 1;
|
|
63
66
|
|
|
64
|
-
let focusableDescendant
|
|
67
|
+
let focusableDescendant;
|
|
65
68
|
try {
|
|
66
69
|
focusableDescendant = el.querySelector(FOCUSABLE_DESCENDANT_SELECTOR);
|
|
67
70
|
} catch {
|
|
@@ -70,7 +73,7 @@ function runInPage(ctx) {
|
|
|
70
73
|
if (!focusableDescendant) continue;
|
|
71
74
|
|
|
72
75
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
73
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
76
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
74
77
|
|
|
75
78
|
const baseOccurrence = {
|
|
76
79
|
selector: stableSelector,
|
|
@@ -99,7 +102,12 @@ function runInPage(ctx) {
|
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
if (occurrences.length) {
|
|
102
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
ruleId: rule.ruleId,
|
|
107
|
+
outcome: 'cantTell',
|
|
108
|
+
severity: rule.defaultSeverity || 'minor',
|
|
109
|
+
occurrences
|
|
110
|
+
};
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|