@surea11y/core 1.5.0 → 1.7.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 +240 -149
- package/README.md +51 -44
- package/docs/ACT_RULE_MAPPING.md +245 -0
- package/docs/API_STABILITY.md +53 -5
- package/docs/BINDING_AUTHORS_GUIDE.md +106 -4
- package/docs/DESIGN_CHALLENGES.md +367 -0
- package/docs/EARL.md +100 -0
- package/docs/ENGINE_OPTIONS.md +42 -4
- package/docs/I18N.md +4 -4
- package/docs/INTEGRATION.md +4 -2
- package/docs/LIMITATIONS.md +9 -5
- package/docs/OUTPUT_SCHEMA.md +44 -6
- package/docs/POLICY.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_AUTHORING.md +63 -36
- package/docs/RULE_CATALOG.md +1928 -169
- package/docs/RULE_HELPERS.md +333 -0
- package/docs/RULE_TAXONOMY.md +27 -6
- package/docs/SARIF.md +21 -2
- package/docs/TROUBLESHOOTING.md +2 -2
- package/docs/WCAG_CONFORMANCE.md +34 -10
- package/package.json +11 -9
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +74 -10
- package/src/checks/automatic/aria-allowed-role.js +34 -25
- package/src/checks/automatic/aria-braille-equivalent.js +21 -13
- package/src/checks/automatic/aria-conditional-attr.js +22 -15
- package/src/checks/automatic/aria-deprecated-role.js +13 -1
- package/src/checks/automatic/aria-hidden-body.js +3 -3
- package/src/checks/automatic/aria-hidden-focus.js +5 -5
- package/src/checks/automatic/aria-prohibited-attr.js +23 -18
- package/src/checks/automatic/aria-prohibited-children.js +136 -43
- package/src/checks/automatic/aria-required-attr.js +119 -24
- package/src/checks/automatic/aria-required-children.js +54 -30
- package/src/checks/automatic/aria-required-parent.js +93 -15
- package/src/checks/automatic/aria-role-name-present.js +37 -23
- package/src/checks/automatic/aria-roles-valid.js +52 -21
- package/src/checks/automatic/aria-valid-attr-value.js +89 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -10
- package/src/checks/automatic/autocomplete-valid.js +2 -2
- package/src/checks/automatic/avoid-inline-spacing.js +133 -6
- package/src/checks/automatic/binary-control-name-present.js +27 -5
- package/src/checks/automatic/button-name-present.js +92 -6
- package/src/checks/automatic/combobox-name-present.js +26 -6
- package/src/checks/automatic/contrast-computable.js +42 -0
- package/src/checks/automatic/contrast-enhanced.js +33 -1
- package/src/checks/automatic/contrast-minimum.js +33 -1
- package/src/checks/automatic/css-orientation-lock.js +138 -24
- package/src/checks/automatic/definition-list-children-valid.js +7 -8
- package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
- package/src/checks/automatic/dialog-name-present.js +20 -2
- package/src/checks/automatic/duplicate-id-aria.js +10 -3
- package/src/checks/automatic/duplicate-id.js +203 -0
- package/src/checks/automatic/embed-text-alternative-present.js +2 -2
- package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
- package/src/checks/automatic/form-control-single-label.js +10 -1
- package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
- package/src/checks/automatic/iframe-focusable-content.js +68 -7
- package/src/checks/automatic/iframe-name-present.js +37 -3
- package/src/checks/automatic/iframe-title-unique.js +1 -1
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +204 -68
- package/src/checks/automatic/link-in-text-block.js +285 -29
- package/src/checks/automatic/link-name-present.js +22 -1
- package/src/checks/automatic/list-children-valid.js +6 -6
- package/src/checks/automatic/listbox-name-present.js +28 -8
- package/src/checks/automatic/listitem-parent-valid.js +4 -4
- package/src/checks/automatic/menuitem-name-present.js +20 -2
- package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
- package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
- package/src/checks/automatic/meter-name-present.js +23 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
- package/src/checks/automatic/option-name-present.js +23 -4
- package/src/checks/automatic/page-title-present.js +21 -3
- package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
- package/src/checks/automatic/progressbar-name-present.js +23 -4
- package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +64 -16
- package/src/checks/automatic/searchbox-name-present.js +28 -8
- package/src/checks/automatic/server-side-image-map-absent.js +1 -1
- package/src/checks/automatic/slider-name-present.js +27 -6
- package/src/checks/automatic/spinbutton-name-present.js +28 -8
- package/src/checks/automatic/summary-name-present.js +18 -2
- package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
- package/src/checks/automatic/svg-text-alternative-present.js +13 -10
- package/src/checks/automatic/tab-name-present.js +21 -2
- package/src/checks/automatic/table-headers-attr-valid.js +43 -8
- package/src/checks/automatic/table-th-has-data-cells.js +61 -5
- package/src/checks/automatic/target-size-minimum.js +155 -58
- package/src/checks/automatic/td-has-header.js +24 -23
- package/src/checks/automatic/textbox-name-present.js +28 -8
- package/src/checks/automatic/tooltip-name-present.js +21 -2
- package/src/checks/automatic/treeitem-name-present.js +23 -4
- package/src/checks/automatic/valid-lang.js +92 -7
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +3 -3
- package/src/checks/manual/area-alt-decorative-manual.js +7 -0
- package/src/checks/manual/area-alt-quality-manual.js +6 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +5 -5
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/empty-heading-manual.js +58 -11
- package/src/checks/manual/empty-table-header-manual.js +8 -8
- package/src/checks/manual/focus-order-semantics-manual.js +15 -15
- package/src/checks/manual/form-control-label-quality-manual.js +563 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -2
- package/src/checks/manual/heading-order-manual.js +3 -3
- package/src/checks/manual/heading-quality-manual.js +338 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
- package/src/checks/manual/image-redundant-alt-manual.js +4 -4
- package/src/checks/manual/img-alt-decorative-manual.js +211 -52
- package/src/checks/manual/img-alt-quality-manual.js +7 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
- package/src/checks/manual/label-title-only-manual.js +4 -4
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
- package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
- package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
- package/src/checks/manual/landmark-one-main-manual.js +6 -6
- package/src/checks/manual/landmark-unique-manual.js +9 -9
- package/src/checks/manual/link-name-quality-manual.js +161 -32
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +2 -2
- package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
- package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
- package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/p-as-heading-manual.js +4 -4
- package/src/checks/manual/page-has-heading-one-manual.js +6 -6
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/password-paste-enabled-manual.js +255 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
- package/src/checks/manual/region-manual.js +19 -19
- package/src/checks/manual/scope-attr-valid-manual.js +2 -2
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +5 -5
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +2 -2
- package/src/checks/manual/table-duplicate-name-manual.js +2 -2
- package/src/checks/manual/table-fake-caption-manual.js +2 -2
- package/src/checks/manual/video-caption-manual.js +3 -3
- package/src/checks/manual-review.js +17 -1
- package/src/core.js +8880 -41883
- package/src/earl.js +144 -0
- package/src/report.js +2 -2
- package/src/sarif.js +22 -2
- package/surea11y.browser.js +10 -37882
- package/surea11y.i18n.de.js +2 -21
- package/surea11y.i18n.es.js +2 -21
- package/surea11y.i18n.fr.js +2 -21
- package/bin/surea11y-core.js +0 -20
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Applies to <a href> elements whose immediate parent element also has
|
|
13
13
|
* at least one direct-child text node with non-whitespace content
|
|
14
14
|
* (i.e. the link sits inline within a run of plain text, not as a
|
|
15
|
-
* standalone item
|
|
15
|
+
* standalone item, e.g. not the sole content of a <li> nav item).
|
|
16
16
|
* @expectation
|
|
17
17
|
* A link inside a text block must be visually distinguishable from the
|
|
18
18
|
* surrounding text by at least one non-color means:
|
|
@@ -20,29 +20,29 @@
|
|
|
20
20
|
* - a different font-weight than the surrounding text, OR
|
|
21
21
|
* - a different font-style than the surrounding text, OR
|
|
22
22
|
* - a contrast ratio of at least 3:1 between the link's text color and
|
|
23
|
-
* the surrounding text's color (WCAG technique G183's threshold
|
|
23
|
+
* the surrounding text's color (WCAG technique G183's threshold,
|
|
24
24
|
* sufficient contrast alone is an accepted alternative to underline).
|
|
25
25
|
* Fails only when none of the above hold AND the color contrast between
|
|
26
|
-
* link and surrounding text is confidently computable and below 3:1
|
|
26
|
+
* link and surrounding text is confidently computable and below 3:1,
|
|
27
27
|
* i.e. color is demonstrably the only cue.
|
|
28
28
|
* @implementation-notes
|
|
29
29
|
* - "Surrounding text style" is approximated as the link's immediate
|
|
30
30
|
* parent element's own computed style, not a full inline-context walk
|
|
31
|
-
* of the actual adjacent text node(s)
|
|
31
|
+
* of the actual adjacent text node(s), a deliberate scope-down, since
|
|
32
32
|
* plain text nodes inherit their rendering from the parent in the
|
|
33
33
|
* overwhelming majority of real markup.
|
|
34
|
-
* -
|
|
35
|
-
*
|
|
36
|
-
* `contrast-
|
|
37
|
-
*
|
|
38
|
-
* reserved for deterministic
|
|
39
|
-
*
|
|
40
|
-
*
|
|
34
|
+
* - A candidate that cannot be evaluated reports cantTell, not pass:
|
|
35
|
+
* contrast not confidently computable (the blockers `contrast-minimum`/
|
|
36
|
+
* `contrast-computable` use), or `text-decoration` unreadable in both
|
|
37
|
+
* the computed style and the CSSOM (see `decorationInfo`). `fail` stays
|
|
38
|
+
* reserved for deterministic violations; this is the computability gate
|
|
39
|
+
* RULE_TAXONOMY.md §1.1 allows automatic rules, as in `contrast-minimum`
|
|
40
|
+
* and `target-size-minimum`.
|
|
41
41
|
* - Reuses the shared `helpers.contrast` subsystem (same
|
|
42
42
|
* computeEffectiveForeground/Background, getComputabilityBlocker,
|
|
43
43
|
* contrastRatio helpers as `contrast-minimum`), rather than re-deriving
|
|
44
44
|
* color math independently.
|
|
45
|
-
* - Scoped to `a[href]` only (not `area[href]` or `[role="link"]`)
|
|
45
|
+
* - Scoped to `a[href]` only (not `area[href]` or `[role="link"]`),
|
|
46
46
|
* matches the common real-world shape of this issue (prose links).
|
|
47
47
|
*/
|
|
48
48
|
|
|
@@ -52,7 +52,7 @@ const meta = {
|
|
|
52
52
|
title:
|
|
53
53
|
'Links in text blocks must be distinguishable from surrounding text without relying on color alone',
|
|
54
54
|
description:
|
|
55
|
-
'Checks that a link inside a run of text is visually distinguishable from the surrounding text by underline, a font-weight/style difference, or a sufficient (>=3:1) color-contrast difference
|
|
55
|
+
'Checks that a link inside a run of text is visually distinguishable from the surrounding text by underline, a font-weight/style difference, or a sufficient (>=3:1) color-contrast difference, not by color alone.',
|
|
56
56
|
i18n: {
|
|
57
57
|
titleKey: 'linkInTextBlock_title',
|
|
58
58
|
descriptionKey: 'linkInTextBlock_description'
|
|
@@ -103,14 +103,176 @@ function runInPage(ctx) {
|
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
// Whether the element is underlined, and whether the computed style can be
|
|
107
|
+
// trusted to say so. A conforming CSSOM serialises the `text-decoration`
|
|
108
|
+
// shorthand with the line value first, so it and `text-decoration-line`
|
|
109
|
+
// always agree on whether `underline` is present. jsdom does not cascade
|
|
110
|
+
// the property at all: the shorthand reads back as the UA's "underline"
|
|
111
|
+
// for every <a> whatever the author CSS says, and the longhand as "none"
|
|
112
|
+
// unless the author used the longhand. Either one taken alone is wrong in
|
|
113
|
+
// one direction, so disagreement is the signal to stop trusting both.
|
|
114
|
+
function decorationInfo(cs) {
|
|
115
|
+
if (!cs) return { underlined: false, trustworthy: false };
|
|
116
|
+
|
|
117
|
+
const lineRaw = String(cs.textDecorationLine || '')
|
|
118
|
+
.trim()
|
|
119
|
+
.toLowerCase();
|
|
120
|
+
const shortRaw = String(cs.textDecoration || '')
|
|
121
|
+
.trim()
|
|
122
|
+
.toLowerCase();
|
|
123
|
+
|
|
124
|
+
const lineTokens = lineRaw.split(/\s+/).filter(Boolean);
|
|
125
|
+
const shortTokens = shortRaw.split(/\s+/).filter(Boolean);
|
|
126
|
+
|
|
127
|
+
// Only one of the two exposed: nothing to cross-check against, take it.
|
|
128
|
+
if (!lineTokens.length) {
|
|
129
|
+
return { underlined: shortTokens.includes('underline'), trustworthy: shortTokens.length > 0 };
|
|
130
|
+
}
|
|
131
|
+
if (!shortTokens.length) {
|
|
132
|
+
return { underlined: lineTokens.includes('underline'), trustworthy: true };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const byLine = lineTokens.includes('underline');
|
|
136
|
+
const byShort = shortTokens.includes('underline');
|
|
137
|
+
return { underlined: byLine, trustworthy: byLine === byShort };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Resolves `text-decoration` from the author stylesheets when the computed
|
|
141
|
+
// style is untrustworthy, reading the CSSOM as `css-orientation-lock` and
|
|
142
|
+
// `css-focus-indicator-suppressed` do. Without it the rule could not decide
|
|
143
|
+
// anything under a DOM emulator, which is how the CLI scans static HTML.
|
|
144
|
+
//
|
|
145
|
+
// A narrow cascade is enough: `text-decoration-line` is not inherited, so
|
|
146
|
+
// only declarations matching the element itself and its inline style apply,
|
|
147
|
+
// ordered by specificity. With no author declaration the UA default stands,
|
|
148
|
+
// and for a link that is an underline. Anything that would make the answer a
|
|
149
|
+
// guess yields `resolved: false` and the caller reports cantTell.
|
|
150
|
+
const CSS_STYLE_RULE = 1;
|
|
151
|
+
const MAX_NESTED_DEPTH = 8;
|
|
152
|
+
|
|
153
|
+
function splitSelectorList(selectorText) {
|
|
154
|
+
const parts = [];
|
|
155
|
+
let depth = 0;
|
|
156
|
+
let current = '';
|
|
157
|
+
for (const ch of String(selectorText || '')) {
|
|
158
|
+
if (ch === '(') depth += 1;
|
|
159
|
+
if (ch === ')') depth = Math.max(0, depth - 1);
|
|
160
|
+
if (ch === ',' && depth === 0) {
|
|
161
|
+
parts.push(current);
|
|
162
|
+
current = '';
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
current += ch;
|
|
166
|
+
}
|
|
167
|
+
parts.push(current);
|
|
168
|
+
return parts.map((p) => p.trim()).filter(Boolean);
|
|
110
169
|
}
|
|
111
170
|
|
|
112
|
-
|
|
113
|
-
|
|
171
|
+
// Approximate CSS specificity as a single sortable integer. Exactness is
|
|
172
|
+
// not required: this only orders declarations of one property against each
|
|
173
|
+
// other, and near-ties are broken by document order as the cascade does.
|
|
174
|
+
function specificityOf(selector) {
|
|
175
|
+
const s = String(selector || '');
|
|
176
|
+
const ids = (s.match(/#[\w-]+/g) || []).length;
|
|
177
|
+
const classesEtc = (s.match(/\.[\w-]+|\[[^\]]*\]|:(?!:)[\w-]+/g) || []).length;
|
|
178
|
+
const types = (s.match(/(^|[\s>+~])[a-z][\w-]*/gi) || []).length;
|
|
179
|
+
return ids * 10000 + classesEtc * 100 + types;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// A declaration wins if it is the last one, in (specificity, order), whose
|
|
183
|
+
// selector matches. `!important` outranks everything non-important.
|
|
184
|
+
function underlineFromDeclaration(style) {
|
|
185
|
+
if (!style || typeof style.getPropertyValue !== 'function') return null;
|
|
186
|
+
for (const prop of ['text-decoration-line', 'text-decoration']) {
|
|
187
|
+
const raw = String(style.getPropertyValue(prop) || '')
|
|
188
|
+
.trim()
|
|
189
|
+
.toLowerCase();
|
|
190
|
+
if (!raw) continue;
|
|
191
|
+
const important = String(style.getPropertyPriority(prop) || '') === 'important';
|
|
192
|
+
return { underlined: /\bunderline\b/.test(raw), important };
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function resolveUnderlineFromCssom(el) {
|
|
198
|
+
const doc = el && el.ownerDocument ? el.ownerDocument : null;
|
|
199
|
+
if (!doc || typeof el.matches !== 'function') return { underlined: false, resolved: false };
|
|
200
|
+
|
|
201
|
+
let best = null; // { rank, order, underlined }
|
|
202
|
+
let order = 0;
|
|
203
|
+
let unreadableSheet = false;
|
|
204
|
+
let unparsableSelector = false;
|
|
205
|
+
|
|
206
|
+
function consider(cssRule) {
|
|
207
|
+
const decl = underlineFromDeclaration(cssRule.style);
|
|
208
|
+
if (!decl) return;
|
|
209
|
+
for (const part of splitSelectorList(cssRule.selectorText)) {
|
|
210
|
+
// A pseudo-element rule paints a box other than the link's own text.
|
|
211
|
+
if (/::[a-z-]+/i.test(part)) continue;
|
|
212
|
+
// A state the static DOM is not in (:hover/:focus/...) does not
|
|
213
|
+
// describe the link's resting appearance, which is what this rule is
|
|
214
|
+
// about.
|
|
215
|
+
if (/:(hover|focus|focus-visible|focus-within|active|target|visited)\b/i.test(part)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
let matched;
|
|
219
|
+
try {
|
|
220
|
+
matched = el.matches(part);
|
|
221
|
+
} catch {
|
|
222
|
+
unparsableSelector = true;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!matched) continue;
|
|
226
|
+
order += 1;
|
|
227
|
+
const rank = (decl.important ? 1e9 : 0) + specificityOf(part);
|
|
228
|
+
if (!best || rank >= best.rank) best = { rank, order, underlined: decl.underlined };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function walk(rules, depth) {
|
|
233
|
+
if (!rules || depth > MAX_NESTED_DEPTH) return;
|
|
234
|
+
for (const cssRule of rules) {
|
|
235
|
+
if (!cssRule) continue;
|
|
236
|
+
if (cssRule.type === CSS_STYLE_RULE && cssRule.selectorText) {
|
|
237
|
+
consider(cssRule);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
let nested;
|
|
241
|
+
try {
|
|
242
|
+
nested = cssRule.cssRules || null;
|
|
243
|
+
} catch {
|
|
244
|
+
nested = null;
|
|
245
|
+
}
|
|
246
|
+
if (nested) walk(nested, depth + 1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
for (const sheet of doc.styleSheets || []) {
|
|
252
|
+
let rules = null;
|
|
253
|
+
try {
|
|
254
|
+
rules = sheet && sheet.cssRules ? sheet.cssRules : null;
|
|
255
|
+
} catch {
|
|
256
|
+
unreadableSheet = true; // cross-origin, not inspectable
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (rules) walk(rules, 0);
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
return { underlined: false, resolved: false };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// The inline style attribute outranks every stylesheet declaration.
|
|
266
|
+
const inline = underlineFromDeclaration(el.style);
|
|
267
|
+
if (inline) return { underlined: inline.underlined, resolved: true };
|
|
268
|
+
|
|
269
|
+
if (best) return { underlined: best.underlined, resolved: true };
|
|
270
|
+
|
|
271
|
+
// No author declaration reached this element. If a sheet or selector was
|
|
272
|
+
// unreadable, one of them might have, so the answer is unknown; otherwise
|
|
273
|
+
// the UA default stands, and for a link that means underlined.
|
|
274
|
+
if (unreadableSheet || unparsableSelector) return { underlined: false, resolved: false };
|
|
275
|
+
return { underlined: true, resolved: true };
|
|
114
276
|
}
|
|
115
277
|
|
|
116
278
|
function hasSurroundingText(el, parent) {
|
|
@@ -141,7 +303,15 @@ function runInPage(ctx) {
|
|
|
141
303
|
: helpers.queryAll(selector);
|
|
142
304
|
|
|
143
305
|
const occurrences = [];
|
|
306
|
+
const undecided = [];
|
|
144
307
|
let applicableCount = 0;
|
|
308
|
+
let decidedCount = 0;
|
|
309
|
+
|
|
310
|
+
// Applicable, but not evaluable. Held separately so the outcome below can
|
|
311
|
+
// tell "checked and sound" apart from "never decided".
|
|
312
|
+
function markUndecided(el, reasonCode) {
|
|
313
|
+
undecided.push({ el, reasonCode });
|
|
314
|
+
}
|
|
145
315
|
|
|
146
316
|
for (const el of nodes) {
|
|
147
317
|
if (!el || !el.getAttribute) continue;
|
|
@@ -159,27 +329,40 @@ function runInPage(ctx) {
|
|
|
159
329
|
const linkCs = safeComputedStyle(el);
|
|
160
330
|
const parentCs = safeComputedStyle(parent);
|
|
161
331
|
|
|
162
|
-
|
|
163
|
-
|
|
332
|
+
// Cues that do not depend on `text-decoration` come first, so a link
|
|
333
|
+
// carrying one is decided even where decoration is unreadable.
|
|
164
334
|
const linkWeight = c && linkCs ? c.normalizeFontWeight(linkCs.fontWeight) : 400;
|
|
165
335
|
const parentWeight = c && parentCs ? c.normalizeFontWeight(parentCs.fontWeight) : 400;
|
|
166
|
-
if (linkWeight !== parentWeight)
|
|
336
|
+
if (linkWeight !== parentWeight) {
|
|
337
|
+
decidedCount += 1;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
167
340
|
|
|
168
341
|
const linkStyle = (linkCs && linkCs.fontStyle) || 'normal';
|
|
169
342
|
const parentStyle = (parentCs && parentCs.fontStyle) || 'normal';
|
|
170
|
-
if (linkStyle !== parentStyle)
|
|
343
|
+
if (linkStyle !== parentStyle) {
|
|
344
|
+
decidedCount += 1;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
171
347
|
|
|
172
|
-
if (!c)
|
|
348
|
+
if (!c) {
|
|
349
|
+
markUndecided(el, 'CONTRAST_HELPERS_UNAVAILABLE');
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
173
352
|
|
|
174
353
|
let flagged = false;
|
|
354
|
+
let computed = false;
|
|
175
355
|
let ratio = null;
|
|
176
356
|
let fgLinkHex = '';
|
|
177
357
|
let fgParentHex = '';
|
|
358
|
+
let undecidedReason = 'COLOR_NOT_COMPUTABLE';
|
|
178
359
|
|
|
179
360
|
try {
|
|
180
361
|
const blocker = c.getComputabilityBlocker(el);
|
|
181
362
|
if (blocker && blocker.ok === false) {
|
|
182
|
-
// Not confidently computable
|
|
363
|
+
// Not confidently computable: recorded below rather than skipped, so
|
|
364
|
+
// it cannot be mistaken for a clean result.
|
|
365
|
+
if (blocker.reasonCode) undecidedReason = String(blocker.reasonCode);
|
|
183
366
|
} else {
|
|
184
367
|
const bg = c.computeEffectiveBackground(el, {
|
|
185
368
|
contrast: { mode, rootCanvasFallback },
|
|
@@ -202,15 +385,49 @@ function runInPage(ctx) {
|
|
|
202
385
|
fgLinkHex = c.rgbToHex ? c.rgbToHex(fgLinkOpaque) : '';
|
|
203
386
|
fgParentHex = c.rgbToHex ? c.rgbToHex(fgParentOpaque) : '';
|
|
204
387
|
|
|
388
|
+
computed = true;
|
|
205
389
|
if (!(ratio >= 3)) flagged = true;
|
|
206
390
|
}
|
|
207
|
-
// else: not confidently computable
|
|
391
|
+
// else: not confidently computable, recorded below.
|
|
208
392
|
}
|
|
209
393
|
} catch {
|
|
210
|
-
//
|
|
394
|
+
// No-throw: treat as not computable and record it.
|
|
395
|
+
undecidedReason = 'ENGINE_EXCEPTION';
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (!computed) {
|
|
399
|
+
markUndecided(el, undecidedReason);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Contrast alone is an accepted alternative to an underline (G183), so a
|
|
404
|
+
// link clearing 3:1 is distinguishable regardless of decoration.
|
|
405
|
+
if (!flagged) {
|
|
406
|
+
decidedCount += 1;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Below 3:1, an underline is the last remaining cue -- and only now does
|
|
411
|
+
// it matter whether this environment can actually report one.
|
|
412
|
+
const decoration = decorationInfo(linkCs);
|
|
413
|
+
let underlined;
|
|
414
|
+
if (decoration.trustworthy) {
|
|
415
|
+
underlined = decoration.underlined;
|
|
416
|
+
} else {
|
|
417
|
+
const fromCssom = resolveUnderlineFromCssom(el);
|
|
418
|
+
if (!fromCssom.resolved) {
|
|
419
|
+
markUndecided(el, 'TEXT_DECORATION_NOT_RESOLVABLE');
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
underlined = fromCssom.underlined;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (underlined) {
|
|
426
|
+
decidedCount += 1;
|
|
427
|
+
continue;
|
|
211
428
|
}
|
|
212
429
|
|
|
213
|
-
|
|
430
|
+
decidedCount += 1;
|
|
214
431
|
|
|
215
432
|
const eligInfo = helpers.getEligibilityInfo
|
|
216
433
|
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
@@ -243,6 +460,9 @@ function runInPage(ctx) {
|
|
|
243
460
|
if (applicableCount === 0) {
|
|
244
461
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
245
462
|
}
|
|
463
|
+
|
|
464
|
+
// A proven violation outranks an undecided candidate, which outranks a clean
|
|
465
|
+
// one. The middle step keeps an unevaluable link out of `pass`.
|
|
246
466
|
if (occurrences.length) {
|
|
247
467
|
return {
|
|
248
468
|
ruleId: rule.ruleId,
|
|
@@ -251,7 +471,43 @@ function runInPage(ctx) {
|
|
|
251
471
|
occurrences
|
|
252
472
|
};
|
|
253
473
|
}
|
|
254
|
-
|
|
474
|
+
|
|
475
|
+
if (undecided.length) {
|
|
476
|
+
return {
|
|
477
|
+
ruleId: rule.ruleId,
|
|
478
|
+
outcome: 'cantTell',
|
|
479
|
+
severity: rule.defaultSeverity || 'serious',
|
|
480
|
+
confidence: 'low',
|
|
481
|
+
occurrences: undecided.map(({ el, reasonCode }) =>
|
|
482
|
+
helpers.reportOccurrence(el, {
|
|
483
|
+
summary:
|
|
484
|
+
'Whether this link is distinguishable from the surrounding text by non-color means could not be determined.',
|
|
485
|
+
hint: 'Confirm by eye that the link carries an underline, a font-weight or font-style difference, or at least 3:1 contrast against the surrounding text. Running the engine in a real browser rather than a DOM emulator resolves most cases automatically.',
|
|
486
|
+
i18n: {
|
|
487
|
+
summaryKey: 'linkInTextBlock_summary_cantTell',
|
|
488
|
+
hintKey: 'linkInTextBlock_hint_cantTell'
|
|
489
|
+
},
|
|
490
|
+
uncertainty: {
|
|
491
|
+
code: 'not-computable',
|
|
492
|
+
needed:
|
|
493
|
+
'Whether the link carries an underline, weight or style difference, or 3:1 contrast against its surrounding text.',
|
|
494
|
+
evidence: { reasonCode }
|
|
495
|
+
},
|
|
496
|
+
data: {
|
|
497
|
+
visibilityFilter: helpers.getEligibilityInfo
|
|
498
|
+
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
499
|
+
: { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
500
|
+
details: { reasonCode }
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
)
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (decidedCount > 0) {
|
|
508
|
+
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
509
|
+
}
|
|
510
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
255
511
|
}
|
|
256
512
|
|
|
257
513
|
module.exports = { id, meta, runInPage };
|
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check link-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Links must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to <a href>, <area href> and elements with role="link" that are
|
|
13
|
+
* included in the accessibility tree. An <a> without an href is not a link
|
|
14
|
+
* and is not matched.
|
|
15
|
+
* @expectation
|
|
16
|
+
* The element has a non-empty accessible name. A programmatic name is
|
|
17
|
+
* taken first (aria-labelledby, aria-label, an associated <label>, title),
|
|
18
|
+
* and failing that the element falls back to its own subtree text,
|
|
19
|
+
* counting each descendant's own name (an <img alt>, aria-label or title),
|
|
20
|
+
* the shape behind the common <a><img alt="..."></a> logo link. The
|
|
21
|
+
* content fallback is suppressed when an explicit, known role that is not
|
|
22
|
+
* name-from-content is present; an unrecognized role token falls back to
|
|
23
|
+
* the implicit role.
|
|
24
|
+
*/
|
|
25
|
+
|
|
5
26
|
const id = 'link-name-present';
|
|
6
27
|
|
|
7
28
|
const meta = {
|
|
@@ -38,7 +59,7 @@ function runInPage(ctx) {
|
|
|
38
59
|
let applicableCount = 0;
|
|
39
60
|
|
|
40
61
|
function getConservativeSubtreeText(container) {
|
|
41
|
-
// "Name from content"
|
|
62
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
42
63
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
43
64
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
44
65
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Applies to <ul>/<ol> elements that have at least one direct element
|
|
13
13
|
* child.
|
|
14
14
|
* @expectation
|
|
15
|
-
* Every direct element child is <li>, <script>, or <template
|
|
15
|
+
* Every direct element child is <li>, <script>, or <template>. UNLESS it
|
|
16
16
|
* has an explicit `role` attribute, in which case the explicit role wins
|
|
17
17
|
* over the tag entirely: a child is valid iff that role is "listitem"
|
|
18
18
|
* (so `<li role="presentation">`/`<li role="menuitem">` are invalid
|
|
@@ -20,19 +20,19 @@
|
|
|
20
20
|
* given `role="listitem"` is valid). A wrapper <div> used for styling
|
|
21
21
|
* (no role at all) still breaks list semantics the same as before.
|
|
22
22
|
* @implementation-notes
|
|
23
|
-
* - Checked via el.children, which already excludes text/comment nodes
|
|
23
|
+
* - Checked via el.children, which already excludes text/comment nodes,
|
|
24
24
|
* no whitespace-node filtering needed.
|
|
25
25
|
* - Distinct, atomic decision from listitem-parent-valid (the
|
|
26
26
|
* inverse relationship: does a given <li> have a valid parent).
|
|
27
27
|
* - Direct children that are not exposed to the accessibility tree (e.g.
|
|
28
28
|
* display:none, [hidden], aria-hidden="true") are excluded from
|
|
29
|
-
* consideration entirely
|
|
29
|
+
* consideration entirely. An element not reachable by assistive
|
|
30
30
|
* technology can't break the list semantics a screen reader announces.
|
|
31
31
|
* Common cases: a stray `<input type="hidden">` as a direct <ul> child
|
|
32
32
|
* (UA-stylesheet display:none by spec), or `<span style="display:none">`
|
|
33
33
|
* hydration markers interleaved with real `<li>`s.
|
|
34
34
|
* - Explicit-role-overrides-tag: if a child has an explicit role, only
|
|
35
|
-
* `['listitem']` is consulted
|
|
35
|
+
* `['listitem']` is consulted, the tag name is never checked. Only
|
|
36
36
|
* without an explicit role does the tag name matter. Catches cases a
|
|
37
37
|
* tag-only check misses: `<li role="none">` hosting a list's own
|
|
38
38
|
* visually-hidden label, `<li role="menuitem">` menu items, or a real
|
|
@@ -71,7 +71,7 @@ const meta = {
|
|
|
71
71
|
function runInPage(ctx) {
|
|
72
72
|
const { helpers, rule } = ctx;
|
|
73
73
|
|
|
74
|
-
// Declared inside runInPage
|
|
74
|
+
// Declared inside runInPage, see scripts/build-core.js header
|
|
75
75
|
// ("runInPage MUST be self-contained").
|
|
76
76
|
const ALLOWED_CHILD_TAGS = new Set(['li', 'script', 'template']);
|
|
77
77
|
|
|
@@ -111,7 +111,7 @@ function runInPage(ctx) {
|
|
|
111
111
|
const roleAttr = child.getAttribute ? String(child.getAttribute('role') || '').trim() : '';
|
|
112
112
|
const explicitRole = roleAttr ? (roleAttr.split(/\s+/)[0] || '').toLowerCase() : '';
|
|
113
113
|
|
|
114
|
-
// An explicit role always wins over the tag
|
|
114
|
+
// An explicit role always wins over the tag, see header comment.
|
|
115
115
|
const valid = explicitRole ? explicitRole === 'listitem' : ALLOWED_CHILD_TAGS.has(tag);
|
|
116
116
|
|
|
117
117
|
if (!valid) invalidTags.push(tag);
|
|
@@ -2,11 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check listbox-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="listbox" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="listbox" (the attribute must name
|
|
13
|
+
* that role alone, not a fallback list) that are included in the
|
|
14
|
+
* accessibility tree. An element with the matching implicit role but no
|
|
15
|
+
* role attribute is out of scope.
|
|
16
|
+
* @expectation
|
|
17
|
+
* The element has a non-empty accessible name from aria-label, from an
|
|
18
|
+
* aria-labelledby that resolves to non-empty text, or from title.
|
|
19
|
+
* role="listbox" is name-from-author-only, so subtree text is never
|
|
20
|
+
* accepted: text sitting inside a custom listbox widget is not reliably
|
|
21
|
+
* exposed as its name. On a labelable element (<select multiple
|
|
22
|
+
* role="listbox">) an associated <label> counts as well.
|
|
23
|
+
*/
|
|
24
|
+
|
|
5
25
|
const id = 'listbox-name-present';
|
|
6
26
|
|
|
7
27
|
const meta = {
|
|
8
|
-
title: '
|
|
9
|
-
description: 'Checks that elements expose a non-empty accessible name.',
|
|
28
|
+
title: 'Listboxes have an accessible name',
|
|
29
|
+
description: 'Checks that elements with role="listbox" expose a non-empty accessible name.',
|
|
10
30
|
i18n: {
|
|
11
31
|
titleKey: 'listboxNamePresent_title',
|
|
12
32
|
descriptionKey: 'listboxNamePresent_description'
|
|
@@ -51,7 +71,7 @@ function runInPage(ctx) {
|
|
|
51
71
|
}
|
|
52
72
|
|
|
53
73
|
function getConservativeSubtreeText(document, container) {
|
|
54
|
-
// "Name from content"
|
|
74
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
55
75
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
56
76
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
57
77
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -68,7 +88,7 @@ function runInPage(ctx) {
|
|
|
68
88
|
|
|
69
89
|
// A <label> contributes a name via its own aria-label/aria-labelledby
|
|
70
90
|
// (checked first, same ARIA-over-content precedence any element's
|
|
71
|
-
// accessible name gives
|
|
91
|
+
// accessible name gives, e.g. <label aria-label="Search"><svg
|
|
72
92
|
// aria-hidden="true">...</svg></label> names its control "Search" even
|
|
73
93
|
// though the label's only child content is aria-hidden) or, failing
|
|
74
94
|
// that, its rendered content (getConservativeSubtreeText).
|
|
@@ -83,10 +103,10 @@ function runInPage(ctx) {
|
|
|
83
103
|
if (content) return content;
|
|
84
104
|
// Final fallback per the general accname text-alternative algorithm,
|
|
85
105
|
// which applies to any element being asked for its name regardless of
|
|
86
|
-
// why (own aria-label, an aria-labelledby reference, or
|
|
106
|
+
// why (own aria-label, an aria-labelledby reference, or, here, native
|
|
87
107
|
// <label for> association): title, when nothing else yields a name.
|
|
88
108
|
// Purely additive (only fills in a name where there was none before),
|
|
89
|
-
// so it carries no false-positive risk
|
|
109
|
+
// so it carries no false-positive risk, see dialog-name-present.js's
|
|
90
110
|
// identical <iframe>-title-fallback fix for the concrete real-world
|
|
91
111
|
// trigger this same accname step covers elsewhere.
|
|
92
112
|
return getAttr(lab, 'title');
|
|
@@ -96,7 +116,7 @@ function runInPage(ctx) {
|
|
|
96
116
|
const raw = getAttr(el, 'aria-labelledby');
|
|
97
117
|
if (!raw) return '';
|
|
98
118
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
99
|
-
// name-from-content of the referenced element
|
|
119
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
100
120
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
101
121
|
// aria-labelledby target's only name source is its title attribute,
|
|
102
122
|
// which name-from-content alone can never see).
|
|
@@ -231,7 +251,7 @@ function runInPage(ctx) {
|
|
|
231
251
|
occurrences.push(
|
|
232
252
|
helpers.reportOccurrence(el, {
|
|
233
253
|
summary: 'This element has no accessible name.',
|
|
234
|
-
hint: "Provide aria-label, aria-labelledby, or a title attribute
|
|
254
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute. Visible text content is not exposed as this listbox's accessible name.",
|
|
235
255
|
i18n: {
|
|
236
256
|
summaryKey: 'listboxNamePresent_summary_fail',
|
|
237
257
|
hintKey: 'listboxNamePresent_hint_fail',
|
|
@@ -24,16 +24,16 @@
|
|
|
24
24
|
* e.g. aria-helpers.js getExplicitRole).
|
|
25
25
|
* - An explicit role on the parent WINS over its tag name, in either
|
|
26
26
|
* direction: a <ul role="menu"> no longer exposes role "list" (its own
|
|
27
|
-
* native role is fully replaced by the explicit one
|
|
27
|
+
* native role is fully replaced by the explicit one, the same "any
|
|
28
28
|
* explicit role overrides the element's native role" ARIA principle
|
|
29
29
|
* applied elsewhere in this engine), so an <li> inside it is invalid
|
|
30
30
|
* despite the <ul> tag (e.g. `<ul role="menu"><li>...`). Conversely
|
|
31
31
|
* role="presentation"/"none" on the parent is still a valid
|
|
32
|
-
* (list-semantics-suppressing) parent
|
|
32
|
+
* (list-semantics-suppressing) parent, the accepted parent roles are
|
|
33
33
|
* presentation, none, and list.
|
|
34
34
|
* - The SAME "explicit role wins" principle applies to the <li> ELEMENT
|
|
35
35
|
* ITSELF: an <li role="tab">/role="menuitem">/role="presentation"> etc.
|
|
36
|
-
* is exposed to AT with that role, never "listitem"
|
|
36
|
+
* is exposed to AT with that role, never "listitem". The whole point
|
|
37
37
|
* of this check (list items need a valid list-container parent) doesn't
|
|
38
38
|
* apply when the element isn't claiming listitem semantics in the first
|
|
39
39
|
* place. Any `<li>` with an explicit `role` attribute is excluded from
|
|
@@ -108,7 +108,7 @@ function runInPage(ctx) {
|
|
|
108
108
|
let valid;
|
|
109
109
|
if (explicitRole) {
|
|
110
110
|
// An explicit role always wins over the tag's native role, in either
|
|
111
|
-
// direction
|
|
111
|
+
// direction, see the header comment.
|
|
112
112
|
valid = explicitRole === 'list' || explicitRole === 'presentation' || explicitRole === 'none';
|
|
113
113
|
} else {
|
|
114
114
|
valid = parentTag === 'ul' || parentTag === 'ol';
|
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check menuitem-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Menu items must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="menuitem", role="menuitemcheckbox" or
|
|
13
|
+
* role="menuitemradio" (the attribute must name one of those roles alone,
|
|
14
|
+
* not a fallback list) that are included in the accessibility tree.
|
|
15
|
+
* @expectation
|
|
16
|
+
* The element has a non-empty accessible name from aria-label, from an
|
|
17
|
+
* aria-labelledby that resolves to non-empty text, from title, or, all
|
|
18
|
+
* three roles being name-from-content, from its own subtree text, where a
|
|
19
|
+
* descendant's own name (an <img alt>, aria-label or title) counts as that
|
|
20
|
+
* descendant's contribution rather than only its text nodes.
|
|
21
|
+
*/
|
|
22
|
+
|
|
5
23
|
const id = 'menuitem-name-present';
|
|
6
24
|
|
|
7
25
|
const meta = {
|
|
@@ -52,7 +70,7 @@ function runInPage(ctx) {
|
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
function getConservativeSubtreeText(document, container) {
|
|
55
|
-
// "Name from content"
|
|
73
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
56
74
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
57
75
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
58
76
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -71,7 +89,7 @@ function runInPage(ctx) {
|
|
|
71
89
|
const raw = getAttr(el, 'aria-labelledby');
|
|
72
90
|
if (!raw) return '';
|
|
73
91
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
74
|
-
// name-from-content of the referenced element
|
|
92
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
75
93
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
76
94
|
// aria-labelledby target's only name source is its title attribute,
|
|
77
95
|
// which name-from-content alone can never see).
|