@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
|
/**
|
|
@@ -11,194 +13,208 @@
|
|
|
11
13
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
const id =
|
|
16
|
+
const id = 'embed-text-alternative-quality';
|
|
15
17
|
|
|
16
18
|
const meta = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
19
|
+
title: '<embed> text alternative must be appropriate (manual review)',
|
|
20
|
+
description: 'Flags <embed> elements with a detected name for human review of appropriateness.',
|
|
21
|
+
i18n: {
|
|
22
|
+
titleKey: 'embed_textAltQuality_title',
|
|
23
|
+
descriptionKey: 'embed_textAltQuality_description'
|
|
24
|
+
},
|
|
25
|
+
helpUrl: null,
|
|
26
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'embed', 'manual', 'atomic'],
|
|
27
|
+
wcagSc: ['1.1.1'],
|
|
28
|
+
normativeMappings: [
|
|
29
|
+
{
|
|
30
|
+
standard: 'WCAG',
|
|
31
|
+
version: '2.2',
|
|
32
|
+
requirement: '1.1.1',
|
|
33
|
+
title: 'Non-text Content',
|
|
34
|
+
conformanceLevel: 'A'
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
defaultSeverity: 'minor',
|
|
38
|
+
category: 'perceivable',
|
|
39
|
+
type: 'manual',
|
|
40
|
+
defaultConfidence: 'medium',
|
|
41
|
+
coverage: {
|
|
42
|
+
facetsBySc: {
|
|
43
|
+
'1.1.1': ['text-alternative-quality']
|
|
37
44
|
}
|
|
45
|
+
}
|
|
38
46
|
};
|
|
39
47
|
|
|
40
48
|
function runInPage(ctx) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
const { document, root, helpers, rule } = ctx;
|
|
50
|
+
const safeRoot = root || document;
|
|
51
|
+
|
|
52
|
+
const trim = (v) => (v == null ? '' : String(v)).trim();
|
|
53
|
+
const getTextFromIdRefs =
|
|
54
|
+
helpers && typeof helpers.getTextFromIdRefs === 'function' ? helpers.getTextFromIdRefs : null;
|
|
55
|
+
|
|
56
|
+
const queryAllSmart =
|
|
57
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
58
|
+
const queryAll =
|
|
59
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
60
|
+
? helpers.queryAll
|
|
61
|
+
: (sel) => {
|
|
62
|
+
try {
|
|
63
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
64
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
65
|
+
: [];
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
57
69
|
};
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
? helpers.isAccTreeEligible
|
|
65
|
-
: null;
|
|
66
|
-
|
|
67
|
-
const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
|
|
68
|
-
? helpers.getFocusableInfo
|
|
69
|
-
: null;
|
|
70
|
-
|
|
71
|
-
function isRolePresentationExcluded(el) {
|
|
72
|
-
const role = (() => {
|
|
73
|
-
try {
|
|
74
|
-
return String(el.getAttribute('role') || '').trim().toLowerCase();
|
|
75
|
-
} catch {
|
|
76
|
-
return '';
|
|
77
|
-
}
|
|
78
|
-
})();
|
|
79
|
-
if (role !== 'presentation' && role !== 'none') return false;
|
|
80
|
-
|
|
81
|
-
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
82
|
-
let focusable = false;
|
|
83
|
-
if (getFocusableInfo) {
|
|
84
|
-
const fi = (() => {
|
|
85
|
-
try {
|
|
86
|
-
return getFocusableInfo(el, ctx);
|
|
87
|
-
} catch {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
})();
|
|
91
|
-
focusable = !!(fi && fi.focusable);
|
|
92
|
-
} else {
|
|
93
|
-
const tabindex = el.getAttribute('tabindex');
|
|
94
|
-
focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
|
|
95
|
-
}
|
|
96
|
-
return !focusable;
|
|
97
|
-
}
|
|
71
|
+
const getEligibilityInfo =
|
|
72
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
73
|
+
|
|
74
|
+
const isAccTreeEligible =
|
|
75
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
98
76
|
|
|
77
|
+
const getFocusableInfo =
|
|
78
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
99
79
|
|
|
100
|
-
|
|
80
|
+
function isRolePresentationExcluded(el) {
|
|
81
|
+
const role = (() => {
|
|
82
|
+
try {
|
|
83
|
+
return String(el.getAttribute('role') || '')
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase();
|
|
86
|
+
} catch {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
if (role !== 'presentation' && role !== 'none') return false;
|
|
91
|
+
|
|
92
|
+
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
93
|
+
let focusable;
|
|
94
|
+
if (getFocusableInfo) {
|
|
95
|
+
const fi = (() => {
|
|
101
96
|
try {
|
|
102
|
-
|
|
97
|
+
return getFocusableInfo(el, ctx);
|
|
103
98
|
} catch {
|
|
104
|
-
|
|
99
|
+
return null;
|
|
105
100
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
})();
|
|
102
|
+
focusable = !!(fi && fi.focusable);
|
|
103
|
+
} else {
|
|
104
|
+
const tabindex = el.getAttribute('tabindex');
|
|
105
|
+
focusable =
|
|
106
|
+
tabindex != null &&
|
|
107
|
+
String(tabindex).trim() !== '' &&
|
|
108
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
109
|
+
}
|
|
110
|
+
return !focusable;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const els = (() => {
|
|
114
|
+
try {
|
|
115
|
+
return Array.from((queryAllSmart ? queryAllSmart('embed') : queryAll('embed')) || []);
|
|
116
|
+
} catch {
|
|
117
|
+
return queryAll('embed');
|
|
110
118
|
}
|
|
119
|
+
})();
|
|
111
120
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (isAccTreeEligible) {
|
|
119
|
-
const elig = (() => {
|
|
120
|
-
try {
|
|
121
|
-
return isAccTreeEligible(el, ctx);
|
|
122
|
-
} catch {
|
|
123
|
-
return {eligible: true, reasons: []};
|
|
124
|
-
}
|
|
125
|
-
})();
|
|
126
|
-
if (elig && elig.eligible === false) continue;
|
|
127
|
-
}
|
|
121
|
+
if (!els.length) {
|
|
122
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const occurrences = [];
|
|
126
|
+
let applicableCount = 0;
|
|
128
127
|
|
|
129
|
-
|
|
128
|
+
for (const el of els) {
|
|
129
|
+
if (!el || !el.getAttribute) continue;
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
let title = '';
|
|
131
|
+
if (isAccTreeEligible) {
|
|
132
|
+
const elig = (() => {
|
|
134
133
|
try {
|
|
135
|
-
|
|
136
|
-
ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
|
|
137
|
-
title = trim(el.getAttribute('title'));
|
|
134
|
+
return isAccTreeEligible(el, ctx);
|
|
138
135
|
} catch {
|
|
139
|
-
|
|
140
|
-
ariaLabelledBy = '';
|
|
141
|
-
title = '';
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Only resolve IDREF text if aria-labelledby is present and aria-label is not already sufficient
|
|
145
|
-
let labelledByText = '';
|
|
146
|
-
let hasLabelledByMechanism = false;
|
|
147
|
-
|
|
148
|
-
if (!ariaLabel && ariaLabelledBy) {
|
|
149
|
-
hasLabelledByMechanism = true;
|
|
150
|
-
|
|
151
|
-
if (getTextFromIdRefs) {
|
|
152
|
-
try {
|
|
153
|
-
const t = getTextFromIdRefs(ariaLabelledBy, ctx);
|
|
154
|
-
labelledByText = trim(t && t.text);
|
|
155
|
-
// If it resolves to empty, still treat as a mechanism present for manual review.
|
|
156
|
-
} catch {
|
|
157
|
-
labelledByText = '';
|
|
158
|
-
}
|
|
159
|
-
}
|
|
136
|
+
return { eligible: true, reasons: [] };
|
|
160
137
|
}
|
|
138
|
+
})();
|
|
139
|
+
if (elig && elig.eligible === false) continue;
|
|
140
|
+
}
|
|
161
141
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
142
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
143
|
+
|
|
144
|
+
let ariaLabel;
|
|
145
|
+
let ariaLabelledBy;
|
|
146
|
+
let title;
|
|
147
|
+
try {
|
|
148
|
+
ariaLabel = trim(el.getAttribute('aria-label'));
|
|
149
|
+
ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
|
|
150
|
+
title = trim(el.getAttribute('title'));
|
|
151
|
+
} catch {
|
|
152
|
+
ariaLabel = '';
|
|
153
|
+
ariaLabelledBy = '';
|
|
154
|
+
title = '';
|
|
155
|
+
}
|
|
171
156
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, {targetSet: 'acc'}) : null;
|
|
175
|
-
|
|
176
|
-
const baseOccurrence = {
|
|
177
|
-
summary: 'Review text alternative for <embed> for accuracy and appropriateness.',
|
|
178
|
-
hint: 'Confirm the ARIA name or title accurately identifies the embedded content in context.',
|
|
179
|
-
i18n: {
|
|
180
|
-
summaryKey: 'embed_textAltQuality_summary_cantTell',
|
|
181
|
-
hintKey: 'embed_textAltQuality_hint_cantTell',
|
|
182
|
-
params: {element: (el.tagName || '').toLowerCase()}
|
|
183
|
-
},
|
|
184
|
-
data: {
|
|
185
|
-
visibilityFilter: eligInfo || {targetSet: 'acc', accEligible: null, reasons: []},
|
|
186
|
-
details
|
|
187
|
-
}
|
|
188
|
-
};
|
|
157
|
+
// Only resolve IDREF text if aria-labelledby is present and aria-label is not already sufficient
|
|
158
|
+
let labelledByText = '';
|
|
189
159
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
160
|
+
if (!ariaLabel && ariaLabelledBy && getTextFromIdRefs) {
|
|
161
|
+
try {
|
|
162
|
+
const t = getTextFromIdRefs(ariaLabelledBy, ctx);
|
|
163
|
+
labelledByText = trim(t && t.text);
|
|
164
|
+
} catch {
|
|
165
|
+
labelledByText = '';
|
|
166
|
+
}
|
|
195
167
|
}
|
|
196
168
|
|
|
197
|
-
|
|
198
|
-
|
|
169
|
+
// A broken/empty-resolving aria-labelledby (e.g. pointing at a
|
|
170
|
+
// nonexistent id) is not a "detected" text alternative to review the
|
|
171
|
+
// QUALITY of -- there's no text here at all, and this element's
|
|
172
|
+
// sibling automatic rule (embed-text-alternative-present) already
|
|
173
|
+
// reports it as a fail (no accessible name). Like object-/svg-/
|
|
174
|
+
// canvas-text-alternative-quality-manual, require aria-labelledby to
|
|
175
|
+
// actually resolve to non-empty text rather than merely being present
|
|
176
|
+
// as an attribute; a present-but-broken aria-labelledby has no text
|
|
177
|
+
// alternative to review.
|
|
178
|
+
const hasNameMechanism = !!(ariaLabel || title || labelledByText);
|
|
179
|
+
if (!hasNameMechanism) continue;
|
|
180
|
+
|
|
181
|
+
const details = {
|
|
182
|
+
ariaLabel: ariaLabel || null,
|
|
183
|
+
ariaLabelledBy: ariaLabelledBy || null,
|
|
184
|
+
ariaLabelledByText: labelledByText || null,
|
|
185
|
+
title: title || null
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
applicableCount += 1;
|
|
189
|
+
|
|
190
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
191
|
+
|
|
192
|
+
const baseOccurrence = {
|
|
193
|
+
summary: 'Review text alternative for <embed> for accuracy and appropriateness.',
|
|
194
|
+
hint: 'Confirm the ARIA name or title accurately identifies the embedded content in context.',
|
|
195
|
+
i18n: {
|
|
196
|
+
summaryKey: 'embed_textAltQuality_summary_cantTell',
|
|
197
|
+
hintKey: 'embed_textAltQuality_hint_cantTell',
|
|
198
|
+
params: { element: (el.tagName || '').toLowerCase() }
|
|
199
|
+
},
|
|
200
|
+
data: {
|
|
201
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
202
|
+
details
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
207
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
208
|
+
} else {
|
|
209
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
199
210
|
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (applicableCount === 0) {
|
|
214
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
215
|
+
}
|
|
200
216
|
|
|
201
|
-
|
|
217
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
202
218
|
}
|
|
203
219
|
|
|
204
|
-
module.exports = {id, meta, runInPage};
|
|
220
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @check empty-heading
|
|
5
7
|
* @atomic true
|
|
6
8
|
* @summary Heading elements must not be empty
|
|
7
|
-
* @standard Best Practices (
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
|
|
8
10
|
* @applicability
|
|
9
11
|
* Applies to elements with a heading role: native <h1>-<h6>, or any
|
|
10
12
|
* element with explicit role="heading" (unless overridden by another
|
|
@@ -19,30 +21,29 @@
|
|
|
19
21
|
* - Not WCAG-normative — authored as an advisory, cantTell-capped
|
|
20
22
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
21
23
|
* header comment for the shared rationale/precedent.
|
|
22
|
-
* - This is also the reconciliation point for the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* reach was still flagged.
|
|
31
|
-
* - 2026-07-21: the subtree-text walker's own hand-rolled descendant
|
|
32
|
-
* handling (aria-label/aria-labelledby only) never checked an `<img>`
|
|
33
|
-
* descendant's `alt` text — found via Party City's
|
|
34
|
-
* `<h1><a><div><img alt="..."></div></a></h1>` logo header, a false
|
|
35
|
-
* "empty heading" cantTell. Replaced with the shared, accname-aligned
|
|
24
|
+
* - This is also the reconciliation point for the ACT-rules
|
|
25
|
+
* "heading-name-present" requirement (see ROADMAP.md "Tier 5
|
|
26
|
+
* candidates"): already covered by this pre-existing rule under a
|
|
27
|
+
* different name, not a separate gap. `title` is accepted as a naming
|
|
28
|
+
* fallback, and hidden/aria-hidden/display:none headings are excluded
|
|
29
|
+
* (gated on `isAccTreeEligible`), so an empty heading no AT user could
|
|
30
|
+
* ever reach is not flagged.
|
|
31
|
+
* - Descendant name resolution uses the shared, accname-aligned
|
|
36
32
|
* `helpers.getContentNameInfo` (see dom-helpers.js) — the same "name
|
|
37
|
-
* from content" implementation the 19 `-name-present` rules already
|
|
38
|
-
* —
|
|
33
|
+
* from content" implementation the 19 `-name-present` rules already
|
|
34
|
+
* use — rather than a narrower hand-rolled walker, so an `<img alt="...">`
|
|
35
|
+
* descendant's alt text (e.g. a
|
|
36
|
+
* `<h1><a><div><img alt="..."></div></a></h1>` logo header) is correctly
|
|
37
|
+
* picked up as the heading's name instead of producing a false "empty
|
|
38
|
+
* heading" cantTell.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
const id = 'empty-heading';
|
|
42
42
|
|
|
43
43
|
const meta = {
|
|
44
44
|
title: 'Headings must not be empty',
|
|
45
|
-
description:
|
|
45
|
+
description:
|
|
46
|
+
'Checks that heading elements (<h1>-<h6> or role="heading") have a non-empty accessible name.',
|
|
46
47
|
i18n: {
|
|
47
48
|
titleKey: 'emptyHeading_title',
|
|
48
49
|
descriptionKey: 'emptyHeading_description'
|
|
@@ -62,7 +63,9 @@ function runInPage(ctx) {
|
|
|
62
63
|
const { document, helpers, rule } = ctx;
|
|
63
64
|
|
|
64
65
|
function normalizeWs(s) {
|
|
65
|
-
return String(s || '')
|
|
66
|
+
return String(s || '')
|
|
67
|
+
.replace(/\s+/g, ' ')
|
|
68
|
+
.trim();
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
function getExplicitRoleToken(el) {
|
|
@@ -112,13 +115,16 @@ function runInPage(ctx) {
|
|
|
112
115
|
return normalizeWs(el.getAttribute && el.getAttribute('title'));
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
const nodes = helpers.queryAllSmart
|
|
118
|
+
const nodes = helpers.queryAllSmart
|
|
119
|
+
? helpers.queryAllSmart('h1, h2, h3, h4, h5, h6, [role]')
|
|
120
|
+
: helpers.queryAll('h1, h2, h3, h4, h5, h6, [role]');
|
|
116
121
|
|
|
117
122
|
const occurrences = [];
|
|
118
123
|
let applicableCount = 0;
|
|
119
124
|
const seen = new Set();
|
|
120
125
|
|
|
121
|
-
const isAccTreeEligible =
|
|
126
|
+
const isAccTreeEligible =
|
|
127
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
122
128
|
|
|
123
129
|
function isEligible(el) {
|
|
124
130
|
if (!isAccTreeEligible) return true;
|
|
@@ -143,9 +149,15 @@ function runInPage(ctx) {
|
|
|
143
149
|
if (name) continue;
|
|
144
150
|
|
|
145
151
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
146
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
152
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
147
153
|
const eligInfo = helpers.getEligibilityInfo
|
|
148
|
-
? (() => {
|
|
154
|
+
? (() => {
|
|
155
|
+
try {
|
|
156
|
+
return helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
})()
|
|
149
161
|
: null;
|
|
150
162
|
|
|
151
163
|
occurrences.push({
|
|
@@ -169,7 +181,12 @@ function runInPage(ctx) {
|
|
|
169
181
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
170
182
|
}
|
|
171
183
|
if (occurrences.length) {
|
|
172
|
-
return {
|
|
184
|
+
return {
|
|
185
|
+
ruleId: rule.ruleId,
|
|
186
|
+
outcome: 'cantTell',
|
|
187
|
+
severity: rule.defaultSeverity || 'minor',
|
|
188
|
+
occurrences
|
|
189
|
+
};
|
|
173
190
|
}
|
|
174
191
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
175
192
|
}
|