@surea11y/core 1.2.0 → 1.3.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 +37 -7
- package/LICENSE +373 -21
- package/README.md +67 -1
- package/bin/core.js +144 -19
- package/docs/API_STABILITY.md +1 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +54 -1
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/INTEGRATION.md +18 -0
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +42 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +55 -16
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +42 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +30 -8
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +109 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
- package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +17 -6
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -7
- package/src/checks/manual/empty-table-header-manual.js +17 -6
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -7
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
- package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +26 -20
- package/src/checks/manual/landmark-unique-manual.js +41 -15
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +16 -5
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +31 -22
- package/src/checks/manual/page-title-patterns-manual.js +78 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +241 -48
- package/src/checks/manual/scope-attr-valid-manual.js +10 -3
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +10 -3
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1127 -886
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4175 -3917
- package/src/core/dom-runner.js +720 -604
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +58 -41
- package/src/core.js +36552 -29111
- package/src/i18n/en.js +1194 -889
- package/src/i18n/fr.js +1136 -795
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +63 -43
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -13,7 +13,13 @@ const meta = {
|
|
|
13
13
|
tags: ['wcag2a', 'wcag111', 'nontext', 'atomic', 'automatic', 'name'],
|
|
14
14
|
wcagSc: ['1.1.1'],
|
|
15
15
|
normativeMappings: [
|
|
16
|
-
{
|
|
16
|
+
{
|
|
17
|
+
standard: 'WCAG',
|
|
18
|
+
version: '2.2',
|
|
19
|
+
requirement: '1.1.1',
|
|
20
|
+
title: 'Non-text Content',
|
|
21
|
+
conformanceLevel: 'A'
|
|
22
|
+
}
|
|
17
23
|
],
|
|
18
24
|
defaultSeverity: 'serious',
|
|
19
25
|
category: 'perceivable',
|
|
@@ -24,36 +30,22 @@ const meta = {
|
|
|
24
30
|
|
|
25
31
|
function runInPage(ctx) {
|
|
26
32
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
33
|
+
const getEligibilityInfo =
|
|
34
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
35
|
|
|
32
36
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
37
|
+
return String(s || '')
|
|
38
|
+
.replace(/\s+/g, ' ')
|
|
39
|
+
.trim();
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
function getAttr(el, name) {
|
|
37
43
|
try {
|
|
38
44
|
if (!el || !el.getAttribute) return '';
|
|
39
45
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
function getConservativeSubtreeText(document, container) {
|
|
44
|
-
// "Name from content" — recurses into descendants and uses each one's
|
|
45
|
-
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
46
|
-
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
47
|
-
// header comment in src/core/dom-helpers.js for the full rationale
|
|
48
|
-
// (this replaced a text-node-only TreeWalker that missed the common
|
|
49
|
-
// "<a><img alt='...'></a>" logo-link / "<button><img alt='...'></button>"
|
|
50
|
-
// icon-button pattern).
|
|
51
|
-
if (helpers.getContentNameInfo) {
|
|
52
|
-
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
|
-
return info && info.present ? info.value : '';
|
|
46
|
+
} catch {
|
|
47
|
+
return '';
|
|
54
48
|
}
|
|
55
|
-
const t = (container && container.textContent) ? String(container.textContent) : '';
|
|
56
|
-
return t.replace(/\s+/g, ' ').trim();
|
|
57
49
|
}
|
|
58
50
|
|
|
59
51
|
function resolveAriaLabelledbyText(document, el, maxRefs) {
|
|
@@ -74,7 +66,8 @@ function runInPage(ctx) {
|
|
|
74
66
|
}
|
|
75
67
|
|
|
76
68
|
function isEligibleAcc(helpers, el, ctx) {
|
|
77
|
-
const fn =
|
|
69
|
+
const fn =
|
|
70
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
78
71
|
if (!fn) return true;
|
|
79
72
|
try {
|
|
80
73
|
const r = fn(el, ctx);
|
|
@@ -85,13 +78,13 @@ function runInPage(ctx) {
|
|
|
85
78
|
}
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
|
|
89
81
|
const occurrences = [];
|
|
90
82
|
let applicableCount = 0;
|
|
91
83
|
|
|
92
84
|
const selector = '[role="progressbar"]';
|
|
93
|
-
const nodes = helpers.queryAllSmart
|
|
94
|
-
|
|
85
|
+
const nodes = helpers.queryAllSmart
|
|
86
|
+
? helpers.queryAllSmart(selector)
|
|
87
|
+
: helpers.queryAll(selector);
|
|
95
88
|
|
|
96
89
|
function evaluate(el) {
|
|
97
90
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -129,36 +122,50 @@ function runInPage(ctx) {
|
|
|
129
122
|
if (res.ok) continue;
|
|
130
123
|
|
|
131
124
|
const eligInfo = getEligibilityInfo
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
? (() => {
|
|
126
|
+
try {
|
|
127
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
})()
|
|
132
|
+
: null;
|
|
134
133
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
135
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
134
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
136
135
|
|
|
137
136
|
occurrences.push({
|
|
138
137
|
selector: stableSelector,
|
|
139
138
|
html,
|
|
140
139
|
summary: 'This progress bar has no accessible name.',
|
|
141
|
-
hint:
|
|
140
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this progress bar's accessible name.",
|
|
142
141
|
i18n: {
|
|
143
142
|
summaryKey: 'progressbarNamePresent_summary_fail',
|
|
144
143
|
hintKey: 'progressbarNamePresent_hint_fail',
|
|
145
144
|
params: {}
|
|
146
145
|
},
|
|
147
146
|
data: {
|
|
148
|
-
details: {
|
|
147
|
+
details: {
|
|
148
|
+
reasonCode: 'name_missing',
|
|
149
|
+
controlType: 'progressbar',
|
|
150
|
+
methodTried: res.method
|
|
151
|
+
},
|
|
149
152
|
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
150
153
|
}
|
|
151
154
|
});
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
|
|
155
157
|
if (applicableCount === 0) {
|
|
156
158
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
157
159
|
}
|
|
158
160
|
if (occurrences.length) {
|
|
159
|
-
return {
|
|
161
|
+
return {
|
|
162
|
+
ruleId: rule.ruleId,
|
|
163
|
+
outcome: 'fail',
|
|
164
|
+
severity: rule.defaultSeverity || 'minor',
|
|
165
|
+
occurrences
|
|
166
|
+
};
|
|
160
167
|
}
|
|
161
168
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
162
169
|
}
|
|
163
170
|
|
|
164
|
-
module.exports = { id, meta, runInPage };
|
|
171
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -21,186 +21,238 @@
|
|
|
21
21
|
const id = 'role-img-text-alternative-present';
|
|
22
22
|
|
|
23
23
|
const meta = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
24
|
+
title: '[role="img"] must have an accessible text alternative',
|
|
25
|
+
description:
|
|
26
|
+
'Checks that elements with role="img" provide an accessible text alternative via aria-label, aria-labelledby, or a title attribute.',
|
|
27
|
+
i18n: {
|
|
28
|
+
titleKey: 'roleImg_textAlternativePresent_title',
|
|
29
|
+
descriptionKey: 'roleImg_textAlternativePresent_description'
|
|
30
|
+
},
|
|
31
|
+
helpUrl: null,
|
|
32
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'aria', 'atomic', 'automatic'],
|
|
33
|
+
wcagSc: ['1.1.1'],
|
|
34
|
+
normativeMappings: [
|
|
35
|
+
{
|
|
36
|
+
standard: 'WCAG',
|
|
37
|
+
version: '2.2',
|
|
38
|
+
requirement: '1.1.1',
|
|
39
|
+
title: 'Non-text Content',
|
|
40
|
+
conformanceLevel: 'A'
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
defaultSeverity: 'serious',
|
|
44
|
+
category: 'perceivable',
|
|
45
|
+
type: 'automatic',
|
|
46
|
+
defaultConfidence: 'high',
|
|
47
|
+
coverage: {
|
|
48
|
+
facetsBySc: {
|
|
49
|
+
'1.1.1': ['role-img-text-alternative-present']
|
|
45
50
|
}
|
|
51
|
+
}
|
|
46
52
|
};
|
|
47
53
|
|
|
48
54
|
function runInPage(ctx) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const { root, helpers, rule } = ctx;
|
|
56
|
+
const safeRoot = root || document;
|
|
57
|
+
|
|
58
|
+
const queryAllSmart =
|
|
59
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
60
|
+
const queryAll =
|
|
61
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
62
|
+
? helpers.queryAll
|
|
63
|
+
: (sel) => {
|
|
64
|
+
try {
|
|
65
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
66
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
67
|
+
: [];
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
58
71
|
};
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
: null;
|
|
73
|
+
const getEligibilityInfo =
|
|
74
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
63
75
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
const trim = (v) => {
|
|
77
|
+
try {
|
|
78
|
+
return String(v == null ? '' : v)
|
|
79
|
+
.replace(/\s+/g, ' ')
|
|
80
|
+
.trim();
|
|
81
|
+
} catch {
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const imgElements = (() => {
|
|
87
|
+
// do not consider element "img" because it has its own rule
|
|
88
|
+
const sel = '[role="img" i]:not(img)';
|
|
89
|
+
try {
|
|
90
|
+
return Array.from((queryAllSmart ? queryAllSmart(sel) : queryAll(sel)) || []);
|
|
91
|
+
} catch {
|
|
92
|
+
return queryAll(sel);
|
|
78
93
|
}
|
|
94
|
+
})();
|
|
79
95
|
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
if (!imgElements.length) {
|
|
97
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
98
|
+
}
|
|
82
99
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
: null;
|
|
100
|
+
const occurrences = [];
|
|
101
|
+
let applicableCount = 0;
|
|
86
102
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
: null;
|
|
103
|
+
const isAccTreeEligible =
|
|
104
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
90
105
|
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
const getAccessibleNameInfo =
|
|
107
|
+
helpers && typeof helpers.getAccessibleNameInfo === 'function'
|
|
108
|
+
? helpers.getAccessibleNameInfo
|
|
109
|
+
: null;
|
|
93
110
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const elig = (() => {
|
|
97
|
-
try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
|
|
98
|
-
})();
|
|
99
|
-
if (elig && elig.eligible === false) continue;
|
|
100
|
-
}
|
|
111
|
+
for (const el of imgElements) {
|
|
112
|
+
if (!el || !el.getAttribute) continue;
|
|
101
113
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const ariaLabelledbyRaw = (() => { try { return el.getAttribute('aria-labelledby'); } catch { return null; } })();
|
|
111
|
-
const ariaLabelledby = trim(ariaLabelledbyRaw);
|
|
112
|
-
|
|
113
|
-
const hasAriaLabelAttr = ariaLabelRaw !== null;
|
|
114
|
-
const hasAriaLabelledbyAttr = ariaLabelledbyRaw !== null;
|
|
115
|
-
|
|
116
|
-
const hasValidAriaLabel = hasAriaLabelAttr && ariaLabel.length > 0;
|
|
117
|
-
const hasValidAriaLabelledbyAttr = hasAriaLabelledbyAttr && ariaLabelledby.length > 0;
|
|
118
|
-
|
|
119
|
-
// Last-resort naming mechanism per HTML-AAM (also accepted by
|
|
120
|
-
// a widely-used reference engine's role-img-alt): a non-empty title attribute.
|
|
121
|
-
const titleRaw = (() => { try { return el.getAttribute('title'); } catch { return null; } })();
|
|
122
|
-
const hasValidTitle = titleRaw !== null && trim(titleRaw).length > 0;
|
|
123
|
-
|
|
124
|
-
let nameInfo = null;
|
|
125
|
-
|
|
126
|
-
// Fast outcomes first (no helper needed)
|
|
127
|
-
let reasonCode = '';
|
|
128
|
-
let hasName = false;
|
|
129
|
-
|
|
130
|
-
if (!hasAriaLabelAttr && !hasAriaLabelledbyAttr) {
|
|
131
|
-
if (hasValidTitle) {
|
|
132
|
-
hasName = true;
|
|
133
|
-
} else {
|
|
134
|
-
reasonCode = 'missingTextAlternative';
|
|
135
|
-
}
|
|
136
|
-
} else if (hasAriaLabelAttr && !hasValidAriaLabel) {
|
|
137
|
-
if (hasValidTitle) {
|
|
138
|
-
hasName = true;
|
|
139
|
-
} else {
|
|
140
|
-
reasonCode = 'emptyAriaLabel';
|
|
141
|
-
}
|
|
142
|
-
} else if (hasAriaLabelledbyAttr && !hasValidAriaLabelledbyAttr) {
|
|
143
|
-
if (hasValidTitle) {
|
|
144
|
-
hasName = true;
|
|
145
|
-
} else {
|
|
146
|
-
reasonCode = 'emptyAriaLabelledby';
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
// Mechanism present + non-empty. Optionally validate resolution via helper.
|
|
150
|
-
if (getAccessibleNameInfo) {
|
|
151
|
-
nameInfo = (() => { try { return getAccessibleNameInfo(el, ctx); } catch { return null; } })();
|
|
152
|
-
const helperSaysHasName = !!(nameInfo && nameInfo.present && trim(nameInfo.value));
|
|
153
|
-
if (helperSaysHasName) {
|
|
154
|
-
hasName = true;
|
|
155
|
-
} else {
|
|
156
|
-
reasonCode = 'nameNotResolved';
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
// Without helper, accept non-empty aria-label/labelledby as sufficient.
|
|
160
|
-
hasName = true;
|
|
161
|
-
}
|
|
114
|
+
// Applicability: eligible in the acc tree (with helper exceptions).
|
|
115
|
+
if (isAccTreeEligible) {
|
|
116
|
+
const elig = (() => {
|
|
117
|
+
try {
|
|
118
|
+
return isAccTreeEligible(el, ctx);
|
|
119
|
+
} catch {
|
|
120
|
+
return { eligible: true, reasons: [] };
|
|
162
121
|
}
|
|
122
|
+
})();
|
|
123
|
+
if (elig && elig.eligible === false) continue;
|
|
124
|
+
}
|
|
163
125
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const baseOccurrence = {
|
|
170
|
-
summary: 'Missing text alternative on element with role="img".',
|
|
171
|
-
hint: 'Provide aria-label or aria-labelledby (referencing non-empty text) to give this image a text alternative.',
|
|
172
|
-
i18n: {
|
|
173
|
-
summaryKey: 'roleImg_textAlternativePresent_summary_fail',
|
|
174
|
-
hintKey: 'roleImg_textAlternativePresent_hint_fail',
|
|
175
|
-
params: { role: 'img' }
|
|
176
|
-
},
|
|
177
|
-
data: {
|
|
178
|
-
details: {
|
|
179
|
-
reasonCode,
|
|
180
|
-
ariaLabel: ariaLabelRaw === null ? null : ariaLabel,
|
|
181
|
-
ariaLabelledby: ariaLabelledbyRaw === null ? null : ariaLabelledby,
|
|
182
|
-
accessibleNameInfo: nameInfo || null
|
|
183
|
-
},
|
|
184
|
-
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
185
|
-
}
|
|
186
|
-
};
|
|
126
|
+
applicableCount += 1;
|
|
127
|
+
|
|
128
|
+
// Expectation: aria-label OR aria-labelledby. We use helper name-info when available,
|
|
129
|
+
// but we also validate the source to keep this rule scoped/deterministic.
|
|
187
130
|
|
|
188
|
-
|
|
189
|
-
|
|
131
|
+
const ariaLabelRaw = (() => {
|
|
132
|
+
try {
|
|
133
|
+
return el.getAttribute('aria-label');
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
const ariaLabel = trim(ariaLabelRaw);
|
|
139
|
+
|
|
140
|
+
const ariaLabelledbyRaw = (() => {
|
|
141
|
+
try {
|
|
142
|
+
return el.getAttribute('aria-labelledby');
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
})();
|
|
147
|
+
const ariaLabelledby = trim(ariaLabelledbyRaw);
|
|
148
|
+
|
|
149
|
+
const hasAriaLabelAttr = ariaLabelRaw !== null;
|
|
150
|
+
const hasAriaLabelledbyAttr = ariaLabelledbyRaw !== null;
|
|
151
|
+
|
|
152
|
+
const hasValidAriaLabel = hasAriaLabelAttr && ariaLabel.length > 0;
|
|
153
|
+
const hasValidAriaLabelledbyAttr = hasAriaLabelledbyAttr && ariaLabelledby.length > 0;
|
|
154
|
+
|
|
155
|
+
// Last-resort naming mechanism per HTML-AAM (also accepted by
|
|
156
|
+
// a widely-used reference engine's role-img-alt): a non-empty title attribute.
|
|
157
|
+
const titleRaw = (() => {
|
|
158
|
+
try {
|
|
159
|
+
return el.getAttribute('title');
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
})();
|
|
164
|
+
const hasValidTitle = titleRaw !== null && trim(titleRaw).length > 0;
|
|
165
|
+
|
|
166
|
+
let nameInfo = null;
|
|
167
|
+
|
|
168
|
+
// Fast outcomes first (no helper needed)
|
|
169
|
+
let reasonCode = '';
|
|
170
|
+
let hasName = false;
|
|
171
|
+
|
|
172
|
+
if (!hasAriaLabelAttr && !hasAriaLabelledbyAttr) {
|
|
173
|
+
if (hasValidTitle) {
|
|
174
|
+
hasName = true;
|
|
175
|
+
} else {
|
|
176
|
+
reasonCode = 'missingTextAlternative';
|
|
177
|
+
}
|
|
178
|
+
} else if (hasAriaLabelAttr && !hasValidAriaLabel) {
|
|
179
|
+
if (hasValidTitle) {
|
|
180
|
+
hasName = true;
|
|
181
|
+
} else {
|
|
182
|
+
reasonCode = 'emptyAriaLabel';
|
|
183
|
+
}
|
|
184
|
+
} else if (hasAriaLabelledbyAttr && !hasValidAriaLabelledbyAttr) {
|
|
185
|
+
if (hasValidTitle) {
|
|
186
|
+
hasName = true;
|
|
187
|
+
} else {
|
|
188
|
+
reasonCode = 'emptyAriaLabelledby';
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
// Mechanism present + non-empty. Optionally validate resolution via helper.
|
|
192
|
+
if (getAccessibleNameInfo) {
|
|
193
|
+
nameInfo = (() => {
|
|
194
|
+
try {
|
|
195
|
+
return getAccessibleNameInfo(el, ctx);
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
})();
|
|
200
|
+
const helperSaysHasName = !!(nameInfo && nameInfo.present && trim(nameInfo.value));
|
|
201
|
+
if (helperSaysHasName) {
|
|
202
|
+
hasName = true;
|
|
190
203
|
} else {
|
|
191
|
-
|
|
204
|
+
reasonCode = 'nameNotResolved';
|
|
192
205
|
}
|
|
206
|
+
} else {
|
|
207
|
+
// Without helper, accept non-empty aria-label/labelledby as sufficient.
|
|
208
|
+
hasName = true;
|
|
209
|
+
}
|
|
193
210
|
}
|
|
194
211
|
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
}
|
|
212
|
+
if (hasName) continue;
|
|
213
|
+
|
|
214
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
215
|
+
|
|
216
|
+
const baseOccurrence = {
|
|
217
|
+
summary: 'Missing text alternative on element with role="img".',
|
|
218
|
+
hint: 'Provide aria-label or aria-labelledby (referencing non-empty text) to give this image a text alternative.',
|
|
219
|
+
i18n: {
|
|
220
|
+
summaryKey: 'roleImg_textAlternativePresent_summary_fail',
|
|
221
|
+
hintKey: 'roleImg_textAlternativePresent_hint_fail',
|
|
222
|
+
params: { role: 'img' }
|
|
223
|
+
},
|
|
224
|
+
data: {
|
|
225
|
+
details: {
|
|
226
|
+
reasonCode,
|
|
227
|
+
ariaLabel: ariaLabelRaw === null ? null : ariaLabel,
|
|
228
|
+
ariaLabelledby: ariaLabelledbyRaw === null ? null : ariaLabelledby,
|
|
229
|
+
accessibleNameInfo: nameInfo || null
|
|
230
|
+
},
|
|
231
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
232
|
+
}
|
|
233
|
+
};
|
|
198
234
|
|
|
199
|
-
if (
|
|
200
|
-
|
|
235
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
236
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
237
|
+
} else {
|
|
238
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
201
239
|
}
|
|
202
|
-
|
|
203
|
-
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (applicableCount === 0) {
|
|
243
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!occurrences.length) {
|
|
247
|
+
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
ruleId: rule.ruleId,
|
|
252
|
+
outcome: 'fail',
|
|
253
|
+
severity: rule.defaultSeverity || 'minor',
|
|
254
|
+
occurrences
|
|
255
|
+
};
|
|
204
256
|
}
|
|
205
257
|
|
|
206
258
|
module.exports = { id, meta, runInPage };
|
|
@@ -10,10 +10,16 @@ const meta = {
|
|
|
10
10
|
descriptionKey: 'searchboxNamePresent_description'
|
|
11
11
|
},
|
|
12
12
|
helpUrl: null,
|
|
13
|
-
tags: [
|
|
13
|
+
tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'searchbox'],
|
|
14
14
|
wcagSc: ['4.1.2'],
|
|
15
15
|
normativeMappings: [
|
|
16
|
-
{
|
|
16
|
+
{
|
|
17
|
+
standard: 'WCAG',
|
|
18
|
+
version: '2.2',
|
|
19
|
+
requirement: '4.1.2',
|
|
20
|
+
title: 'Name, Role, Value',
|
|
21
|
+
conformanceLevel: 'A'
|
|
22
|
+
}
|
|
17
23
|
],
|
|
18
24
|
defaultSeverity: 'serious',
|
|
19
25
|
category: 'robust',
|
|
@@ -24,20 +30,22 @@ const meta = {
|
|
|
24
30
|
|
|
25
31
|
function runInPage(ctx) {
|
|
26
32
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
33
|
+
const getEligibilityInfo =
|
|
34
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
35
|
|
|
32
36
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
37
|
+
return String(s || '')
|
|
38
|
+
.replace(/\s+/g, ' ')
|
|
39
|
+
.trim();
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
function getAttr(el, name) {
|
|
37
43
|
try {
|
|
38
44
|
if (!el || !el.getAttribute) return '';
|
|
39
45
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
46
|
+
} catch {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -52,7 +60,7 @@ function runInPage(ctx) {
|
|
|
52
60
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
61
|
return info && info.present ? info.value : '';
|
|
54
62
|
}
|
|
55
|
-
const t =
|
|
63
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
56
64
|
return t.replace(/\s+/g, ' ').trim();
|
|
57
65
|
}
|
|
58
66
|
|
|
@@ -100,7 +108,8 @@ function runInPage(ctx) {
|
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
function isEligibleAcc(helpers, el, ctx) {
|
|
103
|
-
const fn =
|
|
111
|
+
const fn =
|
|
112
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
104
113
|
if (!fn) return true;
|
|
105
114
|
try {
|
|
106
115
|
const r = fn(el, ctx);
|
|
@@ -126,12 +135,13 @@ function runInPage(ctx) {
|
|
|
126
135
|
return map;
|
|
127
136
|
}
|
|
128
137
|
|
|
129
|
-
|
|
130
138
|
const occurrences = [];
|
|
131
139
|
let applicableCount = 0;
|
|
132
140
|
|
|
133
|
-
const selector =
|
|
134
|
-
const nodes = helpers.queryAllSmart
|
|
141
|
+
const selector = '[role="searchbox"]';
|
|
142
|
+
const nodes = helpers.queryAllSmart
|
|
143
|
+
? helpers.queryAllSmart(selector)
|
|
144
|
+
: helpers.queryAll(selector);
|
|
135
145
|
|
|
136
146
|
// Precompute label[for] map for searchbox elements that are labelable
|
|
137
147
|
// native form controls (e.g. <input role="searchbox">).
|
|
@@ -200,16 +210,22 @@ function runInPage(ctx) {
|
|
|
200
210
|
if (res.ok) continue;
|
|
201
211
|
|
|
202
212
|
const eligInfo = getEligibilityInfo
|
|
203
|
-
|
|
204
|
-
|
|
213
|
+
? (() => {
|
|
214
|
+
try {
|
|
215
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
})()
|
|
220
|
+
: null;
|
|
205
221
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
206
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
222
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
207
223
|
|
|
208
224
|
occurrences.push({
|
|
209
225
|
selector: stableSelector,
|
|
210
226
|
html,
|
|
211
227
|
summary: 'This element has no accessible name.',
|
|
212
|
-
hint:
|
|
228
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this searchbox's accessible name.",
|
|
213
229
|
i18n: {
|
|
214
230
|
summaryKey: 'searchboxNamePresent_summary_fail',
|
|
215
231
|
hintKey: 'searchboxNamePresent_hint_fail',
|
|
@@ -222,12 +238,16 @@ function runInPage(ctx) {
|
|
|
222
238
|
});
|
|
223
239
|
}
|
|
224
240
|
|
|
225
|
-
|
|
226
241
|
if (applicableCount === 0) {
|
|
227
242
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
228
243
|
}
|
|
229
244
|
if (occurrences.length) {
|
|
230
|
-
return {
|
|
245
|
+
return {
|
|
246
|
+
ruleId: rule.ruleId,
|
|
247
|
+
outcome: 'fail',
|
|
248
|
+
severity: rule.defaultSeverity || 'minor',
|
|
249
|
+
occurrences
|
|
250
|
+
};
|
|
231
251
|
}
|
|
232
252
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
233
253
|
}
|