@surea11y/core 1.1.2 → 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 +48 -4
- package/LICENSE +373 -21
- package/README.md +70 -1
- package/bin/core.js +240 -11
- package/docs/API_STABILITY.md +61 -0
- package/docs/BASELINE.md +66 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +80 -1
- package/docs/ENGINE_OPTIONS.md +4 -0
- package/docs/INTEGRATION.md +19 -1
- package/docs/OUTPUT_SCHEMA.md +2 -2
- package/docs/REPORT.md +33 -0
- package/docs/RULE_AUTHORING.md +31 -0
- 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 +50 -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 +125 -29
- 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 +50 -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 +39 -9
- 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 +43 -19
- 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 +117 -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 +37 -8
- package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -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 +26 -7
- 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 -12
- package/src/checks/manual/empty-table-header-manual.js +21 -14
- 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 -12
- 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 +95 -29
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
- package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +35 -21
- package/src/checks/manual/landmark-unique-manual.js +73 -45
- 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 +25 -6
- 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 +40 -23
- package/src/checks/manual/page-title-patterns-manual.js +87 -51
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +275 -62
- package/src/checks/manual/scope-attr-valid-manual.js +11 -9
- 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 +11 -9
- 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 +1128 -823
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4176 -3886
- package/src/core/dom-runner.js +720 -596
- 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 +71 -35
- package/src/core.js +37939 -29121
- package/src/i18n/en.js +1194 -887
- package/src/i18n/fr.js +1136 -793
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +502 -0
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -18,138 +18,169 @@
|
|
|
18
18
|
const id = 'input-image-alt-present';
|
|
19
19
|
|
|
20
20
|
const meta = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
21
|
+
title: '<input type="image"> must have an alt attribute',
|
|
22
|
+
description:
|
|
23
|
+
'Checks that <input type="image"> elements provide an alt attribute to support a text alternative mechanism.',
|
|
24
|
+
i18n: {
|
|
25
|
+
titleKey: 'inputImage_altPresent_title',
|
|
26
|
+
descriptionKey: 'inputImage_altPresent_description'
|
|
27
|
+
},
|
|
28
|
+
helpUrl: null,
|
|
29
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'atomic', 'automatic'],
|
|
30
|
+
wcagSc: ['1.1.1'],
|
|
31
|
+
normativeMappings: [
|
|
32
|
+
{
|
|
33
|
+
standard: 'WCAG',
|
|
34
|
+
version: '2.2',
|
|
35
|
+
requirement: '1.1.1',
|
|
36
|
+
title: 'Non-text Content',
|
|
37
|
+
conformanceLevel: 'A'
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
defaultSeverity: 'serious',
|
|
41
|
+
category: 'perceivable',
|
|
42
|
+
type: 'automatic',
|
|
43
|
+
defaultConfidence: 'high',
|
|
44
|
+
coverage: {
|
|
45
|
+
facetsBySc: {
|
|
46
|
+
'1.1.1': ['input-image-alt-attr-present']
|
|
42
47
|
}
|
|
48
|
+
}
|
|
43
49
|
};
|
|
44
50
|
|
|
45
51
|
function runInPage(ctx) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const { document, root, helpers, rule } = ctx;
|
|
53
|
+
const safeRoot = root || document;
|
|
54
|
+
|
|
55
|
+
const queryAllSmart =
|
|
56
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
57
|
+
const queryAll =
|
|
58
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
59
|
+
? helpers.queryAll
|
|
60
|
+
: (sel) => {
|
|
61
|
+
try {
|
|
62
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
63
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
64
|
+
: [];
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
55
68
|
};
|
|
56
69
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
: null;
|
|
60
|
-
|
|
61
|
-
const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
62
|
-
? helpers.isAccTreeEligible
|
|
63
|
-
: null;
|
|
70
|
+
const getEligibilityInfo =
|
|
71
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
64
72
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
: null;
|
|
73
|
+
const isAccTreeEligible =
|
|
74
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
catch { return queryAll('input[type="image"]'); }
|
|
72
|
-
})();
|
|
76
|
+
const getAriaNameInfo =
|
|
77
|
+
helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
const inputs = (() => {
|
|
80
|
+
try {
|
|
81
|
+
return Array.from(
|
|
82
|
+
(queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) ||
|
|
83
|
+
[]
|
|
84
|
+
);
|
|
85
|
+
} catch {
|
|
86
|
+
return queryAll('input[type="image"]');
|
|
76
87
|
}
|
|
88
|
+
})();
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
if (!inputs.length) {
|
|
91
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
92
|
+
}
|
|
80
93
|
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
const occurrences = [];
|
|
95
|
+
let applicableCount = 0;
|
|
83
96
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const elig = (() => {
|
|
87
|
-
try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
|
|
88
|
-
})();
|
|
89
|
-
if (elig && elig.eligible === false) continue;
|
|
90
|
-
}
|
|
97
|
+
for (const el of inputs) {
|
|
98
|
+
if (!el || !el.getAttribute) continue;
|
|
91
99
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// accessible name computation includes ARIA naming before falling
|
|
100
|
-
// back to alt).
|
|
101
|
-
if (getAriaNameInfo) {
|
|
102
|
-
let ariaName = null;
|
|
103
|
-
try {
|
|
104
|
-
ariaName = getAriaNameInfo(el, ctx);
|
|
105
|
-
} catch {
|
|
106
|
-
ariaName = null;
|
|
107
|
-
}
|
|
108
|
-
if (ariaName && ariaName.present) continue;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// A non-empty title attribute is HTML-AAM's own next fallback naming
|
|
112
|
-
// source once alt is entirely absent -- also accepted by a widely-used
|
|
113
|
-
// reference engine's equivalent input-image-alt rule (non-empty-title, same "any" list
|
|
114
|
-
// as non-empty-alt/aria-label/aria-labelledby). See img-alt-present's
|
|
115
|
-
// sibling fix (2026-07-23, AliExpress's title-only logo <img>) for
|
|
116
|
-
// the real page this was found via -- same gap, same fix, different
|
|
117
|
-
// element.
|
|
118
|
-
const titleRaw = (() => { try { return el.getAttribute('title'); } catch { return null; } })();
|
|
119
|
-
if (titleRaw !== null && String(titleRaw).trim()) continue;
|
|
120
|
-
|
|
121
|
-
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
122
|
-
|
|
123
|
-
const baseOccurrence = {
|
|
124
|
-
summary: 'Missing alt attribute on <input type="image">.',
|
|
125
|
-
hint: 'Add an alt attribute (use alt="" only when a separate accessible name is provided).',
|
|
126
|
-
i18n: {
|
|
127
|
-
summaryKey: 'inputImage_altPresent_summary_fail',
|
|
128
|
-
hintKey: 'inputImage_altPresent_hint_fail',
|
|
129
|
-
params: { element: 'input[type=image]' }
|
|
130
|
-
},
|
|
131
|
-
data: {
|
|
132
|
-
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
137
|
-
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
138
|
-
} else {
|
|
139
|
-
// Never compute selector/snippet in the rule.
|
|
140
|
-
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
100
|
+
// Applicability: eligible in the acc tree (with helper exceptions).
|
|
101
|
+
if (isAccTreeEligible) {
|
|
102
|
+
const elig = (() => {
|
|
103
|
+
try {
|
|
104
|
+
return isAccTreeEligible(el, ctx);
|
|
105
|
+
} catch {
|
|
106
|
+
return { eligible: true, reasons: [] };
|
|
141
107
|
}
|
|
108
|
+
})();
|
|
109
|
+
if (elig && elig.eligible === false) continue;
|
|
142
110
|
}
|
|
143
111
|
|
|
144
|
-
|
|
145
|
-
|
|
112
|
+
applicableCount += 1;
|
|
113
|
+
|
|
114
|
+
const hasAlt = el.getAttribute('alt') !== null;
|
|
115
|
+
if (hasAlt) continue;
|
|
116
|
+
|
|
117
|
+
// aria-label / aria-labelledby is also a valid, standards-recognized
|
|
118
|
+
// text-alternative mechanism for <input type="image"> (HTML-AAM
|
|
119
|
+
// accessible name computation includes ARIA naming before falling
|
|
120
|
+
// back to alt).
|
|
121
|
+
if (getAriaNameInfo) {
|
|
122
|
+
let ariaName;
|
|
123
|
+
try {
|
|
124
|
+
ariaName = getAriaNameInfo(el, ctx);
|
|
125
|
+
} catch {
|
|
126
|
+
ariaName = null;
|
|
127
|
+
}
|
|
128
|
+
if (ariaName && ariaName.present) continue;
|
|
146
129
|
}
|
|
147
130
|
|
|
148
|
-
|
|
149
|
-
|
|
131
|
+
// A non-empty title attribute is HTML-AAM's own next fallback naming
|
|
132
|
+
// source once alt is entirely absent -- also accepted by a widely-used
|
|
133
|
+
// reference engine's equivalent input-image-alt rule (non-empty-title, same "any" list
|
|
134
|
+
// as non-empty-alt/aria-label/aria-labelledby). See img-alt-present's
|
|
135
|
+
// sibling fix (2026-07-23, AliExpress's title-only logo <img>) for
|
|
136
|
+
// the real page this was found via -- same gap, same fix, different
|
|
137
|
+
// element.
|
|
138
|
+
const titleRaw = (() => {
|
|
139
|
+
try {
|
|
140
|
+
return el.getAttribute('title');
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
})();
|
|
145
|
+
if (titleRaw !== null && String(titleRaw).trim()) continue;
|
|
146
|
+
|
|
147
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
148
|
+
|
|
149
|
+
const baseOccurrence = {
|
|
150
|
+
summary: 'Missing alt attribute on <input type="image">.',
|
|
151
|
+
hint: 'Add an alt attribute (use alt="" only when a separate accessible name is provided).',
|
|
152
|
+
i18n: {
|
|
153
|
+
summaryKey: 'inputImage_altPresent_summary_fail',
|
|
154
|
+
hintKey: 'inputImage_altPresent_hint_fail',
|
|
155
|
+
params: { element: 'input[type=image]' }
|
|
156
|
+
},
|
|
157
|
+
data: {
|
|
158
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
163
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
164
|
+
} else {
|
|
165
|
+
// Never compute selector/snippet in the rule.
|
|
166
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
150
167
|
}
|
|
151
|
-
|
|
152
|
-
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (applicableCount === 0) {
|
|
171
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!occurrences.length) {
|
|
175
|
+
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
ruleId: rule.ruleId,
|
|
180
|
+
outcome: 'fail',
|
|
181
|
+
severity: rule.defaultSeverity || 'minor',
|
|
182
|
+
occurrences
|
|
183
|
+
};
|
|
153
184
|
}
|
|
154
185
|
|
|
155
186
|
module.exports = { id, meta, runInPage };
|
|
@@ -5,7 +5,7 @@ const id = 'label-in-name';
|
|
|
5
5
|
const meta = {
|
|
6
6
|
title: 'Label in Name: accessible name contains visible text',
|
|
7
7
|
description:
|
|
8
|
-
|
|
8
|
+
'Checks that when a control has a visible text label, the accessible name (from aria-label/aria-labelledby) contains that visible label text (WCAG 2.5.3).',
|
|
9
9
|
i18n: {
|
|
10
10
|
titleKey: 'labelInName_title',
|
|
11
11
|
descriptionKey: 'labelInName_description'
|
|
@@ -37,20 +37,27 @@ function runInPage(ctx) {
|
|
|
37
37
|
|
|
38
38
|
// Applicability: focus/activation controls with explicit ARIA naming.
|
|
39
39
|
// NOTE: aria-hidden is intentionally NOT excluded here; it does not affect visual rendering.
|
|
40
|
-
const selector =
|
|
40
|
+
const selector =
|
|
41
|
+
':is(button, a[href], summary, input:not([type="hidden"]), textarea, select, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="searchbox"], [role="tab"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], [role="treeitem"], [role="gridcell"]):not([hidden]):not([disabled]):not([aria-disabled="true"]):is([aria-label], [aria-labelledby])';
|
|
41
42
|
|
|
42
|
-
const nodes = helpers.queryAllSmart
|
|
43
|
+
const nodes = helpers.queryAllSmart
|
|
44
|
+
? helpers.queryAllSmart(selector)
|
|
45
|
+
: helpers.queryAll(selector);
|
|
43
46
|
|
|
44
47
|
function norm(s) {
|
|
45
|
-
const v =
|
|
48
|
+
const v = s == null ? '' : String(s);
|
|
46
49
|
// deterministic normalization: trim + collapse whitespace + case-fold
|
|
47
50
|
return v.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
function getElementDescriptor(el) {
|
|
51
|
-
const tag =
|
|
52
|
-
let role
|
|
53
|
-
try {
|
|
54
|
+
const tag = el && el.tagName ? String(el.tagName).toLowerCase() : 'element';
|
|
55
|
+
let role;
|
|
56
|
+
try {
|
|
57
|
+
role = el && el.getAttribute ? el.getAttribute('role') || '' : '';
|
|
58
|
+
} catch {
|
|
59
|
+
role = '';
|
|
60
|
+
}
|
|
54
61
|
const r = String(role || '').trim();
|
|
55
62
|
return r ? `${tag}[role="${r}"]` : tag;
|
|
56
63
|
}
|
|
@@ -62,14 +69,23 @@ function runInPage(ctx) {
|
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
function isNonRenderedTag(el) {
|
|
65
|
-
const tn =
|
|
66
|
-
return
|
|
72
|
+
const tn = el && el.tagName ? String(el.tagName).toLowerCase() : '';
|
|
73
|
+
return (
|
|
74
|
+
tn === 'script' ||
|
|
75
|
+
tn === 'style' ||
|
|
76
|
+
tn === 'template' ||
|
|
77
|
+
tn === 'noscript' ||
|
|
78
|
+
tn === 'meta' ||
|
|
79
|
+
tn === 'link'
|
|
80
|
+
);
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
function isDomVisible(el) {
|
|
70
84
|
if (!el) return false;
|
|
71
|
-
if (helpers.isDomVisibleEligible)
|
|
72
|
-
|
|
85
|
+
if (helpers.isDomVisibleEligible)
|
|
86
|
+
return !!helpers.isDomVisibleEligible(el, ctx, { targetSet: 'dom' }).eligible;
|
|
87
|
+
if (helpers.getEligibilityInfo)
|
|
88
|
+
return !!helpers.getEligibilityInfo(el, ctx, { targetSet: 'dom' }).eligible;
|
|
73
89
|
return true;
|
|
74
90
|
}
|
|
75
91
|
|
|
@@ -90,7 +106,8 @@ function runInPage(ctx) {
|
|
|
90
106
|
// aria-hidden icon rendering literally as "format_color_fill").
|
|
91
107
|
function isAccEligible(el) {
|
|
92
108
|
if (!el) return false;
|
|
93
|
-
const fn =
|
|
109
|
+
const fn =
|
|
110
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
94
111
|
if (!fn) return isDomVisible(el);
|
|
95
112
|
try {
|
|
96
113
|
const r = fn(el, ctx);
|
|
@@ -118,7 +135,7 @@ function runInPage(ctx) {
|
|
|
118
135
|
if (!isDomVisible(container)) return '';
|
|
119
136
|
|
|
120
137
|
// TreeWalker is deterministic in document order.
|
|
121
|
-
let walker
|
|
138
|
+
let walker;
|
|
122
139
|
try {
|
|
123
140
|
walker = document.createTreeWalker(container, SHOW_TEXT, null);
|
|
124
141
|
} catch {
|
|
@@ -134,7 +151,7 @@ function runInPage(ctx) {
|
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
const parts = [];
|
|
137
|
-
let n
|
|
154
|
+
let n;
|
|
138
155
|
// eslint-disable-next-line no-cond-assign
|
|
139
156
|
while ((n = walker.nextNode())) {
|
|
140
157
|
try {
|
|
@@ -183,7 +200,7 @@ function runInPage(ctx) {
|
|
|
183
200
|
// contextSelector support), not a single element with its own
|
|
184
201
|
// .querySelector to call directly.
|
|
185
202
|
try {
|
|
186
|
-
const idAttribute = control && control.getAttribute ?
|
|
203
|
+
const idAttribute = control && control.getAttribute ? control.getAttribute('id') || '' : '';
|
|
187
204
|
const key = String(idAttribute || '').trim();
|
|
188
205
|
if (key && document && document.querySelector) {
|
|
189
206
|
const l = document.querySelector('label[for="' + CSS.escape(key) + '"]');
|
|
@@ -215,10 +232,10 @@ function runInPage(ctx) {
|
|
|
215
232
|
// 1) <label> association for form controls
|
|
216
233
|
// 2) visible text inside the element
|
|
217
234
|
// 3) visible text from aria-labelledby referenced elements
|
|
218
|
-
let text
|
|
235
|
+
let text;
|
|
219
236
|
let source = 'none';
|
|
220
237
|
|
|
221
|
-
const tn =
|
|
238
|
+
const tn = el && el.tagName ? String(el.tagName).toLowerCase() : '';
|
|
222
239
|
|
|
223
240
|
// 1) Label association (native form controls)
|
|
224
241
|
const isFormControl = tn === 'input' || tn === 'select' || tn === 'textarea';
|
|
@@ -244,7 +261,7 @@ function runInPage(ctx) {
|
|
|
244
261
|
if (idrefs && helpers.resolveIdRefs) {
|
|
245
262
|
const r = helpers.resolveIdRefs(idrefs, ctx, { maxRefs: 8 });
|
|
246
263
|
const parts = [];
|
|
247
|
-
for (const ref of
|
|
264
|
+
for (const ref of r && Array.isArray(r.refs) ? r.refs : []) {
|
|
248
265
|
if (!ref || !ref.tagName) continue;
|
|
249
266
|
if (!isDomVisible(ref)) continue;
|
|
250
267
|
const t = collectVisibleTextUnder(ref);
|
|
@@ -275,7 +292,9 @@ function runInPage(ctx) {
|
|
|
275
292
|
|
|
276
293
|
let acc = { present: false, value: '', mechanism: 'none', flags: [] };
|
|
277
294
|
try {
|
|
278
|
-
acc = helpers.getAccessibleNameInfo
|
|
295
|
+
acc = helpers.getAccessibleNameInfo
|
|
296
|
+
? helpers.getAccessibleNameInfo(el, ctx, { maxRefs: 8 })
|
|
297
|
+
: acc;
|
|
279
298
|
} catch {
|
|
280
299
|
acc = { present: false, value: '', mechanism: 'none', flags: ['exception'] };
|
|
281
300
|
}
|
|
@@ -286,7 +305,9 @@ function runInPage(ctx) {
|
|
|
286
305
|
|
|
287
306
|
if (!contains) {
|
|
288
307
|
const selectorOut = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
289
|
-
const html = helpers.getOuterHtmlSnippet
|
|
308
|
+
const html = helpers.getOuterHtmlSnippet
|
|
309
|
+
? helpers.getOuterHtmlSnippet(el)
|
|
310
|
+
: el.outerHTML || '';
|
|
290
311
|
|
|
291
312
|
occurrences.push({
|
|
292
313
|
selector: selectorOut,
|
|
@@ -317,8 +338,15 @@ function runInPage(ctx) {
|
|
|
317
338
|
}
|
|
318
339
|
}
|
|
319
340
|
|
|
320
|
-
if (applicableCount === 0)
|
|
321
|
-
|
|
341
|
+
if (applicableCount === 0)
|
|
342
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
343
|
+
if (occurrences.length)
|
|
344
|
+
return {
|
|
345
|
+
ruleId: rule.ruleId,
|
|
346
|
+
outcome: 'fail',
|
|
347
|
+
severity: rule.defaultSeverity || 'minor',
|
|
348
|
+
occurrences
|
|
349
|
+
};
|
|
322
350
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
323
351
|
}
|
|
324
352
|
|