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