@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
|
@@ -49,7 +49,8 @@ const id = 'bypass-blocks-present';
|
|
|
49
49
|
|
|
50
50
|
const meta = {
|
|
51
51
|
title: 'Page must provide a way to bypass repeated blocks',
|
|
52
|
-
description:
|
|
52
|
+
description:
|
|
53
|
+
'Checks that the page has at least one recognized WCAG 2.4.1 bypass-blocks mechanism: a main landmark, a working same-page anchor link, or a heading.',
|
|
53
54
|
i18n: {
|
|
54
55
|
titleKey: 'bypassBlocksPresent_title',
|
|
55
56
|
descriptionKey: 'bypassBlocksPresent_description'
|
|
@@ -58,7 +59,13 @@ const meta = {
|
|
|
58
59
|
tags: ['wcag2a', 'wcag241', 'navigation', 'atomic', 'automatic'],
|
|
59
60
|
wcagSc: ['2.4.1'],
|
|
60
61
|
normativeMappings: [
|
|
61
|
-
{
|
|
62
|
+
{
|
|
63
|
+
standard: 'WCAG',
|
|
64
|
+
version: '2.2',
|
|
65
|
+
requirement: '2.4.1',
|
|
66
|
+
title: 'Bypass Blocks',
|
|
67
|
+
conformanceLevel: 'A'
|
|
68
|
+
}
|
|
62
69
|
],
|
|
63
70
|
defaultSeverity: 'serious',
|
|
64
71
|
category: 'operable',
|
|
@@ -67,6 +74,14 @@ const meta = {
|
|
|
67
74
|
coverage: { facetsBySc: { '2.4.1': ['bypass-blocks-present'] } }
|
|
68
75
|
};
|
|
69
76
|
|
|
77
|
+
// This check is inherently whole-document (does the PAGE have this
|
|
78
|
+
// property?), not evaluable per-subtree -- notApplicable when contextSelector
|
|
79
|
+
// scoped this run narrower than the whole document, or when
|
|
80
|
+
// engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
|
|
81
|
+
function applicability(ctx) {
|
|
82
|
+
return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
function runInPage(ctx) {
|
|
71
86
|
const { document, helpers, rule } = ctx;
|
|
72
87
|
|
|
@@ -75,7 +90,8 @@ function runInPage(ctx) {
|
|
|
75
90
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
76
91
|
}
|
|
77
92
|
|
|
78
|
-
const isAccTreeEligible =
|
|
93
|
+
const isAccTreeEligible =
|
|
94
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
79
95
|
|
|
80
96
|
function isExposedToAt(el) {
|
|
81
97
|
if (!isAccTreeEligible) return true;
|
|
@@ -113,7 +129,7 @@ function runInPage(ctx) {
|
|
|
113
129
|
}
|
|
114
130
|
|
|
115
131
|
function hasWorkingAnchorLink() {
|
|
116
|
-
let links
|
|
132
|
+
let links;
|
|
117
133
|
try {
|
|
118
134
|
links = document.querySelectorAll('a[href]');
|
|
119
135
|
} catch {
|
|
@@ -132,7 +148,7 @@ function runInPage(ctx) {
|
|
|
132
148
|
fragment = fragment.trim();
|
|
133
149
|
if (!fragment) continue;
|
|
134
150
|
|
|
135
|
-
let target
|
|
151
|
+
let target;
|
|
136
152
|
try {
|
|
137
153
|
target = document.getElementById(fragment);
|
|
138
154
|
} catch {
|
|
@@ -159,32 +175,41 @@ function runInPage(ctx) {
|
|
|
159
175
|
|
|
160
176
|
const mainLandmark = hasMainLandmark();
|
|
161
177
|
const anchorLink = mainLandmark ? false : hasWorkingAnchorLink();
|
|
162
|
-
const heading =
|
|
178
|
+
const heading = mainLandmark || anchorLink ? false : hasHeading();
|
|
163
179
|
|
|
164
180
|
if (mainLandmark || anchorLink || heading) {
|
|
165
181
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
166
182
|
}
|
|
167
183
|
|
|
168
184
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
|
|
169
|
-
const html = helpers.getOuterHtmlSnippet
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
const html = helpers.getOuterHtmlSnippet
|
|
186
|
+
? helpers.getOuterHtmlSnippet(body)
|
|
187
|
+
: (body.outerHTML || '').slice(0, 200);
|
|
188
|
+
|
|
189
|
+
const occurrences = [
|
|
190
|
+
{
|
|
191
|
+
selector: stableSelector,
|
|
192
|
+
html,
|
|
193
|
+
summary: 'This page has no recognized way to bypass repeated blocks of content.',
|
|
194
|
+
hint: 'Add a main landmark (<main> or role="main"), a working "skip to content" link, or heading elements that assistive technology can use to jump past repeated content.',
|
|
195
|
+
i18n: {
|
|
196
|
+
summaryKey: 'bypassBlocksPresent_summary_fail',
|
|
197
|
+
hintKey: 'bypassBlocksPresent_hint_fail',
|
|
198
|
+
params: {}
|
|
199
|
+
},
|
|
200
|
+
data: {
|
|
201
|
+
details: { reasonCode: 'BYPASS_MECHANISM_ABSENT' },
|
|
202
|
+
visibilityFilter: { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
203
|
+
}
|
|
184
204
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return {
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
ruleId: rule.ruleId,
|
|
209
|
+
outcome: 'fail',
|
|
210
|
+
severity: rule.defaultSeverity || 'serious',
|
|
211
|
+
occurrences
|
|
212
|
+
};
|
|
188
213
|
}
|
|
189
214
|
|
|
190
|
-
module.exports = { id, meta, runInPage };
|
|
215
|
+
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -18,7 +18,8 @@ const id = 'canvas-text-alternative-present';
|
|
|
18
18
|
|
|
19
19
|
const meta = {
|
|
20
20
|
title: '<canvas> must provide a text alternative',
|
|
21
|
-
description:
|
|
21
|
+
description:
|
|
22
|
+
'Checks that <canvas> elements provide a text alternative via fallback content or an accessible name.',
|
|
22
23
|
i18n: {
|
|
23
24
|
titleKey: 'canvas_textAltPresent_title',
|
|
24
25
|
descriptionKey: 'canvas_textAltPresent_description'
|
|
@@ -27,7 +28,13 @@ const meta = {
|
|
|
27
28
|
tags: ['wcag2a', 'wcag111', 'nontext', 'canvas', 'atomic', 'automatic'],
|
|
28
29
|
wcagSc: ['1.1.1'],
|
|
29
30
|
normativeMappings: [
|
|
30
|
-
{
|
|
31
|
+
{
|
|
32
|
+
standard: 'WCAG',
|
|
33
|
+
version: '2.2',
|
|
34
|
+
requirement: '1.1.1',
|
|
35
|
+
title: 'Non-text Content',
|
|
36
|
+
conformanceLevel: 'A'
|
|
37
|
+
}
|
|
31
38
|
],
|
|
32
39
|
defaultSeverity: 'serious',
|
|
33
40
|
category: 'perceivable',
|
|
@@ -44,29 +51,38 @@ function runInPage(ctx) {
|
|
|
44
51
|
const { document, root, helpers, rule } = ctx;
|
|
45
52
|
const safeRoot = root || document;
|
|
46
53
|
|
|
47
|
-
const queryAllSmart =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
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;
|
|
74
|
+
|
|
75
|
+
const getTextAlternativeInfo =
|
|
76
|
+
helpers && typeof helpers.getTextAlternativeInfo === 'function'
|
|
77
|
+
? helpers.getTextAlternativeInfo
|
|
78
|
+
: null;
|
|
66
79
|
|
|
67
80
|
const canvases = (() => {
|
|
68
|
-
try {
|
|
69
|
-
|
|
81
|
+
try {
|
|
82
|
+
return Array.from((queryAllSmart ? queryAllSmart('canvas') : queryAll('canvas')) || []);
|
|
83
|
+
} catch {
|
|
84
|
+
return queryAll('canvas');
|
|
85
|
+
}
|
|
70
86
|
})();
|
|
71
87
|
|
|
72
88
|
if (!canvases.length) {
|
|
@@ -82,7 +98,11 @@ function runInPage(ctx) {
|
|
|
82
98
|
// Applicability: eligible in the accessibility tree (with focusable/IDREF exceptions handled by helper).
|
|
83
99
|
if (isAccTreeEligible) {
|
|
84
100
|
const elig = (() => {
|
|
85
|
-
try {
|
|
101
|
+
try {
|
|
102
|
+
return isAccTreeEligible(el, ctx);
|
|
103
|
+
} catch {
|
|
104
|
+
return { eligible: true, reasons: [] };
|
|
105
|
+
}
|
|
86
106
|
})();
|
|
87
107
|
if (elig && elig.eligible === false) continue;
|
|
88
108
|
}
|
|
@@ -91,7 +111,13 @@ function runInPage(ctx) {
|
|
|
91
111
|
|
|
92
112
|
// Expectation: must provide a text alternative.
|
|
93
113
|
const ti = getTextAlternativeInfo
|
|
94
|
-
? (() => {
|
|
114
|
+
? (() => {
|
|
115
|
+
try {
|
|
116
|
+
return getTextAlternativeInfo(el, ctx);
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
})()
|
|
95
121
|
: null;
|
|
96
122
|
|
|
97
123
|
const hasTextAlt = !!(ti && ti.present);
|
|
@@ -134,7 +160,12 @@ function runInPage(ctx) {
|
|
|
134
160
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
135
161
|
}
|
|
136
162
|
|
|
137
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
ruleId: rule.ruleId,
|
|
165
|
+
outcome: 'fail',
|
|
166
|
+
severity: rule.defaultSeverity || 'minor',
|
|
167
|
+
occurrences
|
|
168
|
+
};
|
|
138
169
|
}
|
|
139
170
|
|
|
140
171
|
module.exports = { id, meta, runInPage };
|
|
@@ -10,10 +10,16 @@ const meta = {
|
|
|
10
10
|
descriptionKey: 'comboboxNamePresent_description'
|
|
11
11
|
},
|
|
12
12
|
helpUrl: null,
|
|
13
|
-
tags: ['wcag2a','wcag412','forms','atomic','automatic','name','combobox'],
|
|
13
|
+
tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'combobox'],
|
|
14
14
|
wcagSc: ['4.1.2'],
|
|
15
15
|
normativeMappings: [
|
|
16
|
-
{
|
|
16
|
+
{
|
|
17
|
+
standard: 'WCAG',
|
|
18
|
+
version: '2.2',
|
|
19
|
+
requirement: '4.1.2',
|
|
20
|
+
title: 'Name, Role, Value',
|
|
21
|
+
conformanceLevel: 'A'
|
|
22
|
+
}
|
|
17
23
|
],
|
|
18
24
|
defaultSeverity: 'serious',
|
|
19
25
|
category: 'robust',
|
|
@@ -24,38 +30,22 @@ const meta = {
|
|
|
24
30
|
|
|
25
31
|
function runInPage(ctx) {
|
|
26
32
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
33
|
+
const getEligibilityInfo =
|
|
34
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
35
|
|
|
32
36
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
37
|
+
return String(s || '')
|
|
38
|
+
.replace(/\s+/g, ' ')
|
|
39
|
+
.trim();
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
function getAttr(el, name) {
|
|
37
43
|
try {
|
|
38
44
|
if (!el || !el.getAttribute) return '';
|
|
39
45
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
function hasAttr(el, name) {
|
|
44
|
-
try { return !!(el && el.hasAttribute && el.hasAttribute(name)); } catch { return false; }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isExplicitProgrammatic(el) {
|
|
48
|
-
// Programmatic name mechanisms we treat as authoritative (presence-only):
|
|
49
|
-
// - aria-label (non-empty)
|
|
50
|
-
// - aria-labelledby (non-empty)
|
|
51
|
-
// - title (non-empty) [weak but allowed for presence]
|
|
52
|
-
const al = getAttr(el, 'aria-label');
|
|
53
|
-
if (al) return true;
|
|
54
|
-
const alb = getAttr(el, 'aria-labelledby');
|
|
55
|
-
if (alb) return true;
|
|
56
|
-
const t = getAttr(el, 'title');
|
|
57
|
-
if (t) return true;
|
|
58
|
-
return false;
|
|
46
|
+
} catch {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
59
49
|
}
|
|
60
50
|
|
|
61
51
|
function buildLabelForMap(doc) {
|
|
@@ -85,7 +75,7 @@ function runInPage(ctx) {
|
|
|
85
75
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
86
76
|
return info && info.present ? info.value : '';
|
|
87
77
|
}
|
|
88
|
-
const t =
|
|
78
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
89
79
|
return t.replace(/\s+/g, ' ').trim();
|
|
90
80
|
}
|
|
91
81
|
|
|
@@ -132,17 +122,9 @@ function runInPage(ctx) {
|
|
|
132
122
|
return '';
|
|
133
123
|
}
|
|
134
124
|
|
|
135
|
-
function getInputValueName(el) {
|
|
136
|
-
try {
|
|
137
|
-
const type = getAttr(el, 'type').toLowerCase();
|
|
138
|
-
if (type !== 'button' && type !== 'submit' && type !== 'reset') return '';
|
|
139
|
-
const v = getAttr(el, 'value');
|
|
140
|
-
return v;
|
|
141
|
-
} catch { return ''; }
|
|
142
|
-
}
|
|
143
|
-
|
|
144
125
|
function isEligibleAcc(helpers, el, ctx) {
|
|
145
|
-
const fn =
|
|
126
|
+
const fn =
|
|
127
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
146
128
|
if (!fn) return true;
|
|
147
129
|
try {
|
|
148
130
|
const r = fn(el, ctx);
|
|
@@ -153,12 +135,13 @@ function runInPage(ctx) {
|
|
|
153
135
|
}
|
|
154
136
|
}
|
|
155
137
|
|
|
156
|
-
|
|
157
138
|
const occurrences = [];
|
|
158
139
|
let applicableCount = 0;
|
|
159
140
|
|
|
160
141
|
const selector = '[role="combobox"]';
|
|
161
|
-
const nodes = helpers.queryAllSmart
|
|
142
|
+
const nodes = helpers.queryAllSmart
|
|
143
|
+
? helpers.queryAllSmart(selector)
|
|
144
|
+
: helpers.queryAll(selector);
|
|
162
145
|
|
|
163
146
|
// Precompute label[for] map for combobox elements that are labelable
|
|
164
147
|
// native form controls (e.g. <input role="combobox">).
|
|
@@ -231,16 +214,22 @@ function runInPage(ctx) {
|
|
|
231
214
|
if (res.ok) continue;
|
|
232
215
|
|
|
233
216
|
const eligInfo = getEligibilityInfo
|
|
234
|
-
|
|
235
|
-
|
|
217
|
+
? (() => {
|
|
218
|
+
try {
|
|
219
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
})()
|
|
224
|
+
: null;
|
|
236
225
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
237
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
226
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
238
227
|
|
|
239
228
|
occurrences.push({
|
|
240
229
|
selector: stableSelector,
|
|
241
230
|
html,
|
|
242
231
|
summary: 'This combobox has no accessible name.',
|
|
243
|
-
hint:
|
|
232
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this combobox's accessible name.",
|
|
244
233
|
i18n: {
|
|
245
234
|
summaryKey: 'comboboxNamePresent_summary_fail',
|
|
246
235
|
hintKey: 'comboboxNamePresent_hint_fail',
|
|
@@ -253,12 +242,16 @@ function runInPage(ctx) {
|
|
|
253
242
|
});
|
|
254
243
|
}
|
|
255
244
|
|
|
256
|
-
|
|
257
245
|
if (applicableCount === 0) {
|
|
258
246
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
259
247
|
}
|
|
260
248
|
if (occurrences.length) {
|
|
261
|
-
return {
|
|
249
|
+
return {
|
|
250
|
+
ruleId: rule.ruleId,
|
|
251
|
+
outcome: 'fail',
|
|
252
|
+
severity: rule.defaultSeverity || 'minor',
|
|
253
|
+
occurrences
|
|
254
|
+
};
|
|
262
255
|
}
|
|
263
256
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
264
257
|
}
|