@surea11y/core 1.1.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -4
- package/LICENSE +373 -21
- package/README.md +70 -1
- package/bin/core.js +240 -11
- package/docs/API_STABILITY.md +61 -0
- package/docs/BASELINE.md +66 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +80 -1
- package/docs/ENGINE_OPTIONS.md +4 -0
- package/docs/INTEGRATION.md +19 -1
- package/docs/OUTPUT_SCHEMA.md +2 -2
- package/docs/REPORT.md +33 -0
- package/docs/RULE_AUTHORING.md +31 -0
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +50 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +125 -29
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +50 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +39 -9
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +117 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
- package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +26 -7
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -12
- package/src/checks/manual/empty-table-header-manual.js +21 -14
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -12
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
- package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +35 -21
- package/src/checks/manual/landmark-unique-manual.js +73 -45
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +25 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +40 -23
- package/src/checks/manual/page-title-patterns-manual.js +87 -51
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +275 -62
- package/src/checks/manual/scope-attr-valid-manual.js +11 -9
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +11 -9
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1128 -823
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4176 -3886
- package/src/core/dom-runner.js +720 -596
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +71 -35
- package/src/core.js +37939 -29121
- package/src/i18n/en.js +1194 -887
- package/src/i18n/fr.js +1136 -793
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +502 -0
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -11,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,18 +115,16 @@ function runInPage(ctx) {
|
|
|
112
115
|
return normalizeWs(el.getAttribute && el.getAttribute('title'));
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
} catch {
|
|
119
|
-
nodes = [];
|
|
120
|
-
}
|
|
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]');
|
|
121
121
|
|
|
122
122
|
const occurrences = [];
|
|
123
123
|
let applicableCount = 0;
|
|
124
124
|
const seen = new Set();
|
|
125
125
|
|
|
126
|
-
const isAccTreeEligible =
|
|
126
|
+
const isAccTreeEligible =
|
|
127
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
127
128
|
|
|
128
129
|
function isEligible(el) {
|
|
129
130
|
if (!isAccTreeEligible) return true;
|
|
@@ -148,9 +149,15 @@ function runInPage(ctx) {
|
|
|
148
149
|
if (name) continue;
|
|
149
150
|
|
|
150
151
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
151
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
152
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
152
153
|
const eligInfo = helpers.getEligibilityInfo
|
|
153
|
-
? (() => {
|
|
154
|
+
? (() => {
|
|
155
|
+
try {
|
|
156
|
+
return helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
})()
|
|
154
161
|
: null;
|
|
155
162
|
|
|
156
163
|
occurrences.push({
|
|
@@ -174,7 +181,12 @@ function runInPage(ctx) {
|
|
|
174
181
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
175
182
|
}
|
|
176
183
|
if (occurrences.length) {
|
|
177
|
-
return {
|
|
184
|
+
return {
|
|
185
|
+
ruleId: rule.ruleId,
|
|
186
|
+
outcome: 'cantTell',
|
|
187
|
+
severity: rule.defaultSeverity || 'minor',
|
|
188
|
+
occurrences
|
|
189
|
+
};
|
|
178
190
|
}
|
|
179
191
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
180
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) {
|
|
@@ -94,15 +97,13 @@ function runInPage(ctx) {
|
|
|
94
97
|
return '';
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
nodes = [];
|
|
105
|
-
}
|
|
100
|
+
// Matches a widely-used reference engine's own empty-table-header selector exactly: a <th> with
|
|
101
|
+
// no conflicting explicit role, plus any element carrying an explicit
|
|
102
|
+
// columnheader/rowheader role (native or not).
|
|
103
|
+
const selector = 'th:not([role]), [role="columnheader"], [role="rowheader"]';
|
|
104
|
+
const nodes = helpers.queryAllSmart
|
|
105
|
+
? helpers.queryAllSmart(selector)
|
|
106
|
+
: helpers.queryAll(selector);
|
|
106
107
|
|
|
107
108
|
const occurrences = [];
|
|
108
109
|
let applicableCount = 0;
|
|
@@ -114,14 +115,15 @@ function runInPage(ctx) {
|
|
|
114
115
|
if (getVisibleText(el)) continue;
|
|
115
116
|
|
|
116
117
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
117
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
118
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
118
119
|
|
|
119
120
|
const ariaName = getAriaOnlyName(el);
|
|
120
121
|
if (ariaName) {
|
|
121
122
|
occurrences.push({
|
|
122
123
|
selector: stableSelector,
|
|
123
124
|
html,
|
|
124
|
-
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.',
|
|
125
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.',
|
|
126
128
|
i18n: {
|
|
127
129
|
summaryKey: 'emptyTableHeader_summary_cantTell_ariaOnly',
|
|
@@ -155,7 +157,12 @@ function runInPage(ctx) {
|
|
|
155
157
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
156
158
|
}
|
|
157
159
|
if (occurrences.length) {
|
|
158
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
ruleId: rule.ruleId,
|
|
162
|
+
outcome: 'cantTell',
|
|
163
|
+
severity: rule.defaultSeverity || 'minor',
|
|
164
|
+
occurrences
|
|
165
|
+
};
|
|
159
166
|
}
|
|
160
167
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
161
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: [] };
|