@surea11y/core 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -7
- package/LICENSE +373 -21
- package/README.md +67 -1
- package/bin/core.js +144 -19
- package/docs/API_STABILITY.md +1 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +54 -1
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/INTEGRATION.md +18 -0
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +42 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +55 -16
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +42 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +30 -8
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +109 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
- package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +17 -6
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -7
- package/src/checks/manual/empty-table-header-manual.js +17 -6
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -7
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
- package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +26 -20
- package/src/checks/manual/landmark-unique-manual.js +41 -15
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +16 -5
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +31 -22
- package/src/checks/manual/page-title-patterns-manual.js +78 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +241 -48
- package/src/checks/manual/scope-attr-valid-manual.js +10 -3
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +10 -3
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1127 -886
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4175 -3917
- package/src/core/dom-runner.js +720 -604
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +58 -41
- package/src/core.js +36552 -29111
- package/src/i18n/en.js +1194 -889
- package/src/i18n/fr.js +1136 -795
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +63 -43
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -33,7 +33,8 @@ const id = 'autocomplete-valid';
|
|
|
33
33
|
|
|
34
34
|
const meta = {
|
|
35
35
|
title: 'autocomplete attribute must be a valid autofill value',
|
|
36
|
-
description:
|
|
36
|
+
description:
|
|
37
|
+
'Checks that a non-empty autocomplete attribute is "on"/"off" or a well-formed autofill detail token list.',
|
|
37
38
|
i18n: {
|
|
38
39
|
titleKey: 'autocompleteValid_title',
|
|
39
40
|
descriptionKey: 'autocompleteValid_description'
|
|
@@ -42,7 +43,13 @@ const meta = {
|
|
|
42
43
|
tags: ['wcag21aa', 'wcag135', 'forms', 'atomic', 'automatic'],
|
|
43
44
|
wcagSc: ['1.3.5'],
|
|
44
45
|
normativeMappings: [
|
|
45
|
-
{
|
|
46
|
+
{
|
|
47
|
+
standard: 'WCAG',
|
|
48
|
+
version: '2.2',
|
|
49
|
+
requirement: '1.3.5',
|
|
50
|
+
title: 'Identify Input Purpose',
|
|
51
|
+
conformanceLevel: 'AA'
|
|
52
|
+
}
|
|
46
53
|
],
|
|
47
54
|
defaultSeverity: 'moderate',
|
|
48
55
|
category: 'perceivable',
|
|
@@ -52,20 +59,63 @@ const meta = {
|
|
|
52
59
|
};
|
|
53
60
|
|
|
54
61
|
function runInPage(ctx) {
|
|
55
|
-
const {
|
|
62
|
+
const { helpers, rule } = ctx;
|
|
56
63
|
|
|
57
64
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
58
65
|
// ("runInPage MUST be self-contained").
|
|
59
66
|
const FIELD_NAMES = new Set([
|
|
60
|
-
'name',
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
'
|
|
67
|
+
'name',
|
|
68
|
+
'honorific-prefix',
|
|
69
|
+
'given-name',
|
|
70
|
+
'additional-name',
|
|
71
|
+
'family-name',
|
|
72
|
+
'honorific-suffix',
|
|
73
|
+
'nickname',
|
|
74
|
+
'username',
|
|
75
|
+
'new-password',
|
|
76
|
+
'current-password',
|
|
77
|
+
'one-time-code',
|
|
78
|
+
'organization-title',
|
|
79
|
+
'organization',
|
|
80
|
+
'street-address',
|
|
81
|
+
'address-line1',
|
|
82
|
+
'address-line2',
|
|
83
|
+
'address-line3',
|
|
84
|
+
'address-level4',
|
|
85
|
+
'address-level3',
|
|
86
|
+
'address-level2',
|
|
87
|
+
'address-level1',
|
|
88
|
+
'country',
|
|
89
|
+
'country-name',
|
|
90
|
+
'postal-code',
|
|
91
|
+
'cc-name',
|
|
92
|
+
'cc-given-name',
|
|
93
|
+
'cc-additional-name',
|
|
94
|
+
'cc-family-name',
|
|
95
|
+
'cc-number',
|
|
96
|
+
'cc-exp',
|
|
97
|
+
'cc-exp-month',
|
|
98
|
+
'cc-exp-year',
|
|
99
|
+
'cc-csc',
|
|
100
|
+
'cc-type',
|
|
101
|
+
'transaction-currency',
|
|
102
|
+
'transaction-amount',
|
|
103
|
+
'language',
|
|
104
|
+
'bday',
|
|
105
|
+
'bday-day',
|
|
106
|
+
'bday-month',
|
|
107
|
+
'bday-year',
|
|
108
|
+
'sex',
|
|
109
|
+
'tel',
|
|
110
|
+
'tel-country-code',
|
|
111
|
+
'tel-national',
|
|
112
|
+
'tel-area-code',
|
|
113
|
+
'tel-local',
|
|
114
|
+
'tel-extension',
|
|
115
|
+
'email',
|
|
116
|
+
'impp',
|
|
117
|
+
'url',
|
|
118
|
+
'photo'
|
|
69
119
|
]);
|
|
70
120
|
const CONTACT_MODALITY = new Set(['home', 'work', 'mobile', 'fax', 'pager', 'impp']);
|
|
71
121
|
|
|
@@ -75,7 +125,8 @@ function runInPage(ctx) {
|
|
|
75
125
|
if (tokens.length === 1 && (tokens[0] === 'on' || tokens[0] === 'off')) return true;
|
|
76
126
|
|
|
77
127
|
let i = 0;
|
|
78
|
-
if (tokens[i] && tokens[i].startsWith('section-') && tokens[i].length > 'section-'.length)
|
|
128
|
+
if (tokens[i] && tokens[i].startsWith('section-') && tokens[i].length > 'section-'.length)
|
|
129
|
+
i += 1;
|
|
79
130
|
if (tokens[i] === 'shipping' || tokens[i] === 'billing') i += 1;
|
|
80
131
|
if (CONTACT_MODALITY.has(tokens[i])) i += 1;
|
|
81
132
|
|
|
@@ -87,7 +138,9 @@ function runInPage(ctx) {
|
|
|
87
138
|
return FIELD_NAMES.has(remaining[0]);
|
|
88
139
|
}
|
|
89
140
|
|
|
90
|
-
const nodes = helpers.queryAllSmart
|
|
141
|
+
const nodes = helpers.queryAllSmart
|
|
142
|
+
? helpers.queryAllSmart('input, select, textarea')
|
|
143
|
+
: helpers.queryAll('input, select, textarea');
|
|
91
144
|
|
|
92
145
|
const occurrences = [];
|
|
93
146
|
let applicableCount = 0;
|
|
@@ -103,7 +156,7 @@ function runInPage(ctx) {
|
|
|
103
156
|
|
|
104
157
|
const tag = el.tagName.toLowerCase();
|
|
105
158
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
106
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
159
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
107
160
|
|
|
108
161
|
occurrences.push({
|
|
109
162
|
selector: stableSelector,
|
|
@@ -125,7 +178,12 @@ function runInPage(ctx) {
|
|
|
125
178
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
126
179
|
}
|
|
127
180
|
if (occurrences.length) {
|
|
128
|
-
return {
|
|
181
|
+
return {
|
|
182
|
+
ruleId: rule.ruleId,
|
|
183
|
+
outcome: 'fail',
|
|
184
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
185
|
+
occurrences
|
|
186
|
+
};
|
|
129
187
|
}
|
|
130
188
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
131
189
|
}
|
|
@@ -27,7 +27,8 @@ const id = 'avoid-inline-spacing';
|
|
|
27
27
|
|
|
28
28
|
const meta = {
|
|
29
29
|
title: 'Inline style must not force text spacing with !important',
|
|
30
|
-
description:
|
|
30
|
+
description:
|
|
31
|
+
'Checks that inline style does not set line-height, letter-spacing, or word-spacing with !important, which blocks user text-spacing overrides.',
|
|
31
32
|
i18n: {
|
|
32
33
|
titleKey: 'avoidInlineSpacing_title',
|
|
33
34
|
descriptionKey: 'avoidInlineSpacing_description'
|
|
@@ -36,7 +37,13 @@ const meta = {
|
|
|
36
37
|
tags: ['wcag21aa', 'wcag1412', 'structure', 'atomic', 'automatic'],
|
|
37
38
|
wcagSc: ['1.4.12'],
|
|
38
39
|
normativeMappings: [
|
|
39
|
-
{
|
|
40
|
+
{
|
|
41
|
+
standard: 'WCAG',
|
|
42
|
+
version: '2.2',
|
|
43
|
+
requirement: '1.4.12',
|
|
44
|
+
title: 'Text Spacing',
|
|
45
|
+
conformanceLevel: 'AA'
|
|
46
|
+
}
|
|
40
47
|
],
|
|
41
48
|
defaultSeverity: 'moderate',
|
|
42
49
|
category: 'perceivable',
|
|
@@ -46,11 +53,13 @@ const meta = {
|
|
|
46
53
|
};
|
|
47
54
|
|
|
48
55
|
function runInPage(ctx) {
|
|
49
|
-
const {
|
|
56
|
+
const { helpers, rule } = ctx;
|
|
50
57
|
|
|
51
58
|
const SPACING_PROPS = ['line-height', 'letter-spacing', 'word-spacing'];
|
|
52
59
|
|
|
53
|
-
const nodes = helpers.queryAllSmart
|
|
60
|
+
const nodes = helpers.queryAllSmart
|
|
61
|
+
? helpers.queryAllSmart('[style]')
|
|
62
|
+
: helpers.queryAll('[style]');
|
|
54
63
|
|
|
55
64
|
const occurrences = [];
|
|
56
65
|
let applicableCount = 0;
|
|
@@ -76,7 +85,7 @@ function runInPage(ctx) {
|
|
|
76
85
|
|
|
77
86
|
const tag = el.tagName.toLowerCase();
|
|
78
87
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
79
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
88
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
80
89
|
|
|
81
90
|
occurrences.push({
|
|
82
91
|
selector: stableSelector,
|
|
@@ -98,7 +107,12 @@ function runInPage(ctx) {
|
|
|
98
107
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
99
108
|
}
|
|
100
109
|
if (occurrences.length) {
|
|
101
|
-
return {
|
|
110
|
+
return {
|
|
111
|
+
ruleId: rule.ruleId,
|
|
112
|
+
outcome: 'fail',
|
|
113
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
114
|
+
occurrences
|
|
115
|
+
};
|
|
102
116
|
}
|
|
103
117
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
104
118
|
}
|
|
@@ -4,58 +4,61 @@ const id = 'binary-control-name-present';
|
|
|
4
4
|
|
|
5
5
|
const meta = {
|
|
6
6
|
title: 'Binary controls have an accessible name',
|
|
7
|
-
description:
|
|
7
|
+
description:
|
|
8
|
+
'Checks that checkbox, radio, and switch controls expose a non-empty accessible name.',
|
|
8
9
|
i18n: {
|
|
9
10
|
titleKey: 'binaryControlNamePresent_title',
|
|
10
11
|
descriptionKey: 'binaryControlNamePresent_description'
|
|
11
12
|
},
|
|
12
13
|
helpUrl: null,
|
|
13
|
-
tags: [
|
|
14
|
+
tags: [
|
|
15
|
+
'wcag2a',
|
|
16
|
+
'wcag412',
|
|
17
|
+
'forms',
|
|
18
|
+
'atomic',
|
|
19
|
+
'automatic',
|
|
20
|
+
'name',
|
|
21
|
+
'checkbox',
|
|
22
|
+
'radio',
|
|
23
|
+
'switch'
|
|
24
|
+
],
|
|
14
25
|
wcagSc: ['4.1.2'],
|
|
15
26
|
normativeMappings: [
|
|
16
|
-
{
|
|
27
|
+
{
|
|
28
|
+
standard: 'WCAG',
|
|
29
|
+
version: '2.2',
|
|
30
|
+
requirement: '4.1.2',
|
|
31
|
+
title: 'Name, Role, Value',
|
|
32
|
+
conformanceLevel: 'A'
|
|
33
|
+
}
|
|
17
34
|
],
|
|
18
35
|
defaultSeverity: 'serious',
|
|
19
36
|
category: 'robust',
|
|
20
37
|
type: 'automatic',
|
|
21
38
|
defaultConfidence: 'high',
|
|
22
|
-
coverage: {
|
|
39
|
+
coverage: {
|
|
40
|
+
facetsBySc: { '4.1.2': ['checkbox-name-present', 'radio-name-present', 'switch-name-present'] }
|
|
41
|
+
}
|
|
23
42
|
};
|
|
24
43
|
|
|
25
44
|
function runInPage(ctx) {
|
|
26
45
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
46
|
+
const getEligibilityInfo =
|
|
47
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
48
|
|
|
32
49
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
50
|
+
return String(s || '')
|
|
51
|
+
.replace(/\s+/g, ' ')
|
|
52
|
+
.trim();
|
|
34
53
|
}
|
|
35
54
|
|
|
36
55
|
function getAttr(el, name) {
|
|
37
56
|
try {
|
|
38
57
|
if (!el || !el.getAttribute) return '';
|
|
39
58
|
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;
|
|
59
|
+
} catch {
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
function buildLabelForMap(doc) {
|
|
@@ -85,7 +88,7 @@ function runInPage(ctx) {
|
|
|
85
88
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
86
89
|
return info && info.present ? info.value : '';
|
|
87
90
|
}
|
|
88
|
-
const t =
|
|
91
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
89
92
|
return t.replace(/\s+/g, ' ').trim();
|
|
90
93
|
}
|
|
91
94
|
|
|
@@ -132,17 +135,9 @@ function runInPage(ctx) {
|
|
|
132
135
|
return '';
|
|
133
136
|
}
|
|
134
137
|
|
|
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
138
|
function isEligibleAcc(helpers, el, ctx) {
|
|
145
|
-
const fn =
|
|
139
|
+
const fn =
|
|
140
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
146
141
|
if (!fn) return true;
|
|
147
142
|
try {
|
|
148
143
|
const r = fn(el, ctx);
|
|
@@ -153,13 +148,14 @@ function runInPage(ctx) {
|
|
|
153
148
|
}
|
|
154
149
|
}
|
|
155
150
|
|
|
156
|
-
|
|
157
151
|
const occurrences = [];
|
|
158
152
|
let applicableCount = 0;
|
|
159
153
|
|
|
160
|
-
const selector =
|
|
161
|
-
|
|
162
|
-
|
|
154
|
+
const selector =
|
|
155
|
+
'input[type="checkbox"], input[type="radio"], [role="checkbox"], [role="radio"], [role="switch"]';
|
|
156
|
+
const nodes = helpers.queryAllSmart
|
|
157
|
+
? helpers.queryAllSmart(selector)
|
|
158
|
+
: helpers.queryAll(selector);
|
|
163
159
|
|
|
164
160
|
// Precompute label[for] associations once for speed/determinism.
|
|
165
161
|
const labelForMap = buildLabelForMap(document);
|
|
@@ -223,7 +219,11 @@ function runInPage(ctx) {
|
|
|
223
219
|
}
|
|
224
220
|
|
|
225
221
|
// ARIA roles (checkbox/radio/switch) can use content as name
|
|
226
|
-
if (
|
|
222
|
+
if (
|
|
223
|
+
controlType === 'switch' ||
|
|
224
|
+
controlType === 'aria-checkbox' ||
|
|
225
|
+
controlType === 'aria-radio'
|
|
226
|
+
) {
|
|
227
227
|
const t = getConservativeSubtreeText(document, el);
|
|
228
228
|
if (t) return { ok: true, method: 'content' };
|
|
229
229
|
}
|
|
@@ -240,7 +240,7 @@ function runInPage(ctx) {
|
|
|
240
240
|
const type = getAttr(el, 'type').toLowerCase();
|
|
241
241
|
const role = getAttr(el, 'role').toLowerCase();
|
|
242
242
|
|
|
243
|
-
let controlType
|
|
243
|
+
let controlType;
|
|
244
244
|
if (tag === 'input' && type === 'checkbox') controlType = 'checkbox';
|
|
245
245
|
else if (tag === 'input' && type === 'radio') controlType = 'radio';
|
|
246
246
|
else if (role === 'checkbox') controlType = 'aria-checkbox';
|
|
@@ -254,10 +254,16 @@ function runInPage(ctx) {
|
|
|
254
254
|
if (res.ok) continue;
|
|
255
255
|
|
|
256
256
|
const eligInfo = getEligibilityInfo
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
? (() => {
|
|
258
|
+
try {
|
|
259
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
})()
|
|
264
|
+
: null;
|
|
259
265
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
260
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
266
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
261
267
|
|
|
262
268
|
occurrences.push({
|
|
263
269
|
selector: stableSelector,
|
|
@@ -280,12 +286,16 @@ function runInPage(ctx) {
|
|
|
280
286
|
});
|
|
281
287
|
}
|
|
282
288
|
|
|
283
|
-
|
|
284
289
|
if (applicableCount === 0) {
|
|
285
290
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
286
291
|
}
|
|
287
292
|
if (occurrences.length) {
|
|
288
|
-
return {
|
|
293
|
+
return {
|
|
294
|
+
ruleId: rule.ruleId,
|
|
295
|
+
outcome: 'fail',
|
|
296
|
+
severity: rule.defaultSeverity || 'minor',
|
|
297
|
+
occurrences
|
|
298
|
+
};
|
|
289
299
|
}
|
|
290
300
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
291
301
|
}
|
|
@@ -15,7 +15,13 @@ const meta = {
|
|
|
15
15
|
tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'buttons', 'name'],
|
|
16
16
|
wcagSc: ['4.1.2'],
|
|
17
17
|
normativeMappings: [
|
|
18
|
-
{
|
|
18
|
+
{
|
|
19
|
+
standard: 'WCAG',
|
|
20
|
+
version: '2.2',
|
|
21
|
+
requirement: '4.1.2',
|
|
22
|
+
title: 'Name, Role, Value',
|
|
23
|
+
conformanceLevel: 'A'
|
|
24
|
+
}
|
|
19
25
|
],
|
|
20
26
|
defaultSeverity: 'serious',
|
|
21
27
|
category: 'robust',
|
|
@@ -25,13 +31,15 @@ const meta = {
|
|
|
25
31
|
};
|
|
26
32
|
|
|
27
33
|
function runInPage(ctx) {
|
|
28
|
-
const {
|
|
34
|
+
const { helpers, rule } = ctx;
|
|
29
35
|
|
|
30
36
|
const occurrences = [];
|
|
31
37
|
let applicableCount = 0;
|
|
32
38
|
|
|
33
39
|
function normalizeWs(s) {
|
|
34
|
-
return String(s || '')
|
|
40
|
+
return String(s || '')
|
|
41
|
+
.replace(/\s+/g, ' ')
|
|
42
|
+
.trim();
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
function getConservativeSubtreeText(container) {
|
|
@@ -46,7 +54,7 @@ function runInPage(ctx) {
|
|
|
46
54
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
47
55
|
return info && info.present ? info.value : '';
|
|
48
56
|
}
|
|
49
|
-
const t =
|
|
57
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
50
58
|
return t.replace(/\s+/g, ' ').trim();
|
|
51
59
|
}
|
|
52
60
|
|
|
@@ -55,19 +63,23 @@ function runInPage(ctx) {
|
|
|
55
63
|
const type = normalizeWs(el.getAttribute ? el.getAttribute('type') : '').toLowerCase();
|
|
56
64
|
if (type !== 'button' && type !== 'submit' && type !== 'reset') return '';
|
|
57
65
|
const vAttr = el.getAttribute ? el.getAttribute('value') : '';
|
|
58
|
-
return normalizeWs(vAttr != null ? vAttr :
|
|
66
|
+
return normalizeWs(vAttr != null ? vAttr : typeof el.value === 'string' ? el.value : '');
|
|
59
67
|
} catch {
|
|
60
68
|
return '';
|
|
61
69
|
}
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
const selector =
|
|
65
|
-
|
|
72
|
+
const selector =
|
|
73
|
+
'button, input[type="button"], input[type="submit"], input[type="reset"], [role="button"]';
|
|
74
|
+
const nodes = helpers.queryAllSmart
|
|
75
|
+
? helpers.queryAllSmart(selector)
|
|
76
|
+
: helpers.queryAll(selector);
|
|
66
77
|
|
|
67
78
|
for (const el of nodes) {
|
|
68
79
|
// isAccTreeEligible returns { eligible, reasons }, not a boolean.
|
|
69
80
|
const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
|
|
70
|
-
const eligible =
|
|
81
|
+
const eligible =
|
|
82
|
+
typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
|
|
71
83
|
if (!eligible) continue;
|
|
72
84
|
|
|
73
85
|
applicableCount += 1;
|
|
@@ -81,8 +93,9 @@ function runInPage(ctx) {
|
|
|
81
93
|
// getAccessibleNameInfo only resolves programmatic mechanisms (aria-labelledby,
|
|
82
94
|
// aria-label, native <label> association, title) — it never falls back to
|
|
83
95
|
// subtree content — so it's safe to trust directly whenever present.
|
|
84
|
-
const trustedProgrammaticName =
|
|
85
|
-
|
|
96
|
+
const trustedProgrammaticName = normalizeWs(
|
|
97
|
+
nameInfo && nameInfo.present && typeof nameInfo.value === 'string' ? nameInfo.value : ''
|
|
98
|
+
);
|
|
86
99
|
const explicitProg = !!trustedProgrammaticName;
|
|
87
100
|
|
|
88
101
|
let inputValueName = '';
|
|
@@ -94,15 +107,23 @@ function runInPage(ctx) {
|
|
|
94
107
|
// one of these roles is no longer semantically a button — per the WAI-ARIA
|
|
95
108
|
// Accessible Name and Description Computation spec these roles are
|
|
96
109
|
// name-from-author-only, and their rendered content represents a VALUE,
|
|
97
|
-
// not a NAME (
|
|
98
|
-
// source). Found on a real page (Spotify's "sort by" control): a
|
|
110
|
+
// not a NAME. Found on a real page (Spotify's "sort by" control): a
|
|
99
111
|
// <button role="combobox">List</button> where "List" is the combobox's
|
|
100
112
|
// currently selected value, not a label for what the combobox is —
|
|
101
113
|
// crediting it as the accessible name masked a real missing-name bug.
|
|
102
|
-
const VALUE_ROLES = [
|
|
103
|
-
|
|
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
|
}
|
|
@@ -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',
|
|
@@ -83,7 +90,8 @@ function runInPage(ctx) {
|
|
|
83
90
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
const isAccTreeEligible =
|
|
93
|
+
const isAccTreeEligible =
|
|
94
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
87
95
|
|
|
88
96
|
function isExposedToAt(el) {
|
|
89
97
|
if (!isAccTreeEligible) return true;
|
|
@@ -121,7 +129,7 @@ function runInPage(ctx) {
|
|
|
121
129
|
}
|
|
122
130
|
|
|
123
131
|
function hasWorkingAnchorLink() {
|
|
124
|
-
let links
|
|
132
|
+
let links;
|
|
125
133
|
try {
|
|
126
134
|
links = document.querySelectorAll('a[href]');
|
|
127
135
|
} catch {
|
|
@@ -140,7 +148,7 @@ function runInPage(ctx) {
|
|
|
140
148
|
fragment = fragment.trim();
|
|
141
149
|
if (!fragment) continue;
|
|
142
150
|
|
|
143
|
-
let target
|
|
151
|
+
let target;
|
|
144
152
|
try {
|
|
145
153
|
target = document.getElementById(fragment);
|
|
146
154
|
} catch {
|
|
@@ -167,32 +175,41 @@ function runInPage(ctx) {
|
|
|
167
175
|
|
|
168
176
|
const mainLandmark = hasMainLandmark();
|
|
169
177
|
const anchorLink = mainLandmark ? false : hasWorkingAnchorLink();
|
|
170
|
-
const heading =
|
|
178
|
+
const heading = mainLandmark || anchorLink ? false : hasHeading();
|
|
171
179
|
|
|
172
180
|
if (mainLandmark || anchorLink || heading) {
|
|
173
181
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
174
182
|
}
|
|
175
183
|
|
|
176
184
|
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
|
-
|
|
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
|
+
}
|
|
192
204
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return {
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
ruleId: rule.ruleId,
|
|
209
|
+
outcome: 'fail',
|
|
210
|
+
severity: rule.defaultSeverity || 'serious',
|
|
211
|
+
occurrences
|
|
212
|
+
};
|
|
196
213
|
}
|
|
197
214
|
|
|
198
|
-
module.exports = { id, meta, runInPage, applicability };
|
|
215
|
+
module.exports = { id, meta, runInPage, applicability };
|