@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,184 +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 = 'object-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: '<object> text alternative must be appropriate (manual review)',
|
|
18
|
+
description:
|
|
19
|
+
'Flags <object> elements with detected fallback or name for human review of equivalence and appropriateness.',
|
|
20
|
+
i18n: {
|
|
21
|
+
titleKey: 'object_textAltQuality_title',
|
|
22
|
+
descriptionKey: 'object_textAltQuality_description'
|
|
23
|
+
},
|
|
24
|
+
helpUrl: null,
|
|
25
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'object', 'manual', 'atomic'],
|
|
26
|
+
wcagSc: ['1.1.1'],
|
|
27
|
+
normativeMappings: [
|
|
28
|
+
{
|
|
29
|
+
standard: 'WCAG',
|
|
30
|
+
version: '2.2',
|
|
31
|
+
requirement: '1.1.1',
|
|
32
|
+
title: 'Non-text Content',
|
|
33
|
+
conformanceLevel: 'A'
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
defaultSeverity: 'minor',
|
|
37
|
+
category: 'perceivable',
|
|
38
|
+
type: 'manual',
|
|
39
|
+
defaultConfidence: 'medium',
|
|
40
|
+
coverage: {
|
|
41
|
+
facetsBySc: {
|
|
42
|
+
'1.1.1': ['text-alternative-quality']
|
|
37
43
|
}
|
|
44
|
+
}
|
|
38
45
|
};
|
|
39
46
|
|
|
40
47
|
function runInPage(ctx) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
const { document, root, helpers, rule } = ctx;
|
|
49
|
+
const safeRoot = root || document;
|
|
50
|
+
|
|
51
|
+
const queryAllSmart =
|
|
52
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
53
|
+
const queryAll =
|
|
54
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
55
|
+
? helpers.queryAll
|
|
56
|
+
: (sel) => {
|
|
57
|
+
try {
|
|
58
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
59
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
60
|
+
: [];
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
54
64
|
};
|
|
55
65
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
? helpers.isAccTreeEligible
|
|
62
|
-
: null;
|
|
63
|
-
|
|
64
|
-
const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
|
|
65
|
-
? helpers.getFocusableInfo
|
|
66
|
-
: null;
|
|
67
|
-
|
|
68
|
-
function isRolePresentationExcluded(el) {
|
|
69
|
-
const role = (() => {
|
|
70
|
-
try {
|
|
71
|
-
return String(el.getAttribute('role') || '').trim().toLowerCase();
|
|
72
|
-
} catch {
|
|
73
|
-
return '';
|
|
74
|
-
}
|
|
75
|
-
})();
|
|
76
|
-
if (role !== 'presentation' && role !== 'none') return false;
|
|
77
|
-
|
|
78
|
-
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
79
|
-
let focusable = false;
|
|
80
|
-
if (getFocusableInfo) {
|
|
81
|
-
const fi = (() => {
|
|
82
|
-
try {
|
|
83
|
-
return getFocusableInfo(el, ctx);
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
})();
|
|
88
|
-
focusable = !!(fi && fi.focusable);
|
|
89
|
-
} else {
|
|
90
|
-
const tabindex = el.getAttribute('tabindex');
|
|
91
|
-
focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
|
|
92
|
-
}
|
|
93
|
-
return !focusable;
|
|
94
|
-
}
|
|
66
|
+
const getEligibilityInfo =
|
|
67
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
68
|
+
|
|
69
|
+
const isAccTreeEligible =
|
|
70
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
95
71
|
|
|
72
|
+
const getFocusableInfo =
|
|
73
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
96
74
|
|
|
97
|
-
|
|
75
|
+
function isRolePresentationExcluded(el) {
|
|
76
|
+
const role = (() => {
|
|
77
|
+
try {
|
|
78
|
+
return String(el.getAttribute('role') || '')
|
|
79
|
+
.trim()
|
|
80
|
+
.toLowerCase();
|
|
81
|
+
} catch {
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
if (role !== 'presentation' && role !== 'none') return false;
|
|
86
|
+
|
|
87
|
+
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
88
|
+
let focusable;
|
|
89
|
+
if (getFocusableInfo) {
|
|
90
|
+
const fi = (() => {
|
|
98
91
|
try {
|
|
99
|
-
|
|
92
|
+
return getFocusableInfo(el, ctx);
|
|
100
93
|
} catch {
|
|
101
|
-
|
|
94
|
+
return null;
|
|
102
95
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
})();
|
|
97
|
+
focusable = !!(fi && fi.focusable);
|
|
98
|
+
} else {
|
|
99
|
+
const tabindex = el.getAttribute('tabindex');
|
|
100
|
+
focusable =
|
|
101
|
+
tabindex != null &&
|
|
102
|
+
String(tabindex).trim() !== '' &&
|
|
103
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
107
104
|
}
|
|
105
|
+
return !focusable;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const els = (() => {
|
|
109
|
+
try {
|
|
110
|
+
return Array.from((queryAllSmart ? queryAllSmart('object') : queryAll('object')) || []);
|
|
111
|
+
} catch {
|
|
112
|
+
return queryAll('object');
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
108
115
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
for (const el of els) {
|
|
113
|
-
if (!el || !el.getAttribute) continue;
|
|
114
|
-
|
|
115
|
-
if (isAccTreeEligible) {
|
|
116
|
-
const elig = (() => {
|
|
117
|
-
try {
|
|
118
|
-
return isAccTreeEligible(el, ctx);
|
|
119
|
-
} catch {
|
|
120
|
-
return {eligible: true, reasons: []};
|
|
121
|
-
}
|
|
122
|
-
})();
|
|
123
|
-
if (elig && elig.eligible === false) continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (isRolePresentationExcluded(el)) continue;
|
|
116
|
+
if (!els.length) {
|
|
117
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
118
|
+
}
|
|
127
119
|
|
|
128
|
-
|
|
120
|
+
const occurrences = [];
|
|
121
|
+
let applicableCount = 0;
|
|
129
122
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
let ariaLabelledBy = '';
|
|
133
|
-
let title = '';
|
|
134
|
-
let labelledByText = '';
|
|
123
|
+
for (const el of els) {
|
|
124
|
+
if (!el || !el.getAttribute) continue;
|
|
135
125
|
|
|
126
|
+
if (isAccTreeEligible) {
|
|
127
|
+
const elig = (() => {
|
|
136
128
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
title = trim(el.getAttribute('title'));
|
|
141
|
-
} catch {}
|
|
142
|
-
|
|
143
|
-
// Only resolve idrefs if needed/present
|
|
144
|
-
if (!ariaLabel && ariaLabelledBy && helpers && typeof helpers.getTextFromIdRefs === 'function') {
|
|
145
|
-
try {
|
|
146
|
-
const t = helpers.getTextFromIdRefs(ariaLabelledBy, ctx);
|
|
147
|
-
labelledByText = trim(t && t.text);
|
|
148
|
-
} catch {}
|
|
129
|
+
return isAccTreeEligible(el, ctx);
|
|
130
|
+
} catch {
|
|
131
|
+
return { eligible: true, reasons: [] };
|
|
149
132
|
}
|
|
133
|
+
})();
|
|
134
|
+
if (elig && elig.eligible === false) continue;
|
|
135
|
+
}
|
|
150
136
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
181
|
-
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
182
|
-
} else {
|
|
183
|
-
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
184
|
-
}
|
|
137
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
138
|
+
|
|
139
|
+
const trim = (v) => (v == null ? '' : String(v)).trim();
|
|
140
|
+
|
|
141
|
+
let fallbackText = '';
|
|
142
|
+
let ariaLabel = '';
|
|
143
|
+
let ariaLabelledBy = '';
|
|
144
|
+
let title = '';
|
|
145
|
+
let labelledByText = '';
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
fallbackText = trim(el.textContent || '');
|
|
149
|
+
ariaLabel = trim(el.getAttribute('aria-label'));
|
|
150
|
+
ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
|
|
151
|
+
title = trim(el.getAttribute('title'));
|
|
152
|
+
} catch {}
|
|
153
|
+
|
|
154
|
+
// Only resolve idrefs if needed/present
|
|
155
|
+
if (
|
|
156
|
+
!ariaLabel &&
|
|
157
|
+
ariaLabelledBy &&
|
|
158
|
+
helpers &&
|
|
159
|
+
typeof helpers.getTextFromIdRefs === 'function'
|
|
160
|
+
) {
|
|
161
|
+
try {
|
|
162
|
+
const t = helpers.getTextFromIdRefs(ariaLabelledBy, ctx);
|
|
163
|
+
labelledByText = trim(t && t.text);
|
|
164
|
+
} catch {}
|
|
185
165
|
}
|
|
186
166
|
|
|
187
|
-
|
|
188
|
-
|
|
167
|
+
const details = {
|
|
168
|
+
fallbackText: fallbackText || null,
|
|
169
|
+
ariaLabel: ariaLabel || null,
|
|
170
|
+
ariaLabelledBy: ariaLabelledBy || null,
|
|
171
|
+
ariaLabelledByText: labelledByText || null,
|
|
172
|
+
title: title || null
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const hasMechanism = !!(
|
|
176
|
+
details.fallbackText ||
|
|
177
|
+
details.ariaLabel ||
|
|
178
|
+
details.ariaLabelledByText ||
|
|
179
|
+
details.title
|
|
180
|
+
);
|
|
181
|
+
if (!hasMechanism) continue;
|
|
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 <object> for equivalence and appropriateness.',
|
|
189
|
+
hint: 'Confirm the fallback content or ARIA name provides an equivalent alternative for the embedded content.',
|
|
190
|
+
i18n: {
|
|
191
|
+
summaryKey: 'object_textAltQuality_summary_cantTell',
|
|
192
|
+
hintKey: 'object_textAltQuality_hint_cantTell',
|
|
193
|
+
params: { element: 'object' }
|
|
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 });
|
|
189
205
|
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (applicableCount === 0) {
|
|
209
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
210
|
+
}
|
|
190
211
|
|
|
191
|
-
|
|
212
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
192
213
|
}
|
|
193
214
|
|
|
194
|
-
module.exports = {id, meta, runInPage};
|
|
215
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -34,7 +34,8 @@ const id = 'p-as-heading';
|
|
|
34
34
|
|
|
35
35
|
const meta = {
|
|
36
36
|
title: 'A <p> styled to look like a heading should probably be a real heading',
|
|
37
|
-
description:
|
|
37
|
+
description:
|
|
38
|
+
'Flags short <p> elements whose entire text is bold and rendered at >=18px, for manual review of whether a real heading element should be used instead.',
|
|
38
39
|
i18n: {
|
|
39
40
|
titleKey: 'pAsHeading_title',
|
|
40
41
|
descriptionKey: 'pAsHeading_description'
|
|
@@ -43,7 +44,13 @@ const meta = {
|
|
|
43
44
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'manual'],
|
|
44
45
|
wcagSc: ['1.3.1'],
|
|
45
46
|
normativeMappings: [
|
|
46
|
-
{
|
|
47
|
+
{
|
|
48
|
+
standard: 'WCAG',
|
|
49
|
+
version: '2.2',
|
|
50
|
+
requirement: '1.3.1',
|
|
51
|
+
title: 'Info and Relationships',
|
|
52
|
+
conformanceLevel: 'A'
|
|
53
|
+
}
|
|
47
54
|
],
|
|
48
55
|
defaultSeverity: 'minor',
|
|
49
56
|
category: 'perceivable',
|
|
@@ -53,12 +60,14 @@ const meta = {
|
|
|
53
60
|
};
|
|
54
61
|
|
|
55
62
|
function runInPage(ctx) {
|
|
56
|
-
const {
|
|
63
|
+
const { helpers, rule } = ctx;
|
|
57
64
|
|
|
58
65
|
const MAX_HEADING_LIKE_CHARS = 120;
|
|
59
66
|
const MIN_FONT_SIZE_PX = 18;
|
|
60
67
|
|
|
61
|
-
function trim(v) {
|
|
68
|
+
function trim(v) {
|
|
69
|
+
return (v == null ? '' : String(v)).trim();
|
|
70
|
+
}
|
|
62
71
|
|
|
63
72
|
function safeComputedStyle(el) {
|
|
64
73
|
try {
|
|
@@ -67,7 +76,8 @@ function runInPage(ctx) {
|
|
|
67
76
|
const cs = helpers.computedStyle(el);
|
|
68
77
|
if (cs) return cs;
|
|
69
78
|
}
|
|
70
|
-
const view =
|
|
79
|
+
const view =
|
|
80
|
+
el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : null;
|
|
71
81
|
if (view && typeof view.getComputedStyle === 'function') return view.getComputedStyle(el);
|
|
72
82
|
} catch {}
|
|
73
83
|
return null;
|
|
@@ -124,7 +134,7 @@ function runInPage(ctx) {
|
|
|
124
134
|
if (fontSizePx < MIN_FONT_SIZE_PX) continue;
|
|
125
135
|
|
|
126
136
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
127
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
137
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
128
138
|
|
|
129
139
|
const baseOccurrence = {
|
|
130
140
|
selector: stableSelector,
|
|
@@ -153,7 +163,12 @@ function runInPage(ctx) {
|
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
if (occurrences.length) {
|
|
156
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
ruleId: rule.ruleId,
|
|
168
|
+
outcome: 'cantTell',
|
|
169
|
+
severity: rule.defaultSeverity || 'minor',
|
|
170
|
+
occurrences
|
|
171
|
+
};
|
|
157
172
|
}
|
|
158
173
|
|
|
159
174
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* page (CDC's flu page, 2026-07-30): its only `<h1>` sits inside a
|
|
26
26
|
* `display:none` ancestor — genuinely unreachable by sighted and screen
|
|
27
27
|
* reader users alike — and a raw `document.querySelectorAll` credited it
|
|
28
|
-
* anyway, reporting `notApplicable
|
|
28
|
+
* anyway, incorrectly reporting `notApplicable`. This
|
|
29
29
|
* does NOT regress purely-visually-clipped-but-AT-exposed headings (e.g.
|
|
30
30
|
* eBay's homepage `<h1>` hidden via clip-path/off-screen positioning,
|
|
31
31
|
* `visibility:visible`, no `aria-hidden`) — `isAccTreeEligible` only
|
|
@@ -37,7 +37,8 @@ const id = 'page-has-heading-one';
|
|
|
37
37
|
|
|
38
38
|
const meta = {
|
|
39
39
|
title: 'Page should have a level-one heading',
|
|
40
|
-
description:
|
|
40
|
+
description:
|
|
41
|
+
'Checks that the page has at least one level-one heading (<h1> or role="heading" with aria-level="1").',
|
|
41
42
|
i18n: {
|
|
42
43
|
titleKey: 'pageHasHeadingOne_title',
|
|
43
44
|
descriptionKey: 'pageHasHeadingOne_description'
|
|
@@ -70,7 +71,9 @@ function runInPage(ctx) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
function normalizeWs(s) {
|
|
73
|
-
return String(s || '')
|
|
74
|
+
return String(s || '')
|
|
75
|
+
.replace(/\s+/g, ' ')
|
|
76
|
+
.trim();
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
function getExplicitRoleToken(el) {
|
|
@@ -90,7 +93,8 @@ function runInPage(ctx) {
|
|
|
90
93
|
return tag === 'h1';
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
const isAccTreeEligible =
|
|
96
|
+
const isAccTreeEligible =
|
|
97
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
94
98
|
|
|
95
99
|
function isExposedToAt(el) {
|
|
96
100
|
if (!isAccTreeEligible) return true;
|
|
@@ -105,11 +109,12 @@ function runInPage(ctx) {
|
|
|
105
109
|
|
|
106
110
|
// queryAllSmart (shadow-DOM-aware) instead of plain document.querySelectorAll -- see
|
|
107
111
|
// landmark-one-main-manual.js's identical precedent.
|
|
108
|
-
let nodes
|
|
112
|
+
let nodes;
|
|
109
113
|
try {
|
|
110
|
-
nodes =
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
nodes =
|
|
115
|
+
helpers && typeof helpers.queryAllSmart === 'function'
|
|
116
|
+
? helpers.queryAllSmart('h1, [role]')
|
|
117
|
+
: document.querySelectorAll('h1, [role]');
|
|
113
118
|
} catch {
|
|
114
119
|
nodes = [];
|
|
115
120
|
}
|
|
@@ -121,26 +126,30 @@ function runInPage(ctx) {
|
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
|
|
124
|
-
const html = helpers.getOuterHtmlSnippet
|
|
129
|
+
const html = helpers.getOuterHtmlSnippet
|
|
130
|
+
? helpers.getOuterHtmlSnippet(body)
|
|
131
|
+
: (body.outerHTML || '').slice(0, 200);
|
|
125
132
|
|
|
126
133
|
return {
|
|
127
134
|
ruleId: rule.ruleId,
|
|
128
135
|
outcome: 'cantTell',
|
|
129
136
|
severity: rule.defaultSeverity || 'minor',
|
|
130
|
-
occurrences: [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
occurrences: [
|
|
138
|
+
{
|
|
139
|
+
selector: stableSelector,
|
|
140
|
+
html,
|
|
141
|
+
summary: 'This page has no level-one heading.',
|
|
142
|
+
hint: 'Add a level-one heading (<h1> or role="heading" aria-level="1") that identifies the page\'s main content.',
|
|
143
|
+
i18n: {
|
|
144
|
+
summaryKey: 'pageHasHeadingOne_summary_cantTell',
|
|
145
|
+
hintKey: 'pageHasHeadingOne_hint_cantTell',
|
|
146
|
+
params: {}
|
|
147
|
+
},
|
|
148
|
+
data: {
|
|
149
|
+
details: { reasonCode: 'HEADING_ONE_MISSING' }
|
|
150
|
+
}
|
|
142
151
|
}
|
|
143
|
-
|
|
152
|
+
]
|
|
144
153
|
};
|
|
145
154
|
}
|
|
146
155
|
|