@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
|
/**
|
|
@@ -20,10 +22,9 @@
|
|
|
20
22
|
* technology — activating the outer control and the inner one become
|
|
21
23
|
* ambiguous, and some AT only exposes one of the two.
|
|
22
24
|
* @implementation-notes
|
|
23
|
-
* -
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* outside this element").
|
|
25
|
+
* - Reports on the outer (containing) control, not the nested descendant —
|
|
26
|
+
* this engine considers the container the fixable unit ("move the nested
|
|
27
|
+
* control outside this element").
|
|
27
28
|
* - A container can be reported once even with multiple nested
|
|
28
29
|
* descendants (listed together); a deeply nested chain (A > B > C, all
|
|
29
30
|
* interactive) reports both A and B as separate occurrences, since each
|
|
@@ -34,7 +35,8 @@ const id = 'nested-interactive-controls-absent';
|
|
|
34
35
|
|
|
35
36
|
const meta = {
|
|
36
37
|
title: 'Interactive controls must not be nested',
|
|
37
|
-
description:
|
|
38
|
+
description:
|
|
39
|
+
'Checks that an interactive control (link, button, form control, or ARIA widget role) does not contain another interactive control.',
|
|
38
40
|
i18n: {
|
|
39
41
|
titleKey: 'nestedInteractiveControlsAbsent_title',
|
|
40
42
|
descriptionKey: 'nestedInteractiveControlsAbsent_description'
|
|
@@ -43,7 +45,13 @@ const meta = {
|
|
|
43
45
|
tags: ['wcag2a', 'wcag412', 'structure', 'atomic', 'automatic'],
|
|
44
46
|
wcagSc: ['4.1.2'],
|
|
45
47
|
normativeMappings: [
|
|
46
|
-
{
|
|
48
|
+
{
|
|
49
|
+
standard: 'WCAG',
|
|
50
|
+
version: '2.2',
|
|
51
|
+
requirement: '4.1.2',
|
|
52
|
+
title: 'Name, Role, Value',
|
|
53
|
+
conformanceLevel: 'A'
|
|
54
|
+
}
|
|
47
55
|
],
|
|
48
56
|
defaultSeverity: 'serious',
|
|
49
57
|
category: 'robust',
|
|
@@ -53,7 +61,7 @@ const meta = {
|
|
|
53
61
|
};
|
|
54
62
|
|
|
55
63
|
function runInPage(ctx) {
|
|
56
|
-
const {
|
|
64
|
+
const { helpers, rule } = ctx;
|
|
57
65
|
|
|
58
66
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
59
67
|
// ("runInPage MUST be self-contained").
|
|
@@ -82,29 +90,55 @@ function runInPage(ctx) {
|
|
|
82
90
|
'[role="treeitem"]'
|
|
83
91
|
].join(', ');
|
|
84
92
|
|
|
85
|
-
const
|
|
93
|
+
const isAccTreeEligible =
|
|
94
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
95
|
+
|
|
96
|
+
function isEligible(node) {
|
|
97
|
+
if (!isAccTreeEligible) return true;
|
|
98
|
+
try {
|
|
99
|
+
const r = isAccTreeEligible(node, ctx);
|
|
100
|
+
return !!(r && r.eligible);
|
|
101
|
+
} catch {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const nodes = helpers.queryAllSmart
|
|
107
|
+
? helpers.queryAllSmart(INTERACTIVE_SELECTOR)
|
|
108
|
+
: helpers.queryAll(INTERACTIVE_SELECTOR);
|
|
86
109
|
|
|
87
110
|
const occurrences = [];
|
|
88
111
|
let applicableCount = 0;
|
|
89
112
|
|
|
90
113
|
for (const el of nodes) {
|
|
91
114
|
if (!el || !el.querySelectorAll) continue;
|
|
115
|
+
if (!isEligible(el)) continue;
|
|
92
116
|
|
|
93
117
|
applicableCount += 1;
|
|
94
118
|
|
|
95
|
-
|
|
119
|
+
// The nested search uses the raw native querySelectorAll (not
|
|
120
|
+
// helpers.queryAllSmart), so it isn't subject to that helper's
|
|
121
|
+
// default hidden-content policy at all -- not even hard CSS-based
|
|
122
|
+
// hiding, let alone aria-hidden. A nested descendant that is never
|
|
123
|
+
// actually rendered or exposed to AT (display:none, aria-hidden,
|
|
124
|
+
// etc.) creates no real ambiguity for ANY user, since it isn't there
|
|
125
|
+
// to be confused with the outer control.
|
|
126
|
+
let nested;
|
|
96
127
|
try {
|
|
97
|
-
nested = Array.from(el.querySelectorAll(INTERACTIVE_SELECTOR));
|
|
128
|
+
nested = Array.from(el.querySelectorAll(INTERACTIVE_SELECTOR)).filter(isEligible);
|
|
98
129
|
} catch {
|
|
99
130
|
nested = [];
|
|
100
131
|
}
|
|
101
132
|
if (!nested.length) continue;
|
|
102
133
|
|
|
103
134
|
const nestedTags = nested.map((n) => (n && n.tagName ? n.tagName.toLowerCase() : 'unknown'));
|
|
135
|
+
const dedupedNestedTags = [
|
|
136
|
+
...new Set(nested.map((n) => (n && n.tagName ? n.tagName.toLowerCase() : 'unknown')))
|
|
137
|
+
];
|
|
104
138
|
|
|
105
139
|
const tag = el.tagName.toLowerCase();
|
|
106
140
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
107
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
141
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
108
142
|
|
|
109
143
|
occurrences.push({
|
|
110
144
|
selector: stableSelector,
|
|
@@ -114,10 +148,14 @@ function runInPage(ctx) {
|
|
|
114
148
|
i18n: {
|
|
115
149
|
summaryKey: 'nestedInteractiveControlsAbsent_summary_fail',
|
|
116
150
|
hintKey: 'nestedInteractiveControlsAbsent_hint_fail',
|
|
117
|
-
params: { element: tag, nestedElements:
|
|
151
|
+
params: { element: tag, nestedElements: dedupedNestedTags.join(', ') }
|
|
118
152
|
},
|
|
119
153
|
data: {
|
|
120
|
-
details: {
|
|
154
|
+
details: {
|
|
155
|
+
reasonCode: 'NESTED_INTERACTIVE_CONTROL',
|
|
156
|
+
element: tag,
|
|
157
|
+
nestedElements: nestedTags
|
|
158
|
+
}
|
|
121
159
|
}
|
|
122
160
|
});
|
|
123
161
|
}
|
|
@@ -126,9 +164,14 @@ function runInPage(ctx) {
|
|
|
126
164
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
127
165
|
}
|
|
128
166
|
if (occurrences.length) {
|
|
129
|
-
return {
|
|
167
|
+
return {
|
|
168
|
+
ruleId: rule.ruleId,
|
|
169
|
+
outcome: 'fail',
|
|
170
|
+
severity: rule.defaultSeverity || 'serious',
|
|
171
|
+
occurrences
|
|
172
|
+
};
|
|
130
173
|
}
|
|
131
174
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
132
175
|
}
|
|
133
176
|
|
|
134
|
-
module.exports = { id, meta, runInPage };
|
|
177
|
+
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
|
/**
|
|
@@ -22,7 +24,8 @@ const id = 'object-text-alternative-present';
|
|
|
22
24
|
|
|
23
25
|
const meta = {
|
|
24
26
|
title: '<object> must provide a text alternative',
|
|
25
|
-
description:
|
|
27
|
+
description:
|
|
28
|
+
'Checks that <object> elements provide a text alternative via fallback content or an accessible name.',
|
|
26
29
|
i18n: {
|
|
27
30
|
titleKey: 'object_textAltPresent_title',
|
|
28
31
|
descriptionKey: 'object_textAltPresent_description'
|
|
@@ -31,7 +34,13 @@ const meta = {
|
|
|
31
34
|
tags: ['wcag2a', 'wcag111', 'nontext', 'object', 'atomic', 'automatic'],
|
|
32
35
|
wcagSc: ['1.1.1'],
|
|
33
36
|
normativeMappings: [
|
|
34
|
-
{
|
|
37
|
+
{
|
|
38
|
+
standard: 'WCAG',
|
|
39
|
+
version: '2.2',
|
|
40
|
+
requirement: '1.1.1',
|
|
41
|
+
title: 'Non-text Content',
|
|
42
|
+
conformanceLevel: 'A'
|
|
43
|
+
}
|
|
35
44
|
],
|
|
36
45
|
defaultSeverity: 'serious',
|
|
37
46
|
category: 'perceivable',
|
|
@@ -44,33 +53,39 @@ function runInPage(ctx) {
|
|
|
44
53
|
const { document, root, helpers, rule } = ctx;
|
|
45
54
|
const safeRoot = root || document;
|
|
46
55
|
|
|
47
|
-
const queryAllSmart =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
: null;
|
|
56
|
+
const queryAllSmart =
|
|
57
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
58
|
+
const queryAll =
|
|
59
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
60
|
+
? helpers.queryAll
|
|
61
|
+
: (sel) => {
|
|
62
|
+
try {
|
|
63
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
64
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
65
|
+
: [];
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const getEligibilityInfo =
|
|
72
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
73
|
+
|
|
74
|
+
const isAccTreeEligible =
|
|
75
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
76
|
+
|
|
77
|
+
const getFocusableInfo =
|
|
78
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
79
|
+
|
|
80
|
+
const getAriaNameInfo =
|
|
81
|
+
helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
|
|
70
82
|
|
|
71
83
|
const objects = (() => {
|
|
72
|
-
try {
|
|
73
|
-
|
|
84
|
+
try {
|
|
85
|
+
return Array.from((queryAllSmart ? queryAllSmart('object') : queryAll('object')) || []);
|
|
86
|
+
} catch {
|
|
87
|
+
return queryAll('object');
|
|
88
|
+
}
|
|
74
89
|
})();
|
|
75
90
|
|
|
76
91
|
if (!objects.length) {
|
|
@@ -88,11 +103,22 @@ function runInPage(ctx) {
|
|
|
88
103
|
let aria = null;
|
|
89
104
|
|
|
90
105
|
if (getAriaNameInfo) {
|
|
91
|
-
aria = (() => {
|
|
106
|
+
aria = (() => {
|
|
107
|
+
try {
|
|
108
|
+
return getAriaNameInfo(el, ctx);
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
92
113
|
}
|
|
93
114
|
|
|
94
115
|
if (aria && aria.present && trim(aria.value)) {
|
|
95
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
present: true,
|
|
118
|
+
value: trim(aria.value),
|
|
119
|
+
mechanism: aria.mechanism || 'aria',
|
|
120
|
+
flags: (aria.flags || []).slice(0)
|
|
121
|
+
};
|
|
96
122
|
}
|
|
97
123
|
|
|
98
124
|
const title = trim(el.getAttribute && el.getAttribute('title'));
|
|
@@ -123,20 +149,43 @@ function runInPage(ctx) {
|
|
|
123
149
|
if (!el || !el.getAttribute) continue;
|
|
124
150
|
|
|
125
151
|
if (isAccTreeEligible) {
|
|
126
|
-
const elig = (() => {
|
|
152
|
+
const elig = (() => {
|
|
153
|
+
try {
|
|
154
|
+
return isAccTreeEligible(el, ctx);
|
|
155
|
+
} catch {
|
|
156
|
+
return { eligible: true, reasons: [] };
|
|
157
|
+
}
|
|
158
|
+
})();
|
|
127
159
|
if (elig && elig.eligible === false) continue;
|
|
128
160
|
}
|
|
129
161
|
|
|
130
162
|
// role presentation/none exclusion only when not focusable
|
|
131
|
-
const role = (() => {
|
|
163
|
+
const role = (() => {
|
|
164
|
+
try {
|
|
165
|
+
return String(el.getAttribute('role') || '')
|
|
166
|
+
.trim()
|
|
167
|
+
.toLowerCase();
|
|
168
|
+
} catch {
|
|
169
|
+
return '';
|
|
170
|
+
}
|
|
171
|
+
})();
|
|
132
172
|
if (role === 'presentation' || role === 'none') {
|
|
133
|
-
let focusable
|
|
173
|
+
let focusable;
|
|
134
174
|
if (getFocusableInfo) {
|
|
135
|
-
const fi = (() => {
|
|
175
|
+
const fi = (() => {
|
|
176
|
+
try {
|
|
177
|
+
return getFocusableInfo(el, ctx);
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
})();
|
|
136
182
|
focusable = !!(fi && fi.focusable);
|
|
137
183
|
} else {
|
|
138
184
|
const tabindex = el.getAttribute('tabindex');
|
|
139
|
-
focusable =
|
|
185
|
+
focusable =
|
|
186
|
+
tabindex != null &&
|
|
187
|
+
String(tabindex).trim() !== '' &&
|
|
188
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
140
189
|
}
|
|
141
190
|
if (!focusable) continue;
|
|
142
191
|
}
|
|
@@ -147,18 +196,18 @@ function runInPage(ctx) {
|
|
|
147
196
|
|
|
148
197
|
// Only compute fallback text if there is no name.
|
|
149
198
|
// (textContent can be expensive; avoid when not needed)
|
|
150
|
-
const fb = name.present
|
|
151
|
-
|
|
199
|
+
const fb = name.present
|
|
200
|
+
? { present: false, value: '', mechanism: 'fallback', flags: ['skipped-name-present'] }
|
|
201
|
+
: computeFallbackText(el);
|
|
152
202
|
|
|
153
203
|
const hasTextAlt = !!(name.present || fb.present);
|
|
154
204
|
if (hasTextAlt) continue;
|
|
155
205
|
|
|
156
|
-
|
|
157
206
|
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
158
207
|
|
|
159
208
|
const baseOccurrence = {
|
|
160
209
|
summary: 'Missing text alternative for <object>.',
|
|
161
|
-
hint: 'Provide meaningful fallback content inside <object>,
|
|
210
|
+
hint: 'Provide meaningful fallback content inside <object>, add an accessible name (aria-label/aria-labelledby), or use a title attribute as a best-effort fallback.',
|
|
162
211
|
i18n: {
|
|
163
212
|
summaryKey: 'object_textAltPresent_summary_fail',
|
|
164
213
|
hintKey: 'object_textAltPresent_hint_fail',
|
|
@@ -187,7 +236,12 @@ function runInPage(ctx) {
|
|
|
187
236
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
188
237
|
}
|
|
189
238
|
|
|
190
|
-
return {
|
|
239
|
+
return {
|
|
240
|
+
ruleId: rule.ruleId,
|
|
241
|
+
outcome: 'fail',
|
|
242
|
+
severity: rule.defaultSeverity || 'minor',
|
|
243
|
+
occurrences
|
|
244
|
+
};
|
|
191
245
|
}
|
|
192
246
|
|
|
193
247
|
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 = 'option-name-present';
|
|
@@ -10,10 +12,16 @@ const meta = {
|
|
|
10
12
|
descriptionKey: 'optionNamePresent_description'
|
|
11
13
|
},
|
|
12
14
|
helpUrl: null,
|
|
13
|
-
tags: [
|
|
15
|
+
tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'option'],
|
|
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
|
|
|
@@ -74,7 +84,8 @@ function runInPage(ctx) {
|
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
function isEligibleAcc(helpers, el, ctx) {
|
|
77
|
-
const fn =
|
|
87
|
+
const fn =
|
|
88
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
78
89
|
if (!fn) return true;
|
|
79
90
|
try {
|
|
80
91
|
const r = fn(el, ctx);
|
|
@@ -85,13 +96,13 @@ function runInPage(ctx) {
|
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
|
|
89
99
|
const occurrences = [];
|
|
90
100
|
let applicableCount = 0;
|
|
91
101
|
|
|
92
|
-
const selector =
|
|
93
|
-
const nodes = helpers.queryAllSmart
|
|
94
|
-
|
|
102
|
+
const selector = '[role="option"]';
|
|
103
|
+
const nodes = helpers.queryAllSmart
|
|
104
|
+
? helpers.queryAllSmart(selector)
|
|
105
|
+
: helpers.queryAll(selector);
|
|
95
106
|
|
|
96
107
|
function hasName(el) {
|
|
97
108
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -103,11 +114,9 @@ function runInPage(ctx) {
|
|
|
103
114
|
const title = getAttr(el, 'title');
|
|
104
115
|
if (title) return { ok: true, method: 'title' };
|
|
105
116
|
|
|
106
|
-
|
|
107
117
|
const t = getConservativeSubtreeText(document, el);
|
|
108
118
|
if (t) return { ok: true, method: 'content' };
|
|
109
119
|
|
|
110
|
-
|
|
111
120
|
return { ok: false, method: 'none' };
|
|
112
121
|
}
|
|
113
122
|
|
|
@@ -121,10 +130,16 @@ function runInPage(ctx) {
|
|
|
121
130
|
if (res.ok) continue;
|
|
122
131
|
|
|
123
132
|
const eligInfo = getEligibilityInfo
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
? (() => {
|
|
134
|
+
try {
|
|
135
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
})()
|
|
140
|
+
: null;
|
|
126
141
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
127
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
142
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
128
143
|
|
|
129
144
|
occurrences.push({
|
|
130
145
|
selector: stableSelector,
|
|
@@ -143,12 +158,16 @@ function runInPage(ctx) {
|
|
|
143
158
|
});
|
|
144
159
|
}
|
|
145
160
|
|
|
146
|
-
|
|
147
161
|
if (applicableCount === 0) {
|
|
148
162
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
149
163
|
}
|
|
150
164
|
if (occurrences.length) {
|
|
151
|
-
return {
|
|
165
|
+
return {
|
|
166
|
+
ruleId: rule.ruleId,
|
|
167
|
+
outcome: 'fail',
|
|
168
|
+
severity: rule.defaultSeverity || 'minor',
|
|
169
|
+
occurrences
|
|
170
|
+
};
|
|
152
171
|
}
|
|
153
172
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
154
173
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'page-title-present';
|
|
@@ -13,7 +15,13 @@ const meta = {
|
|
|
13
15
|
tags: ['wcag2a', 'wcag242', 'titles', 'atomic', 'navigation', 'automatic'],
|
|
14
16
|
wcagSc: ['2.4.2'],
|
|
15
17
|
normativeMappings: [
|
|
16
|
-
{
|
|
18
|
+
{
|
|
19
|
+
standard: 'WCAG',
|
|
20
|
+
version: '2.2',
|
|
21
|
+
requirement: '2.4.2',
|
|
22
|
+
title: 'Page Titled',
|
|
23
|
+
conformanceLevel: 'A'
|
|
24
|
+
}
|
|
17
25
|
],
|
|
18
26
|
defaultSeverity: 'serious',
|
|
19
27
|
category: 'operable',
|
|
@@ -54,13 +62,13 @@ function runInPage(ctx) {
|
|
|
54
62
|
selector: 'head > title',
|
|
55
63
|
html: '',
|
|
56
64
|
summary: missingTitleEl
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
? 'The page is missing a <title> element.'
|
|
66
|
+
: 'The page has an empty <title>.',
|
|
59
67
|
hint: 'Provide a descriptive, non-empty <title> that identifies the page topic or purpose.',
|
|
60
68
|
i18n: {
|
|
61
69
|
summaryKey: missingTitleEl
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
? 'pageTitlePresent_summary_fail_missing'
|
|
71
|
+
: 'pageTitlePresent_summary_fail_empty',
|
|
64
72
|
hintKey: 'pageTitlePresent_hint_fail',
|
|
65
73
|
params: {}
|
|
66
74
|
},
|
|
@@ -86,7 +94,12 @@ function runInPage(ctx) {
|
|
|
86
94
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
87
95
|
}
|
|
88
96
|
if (occurrences.length) {
|
|
89
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
ruleId: rule.ruleId,
|
|
99
|
+
outcome: 'fail',
|
|
100
|
+
severity: rule.defaultSeverity || 'minor',
|
|
101
|
+
occurrences
|
|
102
|
+
};
|
|
90
103
|
}
|
|
91
104
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
92
105
|
}
|