@surea11y/core 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -7
- package/LICENSE +373 -21
- package/README.md +175 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +27 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +19 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +63 -18
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +63 -31
- package/src/checks/automatic/aria-allowed-attr.js +204 -80
- package/src/checks/automatic/aria-allowed-role.js +23 -7
- package/src/checks/automatic/aria-braille-equivalent.js +34 -10
- package/src/checks/automatic/aria-conditional-attr.js +32 -14
- package/src/checks/automatic/aria-deprecated-role.js +26 -11
- package/src/checks/automatic/aria-hidden-body.js +48 -23
- package/src/checks/automatic/aria-hidden-focus.js +420 -66
- package/src/checks/automatic/aria-prohibited-attr.js +327 -60
- package/src/checks/automatic/aria-prohibited-children.js +111 -103
- package/src/checks/automatic/aria-required-attr.js +29 -15
- package/src/checks/automatic/aria-required-children.js +44 -24
- package/src/checks/automatic/aria-required-parent.js +64 -35
- package/src/checks/automatic/aria-role-name-present.js +49 -21
- package/src/checks/automatic/aria-roles-valid.js +24 -12
- package/src/checks/automatic/aria-valid-attr-value.js +46 -22
- package/src/checks/automatic/aria-valid-attr.js +19 -5
- package/src/checks/automatic/autocomplete-valid.js +76 -16
- package/src/checks/automatic/avoid-inline-spacing.js +23 -8
- package/src/checks/automatic/binary-control-name-present.js +62 -50
- package/src/checks/automatic/button-name-present.js +54 -24
- package/src/checks/automatic/bypass-blocks-present.js +51 -32
- package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
- package/src/checks/automatic/combobox-name-present.js +40 -45
- package/src/checks/automatic/contrast-computable.js +363 -341
- package/src/checks/automatic/contrast-enhanced.js +489 -466
- package/src/checks/automatic/contrast-minimum.js +488 -465
- package/src/checks/automatic/css-orientation-lock.js +51 -35
- package/src/checks/automatic/definition-list-children-valid.js +46 -25
- package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
- package/src/checks/automatic/dialog-name-present.js +47 -85
- package/src/checks/automatic/dlitem-parent-valid.js +25 -8
- package/src/checks/automatic/duplicate-id-aria.js +28 -9
- package/src/checks/automatic/embed-text-alternative-present.js +88 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
- package/src/checks/automatic/form-control-single-label.js +50 -14
- package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
- package/src/checks/automatic/iframe-focusable-content.js +265 -22
- package/src/checks/automatic/iframe-name-present.js +33 -9
- package/src/checks/automatic/iframe-title-unique.js +32 -9
- package/src/checks/automatic/img-alt-present.js +54 -52
- package/src/checks/automatic/input-image-alt-present.js +141 -112
- package/src/checks/automatic/label-in-name.js +65 -41
- package/src/checks/automatic/language-page-present.js +111 -109
- package/src/checks/automatic/link-in-text-block.js +61 -19
- package/src/checks/automatic/link-name-present.js +47 -14
- package/src/checks/automatic/list-children-valid.js +40 -33
- package/src/checks/automatic/listbox-name-present.js +41 -19
- package/src/checks/automatic/listitem-parent-valid.js +48 -13
- package/src/checks/automatic/menuitem-name-present.js +41 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
- package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
- package/src/checks/automatic/meter-name-present.js +40 -36
- package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
- package/src/checks/automatic/object-text-alternative-present.js +93 -39
- package/src/checks/automatic/option-name-present.js +40 -21
- package/src/checks/automatic/page-title-present.js +19 -6
- package/src/checks/automatic/progressbar-name-present.js +49 -44
- package/src/checks/automatic/role-img-alt-present.js +211 -159
- package/src/checks/automatic/searchbox-name-present.js +41 -19
- package/src/checks/automatic/server-side-image-map-absent.js +27 -11
- package/src/checks/automatic/slider-name-present.js +42 -47
- package/src/checks/automatic/spinbutton-name-present.js +41 -19
- package/src/checks/automatic/summary-name-present.js +39 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
- package/src/checks/automatic/svg-text-alternative-present.js +262 -230
- package/src/checks/automatic/tab-name-present.js +39 -60
- package/src/checks/automatic/table-headers-attr-valid.js +27 -10
- package/src/checks/automatic/table-th-has-data-cells.js +24 -8
- package/src/checks/automatic/target-size-minimum.js +123 -48
- package/src/checks/automatic/td-has-header.js +53 -12
- package/src/checks/automatic/textbox-name-present.js +41 -19
- package/src/checks/automatic/tooltip-name-present.js +39 -18
- package/src/checks/automatic/treeitem-name-present.js +40 -21
- package/src/checks/automatic/valid-lang.js +22 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
- package/src/checks/manual/accesskeys-manual.js +17 -6
- package/src/checks/manual/area-alt-decorative-manual.js +194 -193
- package/src/checks/manual/area-alt-quality-manual.js +184 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
- package/src/checks/manual/aria-text-manual.js +20 -11
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
- package/src/checks/manual/css-hidden-focus.js +375 -169
- package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
- package/src/checks/manual/empty-heading-manual.js +41 -24
- package/src/checks/manual/empty-table-header-manual.js +69 -31
- package/src/checks/manual/focus-order-semantics-manual.js +60 -13
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
- package/src/checks/manual/heading-order-manual.js +50 -8
- package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
- package/src/checks/manual/image-redundant-alt-manual.js +38 -8
- package/src/checks/manual/img-alt-decorative-manual.js +133 -96
- package/src/checks/manual/img-alt-quality-manual.js +178 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
- package/src/checks/manual/label-title-only-manual.js +44 -28
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
- package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
- package/src/checks/manual/landmark-one-main-manual.js +38 -43
- package/src/checks/manual/landmark-unique-manual.js +78 -67
- package/src/checks/manual/link-name-quality-manual.js +45 -12
- package/src/checks/manual/media-transcript-present-manual.js +37 -22
- package/src/checks/manual/meta-viewport-large-manual.js +19 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
- package/src/checks/manual/p-as-heading-manual.js +24 -7
- package/src/checks/manual/page-has-heading-one-manual.js +42 -32
- package/src/checks/manual/page-title-patterns-manual.js +80 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
- package/src/checks/manual/region-manual.js +244 -60
- package/src/checks/manual/scope-attr-valid-manual.js +13 -4
- package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
- package/src/checks/manual/skip-link-manual.js +42 -18
- package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
- package/src/checks/manual/tabindex-manual.js +13 -4
- package/src/checks/manual/table-duplicate-name-manual.js +22 -11
- package/src/checks/manual/table-fake-caption-manual.js +48 -10
- package/src/checks/manual/video-caption-manual.js +17 -4
- package/src/checks/manual-review.js +58 -12
- package/src/core.js +41705 -29650
- package/src/index.js +2 -0
- package/src/report.js +109 -47
- package/src/sarif.js +190 -0
- package/surea11y.browser.js +37774 -0
- package/bin/core.js +0 -348
- package/docs/CLI.md +0 -75
- package/src/catalogs/composites.wcag.js +0 -490
- package/src/checks/rules-and-tags.full.csv +0 -19
- package/src/checks/rules-and-tags.full.json +0 -259
- package/src/core/aria-helpers.js +0 -970
- package/src/core/contrast-helpers.js +0 -1147
- package/src/core/dom-helpers.js +0 -4235
- package/src/core/dom-runner.js +0 -671
- package/src/core/frame-messaging.js +0 -210
- package/src/core/frame-scan.js +0 -178
- package/src/core/rollup-composites.js +0 -135
- package/src/core/rule-meta.js +0 -159
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -923
- package/src/i18n/fr.js +0 -844
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -55
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -7,64 +9,82 @@
|
|
|
7
9
|
* @standard WCAG 2.2
|
|
8
10
|
* @sc 4.1.2
|
|
9
11
|
* @applicability
|
|
10
|
-
* Applies to elements whose explicit, valid role is one of the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* caption, code, deletion, emphasis, generic,
|
|
14
|
-
* paragraph, presentation, strong, subscript,
|
|
15
|
-
* time), and
|
|
12
|
+
* Applies to (a) elements whose explicit, valid role is one of the ARIA
|
|
13
|
+
* 1.2 roles with a documented "Prohibited ARIA States and Properties"
|
|
14
|
+
* list for naming attributes (pure text-semantics / non-naming
|
|
15
|
+
* structural roles: caption, code, deletion, emphasis, generic,
|
|
16
|
+
* insertion, mark, none, paragraph, presentation, strong, subscript,
|
|
17
|
+
* suggestion, superscript, time), and (b) elements with no role at all —
|
|
18
|
+
* a curated set of native HTML tags verified to carry no implicit role
|
|
19
|
+
* (see ROLELESS_NATIVE_TAGS below), or any autonomous custom element (a
|
|
20
|
+
* hyphenated, author-defined tag per the Custom Elements spec; see
|
|
21
|
+
* isRolelessCustomElementTag below) — in both cases, only elements that
|
|
22
|
+
* also carry aria-label or aria-labelledby.
|
|
16
23
|
* @expectation
|
|
17
|
-
* Prohibited attributes must not be present
|
|
18
|
-
*
|
|
19
|
-
*
|
|
24
|
+
* Prohibited attributes must not be present on (a); for (b), the naming
|
|
25
|
+
* attribute is at best unreliable (nothing accessible-name-aware to hang
|
|
26
|
+
* it off) and at worst silently ignored by assistive technology — see the
|
|
27
|
+
* roleless-branch implementation note below for the confidence split
|
|
28
|
+
* this produces.
|
|
20
29
|
* @implementation-notes
|
|
21
30
|
* - Deliberately scoped to the single, well-established prohibition class
|
|
22
|
-
* (naming attributes on pure text-semantics roles) rather than
|
|
23
|
-
*
|
|
31
|
+
* (naming attributes on pure text-semantics roles) rather than an
|
|
32
|
+
* exhaustive per-role prohibited-attribute table; see
|
|
24
33
|
* src/core/aria-helpers.js file header for this engine's confidence-
|
|
25
|
-
* scoping rationale.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* documented 1.2-introduced alias, identical semantics). The
|
|
41
|
-
* pre-existing `presentation-role-conflict` rule already treats
|
|
42
|
-
* aria-label/aria-labelledby as conflicting on these two roles, but at
|
|
43
|
-
* `manual`/cantTell confidence across a ~24-attribute general list —
|
|
44
|
-
* this addition lets the specific, unambiguous naming-prohibition case
|
|
45
|
-
* also fire as a hard, WCAG-normative `fail` via this rule, matching
|
|
46
|
-
* this engine's "one rule = one normative decision" pattern rather than
|
|
47
|
-
* only ever surfacing it as advisory.
|
|
48
|
-
* - Investigated, but deliberately did NOT add, `definition`/`term`
|
|
49
|
-
* despite both appearing on MDN's aria-label reference page's
|
|
50
|
-
* "not supported" list: that MDN list is demonstrably wrong for these
|
|
51
|
-
* two — a widely-used reference engine's own role data explicitly declares
|
|
52
|
-
* `nameFrom: ['author']` (`definition`) / `nameFrom: ['author',
|
|
53
|
-
* 'contents']` (`term`), and the W3C spec's own §5.2.8.4 "Roles
|
|
54
|
-
* Supporting Name From Author" index lists both by name; MDN's
|
|
55
|
-
* `definition_role` page even demonstrates `aria-labelledby` usage on
|
|
56
|
-
* it directly. A real, confirmed documentation bug on MDN's side, not
|
|
57
|
-
* a gap here.
|
|
34
|
+
* scoping rationale. The 14-role list comes from the W3C WAI-ARIA 1.2
|
|
35
|
+
* spec's §5.2.8.6 "Roles which cannot be named"; only roles/attrs with
|
|
36
|
+
* high confidence from the spec text are included, since a wrong entry
|
|
37
|
+
* here causes a false-positive fail.
|
|
38
|
+
* - The pre-existing presentation-role-conflict rule already treats
|
|
39
|
+
* aria-label/aria-labelledby as conflicting on presentation/none, but at
|
|
40
|
+
* manual/cantTell confidence across a broad attribute list; this rule's
|
|
41
|
+
* narrower, unambiguous naming-prohibition case fires as a hard,
|
|
42
|
+
* WCAG-normative fail instead, matching this engine's "one rule = one
|
|
43
|
+
* normative decision" pattern.
|
|
44
|
+
* - Deliberately excludes `definition`/`term` despite both appearing on
|
|
45
|
+
* MDN's aria-label "not supported" list: both support name from author
|
|
46
|
+
* (`nameFrom: ['author']` for definition, `['author', 'contents']` for
|
|
47
|
+
* term), and the W3C spec's §5.2.8.4 "Roles Supporting Name From Author"
|
|
48
|
+
* index lists both by name.
|
|
58
49
|
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
59
50
|
* property, while engine-level hidden-subtree filtering still applies
|
|
60
51
|
* unless engineOptions.includeHiddenElements is true.
|
|
52
|
+
* - Second, independent branch: naming attributes on ROLELESS elements (no
|
|
53
|
+
* explicit role="", no implicit/native role either) — e.g. icon-only
|
|
54
|
+
* `<span aria-label="...">` tiles with no other accessible-name source.
|
|
55
|
+
* ROLELESS_NATIVE_TAGS below is a curated, deliberately conservative
|
|
56
|
+
* list of native tags confirmed to carry no implicit role (common
|
|
57
|
+
* text-level tags like `<p>`/`<strong>`/`<em>`/`<code>`/`<mark>`/`<time>`
|
|
58
|
+
* resolve to role `null`, same as a bare `<div>`/`<span>`);
|
|
59
|
+
* `<section>`/`<form>`/`<a>` are excluded even though they can also
|
|
60
|
+
* resolve to no role, because their native role is conditional
|
|
61
|
+
* (name-dependent/href-dependent) and already has dedicated handling in
|
|
62
|
+
* `getElementRoleKey`'s `section`/`section[named]`/`header`/
|
|
63
|
+
* `header[toplevel]` branches that this rule doesn't duplicate.
|
|
64
|
+
* Two confidence tiers instead of a flat fail: if the element's subtree
|
|
65
|
+
* already produces a non-empty accessible name from its content (via
|
|
66
|
+
* `helpers.getContentNameInfo`, same as
|
|
67
|
+
* link-name-present/button-name-present), the naming attribute might
|
|
68
|
+
* just be a redundant/intentional override — reported as `cantTell`, not
|
|
69
|
+
* a hard fail. Only a roleless element with no other accessible-name
|
|
70
|
+
* source at all is a confident, deterministic `fail`. The
|
|
71
|
+
* widget-ancestor exemption (skip when the closest real ancestor role is
|
|
72
|
+
* a "widget"-type role) avoids over-flagging roleless helper spans/divs
|
|
73
|
+
* used as internal decoration inside a custom composite widget.
|
|
74
|
+
* - The Tier-2 "already has a role, not this branch's concern" guard
|
|
75
|
+
* validates the explicit role via `isValidConcreteRole` before treating
|
|
76
|
+
* it as real (matching `getNearestAncestorRole`'s own pattern): an
|
|
77
|
+
* unrecognized role token (e.g. `role="totally-bogus"`) is ignored by
|
|
78
|
+
* the accessibility tree, not honored, so the element is still
|
|
79
|
+
* effectively roleless and must still be checked by this branch.
|
|
61
80
|
*/
|
|
62
81
|
|
|
63
82
|
const id = 'aria-prohibited-attr';
|
|
64
83
|
|
|
65
84
|
const meta = {
|
|
66
85
|
title: 'ARIA naming attributes must not be used on roles that prohibit them',
|
|
67
|
-
description:
|
|
86
|
+
description:
|
|
87
|
+
'Checks that aria-label/aria-labelledby are not present on WAI-ARIA roles whose specification explicitly prohibits ARIA naming (e.g. generic, emphasis, strong, paragraph).',
|
|
68
88
|
i18n: {
|
|
69
89
|
titleKey: 'ariaProhibitedAttr_title',
|
|
70
90
|
descriptionKey: 'ariaProhibitedAttr_description'
|
|
@@ -73,7 +93,13 @@ const meta = {
|
|
|
73
93
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
74
94
|
wcagSc: ['4.1.2'],
|
|
75
95
|
normativeMappings: [
|
|
76
|
-
{
|
|
96
|
+
{
|
|
97
|
+
standard: 'WCAG',
|
|
98
|
+
version: '2.2',
|
|
99
|
+
requirement: '4.1.2',
|
|
100
|
+
title: 'Name, Role, Value',
|
|
101
|
+
conformanceLevel: 'A'
|
|
102
|
+
}
|
|
77
103
|
],
|
|
78
104
|
defaultSeverity: 'moderate',
|
|
79
105
|
category: 'robust',
|
|
@@ -83,7 +109,7 @@ const meta = {
|
|
|
83
109
|
};
|
|
84
110
|
|
|
85
111
|
function runInPage(ctx) {
|
|
86
|
-
const {
|
|
112
|
+
const { helpers, rule } = ctx;
|
|
87
113
|
|
|
88
114
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
89
115
|
if (!ariaHelpers) {
|
|
@@ -96,19 +122,36 @@ function runInPage(ctx) {
|
|
|
96
122
|
// scope) because the build inlines only this function's own source text
|
|
97
123
|
// — see scripts/build-core.js header ("runInPage MUST be self-contained").
|
|
98
124
|
const ROLES_PROHIBITING_NAME = new Set([
|
|
99
|
-
'caption',
|
|
100
|
-
'
|
|
101
|
-
'
|
|
125
|
+
'caption',
|
|
126
|
+
'code',
|
|
127
|
+
'deletion',
|
|
128
|
+
'emphasis',
|
|
129
|
+
'generic',
|
|
130
|
+
'insertion',
|
|
131
|
+
'mark',
|
|
132
|
+
'none',
|
|
133
|
+
'paragraph',
|
|
134
|
+
'presentation',
|
|
135
|
+
'strong',
|
|
136
|
+
'subscript',
|
|
137
|
+
'suggestion',
|
|
138
|
+
'superscript',
|
|
139
|
+
'time'
|
|
102
140
|
]);
|
|
103
141
|
|
|
104
142
|
const PROHIBITED_NAMING_ATTRS = ['aria-label', 'aria-labelledby'];
|
|
105
143
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const occurrences = [];
|
|
144
|
+
const failOccurrences = [];
|
|
145
|
+
const cantTellOccurrences = [];
|
|
109
146
|
let applicableCount = 0;
|
|
110
147
|
|
|
111
|
-
|
|
148
|
+
// --- Tier 1: explicit, valid role from the naming-prohibited set ---
|
|
149
|
+
|
|
150
|
+
const roleNodes = helpers.queryAllSmart
|
|
151
|
+
? helpers.queryAllSmart('[role]')
|
|
152
|
+
: helpers.queryAll('[role]');
|
|
153
|
+
|
|
154
|
+
for (const el of roleNodes) {
|
|
112
155
|
if (!el || !el.getAttribute) continue;
|
|
113
156
|
|
|
114
157
|
const role = ariaHelpers.getExplicitRole(el);
|
|
@@ -125,12 +168,13 @@ function runInPage(ctx) {
|
|
|
125
168
|
if (!present.length) continue;
|
|
126
169
|
|
|
127
170
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
128
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
171
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
129
172
|
|
|
130
173
|
for (const attr of present) {
|
|
131
|
-
|
|
174
|
+
failOccurrences.push({
|
|
132
175
|
selector: stableSelector,
|
|
133
176
|
html,
|
|
177
|
+
occurrenceOutcome: 'fail',
|
|
134
178
|
summary: 'This attribute is prohibited on this element’s role.',
|
|
135
179
|
hint: 'Remove this attribute; this role must not carry an accessible name.',
|
|
136
180
|
i18n: {
|
|
@@ -145,13 +189,236 @@ function runInPage(ctx) {
|
|
|
145
189
|
}
|
|
146
190
|
}
|
|
147
191
|
|
|
192
|
+
// --- Tier 2: no role at all (see header comment for the full rationale
|
|
193
|
+
// and how ROLELESS_NATIVE_TAGS/WIDGET_TYPE_ROLES were derived) ---
|
|
194
|
+
|
|
195
|
+
// Small, curated set of native tags verified to carry no explicit or
|
|
196
|
+
// implicit ARIA role. Deliberately excludes <section>/<form>/<a> — all
|
|
197
|
+
// conditionally roleless too, but already handled with more nuance
|
|
198
|
+
// elsewhere in this engine (see header comment).
|
|
199
|
+
const ROLELESS_NATIVE_TAGS = new Set([
|
|
200
|
+
'p',
|
|
201
|
+
'b',
|
|
202
|
+
'i',
|
|
203
|
+
'em',
|
|
204
|
+
'strong',
|
|
205
|
+
'span',
|
|
206
|
+
'div',
|
|
207
|
+
'code',
|
|
208
|
+
'mark',
|
|
209
|
+
'time',
|
|
210
|
+
'ins',
|
|
211
|
+
'del',
|
|
212
|
+
'small',
|
|
213
|
+
'sub',
|
|
214
|
+
'sup',
|
|
215
|
+
'abbr',
|
|
216
|
+
'cite',
|
|
217
|
+
'q',
|
|
218
|
+
'kbd',
|
|
219
|
+
'samp',
|
|
220
|
+
'var',
|
|
221
|
+
'address',
|
|
222
|
+
'blockquote',
|
|
223
|
+
'pre',
|
|
224
|
+
'figcaption',
|
|
225
|
+
'picture',
|
|
226
|
+
'template',
|
|
227
|
+
'hgroup',
|
|
228
|
+
'wbr',
|
|
229
|
+
'br',
|
|
230
|
+
'legend'
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
// WAI-ARIA roles typed as "widget" (the role set the roleless-branch
|
|
234
|
+
// exemption below branches on, not the six-category WAI-ARIA taxonomy).
|
|
235
|
+
const WIDGET_TYPE_ROLES = new Set([
|
|
236
|
+
'alert',
|
|
237
|
+
'alertdialog',
|
|
238
|
+
'button',
|
|
239
|
+
'checkbox',
|
|
240
|
+
'combobox',
|
|
241
|
+
'dialog',
|
|
242
|
+
'gridcell',
|
|
243
|
+
'link',
|
|
244
|
+
'listbox',
|
|
245
|
+
'log',
|
|
246
|
+
'marquee',
|
|
247
|
+
'menuitem',
|
|
248
|
+
'menuitemcheckbox',
|
|
249
|
+
'menuitemradio',
|
|
250
|
+
'option',
|
|
251
|
+
'progressbar',
|
|
252
|
+
'radio',
|
|
253
|
+
'scrollbar',
|
|
254
|
+
'searchbox',
|
|
255
|
+
'slider',
|
|
256
|
+
'spinbutton',
|
|
257
|
+
'status',
|
|
258
|
+
'switch',
|
|
259
|
+
'tab',
|
|
260
|
+
'tabpanel',
|
|
261
|
+
'textbox',
|
|
262
|
+
'timer',
|
|
263
|
+
'treeitem'
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
const getComposedParent =
|
|
267
|
+
helpers && typeof helpers.composedParent === 'function'
|
|
268
|
+
? helpers.composedParent
|
|
269
|
+
: function (n) {
|
|
270
|
+
return n && n.parentElement ? n.parentElement : null;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Nearest ancestor's real role (explicit-if-valid, else native/implicit),
|
|
274
|
+
// skipping roleless/presentation/none ancestors — used only to check
|
|
275
|
+
// whether that role is a "widget"-type one (the roleless-branch
|
|
276
|
+
// exemption). Not the same helper as aria-required-parent's containment
|
|
277
|
+
// walk: this one also accepts non-required-context roles.
|
|
278
|
+
function getNearestAncestorRole(el) {
|
|
279
|
+
let cur = getComposedParent(el);
|
|
280
|
+
let guard = 0;
|
|
281
|
+
while (cur && guard++ < 200) {
|
|
282
|
+
if (cur.nodeType !== 1) {
|
|
283
|
+
cur = getComposedParent(cur);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const explicit = ariaHelpers.getExplicitRole(cur);
|
|
287
|
+
const role =
|
|
288
|
+
explicit && ariaHelpers.isValidConcreteRole(explicit)
|
|
289
|
+
? explicit
|
|
290
|
+
: ariaHelpers.getNativeRoleForElement(cur);
|
|
291
|
+
if (!role || role === 'presentation' || role === 'none') {
|
|
292
|
+
cur = getComposedParent(cur);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
return role;
|
|
296
|
+
}
|
|
297
|
+
return '';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// A small, spec-reserved set of hyphenated tag names that are NOT
|
|
301
|
+
// autonomous custom elements despite containing a hyphen (legacy SVG/
|
|
302
|
+
// MathML tags predating the Custom Elements spec) — see
|
|
303
|
+
// https://html.spec.whatwg.org/#valid-custom-element-name's own
|
|
304
|
+
// exclusion list. Excluded so this doesn't misclassify them as
|
|
305
|
+
// always-roleless the same way a real custom element is.
|
|
306
|
+
const RESERVED_HYPHENATED_TAGS = new Set([
|
|
307
|
+
'annotation-xml',
|
|
308
|
+
'color-profile',
|
|
309
|
+
'font-face',
|
|
310
|
+
'font-face-src',
|
|
311
|
+
'font-face-uri',
|
|
312
|
+
'font-face-format',
|
|
313
|
+
'font-face-name',
|
|
314
|
+
'missing-glyph'
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
// An autonomous custom element (author-defined tag, always containing a
|
|
318
|
+
// hyphen per the Custom Elements spec's naming grammar) has no implicit
|
|
319
|
+
// ARIA role at all -- unlike native tags, there is no conditional-role
|
|
320
|
+
// nuance to worry about here (a native <a>/<section>/<form> can gain an
|
|
321
|
+
// implicit role depending on other attributes, which is exactly why
|
|
322
|
+
// ROLELESS_NATIVE_TAGS is a hand-verified allowlist rather than a
|
|
323
|
+
// blanket rule; a custom element has no such spec-defined conditional
|
|
324
|
+
// role logic whatsoever). Covers e.g. a `<play-button aria-label="...">`
|
|
325
|
+
// or `<app-carousel aria-label="...">` with no other name source, which
|
|
326
|
+
// the fixed native-tag allowlist below would otherwise skip.
|
|
327
|
+
function isRolelessCustomElementTag(tag) {
|
|
328
|
+
return tag.includes('-') && !RESERVED_HYPHENATED_TAGS.has(tag);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const namingSelector = '[aria-label],[aria-labelledby]';
|
|
332
|
+
const namingNodes = helpers.queryAllSmart
|
|
333
|
+
? helpers.queryAllSmart(namingSelector)
|
|
334
|
+
: helpers.queryAll(namingSelector);
|
|
335
|
+
|
|
336
|
+
for (const el of namingNodes) {
|
|
337
|
+
if (!el || !el.getAttribute) continue;
|
|
338
|
+
|
|
339
|
+
const tag = String(el.tagName || '').toLowerCase();
|
|
340
|
+
if (!ROLELESS_NATIVE_TAGS.has(tag) && !isRolelessCustomElementTag(tag)) continue;
|
|
341
|
+
const explicitRole = ariaHelpers.getExplicitRole(el);
|
|
342
|
+
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.
|
|
343
|
+
if (ariaHelpers.getNativeRoleForElement(el)) continue; // has a real implicit role after all — not this branch's concern
|
|
344
|
+
|
|
345
|
+
const present = [];
|
|
346
|
+
for (const attr of PROHIBITED_NAMING_ATTRS) {
|
|
347
|
+
const v = el.getAttribute(attr);
|
|
348
|
+
if (v != null && String(v).trim() !== '') present.push(attr);
|
|
349
|
+
}
|
|
350
|
+
if (!present.length) continue;
|
|
351
|
+
|
|
352
|
+
applicableCount += 1;
|
|
353
|
+
|
|
354
|
+
const ancestorRole = getNearestAncestorRole(el);
|
|
355
|
+
if (ancestorRole && WIDGET_TYPE_ROLES.has(ancestorRole)) continue; // roleless helper node inside a real widget — not flagged
|
|
356
|
+
|
|
357
|
+
const nameInfo = helpers.getContentNameInfo ? helpers.getContentNameInfo(el, ctx) : null;
|
|
358
|
+
const hasContentFallback = !!(
|
|
359
|
+
nameInfo &&
|
|
360
|
+
nameInfo.present &&
|
|
361
|
+
String(nameInfo.value || '').trim() !== ''
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
365
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
366
|
+
|
|
367
|
+
for (const attr of present) {
|
|
368
|
+
if (hasContentFallback) {
|
|
369
|
+
cantTellOccurrences.push({
|
|
370
|
+
selector: stableSelector,
|
|
371
|
+
html,
|
|
372
|
+
occurrenceOutcome: 'cantTell',
|
|
373
|
+
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.`,
|
|
374
|
+
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").',
|
|
375
|
+
i18n: {
|
|
376
|
+
summaryKey: 'ariaProhibitedAttr_summary_cantTell_roleless',
|
|
377
|
+
hintKey: 'ariaProhibitedAttr_hint_cantTell_roleless',
|
|
378
|
+
params: { attr, element: tag }
|
|
379
|
+
},
|
|
380
|
+
data: {
|
|
381
|
+
details: {
|
|
382
|
+
reasonCode: 'ARIA_ATTR_PROHIBITED_ROLELESS_NEEDS_REVIEW',
|
|
383
|
+
attr,
|
|
384
|
+
role: null,
|
|
385
|
+
element: tag
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
} else {
|
|
390
|
+
failOccurrences.push({
|
|
391
|
+
selector: stableSelector,
|
|
392
|
+
html,
|
|
393
|
+
occurrenceOutcome: 'fail',
|
|
394
|
+
summary: `This ${tag} has no role and no other accessible-name source, so ${attr} is not reliably exposed to assistive technology.`,
|
|
395
|
+
hint: 'Give this element a role that supports an accessible name (e.g. role="img"/"button"), or remove this attribute if it serves no purpose without one.',
|
|
396
|
+
i18n: {
|
|
397
|
+
summaryKey: 'ariaProhibitedAttr_summary_fail_roleless',
|
|
398
|
+
hintKey: 'ariaProhibitedAttr_hint_fail_roleless',
|
|
399
|
+
params: { attr, element: tag }
|
|
400
|
+
},
|
|
401
|
+
data: {
|
|
402
|
+
details: { reasonCode: 'ARIA_ATTR_PROHIBITED_ROLELESS', attr, role: null, element: tag }
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
148
409
|
if (applicableCount === 0) {
|
|
149
410
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
150
411
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
412
|
+
|
|
413
|
+
// See helpers.resolveTieredOutcome's own header comment (src/core/dom-helpers.js):
|
|
414
|
+
// a fail-tier finding never silently discards cantTell-tier findings from
|
|
415
|
+
// the same run — both are returned together when the outcome is 'fail'.
|
|
416
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
417
|
+
failOccurrences,
|
|
418
|
+
cantTellOccurrences,
|
|
419
|
+
rule.defaultSeverity || 'moderate'
|
|
420
|
+
);
|
|
421
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
155
422
|
}
|
|
156
423
|
|
|
157
424
|
module.exports = { id, meta, runInPage };
|