@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,27 @@
|
|
|
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 = 'canvas-text-alternative-quality';
|
|
15
15
|
|
|
16
16
|
const meta = {
|
|
17
|
-
title:
|
|
18
|
-
description:
|
|
17
|
+
title: '<canvas> text alternative must be appropriate (manual review)',
|
|
18
|
+
description:
|
|
19
|
+
'Flags <canvas> elements with a detected text alternative for human review of equivalence and appropriateness.',
|
|
19
20
|
i18n: {
|
|
20
|
-
titleKey:
|
|
21
|
-
descriptionKey:
|
|
21
|
+
titleKey: 'canvas_textAltQuality_title',
|
|
22
|
+
descriptionKey: 'canvas_textAltQuality_description'
|
|
22
23
|
},
|
|
23
24
|
helpUrl: null,
|
|
24
|
-
tags: [
|
|
25
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'canvas', 'manual', 'atomic'],
|
|
25
26
|
wcagSc: ['1.1.1'],
|
|
26
27
|
normativeMappings: [
|
|
27
|
-
{
|
|
28
|
+
{
|
|
29
|
+
standard: 'WCAG',
|
|
30
|
+
version: '2.2',
|
|
31
|
+
requirement: '1.1.1',
|
|
32
|
+
title: 'Non-text Content',
|
|
33
|
+
conformanceLevel: 'A'
|
|
34
|
+
}
|
|
28
35
|
],
|
|
29
36
|
defaultSeverity: 'minor',
|
|
30
37
|
category: 'perceivable',
|
|
@@ -38,133 +45,161 @@ const meta = {
|
|
|
38
45
|
};
|
|
39
46
|
|
|
40
47
|
function runInPage(ctx) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const { document, root, helpers, rule } = ctx;
|
|
49
|
+
const safeRoot = root || document;
|
|
50
|
+
|
|
51
|
+
const queryAllSmart =
|
|
52
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
53
|
+
const queryAll =
|
|
54
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
55
|
+
? helpers.queryAll
|
|
56
|
+
: (sel) => {
|
|
57
|
+
try {
|
|
58
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
59
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
60
|
+
: [];
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
51
64
|
};
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
__accEligCache.set(node, r);
|
|
71
|
-
return r;
|
|
66
|
+
const getEligibilityInfo =
|
|
67
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
68
|
+
|
|
69
|
+
const isAccTreeEligible =
|
|
70
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
71
|
+
|
|
72
|
+
const __accEligCache = new WeakMap();
|
|
73
|
+
function accEligibleCached(node) {
|
|
74
|
+
if (!isAccTreeEligible) return { eligible: true, reasons: [] };
|
|
75
|
+
if (!node || typeof node !== 'object') return { eligible: true, reasons: [] };
|
|
76
|
+
const c = __accEligCache.get(node);
|
|
77
|
+
if (c) return c;
|
|
78
|
+
let r;
|
|
79
|
+
try {
|
|
80
|
+
r = isAccTreeEligible(node, ctx);
|
|
81
|
+
} catch {
|
|
82
|
+
r = { eligible: true, reasons: [] };
|
|
72
83
|
}
|
|
84
|
+
r = r && typeof r === 'object' ? r : { eligible: !!r, reasons: [] };
|
|
85
|
+
__accEligCache.set(node, r);
|
|
86
|
+
return r;
|
|
87
|
+
}
|
|
73
88
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
const getFocusableInfo =
|
|
90
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
91
|
+
|
|
92
|
+
function isRolePresentationExcluded(el) {
|
|
93
|
+
const role = (() => {
|
|
94
|
+
try {
|
|
95
|
+
return String(el.getAttribute('role') || '')
|
|
96
|
+
.trim()
|
|
97
|
+
.toLowerCase();
|
|
98
|
+
} catch {
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
if (role !== 'presentation' && role !== 'none') return false;
|
|
103
|
+
|
|
104
|
+
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
105
|
+
let focusable;
|
|
106
|
+
if (getFocusableInfo) {
|
|
107
|
+
const fi = (() => {
|
|
108
|
+
try {
|
|
109
|
+
return getFocusableInfo(el, ctx);
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
95
112
|
}
|
|
96
|
-
|
|
113
|
+
})();
|
|
114
|
+
focusable = !!(fi && fi.focusable);
|
|
115
|
+
} else {
|
|
116
|
+
let tabindex;
|
|
117
|
+
try {
|
|
118
|
+
tabindex = el.getAttribute('tabindex');
|
|
119
|
+
} catch {
|
|
120
|
+
tabindex = null;
|
|
121
|
+
}
|
|
122
|
+
const t = tabindex == null ? '' : String(tabindex).trim();
|
|
123
|
+
focusable = t !== '' && !Number.isNaN(Number(t));
|
|
97
124
|
}
|
|
125
|
+
return !focusable;
|
|
126
|
+
}
|
|
98
127
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (!els.length) {
|
|
106
|
-
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
128
|
+
const els = (() => {
|
|
129
|
+
try {
|
|
130
|
+
return Array.from((queryAllSmart ? queryAllSmart('canvas') : queryAll('canvas')) || []);
|
|
131
|
+
} catch {
|
|
132
|
+
return queryAll('canvas');
|
|
107
133
|
}
|
|
134
|
+
})();
|
|
108
135
|
|
|
109
|
-
|
|
110
|
-
|
|
136
|
+
if (!els.length) {
|
|
137
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
138
|
+
}
|
|
111
139
|
|
|
112
|
-
|
|
113
|
-
|
|
140
|
+
const occurrences = [];
|
|
141
|
+
let applicableCount = 0;
|
|
114
142
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (elig && elig.eligible === false) continue;
|
|
118
|
-
}
|
|
143
|
+
for (const el of els) {
|
|
144
|
+
if (!el || !el.getAttribute) continue;
|
|
119
145
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
? helpers.getTextAlternativeInfo
|
|
125
|
-
: null;
|
|
146
|
+
if (isAccTreeEligible) {
|
|
147
|
+
const elig = accEligibleCached(el);
|
|
148
|
+
if (elig && elig.eligible === false) continue;
|
|
149
|
+
}
|
|
126
150
|
|
|
127
|
-
|
|
151
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
128
152
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
153
|
+
const getTextAlternativeInfo =
|
|
154
|
+
helpers && typeof helpers.getTextAlternativeInfo === 'function'
|
|
155
|
+
? helpers.getTextAlternativeInfo
|
|
156
|
+
: null;
|
|
134
157
|
|
|
135
|
-
|
|
136
|
-
if (!hasTextAlt) continue;
|
|
137
|
-
|
|
138
|
-
applicableCount += 1;
|
|
139
|
-
|
|
140
|
-
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
141
|
-
|
|
142
|
-
const baseOccurrence = {
|
|
143
|
-
summary: 'Review text alternative for <canvas> for equivalence and appropriateness.',
|
|
144
|
-
hint: 'Confirm the fallback text or accessible name conveys the same information/function as the canvas content.',
|
|
145
|
-
i18n: {
|
|
146
|
-
summaryKey: 'canvas_textAltQuality_summary_cantTell',
|
|
147
|
-
hintKey: 'canvas_textAltQuality_hint_cantTell',
|
|
148
|
-
params: { element: (el.tagName || '').toLowerCase() }
|
|
149
|
-
},
|
|
150
|
-
data: {
|
|
151
|
-
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
152
|
-
details: textAltInfo || null
|
|
153
|
-
}
|
|
154
|
-
};
|
|
158
|
+
const trim = (v) => (v == null ? '' : String(v)).trim();
|
|
155
159
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
// Rule-specific applicability (only elements that already have a text alternative mechanism)
|
|
161
|
+
let textAltInfo = null;
|
|
162
|
+
if (getTextAlternativeInfo) {
|
|
163
|
+
try {
|
|
164
|
+
textAltInfo = getTextAlternativeInfo(el, ctx);
|
|
165
|
+
} catch {
|
|
166
|
+
textAltInfo = null;
|
|
167
|
+
}
|
|
161
168
|
}
|
|
162
169
|
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
const hasTextAlt = !!(textAltInfo && textAltInfo.present);
|
|
171
|
+
if (!hasTextAlt) continue;
|
|
172
|
+
|
|
173
|
+
applicableCount += 1;
|
|
174
|
+
|
|
175
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
176
|
+
|
|
177
|
+
const baseOccurrence = {
|
|
178
|
+
summary: 'Review text alternative for <canvas> for equivalence and appropriateness.',
|
|
179
|
+
hint: 'Confirm the fallback text or accessible name conveys the same information/function as the canvas content.',
|
|
180
|
+
i18n: {
|
|
181
|
+
summaryKey: 'canvas_textAltQuality_summary_cantTell',
|
|
182
|
+
hintKey: 'canvas_textAltQuality_hint_cantTell',
|
|
183
|
+
params: { element: (el.tagName || '').toLowerCase() }
|
|
184
|
+
},
|
|
185
|
+
data: {
|
|
186
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
187
|
+
details: textAltInfo || null
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
192
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
193
|
+
} else {
|
|
194
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
165
195
|
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (applicableCount === 0) {
|
|
199
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
200
|
+
}
|
|
166
201
|
|
|
167
|
-
|
|
202
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
168
203
|
}
|
|
169
204
|
|
|
170
205
|
module.exports = { id, meta, runInPage };
|