@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
|
const id = 'link-name-present';
|
|
@@ -13,7 +15,13 @@ const meta = {
|
|
|
13
15
|
tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'links', 'name'],
|
|
14
16
|
wcagSc: ['4.1.2'],
|
|
15
17
|
normativeMappings: [
|
|
16
|
-
{
|
|
18
|
+
{
|
|
19
|
+
standard: 'WCAG',
|
|
20
|
+
version: '2.2',
|
|
21
|
+
requirement: '4.1.2',
|
|
22
|
+
title: 'Name, Role, Value',
|
|
23
|
+
conformanceLevel: 'A'
|
|
24
|
+
}
|
|
17
25
|
],
|
|
18
26
|
defaultSeverity: 'serious',
|
|
19
27
|
category: 'robust',
|
|
@@ -24,7 +32,7 @@ const meta = {
|
|
|
24
32
|
};
|
|
25
33
|
|
|
26
34
|
function runInPage(ctx) {
|
|
27
|
-
const {
|
|
35
|
+
const { helpers, rule } = ctx;
|
|
28
36
|
|
|
29
37
|
const occurrences = [];
|
|
30
38
|
let applicableCount = 0;
|
|
@@ -40,48 +48,68 @@ function runInPage(ctx) {
|
|
|
40
48
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
41
49
|
return info && info.present ? info.value : '';
|
|
42
50
|
}
|
|
43
|
-
const t =
|
|
51
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
44
52
|
return t.replace(/\s+/g, ' ').trim();
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
const selector = 'a[href], area[href], [role="link"]';
|
|
48
|
-
const nodes = helpers.queryAllSmart
|
|
56
|
+
const nodes = helpers.queryAllSmart
|
|
57
|
+
? helpers.queryAllSmart(selector)
|
|
58
|
+
: helpers.queryAll(selector);
|
|
49
59
|
|
|
50
60
|
for (const el of nodes) {
|
|
51
61
|
// isAccTreeEligible returns { eligible, reasons }, not a boolean.
|
|
52
62
|
const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
|
|
53
|
-
const eligible =
|
|
63
|
+
const eligible =
|
|
64
|
+
typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
|
|
54
65
|
if (!eligible) continue;
|
|
55
66
|
|
|
56
67
|
applicableCount += 1;
|
|
57
68
|
|
|
58
69
|
const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
|
|
59
|
-
const programmaticName =
|
|
70
|
+
const programmaticName = nameInfo && typeof nameInfo.value === 'string' ? nameInfo.value : '';
|
|
60
71
|
|
|
61
72
|
// A native <a href>/<area href> (or [role="link"]) whose role has been
|
|
62
73
|
// overridden to one of these roles is no longer semantically a link —
|
|
63
74
|
// per the WAI-ARIA Accessible Name and Description Computation spec
|
|
64
75
|
// these roles are name-from-author-only, and their rendered content
|
|
65
|
-
// represents a VALUE, not a NAME (
|
|
66
|
-
// verified against its source; see button-name-present.js's identical
|
|
76
|
+
// represents a VALUE, not a NAME (see button-name-present.js's identical
|
|
67
77
|
// fix for a real-world example — <button role="combobox">List</button> —
|
|
68
78
|
// of the same class of bug on a different host tag).
|
|
69
79
|
const role = el.getAttribute ? el.getAttribute('role') : null;
|
|
70
|
-
const roleNorm = String(role || '')
|
|
71
|
-
|
|
80
|
+
const roleNorm = String(role || '')
|
|
81
|
+
.replace(/\s+/g, ' ')
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase();
|
|
84
|
+
const VALUE_ROLES = [
|
|
85
|
+
'textbox',
|
|
86
|
+
'progressbar',
|
|
87
|
+
'scrollbar',
|
|
88
|
+
'slider',
|
|
89
|
+
'spinbutton',
|
|
90
|
+
'combobox',
|
|
91
|
+
'listbox'
|
|
92
|
+
];
|
|
72
93
|
const isContentNameCandidate = !VALUE_ROLES.includes(roleNorm);
|
|
73
94
|
|
|
74
|
-
const contentName =
|
|
95
|
+
const contentName =
|
|
96
|
+
programmaticName.trim().length === 0 && isContentNameCandidate
|
|
97
|
+
? getConservativeSubtreeText(el)
|
|
98
|
+
: '';
|
|
75
99
|
const finalName = (programmaticName.trim().length ? programmaticName : contentName).trim();
|
|
76
100
|
|
|
77
101
|
if (finalName.length === 0) {
|
|
78
102
|
// Only compute the richer eligibility-info payload (used solely for
|
|
79
103
|
// the occurrence's visibilityFilter) once we know an occurrence is
|
|
80
104
|
// actually being built, rather than for every applicable element.
|
|
81
|
-
const eligInfo = helpers.getEligibilityInfo
|
|
105
|
+
const eligInfo = helpers.getEligibilityInfo
|
|
106
|
+
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
107
|
+
: null;
|
|
82
108
|
|
|
83
109
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
84
|
-
const html = helpers.getOuterHtmlSnippet
|
|
110
|
+
const html = helpers.getOuterHtmlSnippet
|
|
111
|
+
? helpers.getOuterHtmlSnippet(el)
|
|
112
|
+
: el.outerHTML || '';
|
|
85
113
|
const tag = (el.tagName || '').toLowerCase();
|
|
86
114
|
|
|
87
115
|
occurrences.push({
|
|
@@ -118,7 +146,12 @@ function runInPage(ctx) {
|
|
|
118
146
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
119
147
|
}
|
|
120
148
|
if (occurrences.length) {
|
|
121
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
ruleId: rule.ruleId,
|
|
151
|
+
outcome: 'fail',
|
|
152
|
+
severity: rule.defaultSeverity || 'minor',
|
|
153
|
+
occurrences
|
|
154
|
+
};
|
|
122
155
|
}
|
|
123
156
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
124
157
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -24,35 +26,25 @@
|
|
|
24
26
|
* inverse relationship: does a given <li> have a valid parent).
|
|
25
27
|
* - Direct children that are not exposed to the accessibility tree (e.g.
|
|
26
28
|
* display:none, [hidden], aria-hidden="true") are excluded from
|
|
27
|
-
* consideration entirely —
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* DIFFERENT function from the similarly-named, unused `onlyListitemsEvaluate`
|
|
39
|
-
* elsewhere in that engine's source; don't confuse the two when re-verifying). Its
|
|
40
|
-
* exact rule: if a child has an explicit role, only `validRoles` (here,
|
|
41
|
-
* `['listitem']`) is consulted — the tag name is never checked. Only
|
|
42
|
-
* without an explicit role does the tag name (`validNodeNames`) matter.
|
|
43
|
-
* Found via two real sites: Notion's footer nav lists include a
|
|
44
|
-
* `<li role="none">` used purely to host the list's own visually-hidden
|
|
45
|
-
* label (referenced by the `<ul>`'s `aria-labelledby`), and LinkedIn has
|
|
46
|
-
* both an all-`<li role="menuitem">` menu list and a footer list mixing
|
|
47
|
-
* real `<li>`s with one `<li role="presentation">` — that reference engine
|
|
48
|
-
* correctly flags all of these, surea11y's tag-only check missed them entirely.
|
|
29
|
+
* consideration entirely — an element not reachable by assistive
|
|
30
|
+
* technology can't break the list semantics a screen reader announces.
|
|
31
|
+
* Common cases: a stray `<input type="hidden">` as a direct <ul> child
|
|
32
|
+
* (UA-stylesheet display:none by spec), or `<span style="display:none">`
|
|
33
|
+
* hydration markers interleaved with real `<li>`s.
|
|
34
|
+
* - Explicit-role-overrides-tag: if a child has an explicit role, only
|
|
35
|
+
* `['listitem']` is consulted — the tag name is never checked. Only
|
|
36
|
+
* without an explicit role does the tag name matter. Catches cases a
|
|
37
|
+
* tag-only check misses: `<li role="none">` hosting a list's own
|
|
38
|
+
* visually-hidden label, `<li role="menuitem">` menu items, or a real
|
|
39
|
+
* `<li>` mixed with an `<li role="presentation">`.
|
|
49
40
|
*/
|
|
50
41
|
|
|
51
42
|
const id = 'list-children-valid';
|
|
52
43
|
|
|
53
44
|
const meta = {
|
|
54
45
|
title: 'Lists must only directly contain list items',
|
|
55
|
-
description:
|
|
46
|
+
description:
|
|
47
|
+
'Checks that <ul>/<ol> elements only have <li>, <script>, or <template> as direct children.',
|
|
56
48
|
i18n: {
|
|
57
49
|
titleKey: 'listChildrenValid_title',
|
|
58
50
|
descriptionKey: 'listChildrenValid_description'
|
|
@@ -61,7 +53,13 @@ const meta = {
|
|
|
61
53
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'list'],
|
|
62
54
|
wcagSc: ['1.3.1'],
|
|
63
55
|
normativeMappings: [
|
|
64
|
-
{
|
|
56
|
+
{
|
|
57
|
+
standard: 'WCAG',
|
|
58
|
+
version: '2.2',
|
|
59
|
+
requirement: '1.3.1',
|
|
60
|
+
title: 'Info and Relationships',
|
|
61
|
+
conformanceLevel: 'A'
|
|
62
|
+
}
|
|
65
63
|
],
|
|
66
64
|
defaultSeverity: 'serious',
|
|
67
65
|
category: 'perceivable',
|
|
@@ -71,15 +69,18 @@ const meta = {
|
|
|
71
69
|
};
|
|
72
70
|
|
|
73
71
|
function runInPage(ctx) {
|
|
74
|
-
const {
|
|
72
|
+
const { helpers, rule } = ctx;
|
|
75
73
|
|
|
76
74
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
77
75
|
// ("runInPage MUST be self-contained").
|
|
78
76
|
const ALLOWED_CHILD_TAGS = new Set(['li', 'script', 'template']);
|
|
79
77
|
|
|
80
|
-
const nodes = helpers.queryAllSmart
|
|
78
|
+
const nodes = helpers.queryAllSmart
|
|
79
|
+
? helpers.queryAllSmart('ul, ol')
|
|
80
|
+
: helpers.queryAll('ul, ol');
|
|
81
81
|
|
|
82
|
-
const isAccTreeEligible =
|
|
82
|
+
const isAccTreeEligible =
|
|
83
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
83
84
|
|
|
84
85
|
function isExposedToAt(el) {
|
|
85
86
|
if (!isAccTreeEligible) return true;
|
|
@@ -110,8 +111,7 @@ function runInPage(ctx) {
|
|
|
110
111
|
const roleAttr = child.getAttribute ? String(child.getAttribute('role') || '').trim() : '';
|
|
111
112
|
const explicitRole = roleAttr ? (roleAttr.split(/\s+/)[0] || '').toLowerCase() : '';
|
|
112
113
|
|
|
113
|
-
// An explicit role always wins over the tag — see
|
|
114
|
-
// comment's Notion/LinkedIn examples.
|
|
114
|
+
// An explicit role always wins over the tag — see header comment.
|
|
115
115
|
const valid = explicitRole ? explicitRole === 'listitem' : ALLOWED_CHILD_TAGS.has(tag);
|
|
116
116
|
|
|
117
117
|
if (!valid) invalidTags.push(tag);
|
|
@@ -119,9 +119,11 @@ function runInPage(ctx) {
|
|
|
119
119
|
|
|
120
120
|
if (!invalidTags.length) continue;
|
|
121
121
|
|
|
122
|
+
const dedupedInvalidTags = [...new Set(invalidTags)];
|
|
123
|
+
|
|
122
124
|
const tag = el.tagName.toLowerCase();
|
|
123
125
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
124
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
126
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
125
127
|
|
|
126
128
|
occurrences.push({
|
|
127
129
|
selector: stableSelector,
|
|
@@ -131,7 +133,7 @@ function runInPage(ctx) {
|
|
|
131
133
|
i18n: {
|
|
132
134
|
summaryKey: 'listChildrenValid_summary_fail',
|
|
133
135
|
hintKey: 'listChildrenValid_hint_fail',
|
|
134
|
-
params: { element: tag, invalidChildren:
|
|
136
|
+
params: { element: tag, invalidChildren: dedupedInvalidTags.join(', ') }
|
|
135
137
|
},
|
|
136
138
|
data: {
|
|
137
139
|
details: { reasonCode: 'LIST_INVALID_CHILD', element: tag, invalidChildren: invalidTags }
|
|
@@ -143,9 +145,14 @@ function runInPage(ctx) {
|
|
|
143
145
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
144
146
|
}
|
|
145
147
|
if (occurrences.length) {
|
|
146
|
-
return {
|
|
148
|
+
return {
|
|
149
|
+
ruleId: rule.ruleId,
|
|
150
|
+
outcome: 'fail',
|
|
151
|
+
severity: rule.defaultSeverity || 'serious',
|
|
152
|
+
occurrences
|
|
153
|
+
};
|
|
147
154
|
}
|
|
148
155
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
149
156
|
}
|
|
150
157
|
|
|
151
|
-
module.exports = { id, meta, runInPage };
|
|
158
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'listbox-name-present';
|
|
@@ -10,10 +12,16 @@ const meta = {
|
|
|
10
12
|
descriptionKey: 'listboxNamePresent_description'
|
|
11
13
|
},
|
|
12
14
|
helpUrl: null,
|
|
13
|
-
tags: [
|
|
15
|
+
tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'listbox'],
|
|
14
16
|
wcagSc: ['4.1.2'],
|
|
15
17
|
normativeMappings: [
|
|
16
|
-
{
|
|
18
|
+
{
|
|
19
|
+
standard: 'WCAG',
|
|
20
|
+
version: '2.2',
|
|
21
|
+
requirement: '4.1.2',
|
|
22
|
+
title: 'Name, Role, Value',
|
|
23
|
+
conformanceLevel: 'A'
|
|
24
|
+
}
|
|
17
25
|
],
|
|
18
26
|
defaultSeverity: 'serious',
|
|
19
27
|
category: 'robust',
|
|
@@ -24,20 +32,22 @@ const meta = {
|
|
|
24
32
|
|
|
25
33
|
function runInPage(ctx) {
|
|
26
34
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
35
|
+
const getEligibilityInfo =
|
|
36
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
37
|
|
|
32
38
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
39
|
+
return String(s || '')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
function getAttr(el, name) {
|
|
37
45
|
try {
|
|
38
46
|
if (!el || !el.getAttribute) return '';
|
|
39
47
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -52,7 +62,7 @@ function runInPage(ctx) {
|
|
|
52
62
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
63
|
return info && info.present ? info.value : '';
|
|
54
64
|
}
|
|
55
|
-
const t =
|
|
65
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
56
66
|
return t.replace(/\s+/g, ' ').trim();
|
|
57
67
|
}
|
|
58
68
|
|
|
@@ -100,7 +110,8 @@ function runInPage(ctx) {
|
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
function isEligibleAcc(helpers, el, ctx) {
|
|
103
|
-
const fn =
|
|
113
|
+
const fn =
|
|
114
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
104
115
|
if (!fn) return true;
|
|
105
116
|
try {
|
|
106
117
|
const r = fn(el, ctx);
|
|
@@ -126,12 +137,13 @@ function runInPage(ctx) {
|
|
|
126
137
|
return map;
|
|
127
138
|
}
|
|
128
139
|
|
|
129
|
-
|
|
130
140
|
const occurrences = [];
|
|
131
141
|
let applicableCount = 0;
|
|
132
142
|
|
|
133
|
-
const selector =
|
|
134
|
-
const nodes = helpers.queryAllSmart
|
|
143
|
+
const selector = '[role="listbox"]';
|
|
144
|
+
const nodes = helpers.queryAllSmart
|
|
145
|
+
? helpers.queryAllSmart(selector)
|
|
146
|
+
: helpers.queryAll(selector);
|
|
135
147
|
|
|
136
148
|
// Precompute label[for] map for listbox elements that are labelable
|
|
137
149
|
// native form controls (e.g. <select multiple role="listbox">).
|
|
@@ -200,16 +212,22 @@ function runInPage(ctx) {
|
|
|
200
212
|
if (res.ok) continue;
|
|
201
213
|
|
|
202
214
|
const eligInfo = getEligibilityInfo
|
|
203
|
-
|
|
204
|
-
|
|
215
|
+
? (() => {
|
|
216
|
+
try {
|
|
217
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
})()
|
|
222
|
+
: null;
|
|
205
223
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
206
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
224
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
207
225
|
|
|
208
226
|
occurrences.push({
|
|
209
227
|
selector: stableSelector,
|
|
210
228
|
html,
|
|
211
229
|
summary: 'This element has no accessible name.',
|
|
212
|
-
hint:
|
|
230
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this listbox's accessible name.",
|
|
213
231
|
i18n: {
|
|
214
232
|
summaryKey: 'listboxNamePresent_summary_fail',
|
|
215
233
|
hintKey: 'listboxNamePresent_hint_fail',
|
|
@@ -222,12 +240,16 @@ function runInPage(ctx) {
|
|
|
222
240
|
});
|
|
223
241
|
}
|
|
224
242
|
|
|
225
|
-
|
|
226
243
|
if (applicableCount === 0) {
|
|
227
244
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
228
245
|
}
|
|
229
246
|
if (occurrences.length) {
|
|
230
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
ruleId: rule.ruleId,
|
|
249
|
+
outcome: 'fail',
|
|
250
|
+
severity: rule.defaultSeverity || 'minor',
|
|
251
|
+
occurrences
|
|
252
|
+
};
|
|
231
253
|
}
|
|
232
254
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
233
255
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -25,19 +27,27 @@
|
|
|
25
27
|
* native role is fully replaced by the explicit one — the same "any
|
|
26
28
|
* explicit role overrides the element's native role" ARIA principle
|
|
27
29
|
* applied elsewhere in this engine), so an <li> inside it is invalid
|
|
28
|
-
* despite the <ul> tag
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* despite the <ul> tag (e.g. `<ul role="menu"><li>...`). Conversely
|
|
31
|
+
* role="presentation"/"none" on the parent is still a valid
|
|
32
|
+
* (list-semantics-suppressing) parent — the accepted parent roles are
|
|
33
|
+
* presentation, none, and list.
|
|
34
|
+
* - The SAME "explicit role wins" principle applies to the <li> ELEMENT
|
|
35
|
+
* ITSELF: an <li role="tab">/role="menuitem">/role="presentation"> etc.
|
|
36
|
+
* is exposed to AT with that role, never "listitem" — the whole point
|
|
37
|
+
* of this check (list items need a valid list-container parent) doesn't
|
|
38
|
+
* apply when the element isn't claiming listitem semantics in the first
|
|
39
|
+
* place. Any `<li>` with an explicit `role` attribute is excluded from
|
|
40
|
+
* candidacy. `role="listitem"` itself is a no-op restatement (not an
|
|
41
|
+
* override), so it still falls through to the normal parent-validity
|
|
42
|
+
* check below.
|
|
34
43
|
*/
|
|
35
44
|
|
|
36
45
|
const id = 'listitem-parent-valid';
|
|
37
46
|
|
|
38
47
|
const meta = {
|
|
39
48
|
title: 'List items must be inside a list container',
|
|
40
|
-
description:
|
|
49
|
+
description:
|
|
50
|
+
'Checks that <li> elements are contained by <ul>, <ol>, or an element with role="list".',
|
|
41
51
|
i18n: {
|
|
42
52
|
titleKey: 'listitemParentValid_title',
|
|
43
53
|
descriptionKey: 'listitemParentValid_description'
|
|
@@ -46,7 +56,13 @@ const meta = {
|
|
|
46
56
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'list'],
|
|
47
57
|
wcagSc: ['1.3.1'],
|
|
48
58
|
normativeMappings: [
|
|
49
|
-
{
|
|
59
|
+
{
|
|
60
|
+
standard: 'WCAG',
|
|
61
|
+
version: '2.2',
|
|
62
|
+
requirement: '1.3.1',
|
|
63
|
+
title: 'Info and Relationships',
|
|
64
|
+
conformanceLevel: 'A'
|
|
65
|
+
}
|
|
50
66
|
],
|
|
51
67
|
defaultSeverity: 'serious',
|
|
52
68
|
category: 'perceivable',
|
|
@@ -56,7 +72,7 @@ const meta = {
|
|
|
56
72
|
};
|
|
57
73
|
|
|
58
74
|
function runInPage(ctx) {
|
|
59
|
-
const {
|
|
75
|
+
const { helpers, rule } = ctx;
|
|
60
76
|
|
|
61
77
|
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('li') : helpers.queryAll('li');
|
|
62
78
|
|
|
@@ -68,6 +84,20 @@ function runInPage(ctx) {
|
|
|
68
84
|
const parent = el.parentElement;
|
|
69
85
|
if (!parent) continue;
|
|
70
86
|
|
|
87
|
+
// An explicit role on the <li> ITSELF overrides its native "listitem"
|
|
88
|
+
// role entirely, the same "any explicit role wins over the tag's
|
|
89
|
+
// native role" principle this check already applies to the PARENT
|
|
90
|
+
// (see the header comment). An <li role="tab">/
|
|
91
|
+
// role="menuitem">/role="presentation"> etc. is exposed to AT with
|
|
92
|
+
// THAT role, never "listitem" -- so the "list item needs a valid list
|
|
93
|
+
// container parent" concern this rule exists for doesn't apply to it
|
|
94
|
+
// at all; there's no listitem semantics being claimed to validate.
|
|
95
|
+
// role="listitem" itself is a no-op restatement, not an override, so
|
|
96
|
+
// it still falls through to the normal parent check below.
|
|
97
|
+
const ownRoleAttr = el.getAttribute ? String(el.getAttribute('role') || '').trim() : '';
|
|
98
|
+
const ownExplicitRole = ownRoleAttr ? (ownRoleAttr.split(/\s+/)[0] || '').toLowerCase() : '';
|
|
99
|
+
if (ownExplicitRole && ownExplicitRole !== 'listitem') continue;
|
|
100
|
+
|
|
71
101
|
applicableCount += 1;
|
|
72
102
|
|
|
73
103
|
const parentTag = parent.tagName ? parent.tagName.toLowerCase() : '';
|
|
@@ -78,7 +108,7 @@ function runInPage(ctx) {
|
|
|
78
108
|
let valid;
|
|
79
109
|
if (explicitRole) {
|
|
80
110
|
// An explicit role always wins over the tag's native role, in either
|
|
81
|
-
// direction — see the header comment
|
|
111
|
+
// direction — see the header comment.
|
|
82
112
|
valid = explicitRole === 'list' || explicitRole === 'presentation' || explicitRole === 'none';
|
|
83
113
|
} else {
|
|
84
114
|
valid = parentTag === 'ul' || parentTag === 'ol';
|
|
@@ -87,7 +117,7 @@ function runInPage(ctx) {
|
|
|
87
117
|
if (valid) continue;
|
|
88
118
|
|
|
89
119
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
90
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
120
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
91
121
|
|
|
92
122
|
occurrences.push({
|
|
93
123
|
selector: stableSelector,
|
|
@@ -109,9 +139,14 @@ function runInPage(ctx) {
|
|
|
109
139
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
110
140
|
}
|
|
111
141
|
if (occurrences.length) {
|
|
112
|
-
return {
|
|
142
|
+
return {
|
|
143
|
+
ruleId: rule.ruleId,
|
|
144
|
+
outcome: 'fail',
|
|
145
|
+
severity: rule.defaultSeverity || 'serious',
|
|
146
|
+
occurrences
|
|
147
|
+
};
|
|
113
148
|
}
|
|
114
149
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
115
150
|
}
|
|
116
151
|
|
|
117
|
-
module.exports = { id, meta, runInPage };
|
|
152
|
+
module.exports = { id, meta, runInPage };
|