@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
|
@@ -11,20 +11,27 @@
|
|
|
11
11
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
const id =
|
|
14
|
+
const id = 'input-image-alt-decorative';
|
|
15
15
|
|
|
16
16
|
const meta = {
|
|
17
|
-
title:
|
|
18
|
-
description:
|
|
17
|
+
title: '<input type="image"> with alt="" must be appropriate (manual review)',
|
|
18
|
+
description:
|
|
19
|
+
'Flags <input type="image"> elements with empty alt for human review (usually not appropriate for functional controls).',
|
|
19
20
|
i18n: {
|
|
20
|
-
titleKey:
|
|
21
|
-
descriptionKey:
|
|
21
|
+
titleKey: 'inputImage_altDecorative_title',
|
|
22
|
+
descriptionKey: 'inputImage_altDecorative_description'
|
|
22
23
|
},
|
|
23
24
|
helpUrl: null,
|
|
24
|
-
tags: [
|
|
25
|
+
tags: ['wcag2a', 'wcag111', 'forms', 'images', 'manual', 'atomic'],
|
|
25
26
|
wcagSc: ['1.1.1'],
|
|
26
27
|
normativeMappings: [
|
|
27
|
-
{
|
|
28
|
+
{
|
|
29
|
+
standard: 'WCAG',
|
|
30
|
+
version: '2.2',
|
|
31
|
+
requirement: '1.1.1',
|
|
32
|
+
title: 'Non-text Content',
|
|
33
|
+
conformanceLevel: 'A'
|
|
34
|
+
}
|
|
28
35
|
],
|
|
29
36
|
defaultSeverity: 'minor',
|
|
30
37
|
category: 'perceivable',
|
|
@@ -38,107 +45,133 @@ const meta = {
|
|
|
38
45
|
};
|
|
39
46
|
|
|
40
47
|
function runInPage(ctx) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const { document, root, helpers, rule } = ctx;
|
|
49
|
+
const safeRoot = root || document;
|
|
50
|
+
|
|
51
|
+
const queryAllSmart =
|
|
52
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
53
|
+
const queryAll =
|
|
54
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
55
|
+
? helpers.queryAll
|
|
56
|
+
: (sel) => {
|
|
57
|
+
try {
|
|
58
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
59
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
60
|
+
: [];
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
51
64
|
};
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
: null;
|
|
56
|
-
|
|
57
|
-
const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
58
|
-
? helpers.isAccTreeEligible
|
|
59
|
-
: null;
|
|
60
|
-
|
|
61
|
-
const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
|
|
62
|
-
? helpers.getFocusableInfo
|
|
63
|
-
: null;
|
|
64
|
-
|
|
65
|
-
function isRolePresentationExcluded(el) {
|
|
66
|
-
const role = (() => {
|
|
67
|
-
try { return String(el.getAttribute('role') || '').trim().toLowerCase(); }
|
|
68
|
-
catch { return ''; }
|
|
69
|
-
})();
|
|
70
|
-
if (role !== 'presentation' && role !== 'none') return false;
|
|
71
|
-
|
|
72
|
-
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
73
|
-
let focusable = false;
|
|
74
|
-
if (getFocusableInfo) {
|
|
75
|
-
const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
|
|
76
|
-
focusable = !!(fi && fi.focusable);
|
|
77
|
-
} else {
|
|
78
|
-
const tabindex = el.getAttribute('tabindex');
|
|
79
|
-
focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
|
|
80
|
-
}
|
|
81
|
-
return !focusable;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const els = (() => {
|
|
86
|
-
try { return Array.from((queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) || []); }
|
|
87
|
-
catch { return queryAll('input[type="image"]'); }
|
|
88
|
-
})();
|
|
89
|
-
|
|
90
|
-
if (!els.length) {
|
|
91
|
-
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
92
|
-
}
|
|
66
|
+
const getEligibilityInfo =
|
|
67
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
93
68
|
|
|
94
|
-
|
|
95
|
-
|
|
69
|
+
const isAccTreeEligible =
|
|
70
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
96
71
|
|
|
97
|
-
|
|
98
|
-
|
|
72
|
+
const getFocusableInfo =
|
|
73
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
99
74
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
75
|
+
function isRolePresentationExcluded(el) {
|
|
76
|
+
const role = (() => {
|
|
77
|
+
try {
|
|
78
|
+
return String(el.getAttribute('role') || '')
|
|
79
|
+
.trim()
|
|
80
|
+
.toLowerCase();
|
|
81
|
+
} catch {
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
if (role !== 'presentation' && role !== 'none') return false;
|
|
86
|
+
|
|
87
|
+
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
88
|
+
let focusable;
|
|
89
|
+
if (getFocusableInfo) {
|
|
90
|
+
const fi = (() => {
|
|
91
|
+
try {
|
|
92
|
+
return getFocusableInfo(el, ctx);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
105
95
|
}
|
|
96
|
+
})();
|
|
97
|
+
focusable = !!(fi && fi.focusable);
|
|
98
|
+
} else {
|
|
99
|
+
const tabindex = el.getAttribute('tabindex');
|
|
100
|
+
focusable =
|
|
101
|
+
tabindex != null &&
|
|
102
|
+
String(tabindex).trim() !== '' &&
|
|
103
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
104
|
+
}
|
|
105
|
+
return !focusable;
|
|
106
|
+
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
const els = (() => {
|
|
109
|
+
try {
|
|
110
|
+
return Array.from(
|
|
111
|
+
(queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) ||
|
|
112
|
+
[]
|
|
113
|
+
);
|
|
114
|
+
} catch {
|
|
115
|
+
return queryAll('input[type="image"]');
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
111
118
|
|
|
112
|
-
|
|
119
|
+
if (!els.length) {
|
|
120
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
121
|
+
}
|
|
113
122
|
|
|
114
|
-
|
|
123
|
+
const occurrences = [];
|
|
124
|
+
let applicableCount = 0;
|
|
115
125
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
hint: 'This control is typically functional. Confirm it has an equivalent accessible name elsewhere, or provide meaningful alt text.',
|
|
119
|
-
i18n: {
|
|
120
|
-
summaryKey: 'inputImage_altDecorative_summary_cantTell',
|
|
121
|
-
hintKey: 'inputImage_altDecorative_hint_cantTell',
|
|
122
|
-
params: { element: 'input[type=image]' }
|
|
123
|
-
},
|
|
124
|
-
data: {
|
|
125
|
-
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
126
|
-
details: null
|
|
127
|
-
}
|
|
128
|
-
};
|
|
126
|
+
for (const el of els) {
|
|
127
|
+
if (!el || !el.getAttribute) continue;
|
|
129
128
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
if (isAccTreeEligible) {
|
|
130
|
+
const elig = (() => {
|
|
131
|
+
try {
|
|
132
|
+
return isAccTreeEligible(el, ctx);
|
|
133
|
+
} catch {
|
|
134
|
+
return { eligible: true, reasons: [] };
|
|
134
135
|
}
|
|
136
|
+
})();
|
|
137
|
+
if (elig && elig.eligible === false) continue;
|
|
135
138
|
}
|
|
136
139
|
|
|
137
|
-
if (
|
|
138
|
-
|
|
140
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
141
|
+
|
|
142
|
+
// Rule-specific applicability (only elements that already have a text alternative mechanism)
|
|
143
|
+
if (!(el.getAttribute('alt') != null && String(el.getAttribute('alt')).trim() === '')) continue;
|
|
144
|
+
|
|
145
|
+
applicableCount += 1;
|
|
146
|
+
|
|
147
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
148
|
+
|
|
149
|
+
const baseOccurrence = {
|
|
150
|
+
summary: 'Review <input type="image"> with alt="".',
|
|
151
|
+
hint: 'This control is typically functional. Confirm it has an equivalent accessible name elsewhere, or provide meaningful alt text.',
|
|
152
|
+
i18n: {
|
|
153
|
+
summaryKey: 'inputImage_altDecorative_summary_cantTell',
|
|
154
|
+
hintKey: 'inputImage_altDecorative_hint_cantTell',
|
|
155
|
+
params: { element: 'input[type=image]' }
|
|
156
|
+
},
|
|
157
|
+
data: {
|
|
158
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
159
|
+
details: null
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
164
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
165
|
+
} else {
|
|
166
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
139
167
|
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (applicableCount === 0) {
|
|
171
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
172
|
+
}
|
|
140
173
|
|
|
141
|
-
|
|
174
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
142
175
|
}
|
|
143
176
|
|
|
144
177
|
module.exports = { id, meta, runInPage };
|
|
@@ -11,20 +11,27 @@
|
|
|
11
11
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
const id =
|
|
14
|
+
const id = 'input-image-alt-quality';
|
|
15
15
|
|
|
16
16
|
const meta = {
|
|
17
|
-
title:
|
|
18
|
-
description:
|
|
17
|
+
title: '<input type="image"> alt text must be appropriate (manual review)',
|
|
18
|
+
description:
|
|
19
|
+
'Flags <input type="image"> elements with non-empty alt text for human review of appropriateness.',
|
|
19
20
|
i18n: {
|
|
20
|
-
titleKey:
|
|
21
|
-
descriptionKey:
|
|
21
|
+
titleKey: 'inputImage_altQuality_title',
|
|
22
|
+
descriptionKey: 'inputImage_altQuality_description'
|
|
22
23
|
},
|
|
23
24
|
helpUrl: null,
|
|
24
|
-
tags: [
|
|
25
|
+
tags: ['wcag2a', 'wcag111', 'forms', 'images', 'manual', 'atomic'],
|
|
25
26
|
wcagSc: ['1.1.1'],
|
|
26
27
|
normativeMappings: [
|
|
27
|
-
{
|
|
28
|
+
{
|
|
29
|
+
standard: 'WCAG',
|
|
30
|
+
version: '2.2',
|
|
31
|
+
requirement: '1.1.1',
|
|
32
|
+
title: 'Non-text Content',
|
|
33
|
+
conformanceLevel: 'A'
|
|
34
|
+
}
|
|
28
35
|
],
|
|
29
36
|
defaultSeverity: 'minor',
|
|
30
37
|
category: 'perceivable',
|
|
@@ -38,107 +45,133 @@ const meta = {
|
|
|
38
45
|
};
|
|
39
46
|
|
|
40
47
|
function runInPage(ctx) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const { document, root, helpers, rule } = ctx;
|
|
49
|
+
const safeRoot = root || document;
|
|
50
|
+
|
|
51
|
+
const queryAllSmart =
|
|
52
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
53
|
+
const queryAll =
|
|
54
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
55
|
+
? helpers.queryAll
|
|
56
|
+
: (sel) => {
|
|
57
|
+
try {
|
|
58
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
59
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
60
|
+
: [];
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
51
64
|
};
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
: null;
|
|
56
|
-
|
|
57
|
-
const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
58
|
-
? helpers.isAccTreeEligible
|
|
59
|
-
: null;
|
|
60
|
-
|
|
61
|
-
const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
|
|
62
|
-
? helpers.getFocusableInfo
|
|
63
|
-
: null;
|
|
64
|
-
|
|
65
|
-
function isRolePresentationExcluded(el) {
|
|
66
|
-
const role = (() => {
|
|
67
|
-
try { return String(el.getAttribute('role') || '').trim().toLowerCase(); }
|
|
68
|
-
catch { return ''; }
|
|
69
|
-
})();
|
|
70
|
-
if (role !== 'presentation' && role !== 'none') return false;
|
|
71
|
-
|
|
72
|
-
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
73
|
-
let focusable = false;
|
|
74
|
-
if (getFocusableInfo) {
|
|
75
|
-
const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
|
|
76
|
-
focusable = !!(fi && fi.focusable);
|
|
77
|
-
} else {
|
|
78
|
-
const tabindex = el.getAttribute('tabindex');
|
|
79
|
-
focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
|
|
80
|
-
}
|
|
81
|
-
return !focusable;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const els = (() => {
|
|
86
|
-
try { return Array.from((queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) || []); }
|
|
87
|
-
catch { return queryAll('input[type="image"]'); }
|
|
88
|
-
})();
|
|
89
|
-
|
|
90
|
-
if (!els.length) {
|
|
91
|
-
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
92
|
-
}
|
|
66
|
+
const getEligibilityInfo =
|
|
67
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
93
68
|
|
|
94
|
-
|
|
95
|
-
|
|
69
|
+
const isAccTreeEligible =
|
|
70
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
96
71
|
|
|
97
|
-
|
|
98
|
-
|
|
72
|
+
const getFocusableInfo =
|
|
73
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
99
74
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
75
|
+
function isRolePresentationExcluded(el) {
|
|
76
|
+
const role = (() => {
|
|
77
|
+
try {
|
|
78
|
+
return String(el.getAttribute('role') || '')
|
|
79
|
+
.trim()
|
|
80
|
+
.toLowerCase();
|
|
81
|
+
} catch {
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
if (role !== 'presentation' && role !== 'none') return false;
|
|
86
|
+
|
|
87
|
+
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
88
|
+
let focusable;
|
|
89
|
+
if (getFocusableInfo) {
|
|
90
|
+
const fi = (() => {
|
|
91
|
+
try {
|
|
92
|
+
return getFocusableInfo(el, ctx);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
105
95
|
}
|
|
96
|
+
})();
|
|
97
|
+
focusable = !!(fi && fi.focusable);
|
|
98
|
+
} else {
|
|
99
|
+
const tabindex = el.getAttribute('tabindex');
|
|
100
|
+
focusable =
|
|
101
|
+
tabindex != null &&
|
|
102
|
+
String(tabindex).trim() !== '' &&
|
|
103
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
104
|
+
}
|
|
105
|
+
return !focusable;
|
|
106
|
+
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
const els = (() => {
|
|
109
|
+
try {
|
|
110
|
+
return Array.from(
|
|
111
|
+
(queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) ||
|
|
112
|
+
[]
|
|
113
|
+
);
|
|
114
|
+
} catch {
|
|
115
|
+
return queryAll('input[type="image"]');
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
111
118
|
|
|
112
|
-
|
|
119
|
+
if (!els.length) {
|
|
120
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
121
|
+
}
|
|
113
122
|
|
|
114
|
-
|
|
123
|
+
const occurrences = [];
|
|
124
|
+
let applicableCount = 0;
|
|
115
125
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
hint: 'Ensure the alt text describes the control’s action (e.g., “Search”, “Submit order”) in context.',
|
|
119
|
-
i18n: {
|
|
120
|
-
summaryKey: 'inputImage_altQuality_summary_cantTell',
|
|
121
|
-
hintKey: 'inputImage_altQuality_hint_cantTell',
|
|
122
|
-
params: { element: 'input[type=image]' }
|
|
123
|
-
},
|
|
124
|
-
data: {
|
|
125
|
-
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
126
|
-
details: { alt: String(el.getAttribute('alt') || '') } // optional but useful
|
|
127
|
-
}
|
|
128
|
-
};
|
|
126
|
+
for (const el of els) {
|
|
127
|
+
if (!el || !el.getAttribute) continue;
|
|
129
128
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
if (isAccTreeEligible) {
|
|
130
|
+
const elig = (() => {
|
|
131
|
+
try {
|
|
132
|
+
return isAccTreeEligible(el, ctx);
|
|
133
|
+
} catch {
|
|
134
|
+
return { eligible: true, reasons: [] };
|
|
134
135
|
}
|
|
136
|
+
})();
|
|
137
|
+
if (elig && elig.eligible === false) continue;
|
|
135
138
|
}
|
|
136
139
|
|
|
137
|
-
if (
|
|
138
|
-
|
|
140
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
141
|
+
|
|
142
|
+
// Rule-specific applicability (only elements that already have a text alternative mechanism)
|
|
143
|
+
if (!(el.getAttribute('alt') != null && String(el.getAttribute('alt')).trim() !== '')) continue;
|
|
144
|
+
|
|
145
|
+
applicableCount += 1;
|
|
146
|
+
|
|
147
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
148
|
+
|
|
149
|
+
const baseOccurrence = {
|
|
150
|
+
summary: 'Review alt text on <input type="image"> for accuracy and appropriateness.',
|
|
151
|
+
hint: 'Ensure the alt text describes the control’s action (e.g., “Search”, “Submit order”) in context.',
|
|
152
|
+
i18n: {
|
|
153
|
+
summaryKey: 'inputImage_altQuality_summary_cantTell',
|
|
154
|
+
hintKey: 'inputImage_altQuality_hint_cantTell',
|
|
155
|
+
params: { element: 'input[type=image]' }
|
|
156
|
+
},
|
|
157
|
+
data: {
|
|
158
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
159
|
+
details: { alt: String(el.getAttribute('alt') || '') } // optional but useful
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
164
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
165
|
+
} else {
|
|
166
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
139
167
|
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (applicableCount === 0) {
|
|
171
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
172
|
+
}
|
|
140
173
|
|
|
141
|
-
|
|
174
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
142
175
|
}
|
|
143
176
|
|
|
144
177
|
module.exports = { id, meta, runInPage };
|
|
@@ -28,7 +28,8 @@ const id = 'label-title-only';
|
|
|
28
28
|
|
|
29
29
|
const meta = {
|
|
30
30
|
title: 'Form controls should not use title as their only label',
|
|
31
|
-
description:
|
|
31
|
+
description:
|
|
32
|
+
'Checks that a form control with a title attribute also has a real label (label element, aria-label, or aria-labelledby).',
|
|
32
33
|
i18n: {
|
|
33
34
|
titleKey: 'labelTitleOnly_title',
|
|
34
35
|
descriptionKey: 'labelTitleOnly_description'
|
|
@@ -47,8 +48,11 @@ const meta = {
|
|
|
47
48
|
function runInPage(ctx) {
|
|
48
49
|
const { document, helpers, rule } = ctx;
|
|
49
50
|
|
|
50
|
-
const selector =
|
|
51
|
-
|
|
51
|
+
const selector =
|
|
52
|
+
'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
|
|
53
|
+
const nodes = helpers.queryAllSmart
|
|
54
|
+
? helpers.queryAllSmart(selector)
|
|
55
|
+
: helpers.queryAll(selector);
|
|
52
56
|
|
|
53
57
|
const labelsByFor = new Map();
|
|
54
58
|
const allLabels = document.getElementsByTagName ? document.getElementsByTagName('label') : [];
|
|
@@ -84,7 +88,7 @@ function runInPage(ctx) {
|
|
|
84
88
|
|
|
85
89
|
const tag = el.tagName.toLowerCase();
|
|
86
90
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
87
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
91
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
88
92
|
|
|
89
93
|
occurrences.push({
|
|
90
94
|
selector: stableSelector,
|
|
@@ -106,7 +110,12 @@ function runInPage(ctx) {
|
|
|
106
110
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
107
111
|
}
|
|
108
112
|
if (occurrences.length) {
|
|
109
|
-
return {
|
|
113
|
+
return {
|
|
114
|
+
ruleId: rule.ruleId,
|
|
115
|
+
outcome: 'cantTell',
|
|
116
|
+
severity: rule.defaultSeverity || 'minor',
|
|
117
|
+
occurrences
|
|
118
|
+
};
|
|
110
119
|
}
|
|
111
120
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
112
121
|
}
|