@surea11y/core 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -7
- package/LICENSE +373 -21
- package/README.md +67 -1
- package/bin/core.js +144 -19
- package/docs/API_STABILITY.md +1 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +54 -1
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/INTEGRATION.md +18 -0
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +42 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +55 -16
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +42 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +30 -8
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +109 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
- package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +17 -6
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -7
- package/src/checks/manual/empty-table-header-manual.js +17 -6
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -7
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
- package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +26 -20
- package/src/checks/manual/landmark-unique-manual.js +41 -15
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +16 -5
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +31 -22
- package/src/checks/manual/page-title-patterns-manual.js +78 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +241 -48
- package/src/checks/manual/scope-attr-valid-manual.js +10 -3
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +10 -3
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1127 -886
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4175 -3917
- package/src/core/dom-runner.js +720 -604
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +58 -41
- package/src/core.js +36552 -29111
- package/src/i18n/en.js +1194 -889
- package/src/i18n/fr.js +1136 -795
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +63 -43
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -11,194 +11,205 @@
|
|
|
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 = 'embed-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: '<embed> text alternative must be appropriate (manual review)',
|
|
18
|
+
description: 'Flags <embed> elements with a detected name for human review of appropriateness.',
|
|
19
|
+
i18n: {
|
|
20
|
+
titleKey: 'embed_textAltQuality_title',
|
|
21
|
+
descriptionKey: 'embed_textAltQuality_description'
|
|
22
|
+
},
|
|
23
|
+
helpUrl: null,
|
|
24
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'embed', 'manual', 'atomic'],
|
|
25
|
+
wcagSc: ['1.1.1'],
|
|
26
|
+
normativeMappings: [
|
|
27
|
+
{
|
|
28
|
+
standard: 'WCAG',
|
|
29
|
+
version: '2.2',
|
|
30
|
+
requirement: '1.1.1',
|
|
31
|
+
title: 'Non-text Content',
|
|
32
|
+
conformanceLevel: 'A'
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
defaultSeverity: 'minor',
|
|
36
|
+
category: 'perceivable',
|
|
37
|
+
type: 'manual',
|
|
38
|
+
defaultConfidence: 'medium',
|
|
39
|
+
coverage: {
|
|
40
|
+
facetsBySc: {
|
|
41
|
+
'1.1.1': ['text-alternative-quality']
|
|
37
42
|
}
|
|
43
|
+
}
|
|
38
44
|
};
|
|
39
45
|
|
|
40
46
|
function runInPage(ctx) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
const { document, root, helpers, rule } = ctx;
|
|
48
|
+
const safeRoot = root || document;
|
|
49
|
+
|
|
50
|
+
const trim = (v) => (v == null ? '' : String(v)).trim();
|
|
51
|
+
const getTextFromIdRefs =
|
|
52
|
+
helpers && typeof helpers.getTextFromIdRefs === 'function' ? helpers.getTextFromIdRefs : null;
|
|
53
|
+
|
|
54
|
+
const queryAllSmart =
|
|
55
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
56
|
+
const queryAll =
|
|
57
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
58
|
+
? helpers.queryAll
|
|
59
|
+
: (sel) => {
|
|
60
|
+
try {
|
|
61
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
62
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
63
|
+
: [];
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
57
67
|
};
|
|
58
68
|
|
|
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
|
-
}
|
|
69
|
+
const getEligibilityInfo =
|
|
70
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
71
|
+
|
|
72
|
+
const isAccTreeEligible =
|
|
73
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
98
74
|
|
|
75
|
+
const getFocusableInfo =
|
|
76
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
99
77
|
|
|
100
|
-
|
|
78
|
+
function isRolePresentationExcluded(el) {
|
|
79
|
+
const role = (() => {
|
|
80
|
+
try {
|
|
81
|
+
return String(el.getAttribute('role') || '')
|
|
82
|
+
.trim()
|
|
83
|
+
.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;
|
|
92
|
+
if (getFocusableInfo) {
|
|
93
|
+
const fi = (() => {
|
|
101
94
|
try {
|
|
102
|
-
|
|
95
|
+
return getFocusableInfo(el, ctx);
|
|
103
96
|
} catch {
|
|
104
|
-
|
|
97
|
+
return null;
|
|
105
98
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
})();
|
|
100
|
+
focusable = !!(fi && fi.focusable);
|
|
101
|
+
} else {
|
|
102
|
+
const tabindex = el.getAttribute('tabindex');
|
|
103
|
+
focusable =
|
|
104
|
+
tabindex != null &&
|
|
105
|
+
String(tabindex).trim() !== '' &&
|
|
106
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
110
107
|
}
|
|
108
|
+
return !focusable;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const els = (() => {
|
|
112
|
+
try {
|
|
113
|
+
return Array.from((queryAllSmart ? queryAllSmart('embed') : queryAll('embed')) || []);
|
|
114
|
+
} catch {
|
|
115
|
+
return queryAll('embed');
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
111
118
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for (const el of els) {
|
|
116
|
-
if (!el || !el.getAttribute) continue;
|
|
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
|
-
}
|
|
119
|
+
if (!els.length) {
|
|
120
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
121
|
+
}
|
|
128
122
|
|
|
129
|
-
|
|
123
|
+
const occurrences = [];
|
|
124
|
+
let applicableCount = 0;
|
|
130
125
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
126
|
+
for (const el of els) {
|
|
127
|
+
if (!el || !el.getAttribute) continue;
|
|
128
|
+
|
|
129
|
+
if (isAccTreeEligible) {
|
|
130
|
+
const elig = (() => {
|
|
134
131
|
try {
|
|
135
|
-
|
|
136
|
-
ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
|
|
137
|
-
title = trim(el.getAttribute('title'));
|
|
132
|
+
return isAccTreeEligible(el, ctx);
|
|
138
133
|
} 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
|
-
}
|
|
134
|
+
return { eligible: true, reasons: [] };
|
|
160
135
|
}
|
|
136
|
+
})();
|
|
137
|
+
if (elig && elig.eligible === false) continue;
|
|
138
|
+
}
|
|
161
139
|
|
|
162
|
-
|
|
163
|
-
|
|
140
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
141
|
+
|
|
142
|
+
let ariaLabel;
|
|
143
|
+
let ariaLabelledBy;
|
|
144
|
+
let title;
|
|
145
|
+
try {
|
|
146
|
+
ariaLabel = trim(el.getAttribute('aria-label'));
|
|
147
|
+
ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
|
|
148
|
+
title = trim(el.getAttribute('title'));
|
|
149
|
+
} catch {
|
|
150
|
+
ariaLabel = '';
|
|
151
|
+
ariaLabelledBy = '';
|
|
152
|
+
title = '';
|
|
153
|
+
}
|
|
164
154
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
ariaLabelledByText: labelledByText || null,
|
|
169
|
-
title: title || null
|
|
170
|
-
};
|
|
155
|
+
// Only resolve IDREF text if aria-labelledby is present and aria-label is not already sufficient
|
|
156
|
+
let labelledByText = '';
|
|
157
|
+
let hasLabelledByMechanism = false;
|
|
171
158
|
|
|
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
|
-
};
|
|
159
|
+
if (!ariaLabel && ariaLabelledBy) {
|
|
160
|
+
hasLabelledByMechanism = true;
|
|
189
161
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
162
|
+
if (getTextFromIdRefs) {
|
|
163
|
+
try {
|
|
164
|
+
const t = getTextFromIdRefs(ariaLabelledBy, ctx);
|
|
165
|
+
labelledByText = trim(t && t.text);
|
|
166
|
+
// If it resolves to empty, still treat as a mechanism present for manual review.
|
|
167
|
+
} catch {
|
|
168
|
+
labelledByText = '';
|
|
194
169
|
}
|
|
170
|
+
}
|
|
195
171
|
}
|
|
196
172
|
|
|
197
|
-
|
|
198
|
-
|
|
173
|
+
const hasNameMechanism = !!(ariaLabel || title || hasLabelledByMechanism);
|
|
174
|
+
if (!hasNameMechanism) continue;
|
|
175
|
+
|
|
176
|
+
const details = {
|
|
177
|
+
ariaLabel: ariaLabel || null,
|
|
178
|
+
ariaLabelledBy: ariaLabelledBy || null,
|
|
179
|
+
ariaLabelledByText: labelledByText || null,
|
|
180
|
+
title: title || null
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
applicableCount += 1;
|
|
184
|
+
|
|
185
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
186
|
+
|
|
187
|
+
const baseOccurrence = {
|
|
188
|
+
summary: 'Review text alternative for <embed> for accuracy and appropriateness.',
|
|
189
|
+
hint: 'Confirm the ARIA name or title accurately identifies the embedded content in context.',
|
|
190
|
+
i18n: {
|
|
191
|
+
summaryKey: 'embed_textAltQuality_summary_cantTell',
|
|
192
|
+
hintKey: 'embed_textAltQuality_hint_cantTell',
|
|
193
|
+
params: { element: (el.tagName || '').toLowerCase() }
|
|
194
|
+
},
|
|
195
|
+
data: {
|
|
196
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
197
|
+
details
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
202
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
203
|
+
} else {
|
|
204
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
199
205
|
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (applicableCount === 0) {
|
|
209
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
210
|
+
}
|
|
200
211
|
|
|
201
|
-
|
|
212
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
202
213
|
}
|
|
203
214
|
|
|
204
|
-
module.exports = {id, meta, runInPage};
|
|
215
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -42,7 +42,8 @@ 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
|
}
|
|
@@ -46,7 +46,8 @@ const id = 'empty-table-header';
|
|
|
46
46
|
|
|
47
47
|
const meta = {
|
|
48
48
|
title: 'Table header cells must not be empty',
|
|
49
|
-
description:
|
|
49
|
+
description:
|
|
50
|
+
'Checks that table header cells (<th>, or any element with role="columnheader"/"rowheader") have visible text content — a header named only via aria-label/aria-labelledby is also flagged, since real screen-reader/browser support for that is inconsistent.',
|
|
50
51
|
i18n: {
|
|
51
52
|
titleKey: 'emptyTableHeader_title',
|
|
52
53
|
descriptionKey: 'emptyTableHeader_description'
|
|
@@ -66,7 +67,9 @@ function runInPage(ctx) {
|
|
|
66
67
|
const { document, helpers, rule } = ctx;
|
|
67
68
|
|
|
68
69
|
function normalizeWs(s) {
|
|
69
|
-
return String(s || '')
|
|
70
|
+
return String(s || '')
|
|
71
|
+
.replace(/\s+/g, ' ')
|
|
72
|
+
.trim();
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
function getVisibleText(el) {
|
|
@@ -98,7 +101,9 @@ function runInPage(ctx) {
|
|
|
98
101
|
// no conflicting explicit role, plus any element carrying an explicit
|
|
99
102
|
// columnheader/rowheader role (native or not).
|
|
100
103
|
const selector = 'th:not([role]), [role="columnheader"], [role="rowheader"]';
|
|
101
|
-
const nodes = helpers.queryAllSmart
|
|
104
|
+
const nodes = helpers.queryAllSmart
|
|
105
|
+
? helpers.queryAllSmart(selector)
|
|
106
|
+
: helpers.queryAll(selector);
|
|
102
107
|
|
|
103
108
|
const occurrences = [];
|
|
104
109
|
let applicableCount = 0;
|
|
@@ -110,14 +115,15 @@ function runInPage(ctx) {
|
|
|
110
115
|
if (getVisibleText(el)) continue;
|
|
111
116
|
|
|
112
117
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
113
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
118
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
114
119
|
|
|
115
120
|
const ariaName = getAriaOnlyName(el);
|
|
116
121
|
if (ariaName) {
|
|
117
122
|
occurrences.push({
|
|
118
123
|
selector: stableSelector,
|
|
119
124
|
html,
|
|
120
|
-
summary:
|
|
125
|
+
summary:
|
|
126
|
+
'This table header cell has no visible text — its only accessible name comes from aria-label/aria-labelledby, which real screen-reader/browser combinations (e.g. NVDA+Firefox, iOS VoiceOver+Safari) are known to ignore on <th> elements.',
|
|
121
127
|
hint: 'Add visible text content to this header cell (in addition to, or instead of, aria-label/aria-labelledby) — visible text is the only naming mechanism confirmed to work across tested screen readers.',
|
|
122
128
|
i18n: {
|
|
123
129
|
summaryKey: 'emptyTableHeader_summary_cantTell_ariaOnly',
|
|
@@ -151,7 +157,12 @@ function runInPage(ctx) {
|
|
|
151
157
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
152
158
|
}
|
|
153
159
|
if (occurrences.length) {
|
|
154
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
ruleId: rule.ruleId,
|
|
162
|
+
outcome: 'cantTell',
|
|
163
|
+
severity: rule.defaultSeverity || 'minor',
|
|
164
|
+
occurrences
|
|
165
|
+
};
|
|
155
166
|
}
|
|
156
167
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
157
168
|
}
|
|
@@ -31,7 +31,8 @@ const id = 'focus-order-semantics';
|
|
|
31
31
|
|
|
32
32
|
const meta = {
|
|
33
33
|
title: 'Elements added to the tab order should have interactive semantics',
|
|
34
|
-
description:
|
|
34
|
+
description:
|
|
35
|
+
'Flags elements with tabindex >= 0 whose explicit role is a non-interactive structural/document role (e.g. heading, list, region, presentation), for manual review.',
|
|
35
36
|
i18n: {
|
|
36
37
|
titleKey: 'focusOrderSemantics_title',
|
|
37
38
|
descriptionKey: 'focusOrderSemantics_description'
|
|
@@ -48,17 +49,46 @@ const meta = {
|
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
function runInPage(ctx) {
|
|
51
|
-
const {
|
|
52
|
+
const { helpers, rule } = ctx;
|
|
52
53
|
|
|
53
54
|
const NON_INTERACTIVE_ROLES = new Set([
|
|
54
|
-
'generic',
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
55
|
+
'generic',
|
|
56
|
+
'group',
|
|
57
|
+
'text',
|
|
58
|
+
'presentation',
|
|
59
|
+
'none',
|
|
60
|
+
'img',
|
|
61
|
+
'heading',
|
|
62
|
+
'region',
|
|
63
|
+
'article',
|
|
64
|
+
'banner',
|
|
65
|
+
'contentinfo',
|
|
66
|
+
'main',
|
|
67
|
+
'navigation',
|
|
68
|
+
'complementary',
|
|
69
|
+
'document',
|
|
70
|
+
'note',
|
|
71
|
+
'list',
|
|
72
|
+
'listitem',
|
|
73
|
+
'table',
|
|
74
|
+
'row',
|
|
75
|
+
'cell',
|
|
76
|
+
'columnheader',
|
|
77
|
+
'rowheader',
|
|
78
|
+
'figure',
|
|
79
|
+
'term',
|
|
80
|
+
'definition',
|
|
81
|
+
'paragraph',
|
|
82
|
+
'caption',
|
|
83
|
+
'status',
|
|
84
|
+
'alert',
|
|
85
|
+
'log',
|
|
86
|
+
'tooltip'
|
|
59
87
|
]);
|
|
60
88
|
|
|
61
|
-
const nodes = helpers.queryAllSmart
|
|
89
|
+
const nodes = helpers.queryAllSmart
|
|
90
|
+
? helpers.queryAllSmart('[tabindex][role]')
|
|
91
|
+
: helpers.queryAll('[tabindex][role]');
|
|
62
92
|
|
|
63
93
|
const occurrences = [];
|
|
64
94
|
let applicableCount = 0;
|
|
@@ -78,7 +108,7 @@ function runInPage(ctx) {
|
|
|
78
108
|
if (!NON_INTERACTIVE_ROLES.has(role)) continue;
|
|
79
109
|
|
|
80
110
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
81
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
111
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
82
112
|
|
|
83
113
|
const baseOccurrence = {
|
|
84
114
|
selector: stableSelector,
|
|
@@ -107,7 +137,12 @@ function runInPage(ctx) {
|
|
|
107
137
|
}
|
|
108
138
|
|
|
109
139
|
if (occurrences.length) {
|
|
110
|
-
return {
|
|
140
|
+
return {
|
|
141
|
+
ruleId: rule.ruleId,
|
|
142
|
+
outcome: 'cantTell',
|
|
143
|
+
severity: rule.defaultSeverity || 'minor',
|
|
144
|
+
occurrences
|
|
145
|
+
};
|
|
111
146
|
}
|
|
112
147
|
|
|
113
148
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|