@surea11y/core 1.5.0 → 1.7.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 +240 -149
- package/README.md +51 -44
- package/docs/ACT_RULE_MAPPING.md +245 -0
- package/docs/API_STABILITY.md +53 -5
- package/docs/BINDING_AUTHORS_GUIDE.md +106 -4
- package/docs/DESIGN_CHALLENGES.md +367 -0
- package/docs/EARL.md +100 -0
- package/docs/ENGINE_OPTIONS.md +42 -4
- package/docs/I18N.md +4 -4
- package/docs/INTEGRATION.md +4 -2
- package/docs/LIMITATIONS.md +9 -5
- package/docs/OUTPUT_SCHEMA.md +44 -6
- package/docs/POLICY.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_AUTHORING.md +63 -36
- package/docs/RULE_CATALOG.md +1928 -169
- package/docs/RULE_HELPERS.md +333 -0
- package/docs/RULE_TAXONOMY.md +27 -6
- package/docs/SARIF.md +21 -2
- package/docs/TROUBLESHOOTING.md +2 -2
- package/docs/WCAG_CONFORMANCE.md +34 -10
- package/package.json +11 -9
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +74 -10
- package/src/checks/automatic/aria-allowed-role.js +34 -25
- package/src/checks/automatic/aria-braille-equivalent.js +21 -13
- package/src/checks/automatic/aria-conditional-attr.js +22 -15
- package/src/checks/automatic/aria-deprecated-role.js +13 -1
- package/src/checks/automatic/aria-hidden-body.js +3 -3
- package/src/checks/automatic/aria-hidden-focus.js +5 -5
- package/src/checks/automatic/aria-prohibited-attr.js +23 -18
- package/src/checks/automatic/aria-prohibited-children.js +136 -43
- package/src/checks/automatic/aria-required-attr.js +119 -24
- package/src/checks/automatic/aria-required-children.js +54 -30
- package/src/checks/automatic/aria-required-parent.js +93 -15
- package/src/checks/automatic/aria-role-name-present.js +37 -23
- package/src/checks/automatic/aria-roles-valid.js +52 -21
- package/src/checks/automatic/aria-valid-attr-value.js +89 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -10
- package/src/checks/automatic/autocomplete-valid.js +2 -2
- package/src/checks/automatic/avoid-inline-spacing.js +133 -6
- package/src/checks/automatic/binary-control-name-present.js +27 -5
- package/src/checks/automatic/button-name-present.js +92 -6
- package/src/checks/automatic/combobox-name-present.js +26 -6
- package/src/checks/automatic/contrast-computable.js +42 -0
- package/src/checks/automatic/contrast-enhanced.js +33 -1
- package/src/checks/automatic/contrast-minimum.js +33 -1
- package/src/checks/automatic/css-orientation-lock.js +138 -24
- package/src/checks/automatic/definition-list-children-valid.js +7 -8
- package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
- package/src/checks/automatic/dialog-name-present.js +20 -2
- package/src/checks/automatic/duplicate-id-aria.js +10 -3
- package/src/checks/automatic/duplicate-id.js +203 -0
- package/src/checks/automatic/embed-text-alternative-present.js +2 -2
- package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
- package/src/checks/automatic/form-control-single-label.js +10 -1
- package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
- package/src/checks/automatic/iframe-focusable-content.js +68 -7
- package/src/checks/automatic/iframe-name-present.js +37 -3
- package/src/checks/automatic/iframe-title-unique.js +1 -1
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +204 -68
- package/src/checks/automatic/link-in-text-block.js +285 -29
- package/src/checks/automatic/link-name-present.js +22 -1
- package/src/checks/automatic/list-children-valid.js +6 -6
- package/src/checks/automatic/listbox-name-present.js +28 -8
- package/src/checks/automatic/listitem-parent-valid.js +4 -4
- package/src/checks/automatic/menuitem-name-present.js +20 -2
- package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
- package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
- package/src/checks/automatic/meter-name-present.js +23 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
- package/src/checks/automatic/option-name-present.js +23 -4
- package/src/checks/automatic/page-title-present.js +21 -3
- package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
- package/src/checks/automatic/progressbar-name-present.js +23 -4
- package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +64 -16
- package/src/checks/automatic/searchbox-name-present.js +28 -8
- package/src/checks/automatic/server-side-image-map-absent.js +1 -1
- package/src/checks/automatic/slider-name-present.js +27 -6
- package/src/checks/automatic/spinbutton-name-present.js +28 -8
- package/src/checks/automatic/summary-name-present.js +18 -2
- package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
- package/src/checks/automatic/svg-text-alternative-present.js +13 -10
- package/src/checks/automatic/tab-name-present.js +21 -2
- package/src/checks/automatic/table-headers-attr-valid.js +43 -8
- package/src/checks/automatic/table-th-has-data-cells.js +61 -5
- package/src/checks/automatic/target-size-minimum.js +155 -58
- package/src/checks/automatic/td-has-header.js +24 -23
- package/src/checks/automatic/textbox-name-present.js +28 -8
- package/src/checks/automatic/tooltip-name-present.js +21 -2
- package/src/checks/automatic/treeitem-name-present.js +23 -4
- package/src/checks/automatic/valid-lang.js +92 -7
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +3 -3
- package/src/checks/manual/area-alt-decorative-manual.js +7 -0
- package/src/checks/manual/area-alt-quality-manual.js +6 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +5 -5
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/empty-heading-manual.js +58 -11
- package/src/checks/manual/empty-table-header-manual.js +8 -8
- package/src/checks/manual/focus-order-semantics-manual.js +15 -15
- package/src/checks/manual/form-control-label-quality-manual.js +563 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -2
- package/src/checks/manual/heading-order-manual.js +3 -3
- package/src/checks/manual/heading-quality-manual.js +338 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
- package/src/checks/manual/image-redundant-alt-manual.js +4 -4
- package/src/checks/manual/img-alt-decorative-manual.js +211 -52
- package/src/checks/manual/img-alt-quality-manual.js +7 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
- package/src/checks/manual/label-title-only-manual.js +4 -4
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
- package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
- package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
- package/src/checks/manual/landmark-one-main-manual.js +6 -6
- package/src/checks/manual/landmark-unique-manual.js +9 -9
- package/src/checks/manual/link-name-quality-manual.js +161 -32
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +2 -2
- package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
- package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
- package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/p-as-heading-manual.js +4 -4
- package/src/checks/manual/page-has-heading-one-manual.js +6 -6
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/password-paste-enabled-manual.js +255 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
- package/src/checks/manual/region-manual.js +19 -19
- package/src/checks/manual/scope-attr-valid-manual.js +2 -2
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +5 -5
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +2 -2
- package/src/checks/manual/table-duplicate-name-manual.js +2 -2
- package/src/checks/manual/table-fake-caption-manual.js +2 -2
- package/src/checks/manual/video-caption-manual.js +3 -3
- package/src/checks/manual-review.js +17 -1
- package/src/core.js +8880 -41883
- package/src/earl.js +144 -0
- package/src/report.js +2 -2
- package/src/sarif.js +22 -2
- package/surea11y.browser.js +10 -37882
- package/surea11y.i18n.de.js +2 -21
- package/surea11y.i18n.es.js +2 -21
- package/surea11y.i18n.fr.js +2 -21
- package/bin/surea11y-core.js +0 -20
|
@@ -15,9 +15,14 @@
|
|
|
15
15
|
* @expectation
|
|
16
16
|
* Each attribute's value conforms to its WAI-ARIA-declared value type:
|
|
17
17
|
* boolean ("true"/"false"), tristate ("true"/"false"/"mixed"), a token
|
|
18
|
-
* from a fixed enumerated set, an integer, a real number, or an
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* from a fixed enumerated set, an integer, a real number, or an ID
|
|
19
|
+
* reference (list) that resolves to an existing element in the document.
|
|
20
|
+
* Per ACT 6a7281's own applicability ("any state or property that is
|
|
21
|
+
* NOT empty"), an explicitly empty value, including a bare boolean-style
|
|
22
|
+
* attribute with no "=value" at all, e.g. `aria-checked` alone, is out
|
|
23
|
+
* of scope for every value type, not a violation: a common, deliberate
|
|
24
|
+
* pattern in templated markup (e.g. React conditionally rendering
|
|
25
|
+
* `aria-describedby={hasError ? errorId : ''}`).
|
|
21
26
|
* @implementation-notes
|
|
22
27
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
23
28
|
* property, while engine-level hidden-subtree filtering still applies
|
|
@@ -25,16 +30,27 @@
|
|
|
25
30
|
* - ID-reference resolution (see aria-helpers.js's idExists) only flags
|
|
26
31
|
* idref-list attributes (aria-labelledby, aria-describedby,
|
|
27
32
|
* aria-controls, aria-owns, etc.) when NONE of the space-separated ids
|
|
28
|
-
* resolve
|
|
33
|
+
* resolve, a partially-dangling list (some ids exist, some don't) is
|
|
29
34
|
* left unflagged (only invalidate when every token fails to resolve).
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* Of the two single-idref attributes, aria-activedescendant is flagged
|
|
36
|
+
* whenever its one id doesn't resolve; aria-errormessage's existence is
|
|
37
|
+
* never checked (format only), ACT 6a7281's own Background
|
|
38
|
+
* text names it as a non-required property whose target "may be created
|
|
39
|
+
* in response to an event that may or may not happen" (a validation
|
|
40
|
+
* error message rendered only once the error actually occurs).
|
|
41
|
+
* - aria-controls is never a fail on a target that doesn't resolve, and
|
|
42
|
+
* this is the one place the rule reports two tiers. The controlled
|
|
43
|
+
* element is routinely built when the widget opens, so a static scan
|
|
44
|
+
* that cannot find it has not found a defect; it has found markup it
|
|
45
|
+
* cannot decide. A collapsed widget (aria-expanded="false" or
|
|
46
|
+
* aria-selected="false") passes outright, since the absence is exactly
|
|
47
|
+
* what that state means; anything else is a `cantTell` for human
|
|
48
|
+
* review. Every other idref/idref-list attribute keeps its fail: a
|
|
49
|
+
* dangling aria-labelledby or aria-owns names content that was supposed
|
|
50
|
+
* to be there already.
|
|
51
|
+
* - Two tiers in one run means helpers.resolveTieredOutcome decides the
|
|
52
|
+
* aggregate: a real fail elsewhere on the page still reports fail, and
|
|
53
|
+
* the aria-controls occurrences ride along rather than being dropped.
|
|
38
54
|
*/
|
|
39
55
|
|
|
40
56
|
const id = 'aria-valid-attr-value';
|
|
@@ -76,13 +92,15 @@ function runInPage(ctx) {
|
|
|
76
92
|
|
|
77
93
|
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*') : helpers.queryAll('*');
|
|
78
94
|
|
|
79
|
-
const
|
|
95
|
+
const failOccurrences = [];
|
|
96
|
+
const cantTellOccurrences = [];
|
|
80
97
|
let applicableCount = 0;
|
|
81
98
|
|
|
82
99
|
for (const el of nodes) {
|
|
83
100
|
if (!el || !el.attributes || !el.getAttribute) continue;
|
|
84
101
|
|
|
85
102
|
let invalid = null;
|
|
103
|
+
let review = null;
|
|
86
104
|
const attrs = el.attributes;
|
|
87
105
|
for (let i = 0; i < attrs.length; i++) {
|
|
88
106
|
const name = String(attrs[i].name || '').toLowerCase();
|
|
@@ -92,21 +110,26 @@ function runInPage(ctx) {
|
|
|
92
110
|
applicableCount += 1;
|
|
93
111
|
|
|
94
112
|
const rawValue = el.getAttribute(name);
|
|
95
|
-
const result = ariaHelpers.validateAttrValue(name, rawValue);
|
|
96
|
-
if (
|
|
113
|
+
const result = ariaHelpers.validateAttrValue(name, rawValue, el);
|
|
114
|
+
if (result.valid) continue;
|
|
115
|
+
|
|
116
|
+
const item = {
|
|
117
|
+
name,
|
|
118
|
+
value: rawValue == null ? '' : String(rawValue),
|
|
119
|
+
reason: result.reason
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (result.review) {
|
|
123
|
+
if (!review) review = [];
|
|
124
|
+
review.push(item);
|
|
125
|
+
} else {
|
|
97
126
|
if (!invalid) invalid = [];
|
|
98
|
-
invalid.push(
|
|
99
|
-
name,
|
|
100
|
-
value: rawValue == null ? '' : String(rawValue),
|
|
101
|
-
reason: result.reason
|
|
102
|
-
});
|
|
127
|
+
invalid.push(item);
|
|
103
128
|
}
|
|
104
129
|
}
|
|
105
130
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
for (const item of invalid) {
|
|
109
|
-
occurrences.push(
|
|
131
|
+
for (const item of invalid || []) {
|
|
132
|
+
failOccurrences.push(
|
|
110
133
|
helpers.reportOccurrence(el, {
|
|
111
134
|
summary: 'This element has an ARIA attribute with an invalid value.',
|
|
112
135
|
hint: 'Use a value that matches the attribute’s expected type (see the WAI-ARIA specification for this attribute).',
|
|
@@ -126,20 +149,53 @@ function runInPage(ctx) {
|
|
|
126
149
|
})
|
|
127
150
|
);
|
|
128
151
|
}
|
|
152
|
+
|
|
153
|
+
for (const item of review || []) {
|
|
154
|
+
cantTellOccurrences.push(
|
|
155
|
+
helpers.reportOccurrence(el, {
|
|
156
|
+
summary:
|
|
157
|
+
'No element with this id exists right now, so the engine cannot tell whether this reference is wrong.',
|
|
158
|
+
hint: 'Confirm the controlled element is created when the widget opens; if it never exists, remove or correct the reference.',
|
|
159
|
+
i18n: {
|
|
160
|
+
summaryKey: 'ariaValidAttrValue_summary_cantTell_idref',
|
|
161
|
+
hintKey: 'ariaValidAttrValue_hint_cantTell_idref',
|
|
162
|
+
params: { attr: item.name, value: item.value }
|
|
163
|
+
},
|
|
164
|
+
uncertainty: {
|
|
165
|
+
code: 'runtime-dependent',
|
|
166
|
+
needed: 'Whether the widget creates the referenced element when it opens.',
|
|
167
|
+
evidence: {
|
|
168
|
+
attribute: item.name,
|
|
169
|
+
referencedId: item.value,
|
|
170
|
+
resolvedAtScanTime: false
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
data: {
|
|
174
|
+
details: {
|
|
175
|
+
reasonCode: 'ARIA_ATTR_VALUE_TARGET_ABSENT',
|
|
176
|
+
attr: item.name,
|
|
177
|
+
value: item.value,
|
|
178
|
+
valueReason: item.reason
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
}
|
|
129
184
|
}
|
|
130
185
|
|
|
131
186
|
if (applicableCount === 0) {
|
|
132
187
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
133
188
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
189
|
+
|
|
190
|
+
// See helpers.resolveTieredOutcome's own header comment
|
|
191
|
+
// (src/core/dom-helpers.js): a fail-tier finding never silently discards
|
|
192
|
+
// cantTell-tier findings from the same run.
|
|
193
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
194
|
+
failOccurrences,
|
|
195
|
+
cantTellOccurrences,
|
|
196
|
+
rule.defaultSeverity || 'serious'
|
|
197
|
+
);
|
|
198
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
143
199
|
}
|
|
144
200
|
|
|
145
201
|
module.exports = { id, meta, runInPage };
|
|
@@ -16,9 +16,15 @@
|
|
|
16
16
|
* specification (catches typos / made-up attribute names, which are
|
|
17
17
|
* silently ignored by assistive technology and therefore a real,
|
|
18
18
|
* deterministic defect).
|
|
19
|
+
* Reported at CANTTELL rather than FAIL: an aria-* attribute the spec
|
|
20
|
+
* does not define is inert, so nothing about the element's exposed name,
|
|
21
|
+
* role or value changes because it is there. Where the author meant a real
|
|
22
|
+
* attribute and the element ends up without a name, that absence is the
|
|
23
|
+
* naming rules' decision, not this one's. ACT 5f99a7 maps 1.3.1/4.1.2 as
|
|
24
|
+
* secondary requirements, "less strict" than the rule itself.
|
|
19
25
|
* @implementation-notes
|
|
20
26
|
* - Distinct from aria-valid-attr-value (which validates the VALUE
|
|
21
|
-
* of a recognized attribute)
|
|
27
|
+
* of a recognized attribute), this rule only validates the attribute
|
|
22
28
|
* NAME.
|
|
23
29
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
24
30
|
* property, while engine-level hidden-subtree filtering still applies
|
|
@@ -96,6 +102,12 @@ function runInPage(ctx) {
|
|
|
96
102
|
hintKey: 'ariaValidAttr_hint_fail',
|
|
97
103
|
params: { attr: name }
|
|
98
104
|
},
|
|
105
|
+
uncertainty: {
|
|
106
|
+
code: 'spec-only',
|
|
107
|
+
needed:
|
|
108
|
+
'Whether the misspelling cost the element a name or state it has no other source for.',
|
|
109
|
+
evidence: { attribute: name, definedInAria: false, exposedEffect: 'none' }
|
|
110
|
+
},
|
|
99
111
|
data: {
|
|
100
112
|
details: { reasonCode: 'ARIA_ATTR_INVALID', attr: name }
|
|
101
113
|
}
|
|
@@ -107,15 +119,8 @@ function runInPage(ctx) {
|
|
|
107
119
|
if (applicableCount === 0) {
|
|
108
120
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
109
121
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
ruleId: rule.ruleId,
|
|
113
|
-
outcome: 'fail',
|
|
114
|
-
severity: rule.defaultSeverity || 'serious',
|
|
115
|
-
occurrences
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
122
|
+
const resolved = helpers.resolveTieredOutcome([], occurrences, rule.defaultSeverity || 'serious');
|
|
123
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
module.exports = { id, meta, runInPage };
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* order) with the full fixed field-name vocabulary from the HTML
|
|
27
27
|
* Standard, rather than validating every field-specific constraint
|
|
28
28
|
* (e.g. which contact-modality tokens are legal for which field
|
|
29
|
-
* names)
|
|
29
|
+
* names), matches this engine's established "scoped"
|
|
30
30
|
* precedent (see aria-helpers.js) for keeping high-confidence fail
|
|
31
31
|
* without reimplementing the entire spec.
|
|
32
32
|
*/
|
|
@@ -63,7 +63,7 @@ const meta = {
|
|
|
63
63
|
function runInPage(ctx) {
|
|
64
64
|
const { helpers, rule } = ctx;
|
|
65
65
|
|
|
66
|
-
// Declared inside runInPage
|
|
66
|
+
// Declared inside runInPage, see scripts/build-core.js header
|
|
67
67
|
// ("runInPage MUST be self-contained").
|
|
68
68
|
const FIELD_NAMES = new Set([
|
|
69
69
|
'name',
|
|
@@ -25,8 +25,12 @@
|
|
|
25
25
|
* - Within one declaration block, importance outranks order, so the effective
|
|
26
26
|
* declaration is the last `!important` one for that property.
|
|
27
27
|
* - ACT 78fd32/24afc2/9e45ec additionally require the text to contain a soft
|
|
28
|
-
* wrap break, which
|
|
29
|
-
*
|
|
28
|
+
* wrap break, which layout would settle and this cannot. Two shapes do
|
|
29
|
+
* establish that no wrap is possible -- text not allowed to wrap, and a
|
|
30
|
+
* fixed-width element inside a horizontally scrolling ancestor -- and those
|
|
31
|
+
* are reported for review rather than failed. Anything else is treated as
|
|
32
|
+
* wrapping, so a forced value on text that never wraps for some other reason
|
|
33
|
+
* is still reported.
|
|
30
34
|
*/
|
|
31
35
|
|
|
32
36
|
const id = 'avoid-inline-spacing';
|
|
@@ -83,6 +87,8 @@ function runInPage(ctx) {
|
|
|
83
87
|
|
|
84
88
|
const occurrences = [];
|
|
85
89
|
let applicableCount = 0;
|
|
90
|
+
const undecided = [];
|
|
91
|
+
const noWrap = [];
|
|
86
92
|
|
|
87
93
|
// Within one declaration block, importance wins over order, so the last
|
|
88
94
|
// important declaration is the one that takes effect. Passed Example 5 of ACT
|
|
@@ -148,6 +154,36 @@ function runInPage(ctx) {
|
|
|
148
154
|
return null;
|
|
149
155
|
}
|
|
150
156
|
|
|
157
|
+
// ACT 78fd32/24afc2/9e45ec apply only to text that contains a soft wrap
|
|
158
|
+
// break. Layout settles that; short of it, two shapes establish that no wrap
|
|
159
|
+
// can happen -- text not allowed to wrap, and a fixed-width element inside a
|
|
160
|
+
// horizontally scrolling ancestor, which narrowing the viewport cannot reach.
|
|
161
|
+
function cannotSoftWrap(el) {
|
|
162
|
+
const cs = computedStyleOf(el);
|
|
163
|
+
const whiteSpace = cs ? String(cs.whiteSpace || '').toLowerCase() : '';
|
|
164
|
+
if (whiteSpace === 'nowrap' || whiteSpace === 'pre') return true;
|
|
165
|
+
|
|
166
|
+
if (!/(^|;)\s*width\s*:/i.test(String(el.getAttribute('style') || ''))) return false;
|
|
167
|
+
|
|
168
|
+
const chain =
|
|
169
|
+
helpers && typeof helpers.ancestorsIncludingSelf === 'function'
|
|
170
|
+
? helpers.ancestorsIncludingSelf(el)
|
|
171
|
+
: null;
|
|
172
|
+
const ancestors = chain || [];
|
|
173
|
+
if (!chain) {
|
|
174
|
+
for (let a = el.parentElement; a; a = a.parentElement) ancestors.push(a);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const ancestor of ancestors) {
|
|
178
|
+
if (ancestor === el) continue;
|
|
179
|
+
const acs = computedStyleOf(ancestor);
|
|
180
|
+
if (!acs) continue;
|
|
181
|
+
const overflowX = String(acs.overflowX || acs.overflow || '').toLowerCase();
|
|
182
|
+
if (overflowX === 'scroll' || overflowX === 'auto') return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
151
187
|
// ACT scopes these rules to text visible on screen, and text pushed far off
|
|
152
188
|
// canvas is the one hidden shape the shared eligibility check keeps eligible.
|
|
153
189
|
function isOffScreen(el) {
|
|
@@ -228,6 +264,7 @@ function runInPage(ctx) {
|
|
|
228
264
|
if (!hasVisibleTextChild(el) || !isRendered(el) || isOffScreen(el)) continue;
|
|
229
265
|
|
|
230
266
|
const flagged = [];
|
|
267
|
+
const unresolved = [];
|
|
231
268
|
let inScope = false;
|
|
232
269
|
for (const prop of SPACING_PROPS) {
|
|
233
270
|
const decl = effectiveDeclaration(raw, prop);
|
|
@@ -235,14 +272,28 @@ function runInPage(ctx) {
|
|
|
235
272
|
if (INHERITED_KEYWORDS.indexOf(decl.value.toLowerCase()) !== -1) continue;
|
|
236
273
|
inScope = true;
|
|
237
274
|
const ratio = spacingRatio(el, prop, decl.value);
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
if (ratio === null)
|
|
275
|
+
// Not flagged, since `fail` needs a measured value -- but recorded, so
|
|
276
|
+
// the element does not fall through to `pass` unmeasured.
|
|
277
|
+
if (ratio === null) {
|
|
278
|
+
unresolved.push(prop);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
241
281
|
if (ratio < MIN_RATIO[prop]) flagged.push(prop);
|
|
242
282
|
}
|
|
243
283
|
|
|
244
284
|
if (inScope) applicableCount += 1;
|
|
245
|
-
if (!flagged.length)
|
|
285
|
+
if (!flagged.length) {
|
|
286
|
+
if (unresolved.length) undecided.push({ el, props: unresolved.slice() });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// A forced value on text that cannot wrap is outside these ACT rules'
|
|
291
|
+
// applicability, and whether it wraps is not decidable here, so it is
|
|
292
|
+
// reported for review rather than failed.
|
|
293
|
+
if (cannotSoftWrap(el)) {
|
|
294
|
+
noWrap.push({ el, props: flagged.slice() });
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
246
297
|
|
|
247
298
|
const tag = el.tagName.toLowerCase();
|
|
248
299
|
|
|
@@ -273,6 +324,82 @@ function runInPage(ctx) {
|
|
|
273
324
|
occurrences
|
|
274
325
|
};
|
|
275
326
|
}
|
|
327
|
+
if (noWrap.length) {
|
|
328
|
+
return {
|
|
329
|
+
ruleId: rule.ruleId,
|
|
330
|
+
outcome: 'cantTell',
|
|
331
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
332
|
+
confidence: 'low',
|
|
333
|
+
occurrences: noWrap.map(({ el, props }) =>
|
|
334
|
+
helpers.reportOccurrence(el, {
|
|
335
|
+
summary: `This element's inline style forces ${props.join(', ')} with !important, but its text does not appear able to wrap, so the text-spacing criterion may not apply to it.`,
|
|
336
|
+
hint: 'Confirm whether this text ever wraps. If it cannot, the criterion does not apply; if it can, remove !important or set a value that already meets the metric.',
|
|
337
|
+
i18n: {
|
|
338
|
+
summaryKey: 'avoidInlineSpacing_summary_cantTell_noSoftWrap',
|
|
339
|
+
hintKey: 'avoidInlineSpacing_hint_cantTell_noSoftWrap',
|
|
340
|
+
params: {
|
|
341
|
+
element: (el.tagName || '').toLowerCase(),
|
|
342
|
+
properties: props.join(', ')
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
uncertainty: {
|
|
346
|
+
code: 'not-computable',
|
|
347
|
+
needed: 'Whether this text ever contains a soft wrap break, which needs layout.',
|
|
348
|
+
evidence: {
|
|
349
|
+
element: (el.tagName || '').toLowerCase(),
|
|
350
|
+
properties: props,
|
|
351
|
+
reasonCode: 'INLINE_SPACING_NO_SOFT_WRAP'
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
data: {
|
|
355
|
+
details: {
|
|
356
|
+
reasonCode: 'INLINE_SPACING_NO_SOFT_WRAP',
|
|
357
|
+
element: (el.tagName || '').toLowerCase(),
|
|
358
|
+
properties: props
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
)
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (undecided.length) {
|
|
366
|
+
return {
|
|
367
|
+
ruleId: rule.ruleId,
|
|
368
|
+
outcome: 'cantTell',
|
|
369
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
370
|
+
confidence: 'low',
|
|
371
|
+
occurrences: undecided.map(({ el, props }) =>
|
|
372
|
+
helpers.reportOccurrence(el, {
|
|
373
|
+
summary: `This element's inline style sets ${props.join(', ')} with !important, but the value could not be resolved, so whether it meets the WCAG text-spacing metric could not be determined.`,
|
|
374
|
+
hint: 'Check this value by hand against the metric (line-height 1.5, letter-spacing 0.12em, word-spacing 0.16em), or express it in a unit the engine can resolve against the element’s computed font size.',
|
|
375
|
+
i18n: {
|
|
376
|
+
summaryKey: 'avoidInlineSpacing_summary_cantTell',
|
|
377
|
+
hintKey: 'avoidInlineSpacing_hint_cantTell',
|
|
378
|
+
params: {
|
|
379
|
+
element: (el.tagName || '').toLowerCase(),
|
|
380
|
+
properties: props.join(', ')
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
uncertainty: {
|
|
384
|
+
code: 'not-computable',
|
|
385
|
+
needed: 'A resolved value for the spacing declarations marked !important.',
|
|
386
|
+
evidence: {
|
|
387
|
+
element: (el.tagName || '').toLowerCase(),
|
|
388
|
+
properties: props,
|
|
389
|
+
reasonCode: 'INLINE_SPACING_NOT_RESOLVABLE'
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
data: {
|
|
393
|
+
details: {
|
|
394
|
+
reasonCode: 'INLINE_SPACING_NOT_RESOLVABLE',
|
|
395
|
+
element: (el.tagName || '').toLowerCase(),
|
|
396
|
+
properties: props
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
)
|
|
401
|
+
};
|
|
402
|
+
}
|
|
276
403
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
277
404
|
}
|
|
278
405
|
|
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check binary-control-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Checkboxes, radios and switches must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="checkbox", role="radio" or
|
|
13
|
+
* role="switch" (the attribute must name one of those roles alone, not a
|
|
14
|
+
* fallback list) that are included in the accessibility tree. A native
|
|
15
|
+
* <input type="checkbox">/<input type="radio"> is in scope only when it
|
|
16
|
+
* carries one of those roles explicitly; without a role attribute it
|
|
17
|
+
* belongs to form-control-programmatic-label-present.
|
|
18
|
+
* @expectation
|
|
19
|
+
* The control has a non-empty accessible name from aria-label, from an
|
|
20
|
+
* aria-labelledby that resolves to non-empty text, or from title. A native
|
|
21
|
+
* checkbox or radio additionally accepts an associated <label>, the
|
|
22
|
+
* labels API, a wrapping <label>, or label[for], with at most four labels
|
|
23
|
+
* read for determinism, and any other element accepts its own subtree
|
|
24
|
+
* text, those roles being name-from-content.
|
|
25
|
+
*/
|
|
26
|
+
|
|
5
27
|
const id = 'binary-control-name-present';
|
|
6
28
|
|
|
7
29
|
const meta = {
|
|
@@ -79,7 +101,7 @@ function runInPage(ctx) {
|
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
function getConservativeSubtreeText(document, container) {
|
|
82
|
-
// "Name from content"
|
|
104
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
83
105
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
84
106
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
85
107
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -96,7 +118,7 @@ function runInPage(ctx) {
|
|
|
96
118
|
|
|
97
119
|
// A <label> contributes a name via its own aria-label/aria-labelledby
|
|
98
120
|
// (checked first, same ARIA-over-content precedence any element's
|
|
99
|
-
// accessible name gives
|
|
121
|
+
// accessible name gives, e.g. <label aria-label="Search"><svg
|
|
100
122
|
// aria-hidden="true">...</svg></label> names its control "Search" even
|
|
101
123
|
// though the label's only child content is aria-hidden) or, failing
|
|
102
124
|
// that, its rendered content (getConservativeSubtreeText).
|
|
@@ -111,10 +133,10 @@ function runInPage(ctx) {
|
|
|
111
133
|
if (content) return content;
|
|
112
134
|
// Final fallback per the general accname text-alternative algorithm,
|
|
113
135
|
// which applies to any element being asked for its name regardless of
|
|
114
|
-
// why (own aria-label, an aria-labelledby reference, or
|
|
136
|
+
// why (own aria-label, an aria-labelledby reference, or, here, native
|
|
115
137
|
// <label for> association): title, when nothing else yields a name.
|
|
116
138
|
// Purely additive (only fills in a name where there was none before),
|
|
117
|
-
// so it carries no false-positive risk
|
|
139
|
+
// so it carries no false-positive risk, see dialog-name-present.js's
|
|
118
140
|
// identical <iframe>-title-fallback fix for the concrete real-world
|
|
119
141
|
// trigger this same accname step covers elsewhere.
|
|
120
142
|
return getAttr(lab, 'title');
|
|
@@ -124,7 +146,7 @@ function runInPage(ctx) {
|
|
|
124
146
|
const raw = getAttr(el, 'aria-labelledby');
|
|
125
147
|
if (!raw) return '';
|
|
126
148
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
127
|
-
// name-from-content of the referenced element
|
|
149
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
128
150
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
129
151
|
// aria-labelledby target's only name source is its title attribute,
|
|
130
152
|
// which name-from-content alone can never see).
|
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check button-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Buttons must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to <button>, <input type="button">, <input type="submit">,
|
|
13
|
+
* <input type="reset"> and elements with role="button", where the element
|
|
14
|
+
* is included in the accessibility tree. role="presentation"/"none" takes
|
|
15
|
+
* an element out of scope unless a global ARIA attribute or focusability
|
|
16
|
+
* restores its role, per presentational roles conflict resolution.
|
|
17
|
+
* @expectation
|
|
18
|
+
* The element has a non-empty accessible name. A programmatic name is
|
|
19
|
+
* taken first (aria-labelledby, aria-label, an associated <label>, title).
|
|
20
|
+
* Failing that, an <input> button falls back to its value attribute, and
|
|
21
|
+
* type="submit"/type="reset" fall back to the user agent's own
|
|
22
|
+
* "Submit"/"Reset" default, which is why those two are never nameless.
|
|
23
|
+
* Failing both, a button whose role is name-from-content falls back to its
|
|
24
|
+
* subtree text, counting each descendant's own name (an <img alt>,
|
|
25
|
+
* aria-label or title) rather than only text nodes.
|
|
26
|
+
*/
|
|
27
|
+
|
|
5
28
|
// NOTE: Repo ruleId contract requires ENGINE_TAG prefix in the rule id.
|
|
6
29
|
// File name intentionally has no prefix, per request.
|
|
7
30
|
const id = 'button-name-present';
|
|
@@ -45,7 +68,7 @@ function runInPage(ctx) {
|
|
|
45
68
|
}
|
|
46
69
|
|
|
47
70
|
function getConservativeSubtreeText(container) {
|
|
48
|
-
// "Name from content"
|
|
71
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
49
72
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
50
73
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
51
74
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -64,7 +87,16 @@ function runInPage(ctx) {
|
|
|
64
87
|
const type = normalizeWs(el.getAttribute ? el.getAttribute('type') : '').toLowerCase();
|
|
65
88
|
if (type !== 'button' && type !== 'submit' && type !== 'reset') return '';
|
|
66
89
|
const vAttr = el.getAttribute ? el.getAttribute('value') : '';
|
|
67
|
-
|
|
90
|
+
const explicit = normalizeWs(
|
|
91
|
+
vAttr != null ? vAttr : typeof el.value === 'string' ? el.value : ''
|
|
92
|
+
);
|
|
93
|
+
if (explicit) return explicit;
|
|
94
|
+
// HTML spec: input[type=submit]/[type=reset] with no `value` fall back
|
|
95
|
+
// to a UA-supplied default label ("Submit"/"Reset"), so they are never
|
|
96
|
+
// actually nameless -- unlike type=button, whose value defaults to "".
|
|
97
|
+
if (type === 'submit') return 'Submit';
|
|
98
|
+
if (type === 'reset') return 'Reset';
|
|
99
|
+
return '';
|
|
68
100
|
} catch {
|
|
69
101
|
return '';
|
|
70
102
|
}
|
|
@@ -90,17 +122,71 @@ function runInPage(ctx) {
|
|
|
90
122
|
typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
|
|
91
123
|
if (!eligible) continue;
|
|
92
124
|
|
|
93
|
-
applicableCount += 1;
|
|
94
|
-
|
|
95
125
|
const tag = (el.tagName || '').toLowerCase();
|
|
96
126
|
const role = el.getAttribute ? el.getAttribute('role') : null;
|
|
97
127
|
const roleNorm = normalizeWs(role).toLowerCase();
|
|
98
128
|
|
|
129
|
+
// role="none"/"presentation" removes this element from the accessibility
|
|
130
|
+
// tree as a button (WAI-ARIA Presentational Roles Conflict Resolution),
|
|
131
|
+
// UNLESS a conflicting global ARIA attribute or focusability restores
|
|
132
|
+
// its native/explicit role -- mirrors presentation-role-conflict-manual.js's
|
|
133
|
+
// detection logic. Kept local to this rule rather than routed through
|
|
134
|
+
// the shared eligibility helper: several other rules rely
|
|
135
|
+
// on that helper staying permissive for role="none" wrappers they walk
|
|
136
|
+
// through themselves (e.g. aria-prohibited-children's "transparent
|
|
137
|
+
// wrapper" traversal).
|
|
138
|
+
if (roleNorm === 'none' || roleNorm === 'presentation') {
|
|
139
|
+
const ariaHiddenTrue = el.getAttribute && el.getAttribute('aria-hidden') === 'true';
|
|
140
|
+
if (!ariaHiddenTrue) {
|
|
141
|
+
const GLOBAL_ARIA_ATTRS = [
|
|
142
|
+
'aria-atomic',
|
|
143
|
+
'aria-braillelabel',
|
|
144
|
+
'aria-brailleroledescription',
|
|
145
|
+
'aria-busy',
|
|
146
|
+
'aria-controls',
|
|
147
|
+
'aria-current',
|
|
148
|
+
'aria-describedby',
|
|
149
|
+
'aria-description',
|
|
150
|
+
'aria-details',
|
|
151
|
+
'aria-disabled',
|
|
152
|
+
'aria-dropeffect',
|
|
153
|
+
'aria-errormessage',
|
|
154
|
+
'aria-flowto',
|
|
155
|
+
'aria-grabbed',
|
|
156
|
+
'aria-haspopup',
|
|
157
|
+
'aria-hidden',
|
|
158
|
+
'aria-invalid',
|
|
159
|
+
'aria-keyshortcuts',
|
|
160
|
+
'aria-label',
|
|
161
|
+
'aria-labelledby',
|
|
162
|
+
'aria-live',
|
|
163
|
+
'aria-owns',
|
|
164
|
+
'aria-relevant',
|
|
165
|
+
'aria-roledescription'
|
|
166
|
+
];
|
|
167
|
+
const hasConflict = GLOBAL_ARIA_ATTRS.some((a) =>
|
|
168
|
+
el.hasAttribute ? el.hasAttribute(a) : false
|
|
169
|
+
);
|
|
170
|
+
let isFocusable = false;
|
|
171
|
+
if (!hasConflict && helpers.getFocusableInfo) {
|
|
172
|
+
try {
|
|
173
|
+
const fi = helpers.getFocusableInfo(el, ctx);
|
|
174
|
+
isFocusable = !!(fi && fi.focusable);
|
|
175
|
+
} catch {
|
|
176
|
+
isFocusable = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!hasConflict && !isFocusable) continue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
applicableCount += 1;
|
|
184
|
+
|
|
99
185
|
const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
|
|
100
186
|
|
|
101
187
|
// getAccessibleNameInfo only resolves programmatic mechanisms (aria-labelledby,
|
|
102
|
-
// aria-label, native <label> association, title)
|
|
103
|
-
// subtree content
|
|
188
|
+
// aria-label, native <label> association, title), it never falls back to
|
|
189
|
+
// subtree content, so it's safe to trust directly whenever present.
|
|
104
190
|
const trustedProgrammaticName = normalizeWs(
|
|
105
191
|
nameInfo && nameInfo.present && typeof nameInfo.value === 'string' ? nameInfo.value : ''
|
|
106
192
|
);
|