@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
|
@@ -32,267 +32,287 @@
|
|
|
32
32
|
const id = 'svg-text-alternative-present';
|
|
33
33
|
|
|
34
34
|
const meta = {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
35
|
+
title: '<svg> must provide a text alternative',
|
|
36
|
+
description:
|
|
37
|
+
'Checks that inline <svg> elements provide a text alternative via a <title> element or an ARIA name (a <desc> element alone does not count — it only contributes to the accessible description, not the name).',
|
|
38
|
+
i18n: {
|
|
39
|
+
titleKey: 'svg_textAltPresent_title',
|
|
40
|
+
descriptionKey: 'svg_textAltPresent_description'
|
|
41
|
+
},
|
|
42
|
+
helpUrl: null,
|
|
43
|
+
tags: ['wcag2a', 'wcag111', 'svg', 'nontext', 'images', 'atomic', 'automatic'],
|
|
44
|
+
wcagSc: ['1.1.1'],
|
|
45
|
+
normativeMappings: [
|
|
46
|
+
{
|
|
47
|
+
standard: 'WCAG',
|
|
48
|
+
version: '2.2',
|
|
49
|
+
requirement: '1.1.1',
|
|
50
|
+
title: 'Non-text Content',
|
|
51
|
+
conformanceLevel: 'A'
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
defaultSeverity: 'serious',
|
|
55
|
+
category: 'perceivable',
|
|
56
|
+
type: 'automatic',
|
|
57
|
+
defaultConfidence: 'high',
|
|
58
|
+
coverage: {
|
|
59
|
+
facetsBySc: {
|
|
60
|
+
'1.1.1': ['svg-text-alternative-present']
|
|
55
61
|
}
|
|
62
|
+
}
|
|
56
63
|
};
|
|
57
64
|
|
|
58
65
|
function runInPage(ctx) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
const { document, root, helpers, rule } = ctx;
|
|
67
|
+
const safeRoot = root || document;
|
|
68
|
+
|
|
69
|
+
const queryAllSmart =
|
|
70
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
71
|
+
const queryAll =
|
|
72
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
73
|
+
? helpers.queryAll
|
|
74
|
+
: (sel) => {
|
|
75
|
+
try {
|
|
76
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
77
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
78
|
+
: [];
|
|
79
|
+
} catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
71
82
|
};
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
: null;
|
|
84
|
+
const getEligibilityInfo =
|
|
85
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
76
86
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
: null;
|
|
87
|
+
const isAccTreeEligible =
|
|
88
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
80
89
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
: null;
|
|
90
|
+
const getFocusableInfo =
|
|
91
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
: null;
|
|
93
|
+
const getAriaNameInfo =
|
|
94
|
+
helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
|
|
88
95
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
96
|
+
function trim(v) {
|
|
97
|
+
try {
|
|
98
|
+
return (v == null ? '' : String(v)).trim();
|
|
99
|
+
} catch {
|
|
100
|
+
return '';
|
|
95
101
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Per SVG accessible-name conventions, only a <title> that is literally
|
|
105
|
+
// the first child element is used by assistive technologies as the
|
|
106
|
+
// SVG's accessible name; a <title> appearing later among the children
|
|
107
|
+
// is commonly ignored by AT even though it's still a valid DOM child.
|
|
108
|
+
function nonEmptyFirstChildTitleText(svg) {
|
|
109
|
+
try {
|
|
110
|
+
const first = svg.firstElementChild;
|
|
111
|
+
const tn = first ? (first.localName || first.tagName || '').toLowerCase() : '';
|
|
112
|
+
if (tn === 'title') {
|
|
113
|
+
const txt = trim(first.textContent);
|
|
114
|
+
if (txt) return txt;
|
|
115
|
+
}
|
|
116
|
+
} catch {}
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// <desc> counts when it is the first child, or the second child
|
|
121
|
+
// immediately following a <title> — the standard <title>+<desc> pairing
|
|
122
|
+
// (e.g. <svg><title>...</title><desc>...</desc>...</svg>). A <desc>
|
|
123
|
+
// appearing later than that is not reliably read by AT.
|
|
124
|
+
function nonEmptyDescText(svg) {
|
|
125
|
+
try {
|
|
126
|
+
const first = svg.firstElementChild;
|
|
127
|
+
const firstTag = first ? (first.localName || first.tagName || '').toLowerCase() : '';
|
|
128
|
+
if (firstTag === 'desc') {
|
|
129
|
+
const txt = trim(first.textContent);
|
|
130
|
+
if (txt) return txt;
|
|
131
|
+
} else if (firstTag === 'title' && first.nextElementSibling) {
|
|
132
|
+
const second = first.nextElementSibling;
|
|
133
|
+
const secondTag = (second.localName || second.tagName || '').toLowerCase();
|
|
134
|
+
if (secondTag === 'desc') {
|
|
135
|
+
const txt = trim(second.textContent);
|
|
136
|
+
if (txt) return txt;
|
|
110
137
|
}
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
return '';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isFocusable(svg) {
|
|
144
|
+
if (getFocusableInfo) {
|
|
145
|
+
const fi = (() => {
|
|
119
146
|
try {
|
|
120
|
-
|
|
121
|
-
const firstTag = first ? (first.localName || first.tagName || '').toLowerCase() : '';
|
|
122
|
-
if (firstTag === 'desc') {
|
|
123
|
-
const txt = trim(first.textContent);
|
|
124
|
-
if (txt) return txt;
|
|
125
|
-
} else if (firstTag === 'title' && first.nextElementSibling) {
|
|
126
|
-
const second = first.nextElementSibling;
|
|
127
|
-
const secondTag = (second.localName || second.tagName || '').toLowerCase();
|
|
128
|
-
if (secondTag === 'desc') {
|
|
129
|
-
const txt = trim(second.textContent);
|
|
130
|
-
if (txt) return txt;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
147
|
+
return getFocusableInfo(svg, ctx);
|
|
133
148
|
} catch {
|
|
149
|
+
return null;
|
|
134
150
|
}
|
|
135
|
-
|
|
151
|
+
})();
|
|
152
|
+
return !!(fi && fi.focusable);
|
|
136
153
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return !!(fi && fi.focusable);
|
|
148
|
-
}
|
|
149
|
-
// deterministic fallback: tabindex presence/valid number
|
|
150
|
-
try {
|
|
151
|
-
const tabindex = svg && svg.getAttribute ? svg.getAttribute('tabindex') : null;
|
|
152
|
-
return tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
|
|
153
|
-
} catch {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
154
|
+
// deterministic fallback: tabindex presence/valid number
|
|
155
|
+
try {
|
|
156
|
+
const tabindex = svg && svg.getAttribute ? svg.getAttribute('tabindex') : null;
|
|
157
|
+
return (
|
|
158
|
+
tabindex != null &&
|
|
159
|
+
String(tabindex).trim() !== '' &&
|
|
160
|
+
!Number.isNaN(Number(String(tabindex).trim()))
|
|
161
|
+
);
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
156
164
|
}
|
|
165
|
+
}
|
|
157
166
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
})();
|
|
165
|
-
|
|
166
|
-
if (!svgs.length) {
|
|
167
|
-
return {ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: []};
|
|
167
|
+
const svgs = (() => {
|
|
168
|
+
try {
|
|
169
|
+
return Array.from((queryAllSmart ? queryAllSmart('svg') : queryAll('svg')) || []);
|
|
170
|
+
} catch {
|
|
171
|
+
return queryAll('svg');
|
|
168
172
|
}
|
|
173
|
+
})();
|
|
169
174
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
for (const el of svgs) {
|
|
174
|
-
if (!el || !el.getAttribute) continue;
|
|
175
|
-
|
|
176
|
-
// Applicability step 1: only acc-tree eligible nodes (with helper exceptions)
|
|
177
|
-
if (isAccTreeEligible) {
|
|
178
|
-
const elig = (() => {
|
|
179
|
-
try {
|
|
180
|
-
return isAccTreeEligible(el, ctx);
|
|
181
|
-
} catch {
|
|
182
|
-
return {eligible: true, reasons: []};
|
|
183
|
-
}
|
|
184
|
-
})();
|
|
185
|
-
if (elig && elig.eligible === false) continue;
|
|
186
|
-
}
|
|
175
|
+
if (!svgs.length) {
|
|
176
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
177
|
+
}
|
|
187
178
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
return String(el.getAttribute('role') || '').trim().toLowerCase();
|
|
192
|
-
} catch {
|
|
193
|
-
return '';
|
|
194
|
-
}
|
|
195
|
-
})();
|
|
179
|
+
const occurrences = [];
|
|
180
|
+
let applicableCount = 0;
|
|
196
181
|
|
|
197
|
-
|
|
182
|
+
for (const el of svgs) {
|
|
183
|
+
if (!el || !el.getAttribute) continue;
|
|
198
184
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// Applicability step 3: intent signal gating (computed once per element)
|
|
204
|
-
let hasAriaNamingAttr = false;
|
|
185
|
+
// Applicability step 1: only acc-tree eligible nodes (with helper exceptions)
|
|
186
|
+
if (isAccTreeEligible) {
|
|
187
|
+
const elig = (() => {
|
|
205
188
|
try {
|
|
206
|
-
|
|
189
|
+
return isAccTreeEligible(el, ctx);
|
|
207
190
|
} catch {
|
|
191
|
+
return { eligible: true, reasons: [] };
|
|
208
192
|
}
|
|
193
|
+
})();
|
|
194
|
+
if (elig && elig.eligible === false) continue;
|
|
195
|
+
}
|
|
209
196
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (!hasIntent) continue;
|
|
221
|
-
|
|
222
|
-
applicableCount += 1;
|
|
223
|
-
|
|
224
|
-
// Expectation: non-empty title/desc OR ARIA name (but only resolve name if attrs exist)
|
|
225
|
-
let hasAriaName = false;
|
|
226
|
-
if (hasAriaNamingAttr) {
|
|
227
|
-
if (getAriaNameInfo) {
|
|
228
|
-
const info = (() => {
|
|
229
|
-
try {
|
|
230
|
-
return getAriaNameInfo(el, ctx);
|
|
231
|
-
} catch {
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
})();
|
|
235
|
-
hasAriaName = !!(info && info.present && trim(info.value));
|
|
236
|
-
} else {
|
|
237
|
-
// minimal deterministic fallback
|
|
238
|
-
const ariaLabel = trim((() => {
|
|
239
|
-
try {
|
|
240
|
-
return el.getAttribute('aria-label');
|
|
241
|
-
} catch {
|
|
242
|
-
return '';
|
|
243
|
-
}
|
|
244
|
-
})());
|
|
245
|
-
const ariaLabelledby = trim((() => {
|
|
246
|
-
try {
|
|
247
|
-
return el.getAttribute('aria-labelledby');
|
|
248
|
-
} catch {
|
|
249
|
-
return '';
|
|
250
|
-
}
|
|
251
|
-
})());
|
|
252
|
-
hasAriaName = !!(ariaLabel || ariaLabelledby);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
197
|
+
// Applicability step 2: role (presentation/none) exclusion only when not focusable
|
|
198
|
+
const role = (() => {
|
|
199
|
+
try {
|
|
200
|
+
return String(el.getAttribute('role') || '')
|
|
201
|
+
.trim()
|
|
202
|
+
.toLowerCase();
|
|
203
|
+
} catch {
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
})();
|
|
255
207
|
|
|
256
|
-
|
|
257
|
-
// DESCRIPTION, never the accessible NAME — so descText does not
|
|
258
|
-
// count here even though it does count toward applicability above.
|
|
259
|
-
const ok = !!titleText || hasAriaName;
|
|
260
|
-
if (ok) continue;
|
|
208
|
+
const focusable = isFocusable(el);
|
|
261
209
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
210
|
+
if (role === 'presentation' || role === 'none') {
|
|
211
|
+
if (!focusable) continue;
|
|
212
|
+
}
|
|
266
213
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
214
|
+
// Applicability step 3: intent signal gating (computed once per element)
|
|
215
|
+
let hasAriaNamingAttr = false;
|
|
216
|
+
try {
|
|
217
|
+
hasAriaNamingAttr =
|
|
218
|
+
el.getAttribute('aria-label') != null || el.getAttribute('aria-labelledby') != null;
|
|
219
|
+
} catch {}
|
|
220
|
+
|
|
221
|
+
const titleText = nonEmptyFirstChildTitleText(el);
|
|
222
|
+
const descText = titleText ? '' : nonEmptyDescText(el); // avoid second scan if title already passes
|
|
223
|
+
const hasTitleOrDesc = !!(titleText || descText);
|
|
224
|
+
|
|
225
|
+
const hasIntent = role === 'img' || hasAriaNamingAttr || hasTitleOrDesc || focusable;
|
|
226
|
+
|
|
227
|
+
if (!hasIntent) continue;
|
|
228
|
+
|
|
229
|
+
applicableCount += 1;
|
|
230
|
+
|
|
231
|
+
// Expectation: non-empty title/desc OR ARIA name (but only resolve name if attrs exist)
|
|
232
|
+
let hasAriaName = false;
|
|
233
|
+
if (hasAriaNamingAttr) {
|
|
234
|
+
if (getAriaNameInfo) {
|
|
235
|
+
const info = (() => {
|
|
236
|
+
try {
|
|
237
|
+
return getAriaNameInfo(el, ctx);
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
})();
|
|
242
|
+
hasAriaName = !!(info && info.present && trim(info.value));
|
|
243
|
+
} else {
|
|
244
|
+
// minimal deterministic fallback
|
|
245
|
+
const ariaLabel = trim(
|
|
246
|
+
(() => {
|
|
247
|
+
try {
|
|
248
|
+
return el.getAttribute('aria-label');
|
|
249
|
+
} catch {
|
|
250
|
+
return '';
|
|
277
251
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
252
|
+
})()
|
|
253
|
+
);
|
|
254
|
+
const ariaLabelledby = trim(
|
|
255
|
+
(() => {
|
|
256
|
+
try {
|
|
257
|
+
return el.getAttribute('aria-labelledby');
|
|
258
|
+
} catch {
|
|
259
|
+
return '';
|
|
260
|
+
}
|
|
261
|
+
})()
|
|
262
|
+
);
|
|
263
|
+
hasAriaName = !!(ariaLabel || ariaLabelledby);
|
|
264
|
+
}
|
|
285
265
|
}
|
|
286
266
|
|
|
287
|
-
|
|
288
|
-
|
|
267
|
+
// Per SVG-AAM §7.1: <desc> contributes only to the accessible
|
|
268
|
+
// DESCRIPTION, never the accessible NAME — so descText does not
|
|
269
|
+
// count here even though it does count toward applicability above.
|
|
270
|
+
const ok = !!titleText || hasAriaName;
|
|
271
|
+
if (ok) continue;
|
|
272
|
+
|
|
273
|
+
let eligInfo = null;
|
|
274
|
+
if (getEligibilityInfo) {
|
|
275
|
+
try {
|
|
276
|
+
eligInfo = getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
277
|
+
} catch {
|
|
278
|
+
eligInfo = null;
|
|
279
|
+
}
|
|
289
280
|
}
|
|
290
281
|
|
|
291
|
-
|
|
292
|
-
|
|
282
|
+
const baseOccurrence = {
|
|
283
|
+
summary: 'Missing text alternative for <svg>.',
|
|
284
|
+
hint: 'Provide a <title> element with text, or an ARIA name (aria-label/aria-labelledby) — a <desc> element alone does not provide an accessible name.',
|
|
285
|
+
i18n: {
|
|
286
|
+
summaryKey: 'svg_textAltPresent_summary_fail',
|
|
287
|
+
hintKey: 'svg_textAltPresent_hint_fail',
|
|
288
|
+
params: { element: 'svg' }
|
|
289
|
+
},
|
|
290
|
+
data: {
|
|
291
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
296
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
297
|
+
} else {
|
|
298
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
293
299
|
}
|
|
294
|
-
|
|
295
|
-
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (applicableCount === 0) {
|
|
303
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!occurrences.length) {
|
|
307
|
+
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
ruleId: rule.ruleId,
|
|
312
|
+
outcome: 'fail',
|
|
313
|
+
severity: rule.defaultSeverity || 'minor',
|
|
314
|
+
occurrences
|
|
315
|
+
};
|
|
296
316
|
}
|
|
297
317
|
|
|
298
|
-
module.exports = {id, meta, runInPage};
|
|
318
|
+
module.exports = { id, meta, runInPage };
|