@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
|
/**
|
|
@@ -33,7 +35,8 @@ const id = 'autocomplete-valid';
|
|
|
33
35
|
|
|
34
36
|
const meta = {
|
|
35
37
|
title: 'autocomplete attribute must be a valid autofill value',
|
|
36
|
-
description:
|
|
38
|
+
description:
|
|
39
|
+
'Checks that a non-empty autocomplete attribute is "on"/"off" or a well-formed autofill detail token list.',
|
|
37
40
|
i18n: {
|
|
38
41
|
titleKey: 'autocompleteValid_title',
|
|
39
42
|
descriptionKey: 'autocompleteValid_description'
|
|
@@ -42,7 +45,13 @@ const meta = {
|
|
|
42
45
|
tags: ['wcag21aa', 'wcag135', 'forms', 'atomic', 'automatic'],
|
|
43
46
|
wcagSc: ['1.3.5'],
|
|
44
47
|
normativeMappings: [
|
|
45
|
-
{
|
|
48
|
+
{
|
|
49
|
+
standard: 'WCAG',
|
|
50
|
+
version: '2.2',
|
|
51
|
+
requirement: '1.3.5',
|
|
52
|
+
title: 'Identify Input Purpose',
|
|
53
|
+
conformanceLevel: 'AA'
|
|
54
|
+
}
|
|
46
55
|
],
|
|
47
56
|
defaultSeverity: 'moderate',
|
|
48
57
|
category: 'perceivable',
|
|
@@ -52,20 +61,63 @@ const meta = {
|
|
|
52
61
|
};
|
|
53
62
|
|
|
54
63
|
function runInPage(ctx) {
|
|
55
|
-
const {
|
|
64
|
+
const { helpers, rule } = ctx;
|
|
56
65
|
|
|
57
66
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
58
67
|
// ("runInPage MUST be self-contained").
|
|
59
68
|
const FIELD_NAMES = new Set([
|
|
60
|
-
'name',
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
'
|
|
69
|
+
'name',
|
|
70
|
+
'honorific-prefix',
|
|
71
|
+
'given-name',
|
|
72
|
+
'additional-name',
|
|
73
|
+
'family-name',
|
|
74
|
+
'honorific-suffix',
|
|
75
|
+
'nickname',
|
|
76
|
+
'username',
|
|
77
|
+
'new-password',
|
|
78
|
+
'current-password',
|
|
79
|
+
'one-time-code',
|
|
80
|
+
'organization-title',
|
|
81
|
+
'organization',
|
|
82
|
+
'street-address',
|
|
83
|
+
'address-line1',
|
|
84
|
+
'address-line2',
|
|
85
|
+
'address-line3',
|
|
86
|
+
'address-level4',
|
|
87
|
+
'address-level3',
|
|
88
|
+
'address-level2',
|
|
89
|
+
'address-level1',
|
|
90
|
+
'country',
|
|
91
|
+
'country-name',
|
|
92
|
+
'postal-code',
|
|
93
|
+
'cc-name',
|
|
94
|
+
'cc-given-name',
|
|
95
|
+
'cc-additional-name',
|
|
96
|
+
'cc-family-name',
|
|
97
|
+
'cc-number',
|
|
98
|
+
'cc-exp',
|
|
99
|
+
'cc-exp-month',
|
|
100
|
+
'cc-exp-year',
|
|
101
|
+
'cc-csc',
|
|
102
|
+
'cc-type',
|
|
103
|
+
'transaction-currency',
|
|
104
|
+
'transaction-amount',
|
|
105
|
+
'language',
|
|
106
|
+
'bday',
|
|
107
|
+
'bday-day',
|
|
108
|
+
'bday-month',
|
|
109
|
+
'bday-year',
|
|
110
|
+
'sex',
|
|
111
|
+
'tel',
|
|
112
|
+
'tel-country-code',
|
|
113
|
+
'tel-national',
|
|
114
|
+
'tel-area-code',
|
|
115
|
+
'tel-local',
|
|
116
|
+
'tel-extension',
|
|
117
|
+
'email',
|
|
118
|
+
'impp',
|
|
119
|
+
'url',
|
|
120
|
+
'photo'
|
|
69
121
|
]);
|
|
70
122
|
const CONTACT_MODALITY = new Set(['home', 'work', 'mobile', 'fax', 'pager', 'impp']);
|
|
71
123
|
|
|
@@ -75,7 +127,8 @@ function runInPage(ctx) {
|
|
|
75
127
|
if (tokens.length === 1 && (tokens[0] === 'on' || tokens[0] === 'off')) return true;
|
|
76
128
|
|
|
77
129
|
let i = 0;
|
|
78
|
-
if (tokens[i] && tokens[i].startsWith('section-') && tokens[i].length > 'section-'.length)
|
|
130
|
+
if (tokens[i] && tokens[i].startsWith('section-') && tokens[i].length > 'section-'.length)
|
|
131
|
+
i += 1;
|
|
79
132
|
if (tokens[i] === 'shipping' || tokens[i] === 'billing') i += 1;
|
|
80
133
|
if (CONTACT_MODALITY.has(tokens[i])) i += 1;
|
|
81
134
|
|
|
@@ -87,7 +140,9 @@ function runInPage(ctx) {
|
|
|
87
140
|
return FIELD_NAMES.has(remaining[0]);
|
|
88
141
|
}
|
|
89
142
|
|
|
90
|
-
const nodes = helpers.queryAllSmart
|
|
143
|
+
const nodes = helpers.queryAllSmart
|
|
144
|
+
? helpers.queryAllSmart('input, select, textarea')
|
|
145
|
+
: helpers.queryAll('input, select, textarea');
|
|
91
146
|
|
|
92
147
|
const occurrences = [];
|
|
93
148
|
let applicableCount = 0;
|
|
@@ -103,7 +158,7 @@ function runInPage(ctx) {
|
|
|
103
158
|
|
|
104
159
|
const tag = el.tagName.toLowerCase();
|
|
105
160
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
106
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
161
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
107
162
|
|
|
108
163
|
occurrences.push({
|
|
109
164
|
selector: stableSelector,
|
|
@@ -125,7 +180,12 @@ function runInPage(ctx) {
|
|
|
125
180
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
126
181
|
}
|
|
127
182
|
if (occurrences.length) {
|
|
128
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
ruleId: rule.ruleId,
|
|
185
|
+
outcome: 'fail',
|
|
186
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
187
|
+
occurrences
|
|
188
|
+
};
|
|
129
189
|
}
|
|
130
190
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
131
191
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -19,15 +21,15 @@
|
|
|
19
21
|
* @implementation-notes
|
|
20
22
|
* - Static text-based parsing of the style attribute string (regex for
|
|
21
23
|
* the three property names followed by `!important`), not computed
|
|
22
|
-
* style
|
|
23
|
-
* rule checks.
|
|
24
|
+
* style.
|
|
24
25
|
*/
|
|
25
26
|
|
|
26
27
|
const id = 'avoid-inline-spacing';
|
|
27
28
|
|
|
28
29
|
const meta = {
|
|
29
30
|
title: 'Inline style must not force text spacing with !important',
|
|
30
|
-
description:
|
|
31
|
+
description:
|
|
32
|
+
'Checks that inline style does not set line-height, letter-spacing, or word-spacing with !important, which blocks user text-spacing overrides.',
|
|
31
33
|
i18n: {
|
|
32
34
|
titleKey: 'avoidInlineSpacing_title',
|
|
33
35
|
descriptionKey: 'avoidInlineSpacing_description'
|
|
@@ -36,7 +38,13 @@ const meta = {
|
|
|
36
38
|
tags: ['wcag21aa', 'wcag1412', 'structure', 'atomic', 'automatic'],
|
|
37
39
|
wcagSc: ['1.4.12'],
|
|
38
40
|
normativeMappings: [
|
|
39
|
-
{
|
|
41
|
+
{
|
|
42
|
+
standard: 'WCAG',
|
|
43
|
+
version: '2.2',
|
|
44
|
+
requirement: '1.4.12',
|
|
45
|
+
title: 'Text Spacing',
|
|
46
|
+
conformanceLevel: 'AA'
|
|
47
|
+
}
|
|
40
48
|
],
|
|
41
49
|
defaultSeverity: 'moderate',
|
|
42
50
|
category: 'perceivable',
|
|
@@ -46,11 +54,13 @@ const meta = {
|
|
|
46
54
|
};
|
|
47
55
|
|
|
48
56
|
function runInPage(ctx) {
|
|
49
|
-
const {
|
|
57
|
+
const { helpers, rule } = ctx;
|
|
50
58
|
|
|
51
59
|
const SPACING_PROPS = ['line-height', 'letter-spacing', 'word-spacing'];
|
|
52
60
|
|
|
53
|
-
const nodes = helpers.queryAllSmart
|
|
61
|
+
const nodes = helpers.queryAllSmart
|
|
62
|
+
? helpers.queryAllSmart('[style]')
|
|
63
|
+
: helpers.queryAll('[style]');
|
|
54
64
|
|
|
55
65
|
const occurrences = [];
|
|
56
66
|
let applicableCount = 0;
|
|
@@ -76,7 +86,7 @@ function runInPage(ctx) {
|
|
|
76
86
|
|
|
77
87
|
const tag = el.tagName.toLowerCase();
|
|
78
88
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
79
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
89
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
80
90
|
|
|
81
91
|
occurrences.push({
|
|
82
92
|
selector: stableSelector,
|
|
@@ -98,7 +108,12 @@ function runInPage(ctx) {
|
|
|
98
108
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
99
109
|
}
|
|
100
110
|
if (occurrences.length) {
|
|
101
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
ruleId: rule.ruleId,
|
|
113
|
+
outcome: 'fail',
|
|
114
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
115
|
+
occurrences
|
|
116
|
+
};
|
|
102
117
|
}
|
|
103
118
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
104
119
|
}
|
|
@@ -1,61 +1,66 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'binary-control-name-present';
|
|
4
6
|
|
|
5
7
|
const meta = {
|
|
6
8
|
title: 'Binary controls have an accessible name',
|
|
7
|
-
description:
|
|
9
|
+
description:
|
|
10
|
+
'Checks that checkbox, radio, and switch controls expose a non-empty accessible name.',
|
|
8
11
|
i18n: {
|
|
9
12
|
titleKey: 'binaryControlNamePresent_title',
|
|
10
13
|
descriptionKey: 'binaryControlNamePresent_description'
|
|
11
14
|
},
|
|
12
15
|
helpUrl: null,
|
|
13
|
-
tags: [
|
|
16
|
+
tags: [
|
|
17
|
+
'wcag2a',
|
|
18
|
+
'wcag412',
|
|
19
|
+
'forms',
|
|
20
|
+
'atomic',
|
|
21
|
+
'automatic',
|
|
22
|
+
'name',
|
|
23
|
+
'checkbox',
|
|
24
|
+
'radio',
|
|
25
|
+
'switch'
|
|
26
|
+
],
|
|
14
27
|
wcagSc: ['4.1.2'],
|
|
15
28
|
normativeMappings: [
|
|
16
|
-
{
|
|
29
|
+
{
|
|
30
|
+
standard: 'WCAG',
|
|
31
|
+
version: '2.2',
|
|
32
|
+
requirement: '4.1.2',
|
|
33
|
+
title: 'Name, Role, Value',
|
|
34
|
+
conformanceLevel: 'A'
|
|
35
|
+
}
|
|
17
36
|
],
|
|
18
37
|
defaultSeverity: 'serious',
|
|
19
38
|
category: 'robust',
|
|
20
39
|
type: 'automatic',
|
|
21
40
|
defaultConfidence: 'high',
|
|
22
|
-
coverage: {
|
|
41
|
+
coverage: {
|
|
42
|
+
facetsBySc: { '4.1.2': ['checkbox-name-present', 'radio-name-present', 'switch-name-present'] }
|
|
43
|
+
}
|
|
23
44
|
};
|
|
24
45
|
|
|
25
46
|
function runInPage(ctx) {
|
|
26
47
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
48
|
+
const getEligibilityInfo =
|
|
49
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
50
|
|
|
32
51
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
52
|
+
return String(s || '')
|
|
53
|
+
.replace(/\s+/g, ' ')
|
|
54
|
+
.trim();
|
|
34
55
|
}
|
|
35
56
|
|
|
36
57
|
function getAttr(el, name) {
|
|
37
58
|
try {
|
|
38
59
|
if (!el || !el.getAttribute) return '';
|
|
39
60
|
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;
|
|
61
|
+
} catch {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
function buildLabelForMap(doc) {
|
|
@@ -85,7 +90,7 @@ function runInPage(ctx) {
|
|
|
85
90
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
86
91
|
return info && info.present ? info.value : '';
|
|
87
92
|
}
|
|
88
|
-
const t =
|
|
93
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
89
94
|
return t.replace(/\s+/g, ' ').trim();
|
|
90
95
|
}
|
|
91
96
|
|
|
@@ -132,17 +137,9 @@ function runInPage(ctx) {
|
|
|
132
137
|
return '';
|
|
133
138
|
}
|
|
134
139
|
|
|
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
140
|
function isEligibleAcc(helpers, el, ctx) {
|
|
145
|
-
const fn =
|
|
141
|
+
const fn =
|
|
142
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
146
143
|
if (!fn) return true;
|
|
147
144
|
try {
|
|
148
145
|
const r = fn(el, ctx);
|
|
@@ -153,13 +150,14 @@ function runInPage(ctx) {
|
|
|
153
150
|
}
|
|
154
151
|
}
|
|
155
152
|
|
|
156
|
-
|
|
157
153
|
const occurrences = [];
|
|
158
154
|
let applicableCount = 0;
|
|
159
155
|
|
|
160
|
-
const selector =
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
const selector =
|
|
157
|
+
'input[type="checkbox"], input[type="radio"], [role="checkbox"], [role="radio"], [role="switch"]';
|
|
158
|
+
const nodes = helpers.queryAllSmart
|
|
159
|
+
? helpers.queryAllSmart(selector)
|
|
160
|
+
: helpers.queryAll(selector);
|
|
163
161
|
|
|
164
162
|
// Precompute label[for] associations once for speed/determinism.
|
|
165
163
|
const labelForMap = buildLabelForMap(document);
|
|
@@ -223,7 +221,11 @@ function runInPage(ctx) {
|
|
|
223
221
|
}
|
|
224
222
|
|
|
225
223
|
// ARIA roles (checkbox/radio/switch) can use content as name
|
|
226
|
-
if (
|
|
224
|
+
if (
|
|
225
|
+
controlType === 'switch' ||
|
|
226
|
+
controlType === 'aria-checkbox' ||
|
|
227
|
+
controlType === 'aria-radio'
|
|
228
|
+
) {
|
|
227
229
|
const t = getConservativeSubtreeText(document, el);
|
|
228
230
|
if (t) return { ok: true, method: 'content' };
|
|
229
231
|
}
|
|
@@ -240,7 +242,7 @@ function runInPage(ctx) {
|
|
|
240
242
|
const type = getAttr(el, 'type').toLowerCase();
|
|
241
243
|
const role = getAttr(el, 'role').toLowerCase();
|
|
242
244
|
|
|
243
|
-
let controlType
|
|
245
|
+
let controlType;
|
|
244
246
|
if (tag === 'input' && type === 'checkbox') controlType = 'checkbox';
|
|
245
247
|
else if (tag === 'input' && type === 'radio') controlType = 'radio';
|
|
246
248
|
else if (role === 'checkbox') controlType = 'aria-checkbox';
|
|
@@ -254,10 +256,16 @@ function runInPage(ctx) {
|
|
|
254
256
|
if (res.ok) continue;
|
|
255
257
|
|
|
256
258
|
const eligInfo = getEligibilityInfo
|
|
257
|
-
|
|
258
|
-
|
|
259
|
+
? (() => {
|
|
260
|
+
try {
|
|
261
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
262
|
+
} catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
})()
|
|
266
|
+
: null;
|
|
259
267
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
260
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
268
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
261
269
|
|
|
262
270
|
occurrences.push({
|
|
263
271
|
selector: stableSelector,
|
|
@@ -280,12 +288,16 @@ function runInPage(ctx) {
|
|
|
280
288
|
});
|
|
281
289
|
}
|
|
282
290
|
|
|
283
|
-
|
|
284
291
|
if (applicableCount === 0) {
|
|
285
292
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
286
293
|
}
|
|
287
294
|
if (occurrences.length) {
|
|
288
|
-
return {
|
|
295
|
+
return {
|
|
296
|
+
ruleId: rule.ruleId,
|
|
297
|
+
outcome: 'fail',
|
|
298
|
+
severity: rule.defaultSeverity || 'minor',
|
|
299
|
+
occurrences
|
|
300
|
+
};
|
|
289
301
|
}
|
|
290
302
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
291
303
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
// NOTE: Repo ruleId contract requires ENGINE_TAG prefix in the rule id.
|
|
@@ -15,7 +17,13 @@ const meta = {
|
|
|
15
17
|
tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'buttons', 'name'],
|
|
16
18
|
wcagSc: ['4.1.2'],
|
|
17
19
|
normativeMappings: [
|
|
18
|
-
{
|
|
20
|
+
{
|
|
21
|
+
standard: 'WCAG',
|
|
22
|
+
version: '2.2',
|
|
23
|
+
requirement: '4.1.2',
|
|
24
|
+
title: 'Name, Role, Value',
|
|
25
|
+
conformanceLevel: 'A'
|
|
26
|
+
}
|
|
19
27
|
],
|
|
20
28
|
defaultSeverity: 'serious',
|
|
21
29
|
category: 'robust',
|
|
@@ -25,13 +33,15 @@ const meta = {
|
|
|
25
33
|
};
|
|
26
34
|
|
|
27
35
|
function runInPage(ctx) {
|
|
28
|
-
const {
|
|
36
|
+
const { helpers, rule } = ctx;
|
|
29
37
|
|
|
30
38
|
const occurrences = [];
|
|
31
39
|
let applicableCount = 0;
|
|
32
40
|
|
|
33
41
|
function normalizeWs(s) {
|
|
34
|
-
return String(s || '')
|
|
42
|
+
return String(s || '')
|
|
43
|
+
.replace(/\s+/g, ' ')
|
|
44
|
+
.trim();
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
function getConservativeSubtreeText(container) {
|
|
@@ -39,14 +49,13 @@ function runInPage(ctx) {
|
|
|
39
49
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
40
50
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
41
51
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
42
|
-
// (
|
|
43
|
-
// "<
|
|
44
|
-
// icon-button pattern).
|
|
52
|
+
// (covers the common "<a><img alt='...'></a>" logo-link /
|
|
53
|
+
// "<button><img alt='...'></button>" icon-button pattern).
|
|
45
54
|
if (helpers.getContentNameInfo) {
|
|
46
55
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
47
56
|
return info && info.present ? info.value : '';
|
|
48
57
|
}
|
|
49
|
-
const t =
|
|
58
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
50
59
|
return t.replace(/\s+/g, ' ').trim();
|
|
51
60
|
}
|
|
52
61
|
|
|
@@ -55,19 +64,23 @@ function runInPage(ctx) {
|
|
|
55
64
|
const type = normalizeWs(el.getAttribute ? el.getAttribute('type') : '').toLowerCase();
|
|
56
65
|
if (type !== 'button' && type !== 'submit' && type !== 'reset') return '';
|
|
57
66
|
const vAttr = el.getAttribute ? el.getAttribute('value') : '';
|
|
58
|
-
return normalizeWs(vAttr != null ? vAttr :
|
|
67
|
+
return normalizeWs(vAttr != null ? vAttr : typeof el.value === 'string' ? el.value : '');
|
|
59
68
|
} catch {
|
|
60
69
|
return '';
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
const selector =
|
|
65
|
-
|
|
73
|
+
const selector =
|
|
74
|
+
'button, input[type="button"], input[type="submit"], input[type="reset"], [role="button"]';
|
|
75
|
+
const nodes = helpers.queryAllSmart
|
|
76
|
+
? helpers.queryAllSmart(selector)
|
|
77
|
+
: helpers.queryAll(selector);
|
|
66
78
|
|
|
67
79
|
for (const el of nodes) {
|
|
68
80
|
// isAccTreeEligible returns { eligible, reasons }, not a boolean.
|
|
69
81
|
const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
|
|
70
|
-
const eligible =
|
|
82
|
+
const eligible =
|
|
83
|
+
typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
|
|
71
84
|
if (!eligible) continue;
|
|
72
85
|
|
|
73
86
|
applicableCount += 1;
|
|
@@ -81,8 +94,9 @@ function runInPage(ctx) {
|
|
|
81
94
|
// getAccessibleNameInfo only resolves programmatic mechanisms (aria-labelledby,
|
|
82
95
|
// aria-label, native <label> association, title) — it never falls back to
|
|
83
96
|
// subtree content — so it's safe to trust directly whenever present.
|
|
84
|
-
const trustedProgrammaticName =
|
|
85
|
-
|
|
97
|
+
const trustedProgrammaticName = normalizeWs(
|
|
98
|
+
nameInfo && nameInfo.present && typeof nameInfo.value === 'string' ? nameInfo.value : ''
|
|
99
|
+
);
|
|
86
100
|
const explicitProg = !!trustedProgrammaticName;
|
|
87
101
|
|
|
88
102
|
let inputValueName = '';
|
|
@@ -94,15 +108,22 @@ function runInPage(ctx) {
|
|
|
94
108
|
// one of these roles is no longer semantically a button — per the WAI-ARIA
|
|
95
109
|
// Accessible Name and Description Computation spec these roles are
|
|
96
110
|
// name-from-author-only, and their rendered content represents a VALUE,
|
|
97
|
-
// not a NAME
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
// not a NAME. E.g. <button role="combobox">List</button> where "List" is
|
|
112
|
+
// the combobox's currently selected value, not a label — crediting it as
|
|
113
|
+
// the accessible name would mask a missing name.
|
|
114
|
+
const VALUE_ROLES = [
|
|
115
|
+
'textbox',
|
|
116
|
+
'progressbar',
|
|
117
|
+
'scrollbar',
|
|
118
|
+
'slider',
|
|
119
|
+
'spinbutton',
|
|
120
|
+
'combobox',
|
|
121
|
+
'listbox'
|
|
122
|
+
];
|
|
123
|
+
const isContentNameCandidate =
|
|
124
|
+
(tag === 'button' || role === 'button') && !VALUE_ROLES.includes(roleNorm);
|
|
104
125
|
const contentName =
|
|
105
|
-
|
|
126
|
+
!trustedProgrammaticName && !inputValueName && isContentNameCandidate
|
|
106
127
|
? getConservativeSubtreeText(el)
|
|
107
128
|
: '';
|
|
108
129
|
|
|
@@ -112,10 +133,14 @@ function runInPage(ctx) {
|
|
|
112
133
|
// Only compute the richer eligibility-info payload (used solely for
|
|
113
134
|
// the occurrence's visibilityFilter) once we know an occurrence is
|
|
114
135
|
// actually being built, rather than for every applicable element.
|
|
115
|
-
const eligInfo = helpers.getEligibilityInfo
|
|
136
|
+
const eligInfo = helpers.getEligibilityInfo
|
|
137
|
+
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
138
|
+
: null;
|
|
116
139
|
|
|
117
140
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
118
|
-
const html = helpers.getOuterHtmlSnippet
|
|
141
|
+
const html = helpers.getOuterHtmlSnippet
|
|
142
|
+
? helpers.getOuterHtmlSnippet(el)
|
|
143
|
+
: el.outerHTML || '';
|
|
119
144
|
|
|
120
145
|
occurrences.push({
|
|
121
146
|
selector: stableSelector,
|
|
@@ -148,7 +173,12 @@ function runInPage(ctx) {
|
|
|
148
173
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
149
174
|
}
|
|
150
175
|
if (occurrences.length) {
|
|
151
|
-
return {
|
|
176
|
+
return {
|
|
177
|
+
ruleId: rule.ruleId,
|
|
178
|
+
outcome: 'fail',
|
|
179
|
+
severity: rule.defaultSeverity || 'minor',
|
|
180
|
+
occurrences
|
|
181
|
+
};
|
|
152
182
|
}
|
|
153
183
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
154
184
|
}
|