@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
|
/**
|
|
@@ -27,8 +29,8 @@
|
|
|
27
29
|
* mechanism (e.g. a screen reader's "jump by heading" command).
|
|
28
30
|
* @implementation-notes
|
|
29
31
|
* - This rule intentionally checks presence, not position, for the
|
|
30
|
-
* same-page-anchor condition (b): a
|
|
31
|
-
*
|
|
32
|
+
* same-page-anchor condition (b): a full bypass algorithm is heuristic
|
|
33
|
+
* (see ROADMAP.md's Tier 1a note on why this rule was
|
|
32
34
|
* deferred from the rest of that batch), and getting DOM-order /
|
|
33
35
|
* keyboard-focus-order positioning exactly right without introducing
|
|
34
36
|
* false positives is materially harder than the rest of Tier 1a. Being
|
|
@@ -49,7 +51,8 @@ const id = 'bypass-blocks-present';
|
|
|
49
51
|
|
|
50
52
|
const meta = {
|
|
51
53
|
title: 'Page must provide a way to bypass repeated blocks',
|
|
52
|
-
description:
|
|
54
|
+
description:
|
|
55
|
+
'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
56
|
i18n: {
|
|
54
57
|
titleKey: 'bypassBlocksPresent_title',
|
|
55
58
|
descriptionKey: 'bypassBlocksPresent_description'
|
|
@@ -58,7 +61,13 @@ const meta = {
|
|
|
58
61
|
tags: ['wcag2a', 'wcag241', 'navigation', 'atomic', 'automatic'],
|
|
59
62
|
wcagSc: ['2.4.1'],
|
|
60
63
|
normativeMappings: [
|
|
61
|
-
{
|
|
64
|
+
{
|
|
65
|
+
standard: 'WCAG',
|
|
66
|
+
version: '2.2',
|
|
67
|
+
requirement: '2.4.1',
|
|
68
|
+
title: 'Bypass Blocks',
|
|
69
|
+
conformanceLevel: 'A'
|
|
70
|
+
}
|
|
62
71
|
],
|
|
63
72
|
defaultSeverity: 'serious',
|
|
64
73
|
category: 'operable',
|
|
@@ -83,7 +92,8 @@ function runInPage(ctx) {
|
|
|
83
92
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
const isAccTreeEligible =
|
|
95
|
+
const isAccTreeEligible =
|
|
96
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
87
97
|
|
|
88
98
|
function isExposedToAt(el) {
|
|
89
99
|
if (!isAccTreeEligible) return true;
|
|
@@ -107,12 +117,12 @@ function runInPage(ctx) {
|
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
// Filters through isAccTreeEligible (hidden/aria-hidden/display:none/inert
|
|
110
|
-
// don't count as "a bypass mechanism is present") -- see
|
|
111
|
-
// one-manual.js
|
|
112
|
-
// page
|
|
120
|
+
// don't count as "a bypass mechanism is present") -- see
|
|
121
|
+
// page-has-heading-one-manual.js for the identical real-world trigger
|
|
122
|
+
// (e.g. a page whose only <h1> sits inside a display:none ancestor,
|
|
113
123
|
// unreachable by sighted and screen reader users alike). A fully
|
|
114
|
-
// non-rendered <main>/heading
|
|
115
|
-
//
|
|
124
|
+
// non-rendered <main>/heading must not be credited here, since that would
|
|
125
|
+
// wrongly return `pass` for a page with zero actual bypass mechanisms.
|
|
116
126
|
function hasMainLandmark() {
|
|
117
127
|
for (const el of queryAll('main, [role="main"]')) {
|
|
118
128
|
if (el && isExposedToAt(el)) return true;
|
|
@@ -121,7 +131,7 @@ function runInPage(ctx) {
|
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
function hasWorkingAnchorLink() {
|
|
124
|
-
let links
|
|
134
|
+
let links;
|
|
125
135
|
try {
|
|
126
136
|
links = document.querySelectorAll('a[href]');
|
|
127
137
|
} catch {
|
|
@@ -140,7 +150,7 @@ function runInPage(ctx) {
|
|
|
140
150
|
fragment = fragment.trim();
|
|
141
151
|
if (!fragment) continue;
|
|
142
152
|
|
|
143
|
-
let target
|
|
153
|
+
let target;
|
|
144
154
|
try {
|
|
145
155
|
target = document.getElementById(fragment);
|
|
146
156
|
} catch {
|
|
@@ -167,32 +177,41 @@ function runInPage(ctx) {
|
|
|
167
177
|
|
|
168
178
|
const mainLandmark = hasMainLandmark();
|
|
169
179
|
const anchorLink = mainLandmark ? false : hasWorkingAnchorLink();
|
|
170
|
-
const heading =
|
|
180
|
+
const heading = mainLandmark || anchorLink ? false : hasHeading();
|
|
171
181
|
|
|
172
182
|
if (mainLandmark || anchorLink || heading) {
|
|
173
183
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
174
184
|
}
|
|
175
185
|
|
|
176
186
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
|
|
177
|
-
const html = helpers.getOuterHtmlSnippet
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
187
|
+
const html = helpers.getOuterHtmlSnippet
|
|
188
|
+
? helpers.getOuterHtmlSnippet(body)
|
|
189
|
+
: (body.outerHTML || '').slice(0, 200);
|
|
190
|
+
|
|
191
|
+
const occurrences = [
|
|
192
|
+
{
|
|
193
|
+
selector: stableSelector,
|
|
194
|
+
html,
|
|
195
|
+
summary: 'This page has no recognized way to bypass repeated blocks of content.',
|
|
196
|
+
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.',
|
|
197
|
+
i18n: {
|
|
198
|
+
summaryKey: 'bypassBlocksPresent_summary_fail',
|
|
199
|
+
hintKey: 'bypassBlocksPresent_hint_fail',
|
|
200
|
+
params: {}
|
|
201
|
+
},
|
|
202
|
+
data: {
|
|
203
|
+
details: { reasonCode: 'BYPASS_MECHANISM_ABSENT' },
|
|
204
|
+
visibilityFilter: { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
205
|
+
}
|
|
192
206
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return {
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
ruleId: rule.ruleId,
|
|
211
|
+
outcome: 'fail',
|
|
212
|
+
severity: rule.defaultSeverity || 'serious',
|
|
213
|
+
occurrences
|
|
214
|
+
};
|
|
196
215
|
}
|
|
197
216
|
|
|
198
|
-
module.exports = { id, meta, runInPage, applicability };
|
|
217
|
+
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -18,7 +20,8 @@ const id = 'canvas-text-alternative-present';
|
|
|
18
20
|
|
|
19
21
|
const meta = {
|
|
20
22
|
title: '<canvas> must provide a text alternative',
|
|
21
|
-
description:
|
|
23
|
+
description:
|
|
24
|
+
'Checks that <canvas> elements provide a text alternative via fallback content or an accessible name.',
|
|
22
25
|
i18n: {
|
|
23
26
|
titleKey: 'canvas_textAltPresent_title',
|
|
24
27
|
descriptionKey: 'canvas_textAltPresent_description'
|
|
@@ -27,7 +30,13 @@ const meta = {
|
|
|
27
30
|
tags: ['wcag2a', 'wcag111', 'nontext', 'canvas', 'atomic', 'automatic'],
|
|
28
31
|
wcagSc: ['1.1.1'],
|
|
29
32
|
normativeMappings: [
|
|
30
|
-
{
|
|
33
|
+
{
|
|
34
|
+
standard: 'WCAG',
|
|
35
|
+
version: '2.2',
|
|
36
|
+
requirement: '1.1.1',
|
|
37
|
+
title: 'Non-text Content',
|
|
38
|
+
conformanceLevel: 'A'
|
|
39
|
+
}
|
|
31
40
|
],
|
|
32
41
|
defaultSeverity: 'serious',
|
|
33
42
|
category: 'perceivable',
|
|
@@ -44,29 +53,38 @@ function runInPage(ctx) {
|
|
|
44
53
|
const { document, root, helpers, rule } = ctx;
|
|
45
54
|
const safeRoot = root || document;
|
|
46
55
|
|
|
47
|
-
const queryAllSmart =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
56
|
+
const queryAllSmart =
|
|
57
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
58
|
+
const queryAll =
|
|
59
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
60
|
+
? helpers.queryAll
|
|
61
|
+
: (sel) => {
|
|
62
|
+
try {
|
|
63
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
64
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
65
|
+
: [];
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const getEligibilityInfo =
|
|
72
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
73
|
+
|
|
74
|
+
const isAccTreeEligible =
|
|
75
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
76
|
+
|
|
77
|
+
const getTextAlternativeInfo =
|
|
78
|
+
helpers && typeof helpers.getTextAlternativeInfo === 'function'
|
|
79
|
+
? helpers.getTextAlternativeInfo
|
|
80
|
+
: null;
|
|
66
81
|
|
|
67
82
|
const canvases = (() => {
|
|
68
|
-
try {
|
|
69
|
-
|
|
83
|
+
try {
|
|
84
|
+
return Array.from((queryAllSmart ? queryAllSmart('canvas') : queryAll('canvas')) || []);
|
|
85
|
+
} catch {
|
|
86
|
+
return queryAll('canvas');
|
|
87
|
+
}
|
|
70
88
|
})();
|
|
71
89
|
|
|
72
90
|
if (!canvases.length) {
|
|
@@ -82,7 +100,11 @@ function runInPage(ctx) {
|
|
|
82
100
|
// Applicability: eligible in the accessibility tree (with focusable/IDREF exceptions handled by helper).
|
|
83
101
|
if (isAccTreeEligible) {
|
|
84
102
|
const elig = (() => {
|
|
85
|
-
try {
|
|
103
|
+
try {
|
|
104
|
+
return isAccTreeEligible(el, ctx);
|
|
105
|
+
} catch {
|
|
106
|
+
return { eligible: true, reasons: [] };
|
|
107
|
+
}
|
|
86
108
|
})();
|
|
87
109
|
if (elig && elig.eligible === false) continue;
|
|
88
110
|
}
|
|
@@ -91,7 +113,13 @@ function runInPage(ctx) {
|
|
|
91
113
|
|
|
92
114
|
// Expectation: must provide a text alternative.
|
|
93
115
|
const ti = getTextAlternativeInfo
|
|
94
|
-
? (() => {
|
|
116
|
+
? (() => {
|
|
117
|
+
try {
|
|
118
|
+
return getTextAlternativeInfo(el, ctx);
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
})()
|
|
95
123
|
: null;
|
|
96
124
|
|
|
97
125
|
const hasTextAlt = !!(ti && ti.present);
|
|
@@ -134,7 +162,12 @@ function runInPage(ctx) {
|
|
|
134
162
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
135
163
|
}
|
|
136
164
|
|
|
137
|
-
return {
|
|
165
|
+
return {
|
|
166
|
+
ruleId: rule.ruleId,
|
|
167
|
+
outcome: 'fail',
|
|
168
|
+
severity: rule.defaultSeverity || 'minor',
|
|
169
|
+
occurrences
|
|
170
|
+
};
|
|
138
171
|
}
|
|
139
172
|
|
|
140
173
|
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
|
const id = 'combobox-name-present';
|
|
@@ -10,10 +12,16 @@ const meta = {
|
|
|
10
12
|
descriptionKey: 'comboboxNamePresent_description'
|
|
11
13
|
},
|
|
12
14
|
helpUrl: null,
|
|
13
|
-
tags: ['wcag2a','wcag412','forms','atomic','automatic','name','combobox'],
|
|
15
|
+
tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'combobox'],
|
|
14
16
|
wcagSc: ['4.1.2'],
|
|
15
17
|
normativeMappings: [
|
|
16
|
-
{
|
|
18
|
+
{
|
|
19
|
+
standard: 'WCAG',
|
|
20
|
+
version: '2.2',
|
|
21
|
+
requirement: '4.1.2',
|
|
22
|
+
title: 'Name, Role, Value',
|
|
23
|
+
conformanceLevel: 'A'
|
|
24
|
+
}
|
|
17
25
|
],
|
|
18
26
|
defaultSeverity: 'serious',
|
|
19
27
|
category: 'robust',
|
|
@@ -24,38 +32,22 @@ const meta = {
|
|
|
24
32
|
|
|
25
33
|
function runInPage(ctx) {
|
|
26
34
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
35
|
+
const getEligibilityInfo =
|
|
36
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
37
|
|
|
32
38
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
39
|
+
return String(s || '')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
function getAttr(el, name) {
|
|
37
45
|
try {
|
|
38
46
|
if (!el || !el.getAttribute) return '';
|
|
39
47
|
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;
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
59
51
|
}
|
|
60
52
|
|
|
61
53
|
function buildLabelForMap(doc) {
|
|
@@ -85,7 +77,7 @@ function runInPage(ctx) {
|
|
|
85
77
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
86
78
|
return info && info.present ? info.value : '';
|
|
87
79
|
}
|
|
88
|
-
const t =
|
|
80
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
89
81
|
return t.replace(/\s+/g, ' ').trim();
|
|
90
82
|
}
|
|
91
83
|
|
|
@@ -132,17 +124,9 @@ function runInPage(ctx) {
|
|
|
132
124
|
return '';
|
|
133
125
|
}
|
|
134
126
|
|
|
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
127
|
function isEligibleAcc(helpers, el, ctx) {
|
|
145
|
-
const fn =
|
|
128
|
+
const fn =
|
|
129
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
146
130
|
if (!fn) return true;
|
|
147
131
|
try {
|
|
148
132
|
const r = fn(el, ctx);
|
|
@@ -153,12 +137,13 @@ function runInPage(ctx) {
|
|
|
153
137
|
}
|
|
154
138
|
}
|
|
155
139
|
|
|
156
|
-
|
|
157
140
|
const occurrences = [];
|
|
158
141
|
let applicableCount = 0;
|
|
159
142
|
|
|
160
143
|
const selector = '[role="combobox"]';
|
|
161
|
-
const nodes = helpers.queryAllSmart
|
|
144
|
+
const nodes = helpers.queryAllSmart
|
|
145
|
+
? helpers.queryAllSmart(selector)
|
|
146
|
+
: helpers.queryAll(selector);
|
|
162
147
|
|
|
163
148
|
// Precompute label[for] map for combobox elements that are labelable
|
|
164
149
|
// native form controls (e.g. <input role="combobox">).
|
|
@@ -231,16 +216,22 @@ function runInPage(ctx) {
|
|
|
231
216
|
if (res.ok) continue;
|
|
232
217
|
|
|
233
218
|
const eligInfo = getEligibilityInfo
|
|
234
|
-
|
|
235
|
-
|
|
219
|
+
? (() => {
|
|
220
|
+
try {
|
|
221
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
})()
|
|
226
|
+
: null;
|
|
236
227
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
237
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
228
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
238
229
|
|
|
239
230
|
occurrences.push({
|
|
240
231
|
selector: stableSelector,
|
|
241
232
|
html,
|
|
242
233
|
summary: 'This combobox has no accessible name.',
|
|
243
|
-
hint:
|
|
234
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this combobox's accessible name.",
|
|
244
235
|
i18n: {
|
|
245
236
|
summaryKey: 'comboboxNamePresent_summary_fail',
|
|
246
237
|
hintKey: 'comboboxNamePresent_hint_fail',
|
|
@@ -253,12 +244,16 @@ function runInPage(ctx) {
|
|
|
253
244
|
});
|
|
254
245
|
}
|
|
255
246
|
|
|
256
|
-
|
|
257
247
|
if (applicableCount === 0) {
|
|
258
248
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
259
249
|
}
|
|
260
250
|
if (occurrences.length) {
|
|
261
|
-
return {
|
|
251
|
+
return {
|
|
252
|
+
ruleId: rule.ruleId,
|
|
253
|
+
outcome: 'fail',
|
|
254
|
+
severity: rule.defaultSeverity || 'minor',
|
|
255
|
+
occurrences
|
|
256
|
+
};
|
|
262
257
|
}
|
|
263
258
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
264
259
|
}
|