@surea11y/core 1.1.2 → 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 +48 -4
- package/LICENSE +373 -21
- package/README.md +70 -1
- package/bin/core.js +240 -11
- package/docs/API_STABILITY.md +61 -0
- package/docs/BASELINE.md +66 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +80 -1
- package/docs/ENGINE_OPTIONS.md +4 -0
- package/docs/INTEGRATION.md +19 -1
- package/docs/OUTPUT_SCHEMA.md +2 -2
- package/docs/REPORT.md +33 -0
- package/docs/RULE_AUTHORING.md +31 -0
- 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 +50 -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 +125 -29
- 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 +50 -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 +39 -9
- 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 +43 -19
- 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 +117 -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 +37 -8
- package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -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 +26 -7
- 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 -12
- package/src/checks/manual/empty-table-header-manual.js +21 -14
- 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 -12
- 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 +95 -29
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
- package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +35 -21
- package/src/checks/manual/landmark-unique-manual.js +73 -45
- 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 +25 -6
- 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 +40 -23
- package/src/checks/manual/page-title-patterns-manual.js +87 -51
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +275 -62
- package/src/checks/manual/scope-attr-valid-manual.js +11 -9
- 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 +11 -9
- 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 +1128 -823
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4176 -3886
- package/src/core/dom-runner.js +720 -596
- 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 +71 -35
- package/src/core.js +37939 -29121
- package/src/i18n/en.js +1194 -887
- package/src/i18n/fr.js +1136 -793
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +502 -0
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -11,199 +11,240 @@
|
|
|
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 = 'svg-text-alternative-quality';
|
|
15
15
|
|
|
16
16
|
const meta = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
17
|
+
title: '<svg> text alternative must be appropriate (manual review)',
|
|
18
|
+
description:
|
|
19
|
+
'Flags applicable <svg> graphics with a detected text alternative for human review of appropriateness.',
|
|
20
|
+
i18n: {
|
|
21
|
+
titleKey: 'svg_textAltQuality_title',
|
|
22
|
+
descriptionKey: 'svg_textAltQuality_description'
|
|
23
|
+
},
|
|
24
|
+
helpUrl: null,
|
|
25
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'svg', 'manual', 'atomic'],
|
|
26
|
+
wcagSc: ['1.1.1'],
|
|
27
|
+
normativeMappings: [
|
|
28
|
+
{
|
|
29
|
+
standard: 'WCAG',
|
|
30
|
+
version: '2.2',
|
|
31
|
+
requirement: '1.1.1',
|
|
32
|
+
title: 'Non-text Content',
|
|
33
|
+
conformanceLevel: 'A'
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
defaultSeverity: 'minor',
|
|
37
|
+
category: 'perceivable',
|
|
38
|
+
type: 'manual',
|
|
39
|
+
defaultConfidence: 'medium',
|
|
40
|
+
coverage: {
|
|
41
|
+
facetsBySc: {
|
|
42
|
+
'1.1.1': ['text-alternative-quality']
|
|
37
43
|
}
|
|
44
|
+
}
|
|
38
45
|
};
|
|
39
46
|
|
|
40
47
|
function runInPage(ctx) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
}
|
|
50
64
|
};
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
const buildSelector =
|
|
67
|
+
helpers && typeof helpers.buildSelector === 'function'
|
|
68
|
+
? helpers.buildSelector
|
|
69
|
+
: (el) => {
|
|
70
|
+
try {
|
|
71
|
+
if (!el || !el.tagName) return 'html';
|
|
72
|
+
const tag = (el.tagName || 'html').toLowerCase();
|
|
73
|
+
return el.id ? `${tag}#${el.id}` : tag;
|
|
74
|
+
} catch {
|
|
75
|
+
return 'html';
|
|
76
|
+
}
|
|
62
77
|
};
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
? helpers.getFocusableInfo
|
|
78
|
-
: null;
|
|
79
|
-
|
|
80
|
-
function isRolePresentationExcluded(el) {
|
|
81
|
-
const role = (() => {
|
|
82
|
-
try {
|
|
83
|
-
return String(el.getAttribute('role') || '').trim().toLowerCase();
|
|
84
|
-
} catch {
|
|
85
|
-
return '';
|
|
86
|
-
}
|
|
87
|
-
})();
|
|
88
|
-
if (role !== 'presentation' && role !== 'none') return false;
|
|
89
|
-
|
|
90
|
-
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
91
|
-
let focusable = false;
|
|
92
|
-
if (getFocusableInfo) {
|
|
93
|
-
const fi = (() => {
|
|
94
|
-
try {
|
|
95
|
-
return getFocusableInfo(el, ctx);
|
|
96
|
-
} catch {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
})();
|
|
100
|
-
focusable = !!(fi && fi.focusable);
|
|
101
|
-
} else {
|
|
102
|
-
const tabindex = el.getAttribute('tabindex');
|
|
103
|
-
focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
|
|
104
|
-
}
|
|
105
|
-
return !focusable;
|
|
106
|
-
}
|
|
79
|
+
const getOuterHtmlSnippet =
|
|
80
|
+
helpers && typeof helpers.getOuterHtmlSnippet === 'function'
|
|
81
|
+
? helpers.getOuterHtmlSnippet
|
|
82
|
+
: (el) => {
|
|
83
|
+
try {
|
|
84
|
+
return el && el.outerHTML ? String(el.outerHTML).slice(0, 2000) : '';
|
|
85
|
+
} catch {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const getEligibilityInfo =
|
|
91
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
107
92
|
|
|
93
|
+
const isAccTreeEligible =
|
|
94
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
108
95
|
|
|
109
|
-
|
|
96
|
+
const getFocusableInfo =
|
|
97
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
98
|
+
|
|
99
|
+
function isRolePresentationExcluded(el) {
|
|
100
|
+
const role = (() => {
|
|
101
|
+
try {
|
|
102
|
+
return String(el.getAttribute('role') || '')
|
|
103
|
+
.trim()
|
|
104
|
+
.toLowerCase();
|
|
105
|
+
} catch {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
if (role !== 'presentation' && role !== 'none') return false;
|
|
110
|
+
|
|
111
|
+
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
112
|
+
let focusable;
|
|
113
|
+
if (getFocusableInfo) {
|
|
114
|
+
const fi = (() => {
|
|
110
115
|
try {
|
|
111
|
-
|
|
116
|
+
return getFocusableInfo(el, ctx);
|
|
112
117
|
} catch {
|
|
113
|
-
|
|
118
|
+
return null;
|
|
114
119
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
})();
|
|
121
|
+
focusable = !!(fi && fi.focusable);
|
|
122
|
+
} else {
|
|
123
|
+
const tabindex = el.getAttribute('tabindex');
|
|
124
|
+
focusable =
|
|
125
|
+
tabindex != null &&
|
|
126
|
+
String(tabindex).trim() !== '' &&
|
|
127
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
119
128
|
}
|
|
129
|
+
return !focusable;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const els = (() => {
|
|
133
|
+
try {
|
|
134
|
+
return Array.from((queryAllSmart ? queryAllSmart('svg') : queryAll('svg')) || []);
|
|
135
|
+
} catch {
|
|
136
|
+
return queryAll('svg');
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
120
139
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const trim = (v) => (v == null ? '' : String(v)).trim();
|
|
125
|
-
|
|
126
|
-
for (const el of els) {
|
|
127
|
-
if (!el || !el.getAttribute) continue;
|
|
140
|
+
if (!els.length) {
|
|
141
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
142
|
+
}
|
|
128
143
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const elig = (() => { try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; } })();
|
|
132
|
-
if (elig && elig.eligible === false) continue;
|
|
133
|
-
}
|
|
144
|
+
const occurrences = [];
|
|
145
|
+
let applicableCount = 0;
|
|
134
146
|
|
|
135
|
-
|
|
147
|
+
const trim = (v) => (v == null ? '' : String(v)).trim();
|
|
136
148
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
let descText = '';
|
|
140
|
-
let ariaLabel = '';
|
|
141
|
-
let ariaLabelledBy = '';
|
|
142
|
-
let labelledByText = '';
|
|
149
|
+
for (const el of els) {
|
|
150
|
+
if (!el || !el.getAttribute) continue;
|
|
143
151
|
|
|
152
|
+
// acc eligibility
|
|
153
|
+
if (isAccTreeEligible) {
|
|
154
|
+
const elig = (() => {
|
|
144
155
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
descText = trim(descEl && descEl.textContent);
|
|
149
|
-
|
|
150
|
-
ariaLabel = trim(el.getAttribute('aria-label'));
|
|
151
|
-
ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
|
|
152
|
-
} catch {}
|
|
153
|
-
|
|
154
|
-
if (!ariaLabel && ariaLabelledBy && helpers && typeof helpers.getTextFromIdRefs === 'function') {
|
|
155
|
-
try {
|
|
156
|
-
const t = helpers.getTextFromIdRefs(ariaLabelledBy, ctx);
|
|
157
|
-
labelledByText = trim(t && t.text);
|
|
158
|
-
} catch {}
|
|
156
|
+
return isAccTreeEligible(el, ctx);
|
|
157
|
+
} catch {
|
|
158
|
+
return { eligible: true, reasons: [] };
|
|
159
159
|
}
|
|
160
|
+
})();
|
|
161
|
+
if (elig && elig.eligible === false) continue;
|
|
162
|
+
}
|
|
160
163
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
};
|
|
164
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
165
|
+
|
|
166
|
+
// Detect mechanisms once
|
|
167
|
+
let titleText = '';
|
|
168
|
+
let descText = '';
|
|
169
|
+
let ariaLabel = '';
|
|
170
|
+
let ariaLabelledBy = '';
|
|
171
|
+
let labelledByText = '';
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const titleEl = el.querySelector ? el.querySelector('title') : null;
|
|
175
|
+
const descEl = el.querySelector ? el.querySelector('desc') : null;
|
|
176
|
+
titleText = trim(titleEl && titleEl.textContent);
|
|
177
|
+
descText = trim(descEl && descEl.textContent);
|
|
178
|
+
|
|
179
|
+
ariaLabel = trim(el.getAttribute('aria-label'));
|
|
180
|
+
ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
|
|
181
|
+
} catch {}
|
|
182
|
+
|
|
183
|
+
if (
|
|
184
|
+
!ariaLabel &&
|
|
185
|
+
ariaLabelledBy &&
|
|
186
|
+
helpers &&
|
|
187
|
+
typeof helpers.getTextFromIdRefs === 'function'
|
|
188
|
+
) {
|
|
189
|
+
try {
|
|
190
|
+
const t = helpers.getTextFromIdRefs(ariaLabelledBy, ctx);
|
|
191
|
+
labelledByText = trim(t && t.text);
|
|
192
|
+
} catch {}
|
|
193
|
+
}
|
|
192
194
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
195
|
+
const hasNonEmptyTitle = !!titleText;
|
|
196
|
+
const hasNonEmptyDesc = !!descText;
|
|
197
|
+
const hasAriaLabel = !!ariaLabel;
|
|
198
|
+
const hasResolvedLabelledBy = !!labelledByText;
|
|
199
|
+
|
|
200
|
+
const hasMechanism =
|
|
201
|
+
hasNonEmptyTitle || hasNonEmptyDesc || hasAriaLabel || hasResolvedLabelledBy;
|
|
202
|
+
if (!hasMechanism) continue;
|
|
203
|
+
|
|
204
|
+
applicableCount += 1;
|
|
205
|
+
|
|
206
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
207
|
+
|
|
208
|
+
const baseOccurrence = {
|
|
209
|
+
summary: 'Review text alternative for <svg> for accuracy and appropriateness.',
|
|
210
|
+
hint: 'Confirm the <title>/<desc> or ARIA name conveys the meaning/purpose of the graphic in context.',
|
|
211
|
+
i18n: {
|
|
212
|
+
summaryKey: 'svg_textAltQuality_summary_cantTell',
|
|
213
|
+
hintKey: 'svg_textAltQuality_hint_cantTell',
|
|
214
|
+
params: { element: 'svg' }
|
|
215
|
+
},
|
|
216
|
+
data: {
|
|
217
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
218
|
+
details: {
|
|
219
|
+
hasNonEmptyTitle,
|
|
220
|
+
hasNonEmptyDesc,
|
|
221
|
+
ariaLabel: ariaLabel || null,
|
|
222
|
+
ariaLabelledBy: ariaLabelledBy || null,
|
|
223
|
+
ariaLabelledByText: labelledByText ? labelledByText.slice(0, 120) : null // deterministic truncation
|
|
199
224
|
}
|
|
200
|
-
|
|
225
|
+
}
|
|
226
|
+
};
|
|
201
227
|
|
|
202
|
-
if (
|
|
203
|
-
|
|
228
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
229
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
230
|
+
} else {
|
|
231
|
+
const selectorStr = (() => {
|
|
232
|
+
try {
|
|
233
|
+
return buildSelector(el);
|
|
234
|
+
} catch {
|
|
235
|
+
return 'html';
|
|
236
|
+
}
|
|
237
|
+
})();
|
|
238
|
+
const html = getOuterHtmlSnippet(el);
|
|
239
|
+
occurrences.push({ selector: selectorStr, html, ...baseOccurrence });
|
|
204
240
|
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (applicableCount === 0) {
|
|
244
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
245
|
+
}
|
|
205
246
|
|
|
206
|
-
|
|
247
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
207
248
|
}
|
|
208
249
|
|
|
209
|
-
module.exports = {id, meta, runInPage};
|
|
250
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -40,14 +40,11 @@ const meta = {
|
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
function runInPage(ctx) {
|
|
43
|
-
const {
|
|
43
|
+
const { helpers, rule } = ctx;
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} catch {
|
|
49
|
-
nodes = [];
|
|
50
|
-
}
|
|
45
|
+
const nodes = helpers.queryAllSmart
|
|
46
|
+
? helpers.queryAllSmart('[tabindex]')
|
|
47
|
+
: helpers.queryAll('[tabindex]');
|
|
51
48
|
|
|
52
49
|
const occurrences = [];
|
|
53
50
|
let applicableCount = 0;
|
|
@@ -64,7 +61,7 @@ function runInPage(ctx) {
|
|
|
64
61
|
if (n <= 0) continue;
|
|
65
62
|
|
|
66
63
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
67
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
64
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
68
65
|
|
|
69
66
|
occurrences.push({
|
|
70
67
|
selector: stableSelector,
|
|
@@ -86,7 +83,12 @@ function runInPage(ctx) {
|
|
|
86
83
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
87
84
|
}
|
|
88
85
|
if (occurrences.length) {
|
|
89
|
-
return {
|
|
86
|
+
return {
|
|
87
|
+
ruleId: rule.ruleId,
|
|
88
|
+
outcome: 'cantTell',
|
|
89
|
+
severity: rule.defaultSeverity || 'minor',
|
|
90
|
+
occurrences
|
|
91
|
+
};
|
|
90
92
|
}
|
|
91
93
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
92
94
|
}
|
|
@@ -26,7 +26,8 @@ const id = 'table-duplicate-name';
|
|
|
26
26
|
|
|
27
27
|
const meta = {
|
|
28
28
|
title: 'Table caption must not duplicate its summary attribute',
|
|
29
|
-
description:
|
|
29
|
+
description:
|
|
30
|
+
"Checks that a <table>'s <caption> text is not identical to its (deprecated) summary attribute.",
|
|
30
31
|
i18n: {
|
|
31
32
|
titleKey: 'tableDuplicateName_title',
|
|
32
33
|
descriptionKey: 'tableDuplicateName_description'
|
|
@@ -43,13 +44,17 @@ const meta = {
|
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
function runInPage(ctx) {
|
|
46
|
-
const {
|
|
47
|
+
const { helpers, rule } = ctx;
|
|
47
48
|
|
|
48
49
|
function normalizeWs(s) {
|
|
49
|
-
return String(s || '')
|
|
50
|
+
return String(s || '')
|
|
51
|
+
.replace(/\s+/g, ' ')
|
|
52
|
+
.trim();
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
const nodes = helpers.queryAllSmart
|
|
55
|
+
const nodes = helpers.queryAllSmart
|
|
56
|
+
? helpers.queryAllSmart('table[summary]')
|
|
57
|
+
: helpers.queryAll('table[summary]');
|
|
53
58
|
|
|
54
59
|
const occurrences = [];
|
|
55
60
|
let applicableCount = 0;
|
|
@@ -68,12 +73,12 @@ function runInPage(ctx) {
|
|
|
68
73
|
if (captionText.toLowerCase() !== summary.toLowerCase()) continue;
|
|
69
74
|
|
|
70
75
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
71
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
76
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
72
77
|
|
|
73
78
|
occurrences.push({
|
|
74
79
|
selector: stableSelector,
|
|
75
80
|
html,
|
|
76
|
-
summary:
|
|
81
|
+
summary: "This table's caption duplicates its summary attribute.",
|
|
77
82
|
hint: 'Remove the redundant summary attribute, or make it provide different information than the caption.',
|
|
78
83
|
i18n: {
|
|
79
84
|
summaryKey: 'tableDuplicateName_summary_cantTell',
|
|
@@ -90,7 +95,12 @@ function runInPage(ctx) {
|
|
|
90
95
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
91
96
|
}
|
|
92
97
|
if (occurrences.length) {
|
|
93
|
-
return {
|
|
98
|
+
return {
|
|
99
|
+
ruleId: rule.ruleId,
|
|
100
|
+
outcome: 'cantTell',
|
|
101
|
+
severity: rule.defaultSeverity || 'minor',
|
|
102
|
+
occurrences
|
|
103
|
+
};
|
|
94
104
|
}
|
|
95
105
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
96
106
|
}
|
|
@@ -32,7 +32,8 @@ const id = 'table-fake-caption';
|
|
|
32
32
|
|
|
33
33
|
const meta = {
|
|
34
34
|
title: "A table's first row should not stand in for a real <caption>",
|
|
35
|
-
description:
|
|
35
|
+
description:
|
|
36
|
+
'Flags tables with no <caption> whose first row has a single non-empty cell while other rows have multiple cells, for manual review of whether that cell is acting as a fake caption.',
|
|
36
37
|
i18n: {
|
|
37
38
|
titleKey: 'tableFakeCaption_title',
|
|
38
39
|
descriptionKey: 'tableFakeCaption_description'
|
|
@@ -41,7 +42,13 @@ const meta = {
|
|
|
41
42
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'manual'],
|
|
42
43
|
wcagSc: ['1.3.1'],
|
|
43
44
|
normativeMappings: [
|
|
44
|
-
{
|
|
45
|
+
{
|
|
46
|
+
standard: 'WCAG',
|
|
47
|
+
version: '2.2',
|
|
48
|
+
requirement: '1.3.1',
|
|
49
|
+
title: 'Info and Relationships',
|
|
50
|
+
conformanceLevel: 'A'
|
|
51
|
+
}
|
|
45
52
|
],
|
|
46
53
|
defaultSeverity: 'minor',
|
|
47
54
|
category: 'perceivable',
|
|
@@ -51,9 +58,11 @@ const meta = {
|
|
|
51
58
|
};
|
|
52
59
|
|
|
53
60
|
function runInPage(ctx) {
|
|
54
|
-
const {
|
|
61
|
+
const { helpers, rule } = ctx;
|
|
55
62
|
|
|
56
|
-
function trim(v) {
|
|
63
|
+
function trim(v) {
|
|
64
|
+
return (v == null ? '' : String(v)).trim();
|
|
65
|
+
}
|
|
57
66
|
|
|
58
67
|
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
|
|
59
68
|
|
|
@@ -83,12 +92,15 @@ function runInPage(ctx) {
|
|
|
83
92
|
if (!hasMultiCellRow) continue;
|
|
84
93
|
|
|
85
94
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(table) : 'html';
|
|
86
|
-
const html = helpers.getOuterHtmlSnippet
|
|
95
|
+
const html = helpers.getOuterHtmlSnippet
|
|
96
|
+
? helpers.getOuterHtmlSnippet(table)
|
|
97
|
+
: table.outerHTML || '';
|
|
87
98
|
|
|
88
99
|
const baseOccurrence = {
|
|
89
100
|
selector: stableSelector,
|
|
90
101
|
html,
|
|
91
|
-
summary:
|
|
102
|
+
summary:
|
|
103
|
+
'This table has no <caption>, but its first row is a single cell sitting above multi-cell rows — it may be acting as a fake caption.',
|
|
92
104
|
hint: 'If this cell is meant to describe the table, use a real <caption> element instead of a lone first-row cell.',
|
|
93
105
|
i18n: {
|
|
94
106
|
summaryKey: 'tableFakeCaption_summary_cantTell',
|
|
@@ -112,7 +124,12 @@ function runInPage(ctx) {
|
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
if (occurrences.length) {
|
|
115
|
-
return {
|
|
127
|
+
return {
|
|
128
|
+
ruleId: rule.ruleId,
|
|
129
|
+
outcome: 'cantTell',
|
|
130
|
+
severity: rule.defaultSeverity || 'minor',
|
|
131
|
+
occurrences
|
|
132
|
+
};
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
@@ -41,7 +41,13 @@ const meta = {
|
|
|
41
41
|
tags: ['wcag2a', 'wcag122', 'media', 'timebasedmedia', 'atomic', 'manual'],
|
|
42
42
|
wcagSc: ['1.2.2'],
|
|
43
43
|
normativeMappings: [
|
|
44
|
-
{
|
|
44
|
+
{
|
|
45
|
+
standard: 'WCAG',
|
|
46
|
+
version: '2.2',
|
|
47
|
+
requirement: '1.2.2',
|
|
48
|
+
title: 'Captions (Prerecorded)',
|
|
49
|
+
conformanceLevel: 'A'
|
|
50
|
+
}
|
|
45
51
|
],
|
|
46
52
|
defaultSeverity: 'moderate',
|
|
47
53
|
category: 'perceivable',
|
|
@@ -51,7 +57,7 @@ const meta = {
|
|
|
51
57
|
};
|
|
52
58
|
|
|
53
59
|
function runInPage(ctx) {
|
|
54
|
-
const {
|
|
60
|
+
const { helpers, rule } = ctx;
|
|
55
61
|
|
|
56
62
|
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('video') : helpers.queryAll('video');
|
|
57
63
|
|
|
@@ -77,7 +83,7 @@ function runInPage(ctx) {
|
|
|
77
83
|
if (hasCaptionsTrack) continue;
|
|
78
84
|
|
|
79
85
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
80
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
86
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
81
87
|
|
|
82
88
|
const baseOccurrence = {
|
|
83
89
|
selector: stableSelector,
|
|
@@ -106,7 +112,12 @@ function runInPage(ctx) {
|
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
if (occurrences.length) {
|
|
109
|
-
return {
|
|
115
|
+
return {
|
|
116
|
+
ruleId: rule.ruleId,
|
|
117
|
+
outcome: 'cantTell',
|
|
118
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
119
|
+
occurrences
|
|
120
|
+
};
|
|
110
121
|
}
|
|
111
122
|
|
|
112
123
|
// Manual rules may only emit cantTell/notApplicable (never pass/fail):
|