@surea11y/core 1.2.0 → 1.4.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 +81 -7
- package/LICENSE +373 -21
- package/README.md +175 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +27 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +19 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +63 -18
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +63 -31
- package/src/checks/automatic/aria-allowed-attr.js +204 -80
- package/src/checks/automatic/aria-allowed-role.js +23 -7
- package/src/checks/automatic/aria-braille-equivalent.js +34 -10
- package/src/checks/automatic/aria-conditional-attr.js +32 -14
- package/src/checks/automatic/aria-deprecated-role.js +26 -11
- package/src/checks/automatic/aria-hidden-body.js +48 -23
- package/src/checks/automatic/aria-hidden-focus.js +420 -66
- package/src/checks/automatic/aria-prohibited-attr.js +327 -60
- package/src/checks/automatic/aria-prohibited-children.js +111 -103
- package/src/checks/automatic/aria-required-attr.js +29 -15
- package/src/checks/automatic/aria-required-children.js +44 -24
- package/src/checks/automatic/aria-required-parent.js +64 -35
- package/src/checks/automatic/aria-role-name-present.js +49 -21
- package/src/checks/automatic/aria-roles-valid.js +24 -12
- package/src/checks/automatic/aria-valid-attr-value.js +46 -22
- package/src/checks/automatic/aria-valid-attr.js +19 -5
- package/src/checks/automatic/autocomplete-valid.js +76 -16
- package/src/checks/automatic/avoid-inline-spacing.js +23 -8
- package/src/checks/automatic/binary-control-name-present.js +62 -50
- package/src/checks/automatic/button-name-present.js +54 -24
- package/src/checks/automatic/bypass-blocks-present.js +51 -32
- package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
- package/src/checks/automatic/combobox-name-present.js +40 -45
- package/src/checks/automatic/contrast-computable.js +363 -341
- package/src/checks/automatic/contrast-enhanced.js +489 -466
- package/src/checks/automatic/contrast-minimum.js +488 -465
- package/src/checks/automatic/css-orientation-lock.js +51 -35
- package/src/checks/automatic/definition-list-children-valid.js +46 -25
- package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
- package/src/checks/automatic/dialog-name-present.js +47 -85
- package/src/checks/automatic/dlitem-parent-valid.js +25 -8
- package/src/checks/automatic/duplicate-id-aria.js +28 -9
- package/src/checks/automatic/embed-text-alternative-present.js +88 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
- package/src/checks/automatic/form-control-single-label.js +50 -14
- package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
- package/src/checks/automatic/iframe-focusable-content.js +265 -22
- package/src/checks/automatic/iframe-name-present.js +33 -9
- package/src/checks/automatic/iframe-title-unique.js +32 -9
- package/src/checks/automatic/img-alt-present.js +54 -52
- package/src/checks/automatic/input-image-alt-present.js +141 -112
- package/src/checks/automatic/label-in-name.js +65 -41
- package/src/checks/automatic/language-page-present.js +111 -109
- package/src/checks/automatic/link-in-text-block.js +61 -19
- package/src/checks/automatic/link-name-present.js +47 -14
- package/src/checks/automatic/list-children-valid.js +40 -33
- package/src/checks/automatic/listbox-name-present.js +41 -19
- package/src/checks/automatic/listitem-parent-valid.js +48 -13
- package/src/checks/automatic/menuitem-name-present.js +41 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
- package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
- package/src/checks/automatic/meter-name-present.js +40 -36
- package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
- package/src/checks/automatic/object-text-alternative-present.js +93 -39
- package/src/checks/automatic/option-name-present.js +40 -21
- package/src/checks/automatic/page-title-present.js +19 -6
- package/src/checks/automatic/progressbar-name-present.js +49 -44
- package/src/checks/automatic/role-img-alt-present.js +211 -159
- package/src/checks/automatic/searchbox-name-present.js +41 -19
- package/src/checks/automatic/server-side-image-map-absent.js +27 -11
- package/src/checks/automatic/slider-name-present.js +42 -47
- package/src/checks/automatic/spinbutton-name-present.js +41 -19
- package/src/checks/automatic/summary-name-present.js +39 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
- package/src/checks/automatic/svg-text-alternative-present.js +262 -230
- package/src/checks/automatic/tab-name-present.js +39 -60
- package/src/checks/automatic/table-headers-attr-valid.js +27 -10
- package/src/checks/automatic/table-th-has-data-cells.js +24 -8
- package/src/checks/automatic/target-size-minimum.js +123 -48
- package/src/checks/automatic/td-has-header.js +53 -12
- package/src/checks/automatic/textbox-name-present.js +41 -19
- package/src/checks/automatic/tooltip-name-present.js +39 -18
- package/src/checks/automatic/treeitem-name-present.js +40 -21
- package/src/checks/automatic/valid-lang.js +22 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
- package/src/checks/manual/accesskeys-manual.js +17 -6
- package/src/checks/manual/area-alt-decorative-manual.js +194 -193
- package/src/checks/manual/area-alt-quality-manual.js +184 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
- package/src/checks/manual/aria-text-manual.js +20 -11
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
- package/src/checks/manual/css-hidden-focus.js +375 -169
- package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
- package/src/checks/manual/empty-heading-manual.js +41 -24
- package/src/checks/manual/empty-table-header-manual.js +69 -31
- package/src/checks/manual/focus-order-semantics-manual.js +60 -13
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
- package/src/checks/manual/heading-order-manual.js +50 -8
- package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
- package/src/checks/manual/image-redundant-alt-manual.js +38 -8
- package/src/checks/manual/img-alt-decorative-manual.js +133 -96
- package/src/checks/manual/img-alt-quality-manual.js +178 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
- package/src/checks/manual/label-title-only-manual.js +44 -28
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
- package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
- package/src/checks/manual/landmark-one-main-manual.js +38 -43
- package/src/checks/manual/landmark-unique-manual.js +78 -67
- package/src/checks/manual/link-name-quality-manual.js +45 -12
- package/src/checks/manual/media-transcript-present-manual.js +37 -22
- package/src/checks/manual/meta-viewport-large-manual.js +19 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
- package/src/checks/manual/p-as-heading-manual.js +24 -7
- package/src/checks/manual/page-has-heading-one-manual.js +42 -32
- package/src/checks/manual/page-title-patterns-manual.js +80 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
- package/src/checks/manual/region-manual.js +244 -60
- package/src/checks/manual/scope-attr-valid-manual.js +13 -4
- package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
- package/src/checks/manual/skip-link-manual.js +42 -18
- package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
- package/src/checks/manual/tabindex-manual.js +13 -4
- package/src/checks/manual/table-duplicate-name-manual.js +22 -11
- package/src/checks/manual/table-fake-caption-manual.js +48 -10
- package/src/checks/manual/video-caption-manual.js +17 -4
- package/src/checks/manual-review.js +58 -12
- package/src/core.js +41705 -29650
- package/src/index.js +2 -0
- package/src/report.js +109 -47
- package/src/sarif.js +190 -0
- package/surea11y.browser.js +37774 -0
- package/bin/core.js +0 -348
- package/docs/CLI.md +0 -75
- package/src/catalogs/composites.wcag.js +0 -490
- package/src/checks/rules-and-tags.full.csv +0 -19
- package/src/checks/rules-and-tags.full.json +0 -259
- package/src/core/aria-helpers.js +0 -970
- package/src/core/contrast-helpers.js +0 -1147
- package/src/core/dom-helpers.js +0 -4235
- package/src/core/dom-runner.js +0 -671
- package/src/core/frame-messaging.js +0 -210
- package/src/core/frame-scan.js +0 -178
- package/src/core/rollup-composites.js +0 -135
- package/src/core/rule-meta.js +0 -159
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -923
- package/src/i18n/fr.js +0 -844
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -55
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -9,7 +11,12 @@
|
|
|
9
11
|
* @applicability
|
|
10
12
|
* Applies to inline <svg> elements that are exposed to assistive technologies AND appear intended to be conveyed.
|
|
11
13
|
* "Intended to be conveyed" is approximated deterministically by at least one of:
|
|
12
|
-
* - role="img"
|
|
14
|
+
* - role="img", role="graphics-symbol", or role="graphics-document"
|
|
15
|
+
* on the SVG root element itself (the WAI-ARIA Graphics Module
|
|
16
|
+
* roles, alongside img). Deliberately does NOT extend to arbitrary
|
|
17
|
+
* role="graphics-symbol" descendants nested inside an <svg> — this
|
|
18
|
+
* check's scope is the <svg> root only; a broader feature, not
|
|
19
|
+
* attempted here.
|
|
13
20
|
* - aria-label / aria-labelledby present
|
|
14
21
|
* - <title> or <desc> present (desc alone is an applicability signal only — see @expectation)
|
|
15
22
|
* - focusable/tabbable (e.g., tabindex, native focusability)
|
|
@@ -23,276 +30,301 @@
|
|
|
23
30
|
* - an ARIA name (aria-label / aria-labelledby).
|
|
24
31
|
* A <desc> element alone does NOT satisfy this — per the SVG Accessibility
|
|
25
32
|
* API Mappings spec §7.1, <desc> only ever contributes to the accessible
|
|
26
|
-
* DESCRIPTION, never the accessible NAME
|
|
27
|
-
* against the SVG-AAM spec text, not assumed). An <svg> with only a
|
|
33
|
+
* DESCRIPTION, never the accessible NAME. An <svg> with only a
|
|
28
34
|
* <desc> and no <title>/ARIA name is still "applicable" (desc signals
|
|
29
|
-
* authorial intent) but fails
|
|
35
|
+
* authorial intent) but fails.
|
|
30
36
|
*/
|
|
31
37
|
|
|
32
38
|
const id = 'svg-text-alternative-present';
|
|
33
39
|
|
|
34
40
|
const meta = {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
facetsBySc: {
|
|
53
|
-
'1.1.1': ['svg-text-alternative-present']
|
|
54
|
-
}
|
|
41
|
+
title: '<svg> must provide a text alternative',
|
|
42
|
+
description:
|
|
43
|
+
'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).',
|
|
44
|
+
i18n: {
|
|
45
|
+
titleKey: 'svg_textAltPresent_title',
|
|
46
|
+
descriptionKey: 'svg_textAltPresent_description'
|
|
47
|
+
},
|
|
48
|
+
helpUrl: null,
|
|
49
|
+
tags: ['wcag2a', 'wcag111', 'svg', 'nontext', 'images', 'atomic', 'automatic'],
|
|
50
|
+
wcagSc: ['1.1.1'],
|
|
51
|
+
normativeMappings: [
|
|
52
|
+
{
|
|
53
|
+
standard: 'WCAG',
|
|
54
|
+
version: '2.2',
|
|
55
|
+
requirement: '1.1.1',
|
|
56
|
+
title: 'Non-text Content',
|
|
57
|
+
conformanceLevel: 'A'
|
|
55
58
|
}
|
|
59
|
+
],
|
|
60
|
+
defaultSeverity: 'serious',
|
|
61
|
+
category: 'perceivable',
|
|
62
|
+
type: 'automatic',
|
|
63
|
+
defaultConfidence: 'high',
|
|
64
|
+
coverage: {
|
|
65
|
+
facetsBySc: {
|
|
66
|
+
'1.1.1': ['svg-text-alternative-present']
|
|
67
|
+
}
|
|
68
|
+
}
|
|
56
69
|
};
|
|
57
70
|
|
|
58
71
|
function runInPage(ctx) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
const { document, root, helpers, rule } = ctx;
|
|
73
|
+
const safeRoot = root || document;
|
|
74
|
+
|
|
75
|
+
const queryAllSmart =
|
|
76
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
77
|
+
const queryAll =
|
|
78
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
79
|
+
? helpers.queryAll
|
|
80
|
+
: (sel) => {
|
|
81
|
+
try {
|
|
82
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
83
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
84
|
+
: [];
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
71
88
|
};
|
|
72
89
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
: null;
|
|
90
|
+
const getEligibilityInfo =
|
|
91
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
76
92
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
: null;
|
|
93
|
+
const isAccTreeEligible =
|
|
94
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
80
95
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
: null;
|
|
96
|
+
const getFocusableInfo =
|
|
97
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
84
98
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
: null;
|
|
99
|
+
const getAriaNameInfo =
|
|
100
|
+
helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
102
|
+
function trim(v) {
|
|
103
|
+
try {
|
|
104
|
+
return (v == null ? '' : String(v)).trim();
|
|
105
|
+
} catch {
|
|
106
|
+
return '';
|
|
95
107
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Per SVG accessible-name conventions, only a <title> that is literally
|
|
111
|
+
// the first child element is used by assistive technologies as the
|
|
112
|
+
// SVG's accessible name; a <title> appearing later among the children
|
|
113
|
+
// is commonly ignored by AT even though it's still a valid DOM child.
|
|
114
|
+
function nonEmptyFirstChildTitleText(svg) {
|
|
115
|
+
try {
|
|
116
|
+
const first = svg.firstElementChild;
|
|
117
|
+
const tn = first ? (first.localName || first.tagName || '').toLowerCase() : '';
|
|
118
|
+
if (tn === 'title') {
|
|
119
|
+
const txt = trim(first.textContent);
|
|
120
|
+
if (txt) return txt;
|
|
121
|
+
}
|
|
122
|
+
} catch {}
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// <desc> counts when it is the first child, or the second child
|
|
127
|
+
// immediately following a <title> — the standard <title>+<desc> pairing
|
|
128
|
+
// (e.g. <svg><title>...</title><desc>...</desc>...</svg>). A <desc>
|
|
129
|
+
// appearing later than that is not reliably read by AT.
|
|
130
|
+
function nonEmptyDescText(svg) {
|
|
131
|
+
try {
|
|
132
|
+
const first = svg.firstElementChild;
|
|
133
|
+
const firstTag = first ? (first.localName || first.tagName || '').toLowerCase() : '';
|
|
134
|
+
if (firstTag === 'desc') {
|
|
135
|
+
const txt = trim(first.textContent);
|
|
136
|
+
if (txt) return txt;
|
|
137
|
+
} else if (firstTag === 'title' && first.nextElementSibling) {
|
|
138
|
+
const second = first.nextElementSibling;
|
|
139
|
+
const secondTag = (second.localName || second.tagName || '').toLowerCase();
|
|
140
|
+
if (secondTag === 'desc') {
|
|
141
|
+
const txt = trim(second.textContent);
|
|
142
|
+
if (txt) return txt;
|
|
110
143
|
}
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
144
|
+
}
|
|
145
|
+
} catch {}
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isFocusable(svg) {
|
|
150
|
+
if (getFocusableInfo) {
|
|
151
|
+
const fi = (() => {
|
|
119
152
|
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
|
-
}
|
|
153
|
+
return getFocusableInfo(svg, ctx);
|
|
133
154
|
} catch {
|
|
155
|
+
return null;
|
|
134
156
|
}
|
|
135
|
-
|
|
157
|
+
})();
|
|
158
|
+
return !!(fi && fi.focusable);
|
|
136
159
|
}
|
|
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
|
-
}
|
|
160
|
+
// deterministic fallback: tabindex presence/valid number
|
|
161
|
+
try {
|
|
162
|
+
const tabindex = svg && svg.getAttribute ? svg.getAttribute('tabindex') : null;
|
|
163
|
+
return (
|
|
164
|
+
tabindex != null &&
|
|
165
|
+
String(tabindex).trim() !== '' &&
|
|
166
|
+
!Number.isNaN(Number(String(tabindex).trim()))
|
|
167
|
+
);
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
156
170
|
}
|
|
171
|
+
}
|
|
157
172
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
})();
|
|
165
|
-
|
|
166
|
-
if (!svgs.length) {
|
|
167
|
-
return {ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: []};
|
|
173
|
+
const svgs = (() => {
|
|
174
|
+
try {
|
|
175
|
+
return Array.from((queryAllSmart ? queryAllSmart('svg') : queryAll('svg')) || []);
|
|
176
|
+
} catch {
|
|
177
|
+
return queryAll('svg');
|
|
168
178
|
}
|
|
179
|
+
})();
|
|
169
180
|
|
|
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
|
-
}
|
|
181
|
+
if (!svgs.length) {
|
|
182
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
183
|
+
}
|
|
187
184
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
return String(el.getAttribute('role') || '').trim().toLowerCase();
|
|
192
|
-
} catch {
|
|
193
|
-
return '';
|
|
194
|
-
}
|
|
195
|
-
})();
|
|
185
|
+
const occurrences = [];
|
|
186
|
+
let applicableCount = 0;
|
|
196
187
|
|
|
197
|
-
|
|
188
|
+
for (const el of svgs) {
|
|
189
|
+
if (!el || !el.getAttribute) continue;
|
|
198
190
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// Applicability step 3: intent signal gating (computed once per element)
|
|
204
|
-
let hasAriaNamingAttr = false;
|
|
191
|
+
// Applicability step 1: only acc-tree eligible nodes (with helper exceptions)
|
|
192
|
+
if (isAccTreeEligible) {
|
|
193
|
+
const elig = (() => {
|
|
205
194
|
try {
|
|
206
|
-
|
|
195
|
+
return isAccTreeEligible(el, ctx);
|
|
207
196
|
} catch {
|
|
197
|
+
return { eligible: true, reasons: [] };
|
|
208
198
|
}
|
|
199
|
+
})();
|
|
200
|
+
if (elig && elig.eligible === false) continue;
|
|
201
|
+
}
|
|
209
202
|
|
|
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
|
-
}
|
|
203
|
+
// Applicability step 2: role (presentation/none) exclusion only when not focusable
|
|
204
|
+
const role = (() => {
|
|
205
|
+
try {
|
|
206
|
+
return String(el.getAttribute('role') || '')
|
|
207
|
+
.trim()
|
|
208
|
+
.toLowerCase();
|
|
209
|
+
} catch {
|
|
210
|
+
return '';
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
255
213
|
|
|
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;
|
|
214
|
+
const focusable = isFocusable(el);
|
|
261
215
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
216
|
+
if (role === 'presentation' || role === 'none') {
|
|
217
|
+
if (!focusable) continue;
|
|
218
|
+
}
|
|
266
219
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
220
|
+
// Applicability step 3: intent signal gating (computed once per element)
|
|
221
|
+
let hasAriaNamingAttr = false;
|
|
222
|
+
try {
|
|
223
|
+
hasAriaNamingAttr =
|
|
224
|
+
el.getAttribute('aria-label') != null || el.getAttribute('aria-labelledby') != null;
|
|
225
|
+
} catch {}
|
|
226
|
+
|
|
227
|
+
const titleText = nonEmptyFirstChildTitleText(el);
|
|
228
|
+
const descText = titleText ? '' : nonEmptyDescText(el); // avoid second scan if title already passes
|
|
229
|
+
const hasTitleOrDesc = !!(titleText || descText);
|
|
230
|
+
|
|
231
|
+
const hasIntent =
|
|
232
|
+
role === 'img' ||
|
|
233
|
+
role === 'graphics-symbol' ||
|
|
234
|
+
role === 'graphics-document' ||
|
|
235
|
+
hasAriaNamingAttr ||
|
|
236
|
+
hasTitleOrDesc ||
|
|
237
|
+
focusable;
|
|
238
|
+
|
|
239
|
+
if (!hasIntent) continue;
|
|
240
|
+
|
|
241
|
+
applicableCount += 1;
|
|
242
|
+
|
|
243
|
+
// Expectation: non-empty title/desc OR ARIA name (but only resolve name if attrs exist)
|
|
244
|
+
let hasAriaName = false;
|
|
245
|
+
if (hasAriaNamingAttr) {
|
|
246
|
+
if (getAriaNameInfo) {
|
|
247
|
+
const info = (() => {
|
|
248
|
+
try {
|
|
249
|
+
return getAriaNameInfo(el, ctx);
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
})();
|
|
254
|
+
hasAriaName = !!(info && info.present && trim(info.value));
|
|
255
|
+
} else {
|
|
256
|
+
// minimal deterministic fallback
|
|
257
|
+
const ariaLabel = trim(
|
|
258
|
+
(() => {
|
|
259
|
+
try {
|
|
260
|
+
return el.getAttribute('aria-label');
|
|
261
|
+
} catch {
|
|
262
|
+
return '';
|
|
277
263
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
264
|
+
})()
|
|
265
|
+
);
|
|
266
|
+
const ariaLabelledby = trim(
|
|
267
|
+
(() => {
|
|
268
|
+
try {
|
|
269
|
+
return el.getAttribute('aria-labelledby');
|
|
270
|
+
} catch {
|
|
271
|
+
return '';
|
|
272
|
+
}
|
|
273
|
+
})()
|
|
274
|
+
);
|
|
275
|
+
hasAriaName = !!(ariaLabel || ariaLabelledby);
|
|
276
|
+
}
|
|
285
277
|
}
|
|
286
278
|
|
|
287
|
-
|
|
288
|
-
|
|
279
|
+
// Per SVG-AAM §7.1: <desc> contributes only to the accessible
|
|
280
|
+
// DESCRIPTION, never the accessible NAME — so descText does not
|
|
281
|
+
// count here even though it does count toward applicability above.
|
|
282
|
+
const ok = !!titleText || hasAriaName;
|
|
283
|
+
if (ok) continue;
|
|
284
|
+
|
|
285
|
+
let eligInfo = null;
|
|
286
|
+
if (getEligibilityInfo) {
|
|
287
|
+
try {
|
|
288
|
+
eligInfo = getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
289
|
+
} catch {
|
|
290
|
+
eligInfo = null;
|
|
291
|
+
}
|
|
289
292
|
}
|
|
290
293
|
|
|
291
|
-
|
|
292
|
-
|
|
294
|
+
const baseOccurrence = {
|
|
295
|
+
summary: 'Missing text alternative for <svg>.',
|
|
296
|
+
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.',
|
|
297
|
+
i18n: {
|
|
298
|
+
summaryKey: 'svg_textAltPresent_summary_fail',
|
|
299
|
+
hintKey: 'svg_textAltPresent_hint_fail',
|
|
300
|
+
params: { element: 'svg' }
|
|
301
|
+
},
|
|
302
|
+
data: {
|
|
303
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
308
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
309
|
+
} else {
|
|
310
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
293
311
|
}
|
|
294
|
-
|
|
295
|
-
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (applicableCount === 0) {
|
|
315
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!occurrences.length) {
|
|
319
|
+
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
ruleId: rule.ruleId,
|
|
324
|
+
outcome: 'fail',
|
|
325
|
+
severity: rule.defaultSeverity || 'minor',
|
|
326
|
+
occurrences
|
|
327
|
+
};
|
|
296
328
|
}
|
|
297
329
|
|
|
298
|
-
module.exports = {id, meta, runInPage};
|
|
330
|
+
module.exports = { id, meta, runInPage };
|