@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
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 4.1.2
|
|
11
11
|
* @applicability
|
|
12
|
-
* Applies to elements
|
|
13
|
-
*
|
|
12
|
+
* Applies to elements carrying at least one recognized, non-global
|
|
13
|
+
* aria-* attribute, judged against the role they actually have: an
|
|
14
|
+
* explicit valid role, else the implicit role of their tag, else, for
|
|
15
|
+
* the elements HTML-AAM maps to no role at all, nothing.
|
|
14
16
|
* @expectation
|
|
15
17
|
* Every recognized aria-* attribute present is either: (a) globally
|
|
16
18
|
* supported on any element (the "global" ARIA states/properties, e.g.
|
|
@@ -20,10 +22,19 @@
|
|
|
20
22
|
* still allowed: it is reported as CANTTELL (see helpers.aria.isDeprecatedAttr)
|
|
21
23
|
* so the author decides, not as a not-allowed FAIL.
|
|
22
24
|
* @implementation-notes
|
|
23
|
-
* -
|
|
24
|
-
*
|
|
25
|
-
* role
|
|
26
|
-
*
|
|
25
|
+
* - Three tiers of role resolution, in order: an explicit `role`; the
|
|
26
|
+
* implicit role of the tag (IMPLICIT_ROLE_BY_ELEMENT, generated only for
|
|
27
|
+
* elements whose role is the same in every context, see
|
|
28
|
+
* `scripts/generate-aria-tables.js` for what is excluded and why); and
|
|
29
|
+
* ROLELESS_ELEMENTS, the tags HTML-AAM gives no role at all. A
|
|
30
|
+
* role-specific attribute on one of those is supported by nothing, which
|
|
31
|
+
* is exactly ACT 5c01ea's `<audio controls aria-orientation="horizontal">`.
|
|
32
|
+
* An element whose role is context-dependent (`<a>`, `<section>`,
|
|
33
|
+
* `<td>`, ...) is still skipped rather than guessed at.
|
|
34
|
+
* - `<div>`/`<span>` resolve to `generic`, whose supported set is empty, so
|
|
35
|
+
* `<div aria-expanded="true">` is reported: the attribute announces
|
|
36
|
+
* nothing on an element with no widget semantics, which is a real defect
|
|
37
|
+
* and not a spec technicality.
|
|
27
38
|
* - SUPPORTED_ATTRS_BY_ROLE holds unambiguous, well-established ARIA facts:
|
|
28
39
|
* subclass relationships like `searchbox`==`textbox`; same-family widget
|
|
29
40
|
* properties like `columnheader`/`rowheader` sort/col-row-index/span
|
|
@@ -82,7 +93,7 @@ function runInPage(ctx) {
|
|
|
82
93
|
// Global ARIA states/properties supported on (almost) any element,
|
|
83
94
|
// regardless of role, per the WAI-ARIA "Global States and Properties" list.
|
|
84
95
|
// Declared inside runInPage (rather than at module scope) because the
|
|
85
|
-
// build inlines only this function's own source text
|
|
96
|
+
// build inlines only this function's own source text, see
|
|
86
97
|
// scripts/build-core.js header ("runInPage MUST be self-contained").
|
|
87
98
|
// <generated:aria-global-attrs>
|
|
88
99
|
const GLOBAL_ATTRS = [
|
|
@@ -109,8 +120,8 @@ function runInPage(ctx) {
|
|
|
109
120
|
];
|
|
110
121
|
// </generated:aria-global-attrs>
|
|
111
122
|
|
|
112
|
-
// Per-role supported (non-global) states/properties.
|
|
113
|
-
// conservative
|
|
123
|
+
// Per-role supported (non-global) states/properties.
|
|
124
|
+
// conservative, see src/core/aria-helpers.js file header for the same
|
|
114
125
|
// confidence-scoping rationale; only well-established, unambiguous
|
|
115
126
|
// role/attribute pairings from the WAI-ARIA role definitions are listed.
|
|
116
127
|
// <generated:aria-implicit-roles>
|
|
@@ -125,6 +136,7 @@ function runInPage(ctx) {
|
|
|
125
136
|
details: 'group',
|
|
126
137
|
dfn: 'term',
|
|
127
138
|
dialog: 'dialog',
|
|
139
|
+
div: 'generic',
|
|
128
140
|
dt: 'term',
|
|
129
141
|
em: 'emphasis',
|
|
130
142
|
fieldset: 'group',
|
|
@@ -149,6 +161,7 @@ function runInPage(ctx) {
|
|
|
149
161
|
p: 'paragraph',
|
|
150
162
|
progress: 'progressbar',
|
|
151
163
|
strong: 'strong',
|
|
164
|
+
span: 'generic',
|
|
152
165
|
sub: 'subscript',
|
|
153
166
|
sup: 'superscript',
|
|
154
167
|
textarea: 'textbox',
|
|
@@ -173,6 +186,10 @@ function runInPage(ctx) {
|
|
|
173
186
|
'[aria-activedescendant], [aria-autocomplete], [aria-checked], [aria-colcount], [aria-colindex], [aria-colspan], [aria-disabled], [aria-errormessage], [aria-expanded], [aria-haspopup], [aria-invalid], [aria-level], [aria-modal], [aria-multiline], [aria-multiselectable], [aria-orientation], [aria-placeholder], [aria-posinset], [aria-pressed], [aria-readonly], [aria-required], [aria-rowcount], [aria-rowindex], [aria-rowspan], [aria-selected], [aria-setsize], [aria-sort], [aria-valuemax], [aria-valuemin], [aria-valuenow], [aria-valuetext]';
|
|
174
187
|
// </generated:aria-implicit-roles>
|
|
175
188
|
|
|
189
|
+
// <generated:aria-roleless-elements>
|
|
190
|
+
const ROLELESS_ELEMENTS = new Set(['audio', 'video']);
|
|
191
|
+
// </generated:aria-roleless-elements>
|
|
192
|
+
|
|
176
193
|
// <generated:aria-role-attrs>
|
|
177
194
|
const SUPPORTED_ATTRS_BY_ROLE = {
|
|
178
195
|
alert: [],
|
|
@@ -806,6 +823,39 @@ function runInPage(ctx) {
|
|
|
806
823
|
const cantTellOccurrences = [];
|
|
807
824
|
let applicableCount = 0;
|
|
808
825
|
|
|
826
|
+
// An element HTML-AAM maps to no role has nothing to support a
|
|
827
|
+
// role-specific attribute, so every non-global one present is reported.
|
|
828
|
+
// Returns how many recognized aria-* attributes it carried, for the
|
|
829
|
+
// applicable count.
|
|
830
|
+
function rolelessOccurrences(el, tag) {
|
|
831
|
+
let seen = 0;
|
|
832
|
+
const attrs = el.attributes;
|
|
833
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
834
|
+
const name = String(attrs[i].name || '').toLowerCase();
|
|
835
|
+
if (name.slice(0, 5) !== 'aria-') continue;
|
|
836
|
+
if (!ariaHelpers.isValidAriaAttrName(name)) continue; // aria-valid-attr's concern
|
|
837
|
+
|
|
838
|
+
seen += 1;
|
|
839
|
+
if (globalSet.has(name)) continue;
|
|
840
|
+
|
|
841
|
+
failOccurrences.push(
|
|
842
|
+
helpers.reportOccurrence(el, {
|
|
843
|
+
summary: `<${tag}> has no ARIA role, so nothing supports the ${name} attribute on it.`,
|
|
844
|
+
hint: `Remove ${name}, or move it to an element whose role supports it. A role-specific ARIA attribute on an element with no role is ignored by assistive technology.`,
|
|
845
|
+
i18n: {
|
|
846
|
+
summaryKey: 'ariaAllowedAttr_summary_fail_roleless',
|
|
847
|
+
hintKey: 'ariaAllowedAttr_hint_fail_roleless',
|
|
848
|
+
params: { attr: name, element: tag }
|
|
849
|
+
},
|
|
850
|
+
data: {
|
|
851
|
+
details: { reasonCode: 'ARIA_ATTR_NOT_ALLOWED_ROLELESS', attr: name, element: tag }
|
|
852
|
+
}
|
|
853
|
+
})
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
return seen;
|
|
857
|
+
}
|
|
858
|
+
|
|
809
859
|
for (const el of nodes) {
|
|
810
860
|
if (!el || !el.attributes) continue;
|
|
811
861
|
|
|
@@ -824,6 +874,14 @@ function runInPage(ctx) {
|
|
|
824
874
|
role = Object.prototype.hasOwnProperty.call(IMPLICIT_ROLE_BY_ELEMENT, key)
|
|
825
875
|
? IMPLICIT_ROLE_BY_ELEMENT[key]
|
|
826
876
|
: '';
|
|
877
|
+
// No role from either source: for a tag HTML-AAM maps to no role at
|
|
878
|
+
// all, that IS the answer. Nothing supports a role-specific
|
|
879
|
+
// attribute here. Any other tag has a role this table does not model
|
|
880
|
+
// (context-dependent ones), so it stays out of scope.
|
|
881
|
+
if (!role && ROLELESS_ELEMENTS.has(tag)) {
|
|
882
|
+
applicableCount += rolelessOccurrences(el, tag);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
827
885
|
}
|
|
828
886
|
if (!role || !ariaHelpers.isValidConcreteRole(role)) continue; // aria-roles-valid's concern
|
|
829
887
|
|
|
@@ -860,7 +918,7 @@ function runInPage(ctx) {
|
|
|
860
918
|
|
|
861
919
|
for (const name of disallowed) {
|
|
862
920
|
// A property ARIA deprecated (rather than prohibited) on this role is
|
|
863
|
-
// still allowed
|
|
921
|
+
// still allowed, surfaced as cantTell for the author to decide, not a
|
|
864
922
|
// not-allowed fail.
|
|
865
923
|
const deprecated =
|
|
866
924
|
typeof ariaHelpers.isDeprecatedAttr === 'function' &&
|
|
@@ -876,6 +934,12 @@ function runInPage(ctx) {
|
|
|
876
934
|
hintKey: 'ariaAllowedAttr_hint_cantTell',
|
|
877
935
|
params: { attr: name, role }
|
|
878
936
|
},
|
|
937
|
+
uncertainty: {
|
|
938
|
+
code: 'spec-only',
|
|
939
|
+
needed:
|
|
940
|
+
'Whether the deprecated attribute is still honoured by the assistive technology in use.',
|
|
941
|
+
evidence: { attribute: name, role, status: 'deprecated-for-role' }
|
|
942
|
+
},
|
|
879
943
|
data: {
|
|
880
944
|
details: { reasonCode: 'ARIA_ATTR_DEPRECATED', attr: name, role }
|
|
881
945
|
}
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* @check aria-allowed-role
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary Explicit role must be permitted by the ARIA-in-HTML spec for its host element
|
|
9
|
-
* @standard WCAG
|
|
10
|
-
* @sc 4.1.2
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
11
10
|
* @applicability
|
|
12
11
|
* Applies to elements with an explicit, valid, non-abstract role, where
|
|
13
12
|
* the host element/attribute combination has an asserted permitted-roles
|
|
@@ -16,10 +15,21 @@
|
|
|
16
15
|
* @expectation
|
|
17
16
|
* The explicit role is one of the roles the ARIA-in-HTML specification
|
|
18
17
|
* permits for that host element.
|
|
18
|
+
* Reported at CANTTELL rather than FAIL: ARIA-in-HTML's permitted-roles
|
|
19
|
+
* table is an author conformance requirement with no ACT rule and no WCAG
|
|
20
|
+
* mapping in any source. The role the author asked for is still the role
|
|
21
|
+
* assistive technology exposes, so whether the combination harms anyone
|
|
22
|
+
* depends on the widget, not on the table.
|
|
19
23
|
* @implementation-notes
|
|
20
|
-
* -
|
|
24
|
+
* - Not WCAG-normative. ARIA-in-HTML's permitted-roles table is an author
|
|
25
|
+
* conformance requirement of that specification; no ACT rule covers it and
|
|
26
|
+
* no source maps it to a Success Criterion, so the rule reports the
|
|
27
|
+
* violation without claiming a criterion is failed. Deterministic all the
|
|
28
|
+
* same, so it stays `type: 'automatic'` and keeps deciding rather than
|
|
29
|
+
* deferring to a reviewer -- see docs/RULE_TAXONOMY.md 1.1.
|
|
30
|
+
* - Scoped to elements present in ALLOWED_ROLES_BY_ELEMENT;
|
|
21
31
|
* elements without an asserted constraint are treated as "no constraint"
|
|
22
|
-
* (not flagged) rather than guessed at
|
|
32
|
+
* (not flagged) rather than guessed at, see that table's header comment.
|
|
23
33
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
24
34
|
* property, while engine-level hidden-subtree filtering still applies
|
|
25
35
|
* unless engineOptions.includeHiddenElements is true.
|
|
@@ -36,22 +46,14 @@ const meta = {
|
|
|
36
46
|
descriptionKey: 'ariaAllowedRole_description'
|
|
37
47
|
},
|
|
38
48
|
helpUrl: null,
|
|
39
|
-
tags: ['
|
|
40
|
-
wcagSc: [
|
|
41
|
-
normativeMappings: [
|
|
42
|
-
{
|
|
43
|
-
standard: 'WCAG',
|
|
44
|
-
version: '2.2',
|
|
45
|
-
requirement: '4.1.2',
|
|
46
|
-
title: 'Name, Role, Value',
|
|
47
|
-
conformanceLevel: 'A'
|
|
48
|
-
}
|
|
49
|
-
],
|
|
49
|
+
tags: ['best-practice', 'aria', 'structure', 'atomic', 'automatic'],
|
|
50
|
+
wcagSc: [],
|
|
51
|
+
normativeMappings: [],
|
|
50
52
|
defaultSeverity: 'moderate',
|
|
51
53
|
category: 'robust',
|
|
52
54
|
type: 'automatic',
|
|
53
55
|
defaultConfidence: 'high',
|
|
54
|
-
coverage: {
|
|
56
|
+
coverage: {}
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
function runInPage(ctx) {
|
|
@@ -93,6 +95,16 @@ function runInPage(ctx) {
|
|
|
93
95
|
hintKey: 'ariaAllowedRole_hint_fail',
|
|
94
96
|
params: { role, element: tag }
|
|
95
97
|
},
|
|
98
|
+
uncertainty: {
|
|
99
|
+
code: 'spec-only',
|
|
100
|
+
needed: 'Whether the role misstates this element to assistive technology in practice.',
|
|
101
|
+
evidence: {
|
|
102
|
+
role,
|
|
103
|
+
element: tag,
|
|
104
|
+
source: 'ARIA in HTML permitted-roles table',
|
|
105
|
+
wcagSc: []
|
|
106
|
+
}
|
|
107
|
+
},
|
|
96
108
|
data: {
|
|
97
109
|
details: { reasonCode: 'ARIA_ROLE_NOT_ALLOWED_FOR_ELEMENT', role, element: tag }
|
|
98
110
|
}
|
|
@@ -103,15 +115,12 @@ function runInPage(ctx) {
|
|
|
103
115
|
if (applicableCount === 0) {
|
|
104
116
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
105
117
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
118
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
119
|
+
[],
|
|
120
|
+
occurrences,
|
|
121
|
+
rule.defaultSeverity || 'moderate'
|
|
122
|
+
);
|
|
123
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
module.exports = { id, meta, runInPage };
|
|
@@ -23,11 +23,16 @@
|
|
|
23
23
|
* Using either braille-specific attribute as the ONLY naming mechanism
|
|
24
24
|
* leaves non-braille assistive technology (most screen readers, voice
|
|
25
25
|
* control, etc.) with no accessible name/role description at all.
|
|
26
|
+
* Reported at CANTTELL rather than FAIL: aria-brailleroledescription
|
|
27
|
+
* without aria-roledescription reaches no user at all, and a missing
|
|
28
|
+
* accessible name is the naming rules' decision for the roles that require
|
|
29
|
+
* one. The braille attribute being unpaired is worth surfacing, but it is
|
|
30
|
+
* not itself a criterion failing.
|
|
26
31
|
* @implementation-notes
|
|
27
32
|
* - `aria-braillelabel`/`aria-brailleroledescription` do not participate
|
|
28
33
|
* in the standard accessible-name computation, so
|
|
29
34
|
* `helpers.getAccessibleNameInfo` already reflects only the "regular"
|
|
30
|
-
* (non-braille) name
|
|
35
|
+
* (non-braille) name, no special-casing needed there.
|
|
31
36
|
*/
|
|
32
37
|
|
|
33
38
|
const id = 'aria-braille-equivalent';
|
|
@@ -52,7 +57,7 @@ const meta = {
|
|
|
52
57
|
conformanceLevel: 'A'
|
|
53
58
|
}
|
|
54
59
|
],
|
|
55
|
-
defaultSeverity: '
|
|
60
|
+
defaultSeverity: 'moderate',
|
|
56
61
|
category: 'robust',
|
|
57
62
|
type: 'automatic',
|
|
58
63
|
defaultConfidence: 'high',
|
|
@@ -67,7 +72,7 @@ function runInPage(ctx) {
|
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
function getConservativeSubtreeText(container) {
|
|
70
|
-
// "Name from content"
|
|
75
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
71
76
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
72
77
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
73
78
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -125,12 +130,18 @@ function runInPage(ctx) {
|
|
|
125
130
|
occurrences.push(
|
|
126
131
|
helpers.reportOccurrence(el, {
|
|
127
132
|
summary: `This element has ${m.attr} but no ${m.requires}, its non-braille equivalent.`,
|
|
128
|
-
hint: `${m.attr} is a Braille-specific supplement, not a replacement
|
|
133
|
+
hint: `${m.attr} is a Braille-specific supplement, not a replacement, so also provide ${m.requires}.`,
|
|
129
134
|
i18n: {
|
|
130
135
|
summaryKey: 'ariaBrailleEquivalent_summary_fail',
|
|
131
136
|
hintKey: 'ariaBrailleEquivalent_hint_fail',
|
|
132
137
|
params: { element: tag, attr: m.attr, requires: m.requires }
|
|
133
138
|
},
|
|
139
|
+
uncertainty: {
|
|
140
|
+
code: 'spec-only',
|
|
141
|
+
needed:
|
|
142
|
+
'Whether any user reaches this attribute, given no braille equivalent is exposed.',
|
|
143
|
+
evidence: { element: tag, attribute: m.attr, requires: m.requires }
|
|
144
|
+
},
|
|
134
145
|
data: {
|
|
135
146
|
details: {
|
|
136
147
|
reasonCode: 'BRAILLE_ATTR_WITHOUT_EQUIVALENT',
|
|
@@ -146,15 +157,12 @@ function runInPage(ctx) {
|
|
|
146
157
|
if (applicableCount === 0) {
|
|
147
158
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
148
159
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
160
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
161
|
+
[],
|
|
162
|
+
occurrences,
|
|
163
|
+
rule.defaultSeverity || 'moderate'
|
|
164
|
+
);
|
|
165
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
module.exports = { id, meta, runInPage };
|
|
@@ -16,17 +16,22 @@
|
|
|
16
16
|
* other than `"false"` (i.e. `"true"`, `"grammar"`, or `"spelling"`).
|
|
17
17
|
* An element with `aria-errormessage` but `aria-invalid` absent or
|
|
18
18
|
* `"false"` silently drops the error message from the accessibility
|
|
19
|
-
* tree
|
|
19
|
+
* tree, authors almost always intend it to be exposed.
|
|
20
|
+
* Reported at CANTTELL rather than FAIL: aria-errormessage is only
|
|
21
|
+
* exposed once aria-invalid is set, so the reference is currently inert.
|
|
22
|
+
* Whether that costs the user anything depends on whether the message is
|
|
23
|
+
* conveyed some other way (visible text next to the field, aria-describedby),
|
|
24
|
+
* which static markup does not settle.
|
|
20
25
|
* @implementation-notes
|
|
21
|
-
* - This is
|
|
26
|
+
* - This is narrow: the broader space is a table of many
|
|
22
27
|
* attribute/condition pairs. This rule implements only the one pairing
|
|
23
28
|
* (`aria-errormessage` / `aria-invalid`) that is unambiguous and
|
|
24
|
-
* explicitly stated in the ARIA spec, to keep `fail` high-confidence
|
|
29
|
+
* explicitly stated in the ARIA spec, to keep `fail` high-confidence,
|
|
25
30
|
* matches this repo's established pattern (see `aria-required-attr`/
|
|
26
31
|
* `aria-prohibited-attr` for the same "narrow but zero false positives"
|
|
27
32
|
* trade-off).
|
|
28
33
|
* - Does not check whether the `aria-errormessage` ID reference itself
|
|
29
|
-
* resolves to an existing element
|
|
34
|
+
* resolves to an existing element. That's `aria-valid-attr-value`'s
|
|
30
35
|
* concern, not this rule's.
|
|
31
36
|
*/
|
|
32
37
|
|
|
@@ -35,7 +40,7 @@ const id = 'aria-conditional-attr';
|
|
|
35
40
|
const meta = {
|
|
36
41
|
title: 'aria-errormessage requires aria-invalid to be set to a non-false value',
|
|
37
42
|
description:
|
|
38
|
-
'Checks that elements with aria-errormessage also have aria-invalid set to "true", "grammar", or "spelling"
|
|
43
|
+
'Checks that elements with aria-errormessage also have aria-invalid set to "true", "grammar", or "spelling"; otherwise the error message is dropped from the accessibility tree.',
|
|
39
44
|
i18n: {
|
|
40
45
|
titleKey: 'ariaConditionalAttr_title',
|
|
41
46
|
descriptionKey: 'ariaConditionalAttr_description'
|
|
@@ -52,7 +57,7 @@ const meta = {
|
|
|
52
57
|
conformanceLevel: 'A'
|
|
53
58
|
}
|
|
54
59
|
],
|
|
55
|
-
defaultSeverity: '
|
|
60
|
+
defaultSeverity: 'moderate',
|
|
56
61
|
category: 'robust',
|
|
57
62
|
type: 'automatic',
|
|
58
63
|
defaultConfidence: 'high',
|
|
@@ -98,6 +103,11 @@ function runInPage(ctx) {
|
|
|
98
103
|
hintKey: 'ariaConditionalAttr_hint_fail',
|
|
99
104
|
params: { element: tag, ariaInvalid: invalidValue || '(absent)' }
|
|
100
105
|
},
|
|
106
|
+
uncertainty: {
|
|
107
|
+
code: 'spec-only',
|
|
108
|
+
needed: 'Whether the field is ever in an invalid state that should expose this message.',
|
|
109
|
+
evidence: { element: tag, ariaInvalid: invalidValue || null }
|
|
110
|
+
},
|
|
101
111
|
data: {
|
|
102
112
|
details: {
|
|
103
113
|
reasonCode: 'ARIA_ERRORMESSAGE_WITHOUT_TRUTHY_INVALID',
|
|
@@ -111,15 +121,12 @@ function runInPage(ctx) {
|
|
|
111
121
|
if (applicableCount === 0) {
|
|
112
122
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
113
123
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
124
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
125
|
+
[],
|
|
126
|
+
occurrences,
|
|
127
|
+
rule.defaultSeverity || 'moderate'
|
|
128
|
+
);
|
|
129
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
module.exports = { id, meta, runInPage };
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @applicability
|
|
12
12
|
* Applies to any element whose role attribute's first (used) token is a
|
|
13
13
|
* valid, non-abstract ARIA role that authors should not explicitly
|
|
14
|
-
* declare
|
|
14
|
+
* declare, either because WAI-ARIA has deprecated it (e.g. "directory",
|
|
15
15
|
* superseded by role="list") or because it is reserved for user-agent-
|
|
16
16
|
* internal use (role="generic", which ARIA 1.2 §5.4 says authors SHOULD
|
|
17
17
|
* NOT use in content).
|
|
@@ -161,6 +161,12 @@ function runInPage(ctx) {
|
|
|
161
161
|
hintKey: guidance.key,
|
|
162
162
|
params: { role }
|
|
163
163
|
},
|
|
164
|
+
uncertainty: {
|
|
165
|
+
code: 'spec-only',
|
|
166
|
+
needed:
|
|
167
|
+
'Whether this user-agent-reserved role misleads the assistive technology in use.',
|
|
168
|
+
evidence: { role, ariaStatus: 'discouraged-for-authors' }
|
|
169
|
+
},
|
|
164
170
|
data: {
|
|
165
171
|
details: { reasonCode: 'ARIA_ROLE_AUTHOR_DISCOURAGED', role, guidance: guidance.text }
|
|
166
172
|
}
|
|
@@ -178,6 +184,12 @@ function runInPage(ctx) {
|
|
|
178
184
|
hintKey: guidance.key,
|
|
179
185
|
params: { role }
|
|
180
186
|
},
|
|
187
|
+
uncertainty: {
|
|
188
|
+
code: 'spec-only',
|
|
189
|
+
needed:
|
|
190
|
+
'Whether the deprecated role is still honoured, and what it should be replaced with.',
|
|
191
|
+
evidence: { role, ariaStatus: 'deprecated' }
|
|
192
|
+
},
|
|
181
193
|
data: {
|
|
182
194
|
details: { reasonCode: 'ARIA_ROLE_DEPRECATED', role, guidance: guidance.text }
|
|
183
195
|
}
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* @sc 1.3.1, 4.1.2
|
|
11
11
|
* @applicability
|
|
12
12
|
* Always applicable to any HTML document with a <body> element,
|
|
13
|
-
* independent of contextSelector/root scoping
|
|
13
|
+
* independent of contextSelector/root scoping, this is a whole-
|
|
14
14
|
* document concern, matching page-title-present's pattern of
|
|
15
15
|
* evaluating document.body directly rather than the scoped root.
|
|
16
16
|
* @expectation
|
|
17
17
|
* <body> does not have aria-hidden="true". Hiding the document body
|
|
18
18
|
* removes the entire page's content and structure from the
|
|
19
|
-
* accessibility tree at once
|
|
19
|
+
* accessibility tree at once, both 1.3.1 (Info and Relationships: the
|
|
20
20
|
* page's structure becomes entirely non-determinable) and 4.1.2 (Name,
|
|
21
21
|
* Role, Value: nothing in the document exposes a role/name/value any
|
|
22
22
|
* longer) apply.
|
|
@@ -33,7 +33,7 @@ const meta = {
|
|
|
33
33
|
descriptionKey: 'ariaHiddenBody_description'
|
|
34
34
|
},
|
|
35
35
|
helpUrl: null,
|
|
36
|
-
tags: ['wcag2a', 'wcag131', 'wcag412', 'structure', 'atomic', 'automatic'],
|
|
36
|
+
tags: ['wcag2a', 'wcag131', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
37
37
|
wcagSc: ['1.3.1', '4.1.2'],
|
|
38
38
|
normativeMappings: [
|
|
39
39
|
{
|
|
@@ -109,7 +109,7 @@ function runInPage(ctx) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Flat-tree ancestor walk (assignedSlot wins over parentNode, then shadow
|
|
112
|
-
// host)
|
|
112
|
+
// host), shared with every other rule via ctx.helpers.composedParent
|
|
113
113
|
// (src/core/dom-helpers.js), not reimplemented here, so a fix to the one
|
|
114
114
|
// canonical definition can't drift out of sync with this rule's copy.
|
|
115
115
|
const composedParent =
|
|
@@ -533,8 +533,8 @@ function runInPage(ctx) {
|
|
|
533
533
|
if (isDisabledFormControl(el)) return false;
|
|
534
534
|
|
|
535
535
|
// An explicit negative tabindex removes the element from the keyboard
|
|
536
|
-
// tab sequence entirely, regardless of tag
|
|
537
|
-
// recommended technique for safely hiding focusable content behind
|
|
536
|
+
// tab sequence entirely, regardless of tag. It's the standard,
|
|
537
|
+
// WAI-recommended technique for safely hiding focusable content behind
|
|
538
538
|
// aria-hidden (e.g. <button tabindex="-1"> / <a tabindex="-1"> inside
|
|
539
539
|
// an aria-hidden container). This cares about tabbability, not raw
|
|
540
540
|
// focusability. Such an element is still programmatically focusable
|
|
@@ -684,7 +684,7 @@ function runInPage(ctx) {
|
|
|
684
684
|
// Cheap check first: a plain ancestor-attribute walk with no CSS
|
|
685
685
|
// computation, vs. isActuallyFocusable's getComputedStyle-per-ancestor
|
|
686
686
|
// cost. Both conditions are required (AND), so checking whichever is
|
|
687
|
-
// cheaper first cannot change which elements end up in the bucket
|
|
687
|
+
// cheaper first cannot change which elements end up in the bucket,
|
|
688
688
|
// it only skips the expensive check for the (typically vast) majority
|
|
689
689
|
// of focusable candidates that were never inside an aria-hidden root
|
|
690
690
|
// in the first place. On pages with many focusable candidates and a
|
|
@@ -903,7 +903,7 @@ function runInPage(ctx) {
|
|
|
903
903
|
|
|
904
904
|
// See helpers.resolveTieredOutcome's own header comment (src/core/dom-helpers.js):
|
|
905
905
|
// a fail-tier finding never silently discards cantTell-tier findings from
|
|
906
|
-
// the same run
|
|
906
|
+
// the same run, both are returned together when the outcome is 'fail'.
|
|
907
907
|
const resolved = helpers.resolveTieredOutcome(
|
|
908
908
|
failOccurrences,
|
|
909
909
|
uncertainOccurrences,
|
|
@@ -14,20 +14,20 @@
|
|
|
14
14
|
* list for naming attributes (pure text-semantics / non-naming
|
|
15
15
|
* structural roles: caption, code, deletion, emphasis, generic,
|
|
16
16
|
* insertion, mark, none, paragraph, presentation, strong, subscript,
|
|
17
|
-
* suggestion, superscript, time), and (b) elements with no role at all
|
|
17
|
+
* suggestion, superscript, time), and (b) elements with no role at all:
|
|
18
18
|
* a curated set of native HTML tags verified to carry no implicit role
|
|
19
19
|
* (see ROLELESS_NATIVE_TAGS below), or any autonomous custom element (a
|
|
20
20
|
* hyphenated, author-defined tag per the Custom Elements spec; see
|
|
21
|
-
* isRolelessCustomElementTag below)
|
|
21
|
+
* isRolelessCustomElementTag below). In both cases, only elements that
|
|
22
22
|
* also carry aria-label or aria-labelledby.
|
|
23
23
|
* @expectation
|
|
24
24
|
* Prohibited attributes must not be present on (a); for (b), the naming
|
|
25
25
|
* attribute is at best unreliable (nothing accessible-name-aware to hang
|
|
26
|
-
* it off) and at worst silently ignored by assistive technology
|
|
26
|
+
* it off) and at worst silently ignored by assistive technology. See the
|
|
27
27
|
* roleless-branch implementation note below for the confidence split
|
|
28
28
|
* this produces.
|
|
29
29
|
* @implementation-notes
|
|
30
|
-
* -
|
|
30
|
+
* - Scoped to the single, well-established prohibition class
|
|
31
31
|
* (naming attributes on pure text-semantics roles) rather than an
|
|
32
32
|
* exhaustive per-role prohibited-attribute table; see
|
|
33
33
|
* src/core/aria-helpers.js file header for this engine's confidence-
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* narrower, unambiguous naming-prohibition case fires as a hard,
|
|
42
42
|
* WCAG-normative fail instead, matching this engine's "one rule = one
|
|
43
43
|
* normative decision" pattern.
|
|
44
|
-
* -
|
|
44
|
+
* - Excludes `definition`/`term` on purpose, despite both appearing on
|
|
45
45
|
* MDN's aria-label "not supported" list: both support name from author
|
|
46
46
|
* (`nameFrom: ['author']` for definition, `['author', 'contents']` for
|
|
47
47
|
* term), and the W3C spec's §5.2.8.4 "Roles Supporting Name From Author"
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
* property, while engine-level hidden-subtree filtering still applies
|
|
51
51
|
* unless engineOptions.includeHiddenElements is true.
|
|
52
52
|
* - Second, independent branch: naming attributes on ROLELESS elements (no
|
|
53
|
-
* explicit role="", no implicit/native role either)
|
|
53
|
+
* explicit role="", no implicit/native role either), e.g. icon-only
|
|
54
54
|
* `<span aria-label="...">` tiles with no other accessible-name source.
|
|
55
|
-
* ROLELESS_NATIVE_TAGS below is a curated,
|
|
55
|
+
* ROLELESS_NATIVE_TAGS below is a curated, intentionally conservative
|
|
56
56
|
* list of native tags confirmed to carry no implicit role (common
|
|
57
57
|
* text-level tags like `<p>`/`<strong>`/`<em>`/`<code>`/`<mark>`/`<time>`
|
|
58
58
|
* resolve to role `null`, same as a bare `<div>`/`<span>`);
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
* already produces a non-empty accessible name from its content (via
|
|
66
66
|
* `helpers.getContentNameInfo`, same as
|
|
67
67
|
* link-name-present/button-name-present), the naming attribute might
|
|
68
|
-
* just be a redundant/intentional override
|
|
68
|
+
* just be a redundant/intentional override, so it's reported as `cantTell`, not
|
|
69
69
|
* a hard fail. Only a roleless element with no other accessible-name
|
|
70
70
|
* source at all is a confident, deterministic `fail`. The
|
|
71
71
|
* widget-ancestor exemption (skip when the closest real ancestor role is
|
|
@@ -119,8 +119,8 @@ function runInPage(ctx) {
|
|
|
119
119
|
// Roles whose WAI-ARIA 1.2 definition lists a "Prohibited ARIA States and
|
|
120
120
|
// Properties" entry for naming attributes (these roles must never carry an
|
|
121
121
|
// accessible name). Declared inside runInPage (rather than at module
|
|
122
|
-
// scope) because the build inlines only this function's own source text
|
|
123
|
-
//
|
|
122
|
+
// scope) because the build inlines only this function's own source text.
|
|
123
|
+
// See scripts/build-core.js header ("runInPage MUST be self-contained").
|
|
124
124
|
const ROLES_PROHIBITING_NAME = new Set([
|
|
125
125
|
'caption',
|
|
126
126
|
'code',
|
|
@@ -190,7 +190,7 @@ function runInPage(ctx) {
|
|
|
190
190
|
// and how ROLELESS_NATIVE_TAGS/WIDGET_TYPE_ROLES were derived) ---
|
|
191
191
|
|
|
192
192
|
// Small, curated set of native tags verified to carry no explicit or
|
|
193
|
-
// implicit ARIA role.
|
|
193
|
+
// implicit ARIA role. Excludes <section>/<form>/<a>, which are
|
|
194
194
|
// conditionally roleless too, but already handled with more nuance
|
|
195
195
|
// elsewhere in this engine (see header comment).
|
|
196
196
|
const ROLELESS_NATIVE_TAGS = new Set([
|
|
@@ -268,7 +268,7 @@ function runInPage(ctx) {
|
|
|
268
268
|
};
|
|
269
269
|
|
|
270
270
|
// Nearest ancestor's real role (explicit-if-valid, else native/implicit),
|
|
271
|
-
// skipping roleless/presentation/none ancestors
|
|
271
|
+
// skipping roleless/presentation/none ancestors. Used only to check
|
|
272
272
|
// whether that role is a "widget"-type one (the roleless-branch
|
|
273
273
|
// exemption). Not the same helper as aria-required-parent's containment
|
|
274
274
|
// walk: this one also accepts non-required-context roles.
|
|
@@ -296,7 +296,7 @@ function runInPage(ctx) {
|
|
|
296
296
|
|
|
297
297
|
// A small, spec-reserved set of hyphenated tag names that are NOT
|
|
298
298
|
// autonomous custom elements despite containing a hyphen (legacy SVG/
|
|
299
|
-
// MathML tags predating the Custom Elements spec)
|
|
299
|
+
// MathML tags predating the Custom Elements spec). See
|
|
300
300
|
// https://html.spec.whatwg.org/#valid-custom-element-name's own
|
|
301
301
|
// exclusion list. Excluded so this doesn't misclassify them as
|
|
302
302
|
// always-roleless the same way a real custom element is.
|
|
@@ -336,8 +336,8 @@ function runInPage(ctx) {
|
|
|
336
336
|
const tag = String(el.tagName || '').toLowerCase();
|
|
337
337
|
if (!ROLELESS_NATIVE_TAGS.has(tag) && !isRolelessCustomElementTag(tag)) continue;
|
|
338
338
|
const explicitRole = ariaHelpers.getExplicitRole(el);
|
|
339
|
-
if (explicitRole && ariaHelpers.isValidConcreteRole(explicitRole)) continue; // has a real, recognized role
|
|
340
|
-
if (ariaHelpers.getNativeRoleForElement(el)) continue; // has a real implicit role after all
|
|
339
|
+
if (explicitRole && ariaHelpers.isValidConcreteRole(explicitRole)) continue; // has a real, recognized role: Tier 1's concern (if in ROLES_PROHIBITING_NAME) or a role this rule has no opinion on. An INVALID role token (e.g. a typo) is ignored per spec, same as no role attribute at all, and must still fall through to this branch.
|
|
340
|
+
if (ariaHelpers.getNativeRoleForElement(el)) continue; // has a real implicit role after all, not this branch's concern
|
|
341
341
|
|
|
342
342
|
const present = [];
|
|
343
343
|
for (const attr of PROHIBITED_NAMING_ATTRS) {
|
|
@@ -349,7 +349,7 @@ function runInPage(ctx) {
|
|
|
349
349
|
applicableCount += 1;
|
|
350
350
|
|
|
351
351
|
const ancestorRole = getNearestAncestorRole(el);
|
|
352
|
-
if (ancestorRole && WIDGET_TYPE_ROLES.has(ancestorRole)) continue; // roleless helper node inside a real widget
|
|
352
|
+
if (ancestorRole && WIDGET_TYPE_ROLES.has(ancestorRole)) continue; // roleless helper node inside a real widget, not flagged
|
|
353
353
|
|
|
354
354
|
const nameInfo = helpers.getContentNameInfo ? helpers.getContentNameInfo(el, ctx) : null;
|
|
355
355
|
const hasContentFallback = !!(
|
|
@@ -363,13 +363,18 @@ function runInPage(ctx) {
|
|
|
363
363
|
cantTellOccurrences.push(
|
|
364
364
|
helpers.reportOccurrence(el, {
|
|
365
365
|
occurrenceOutcome: 'cantTell',
|
|
366
|
-
summary: `This ${tag} has no role, so ${attr} may not be exposed as its accessible name by assistive technology
|
|
366
|
+
summary: `This ${tag} has no role, so ${attr} may not be exposed as its accessible name by assistive technology, but the element's own content already provides one.`,
|
|
367
367
|
hint: 'Verify whether the existing text content already serves as this element’s label; if so the naming attribute is redundant, otherwise give the element a role that supports naming (e.g. role="img").',
|
|
368
368
|
i18n: {
|
|
369
369
|
summaryKey: 'ariaProhibitedAttr_summary_cantTell_roleless',
|
|
370
370
|
hintKey: 'ariaProhibitedAttr_hint_cantTell_roleless',
|
|
371
371
|
params: { attr, element: tag }
|
|
372
372
|
},
|
|
373
|
+
uncertainty: {
|
|
374
|
+
code: 'judgement-required',
|
|
375
|
+
needed: 'Whether the element’s own content already serves as its label.',
|
|
376
|
+
evidence: { attribute: attr, element: tag, role: null, hasOwnContent: true }
|
|
377
|
+
},
|
|
373
378
|
data: {
|
|
374
379
|
details: {
|
|
375
380
|
reasonCode: 'ARIA_ATTR_PROHIBITED_ROLELESS_NEEDS_REVIEW',
|
|
@@ -411,7 +416,7 @@ function runInPage(ctx) {
|
|
|
411
416
|
|
|
412
417
|
// See helpers.resolveTieredOutcome's own header comment (src/core/dom-helpers.js):
|
|
413
418
|
// a fail-tier finding never silently discards cantTell-tier findings from
|
|
414
|
-
// the same run
|
|
419
|
+
// the same run. Both are returned together when the outcome is 'fail'.
|
|
415
420
|
const resolved = helpers.resolveTieredOutcome(
|
|
416
421
|
failOccurrences,
|
|
417
422
|
cantTellOccurrences,
|