@surea11y/core 1.4.1 → 1.5.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 +47 -7
- package/README.md +19 -3
- package/docs/API_STABILITY.md +2 -2
- package/docs/ENGINE_OPTIONS.md +14 -10
- package/docs/I18N.md +176 -20
- package/docs/INTEGRATION.md +28 -6
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +13 -3
- package/docs/REPORT.md +2 -0
- package/docs/RULE_AUTHORING.md +53 -0
- package/docs/TROUBLESHOOTING.md +2 -2
- package/package.json +6 -1
- package/src/checks/automatic/aria-allowed-attr.js +27 -30
- package/src/checks/automatic/aria-allowed-role.js +14 -16
- package/src/checks/automatic/aria-braille-equivalent.js +17 -19
- package/src/checks/automatic/aria-conditional-attr.js +17 -19
- package/src/checks/automatic/aria-deprecated-role.js +62 -49
- package/src/checks/automatic/aria-hidden-body.js +2 -9
- package/src/checks/automatic/aria-hidden-focus.js +99 -18
- package/src/checks/automatic/aria-prohibited-attr.js +54 -55
- package/src/checks/automatic/aria-prohibited-children.js +26 -26
- package/src/checks/automatic/aria-required-attr.js +14 -17
- package/src/checks/automatic/aria-required-children.js +17 -20
- package/src/checks/automatic/aria-required-parent.js +17 -20
- package/src/checks/automatic/aria-roles-valid.js +37 -23
- package/src/checks/automatic/aria-valid-attr-value.js +18 -21
- package/src/checks/automatic/aria-valid-attr.js +14 -17
- package/src/checks/automatic/autocomplete-valid.js +15 -17
- package/src/checks/automatic/avoid-inline-spacing.js +14 -16
- package/src/checks/automatic/binary-control-name-present.js +19 -21
- package/src/checks/automatic/button-name-present.js +23 -28
- package/src/checks/automatic/combobox-name-present.js +15 -17
- package/src/checks/automatic/css-orientation-lock.js +22 -22
- package/src/checks/automatic/definition-list-children-valid.js +18 -21
- package/src/checks/automatic/deprecated-elements-not-used.js +14 -16
- package/src/checks/automatic/dialog-name-present.js +16 -18
- package/src/checks/automatic/dlitem-parent-valid.js +15 -17
- package/src/checks/automatic/duplicate-id-aria.js +45 -37
- package/src/checks/automatic/form-control-single-label.js +38 -40
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
- package/src/checks/automatic/iframe-focusable-content.js +29 -31
- package/src/checks/automatic/iframe-name-present.js +15 -17
- package/src/checks/automatic/iframe-title-unique.js +18 -23
- package/src/checks/automatic/label-in-name.js +31 -36
- package/src/checks/automatic/link-in-text-block.js +19 -21
- package/src/checks/automatic/link-name-present.js +25 -30
- package/src/checks/automatic/list-children-valid.js +15 -17
- package/src/checks/automatic/listbox-name-present.js +15 -17
- package/src/checks/automatic/listitem-parent-valid.js +14 -17
- package/src/checks/automatic/menuitem-name-present.js +16 -18
- package/src/checks/automatic/meta-refresh-no-exceptions.js +15 -18
- package/src/checks/automatic/meta-refresh-timing-absent.js +14 -17
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
- package/src/checks/automatic/meter-name-present.js +16 -18
- package/src/checks/automatic/nested-interactive-controls-absent.js +17 -19
- package/src/checks/automatic/option-name-present.js +16 -18
- package/src/checks/automatic/progressbar-name-present.js +19 -21
- package/src/checks/automatic/searchbox-name-present.js +19 -17
- package/src/checks/automatic/server-side-image-map-absent.js +15 -18
- package/src/checks/automatic/slider-name-present.js +15 -17
- package/src/checks/automatic/spinbutton-name-present.js +19 -17
- package/src/checks/automatic/summary-name-present.js +16 -18
- package/src/checks/automatic/tab-name-present.js +16 -18
- package/src/checks/automatic/table-headers-attr-valid.js +15 -17
- package/src/checks/automatic/table-th-has-data-cells.js +15 -19
- package/src/checks/automatic/target-size-minimum.js +104 -81
- package/src/checks/automatic/td-has-header.js +15 -20
- package/src/checks/automatic/textbox-name-present.js +15 -17
- package/src/checks/automatic/tooltip-name-present.js +16 -18
- package/src/checks/automatic/treeitem-name-present.js +16 -18
- package/src/checks/automatic/valid-lang.js +15 -17
- package/src/checks/manual/accesskeys-manual.js +18 -19
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +19 -22
- package/src/checks/manual/bypass-blocks-present-manual.js +4 -12
- package/src/checks/manual/empty-heading-manual.js +15 -17
- package/src/checks/manual/empty-table-header-manual.js +27 -30
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -9
- package/src/checks/manual/heading-order-manual.js +17 -22
- package/src/checks/manual/image-redundant-alt-manual.js +14 -17
- package/src/checks/manual/label-title-only-manual.js +15 -17
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-main-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -7
- package/src/checks/manual/landmark-one-main-manual.js +2 -9
- package/src/checks/manual/landmark-unique-manual.js +22 -27
- package/src/checks/manual/link-name-quality-manual.js +15 -17
- package/src/checks/manual/meta-viewport-large-manual.js +14 -17
- package/src/checks/manual/mouse-only-event-handlers-manual.js +17 -19
- package/src/checks/manual/page-has-heading-one-manual.js +2 -9
- package/src/checks/manual/presentation-role-conflict-manual.js +19 -21
- package/src/checks/manual/region-manual.js +13 -6
- package/src/checks/manual/scope-attr-valid-manual.js +14 -17
- package/src/checks/manual/skip-link-manual.js +39 -47
- package/src/checks/manual/tabindex-manual.js +14 -17
- package/src/checks/manual/table-duplicate-name-manual.js +14 -17
- package/src/core.js +4203 -3604
- package/src/report.js +14 -0
- package/surea11y.browser.js +1960 -3671
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
|
@@ -140,51 +140,49 @@ function runInPage(ctx) {
|
|
|
140
140
|
: [...eligibleLabels];
|
|
141
141
|
|
|
142
142
|
const tag = el.tagName.toLowerCase();
|
|
143
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
144
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
145
143
|
|
|
146
144
|
if (contributing.length >= 2) {
|
|
147
|
-
failOccurrences.push(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
145
|
+
failOccurrences.push(
|
|
146
|
+
helpers.reportOccurrence(el, {
|
|
147
|
+
summary: 'This form control is associated with more than one <label>.',
|
|
148
|
+
hint: 'Keep only one <label> per form control (either wrapping it or referencing it via for/id).',
|
|
149
|
+
occurrenceOutcome: 'fail',
|
|
150
|
+
i18n: {
|
|
151
|
+
summaryKey: 'formControlSingleLabel_summary_fail',
|
|
152
|
+
hintKey: 'formControlSingleLabel_hint_fail',
|
|
153
|
+
params: { element: tag, labelCount: String(contributing.length) }
|
|
154
|
+
},
|
|
155
|
+
data: {
|
|
156
|
+
details: {
|
|
157
|
+
reasonCode: 'FORM_FIELD_MULTIPLE_LABELS',
|
|
158
|
+
element: tag,
|
|
159
|
+
labelCount: contributing.length
|
|
160
|
+
}
|
|
163
161
|
}
|
|
164
|
-
}
|
|
165
|
-
|
|
162
|
+
})
|
|
163
|
+
);
|
|
166
164
|
} else if (contributing.length === 1) {
|
|
167
|
-
cantTellOccurrences.push(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
'
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
165
|
+
cantTellOccurrences.push(
|
|
166
|
+
helpers.reportOccurrence(el, {
|
|
167
|
+
summary:
|
|
168
|
+
'This form control has one labelling <label> plus an extra empty <label> association.',
|
|
169
|
+
hint: 'Remove the redundant empty <label> so exactly one <label> is associated with the control.',
|
|
170
|
+
occurrenceOutcome: 'cantTell',
|
|
171
|
+
i18n: {
|
|
172
|
+
summaryKey: 'formControlSingleLabel_summary_cantTell',
|
|
173
|
+
hintKey: 'formControlSingleLabel_hint_cantTell',
|
|
174
|
+
params: { element: tag, labelCount: String(eligibleLabels.size) }
|
|
175
|
+
},
|
|
176
|
+
data: {
|
|
177
|
+
details: {
|
|
178
|
+
reasonCode: 'FORM_FIELD_EXTRA_EMPTY_LABEL',
|
|
179
|
+
element: tag,
|
|
180
|
+
labelCount: eligibleLabels.size,
|
|
181
|
+
contributingLabelCount: contributing.length
|
|
182
|
+
}
|
|
185
183
|
}
|
|
186
|
-
}
|
|
187
|
-
|
|
184
|
+
})
|
|
185
|
+
);
|
|
188
186
|
}
|
|
189
187
|
// contributing.length === 0: no label carries text, so nothing competes
|
|
190
188
|
// for the name. A control left unnamed is form-control-programmatic-
|
|
@@ -84,15 +84,8 @@ function runInPage(ctx) {
|
|
|
84
84
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(html) : 'html';
|
|
88
|
-
const htmlSnippet = helpers.getOuterHtmlSnippet
|
|
89
|
-
? helpers.getOuterHtmlSnippet(html)
|
|
90
|
-
: (html.outerHTML || '').slice(0, 200);
|
|
91
|
-
|
|
92
87
|
const occurrences = [
|
|
93
|
-
{
|
|
94
|
-
selector: stableSelector,
|
|
95
|
-
html: htmlSnippet,
|
|
88
|
+
helpers.reportOccurrence(html, {
|
|
96
89
|
summary: `The lang ("${lang}") and xml:lang ("${xmlLang}") attributes declare different languages.`,
|
|
97
90
|
hint: 'Make lang and xml:lang declare the same primary language, or remove the deprecated xml:lang attribute.',
|
|
98
91
|
i18n: {
|
|
@@ -103,7 +96,7 @@ function runInPage(ctx) {
|
|
|
103
96
|
data: {
|
|
104
97
|
details: { reasonCode: 'HTML_XML_LANG_MISMATCH', lang, xmlLang }
|
|
105
98
|
}
|
|
106
|
-
}
|
|
99
|
+
})
|
|
107
100
|
];
|
|
108
101
|
|
|
109
102
|
return {
|
|
@@ -322,47 +322,45 @@ function runInPage(ctx) {
|
|
|
322
322
|
if (!candidates.length) continue;
|
|
323
323
|
|
|
324
324
|
const tag = el.tagName.toLowerCase();
|
|
325
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
326
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
327
325
|
const shouldProbe = candidates.length === 1;
|
|
328
326
|
const runtimeProbe = shouldProbe
|
|
329
327
|
? probeImmediateFocusRedirect(el, contentDoc, candidates[0])
|
|
330
328
|
: null;
|
|
331
329
|
|
|
332
330
|
if (runtimeProbe && runtimeProbe.redirected) {
|
|
333
|
-
cantTellOccurrences.push(
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
'
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
331
|
+
cantTellOccurrences.push(
|
|
332
|
+
helpers.reportOccurrence(el, {
|
|
333
|
+
summary:
|
|
334
|
+
'This frame has tabindex="-1" and a focusable candidate, but focus moves immediately to another target. Verify keyboard reachability in a real browser.',
|
|
335
|
+
hint: 'If this is an intentional focus handoff, ensure keyboard users cannot remain on hidden/intermediate frame content.',
|
|
336
|
+
i18n: null,
|
|
337
|
+
data: {
|
|
338
|
+
details: {
|
|
339
|
+
reasonCode: 'IFRAME_TABINDEX_NEGATIVE_CONTENT_RUNTIME_REDIRECT',
|
|
340
|
+
element: tag,
|
|
341
|
+
runtimeProbe
|
|
342
|
+
}
|
|
345
343
|
}
|
|
346
|
-
}
|
|
347
|
-
|
|
344
|
+
})
|
|
345
|
+
);
|
|
348
346
|
continue;
|
|
349
347
|
}
|
|
350
348
|
|
|
351
|
-
failOccurrences.push(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
'
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
|
|
349
|
+
failOccurrences.push(
|
|
350
|
+
helpers.reportOccurrence(el, {
|
|
351
|
+
summary:
|
|
352
|
+
'This frame has tabindex="-1" but its content contains focusable elements, which remain reachable by keyboard.',
|
|
353
|
+
hint: 'Remove focusable content from the frame, or remove tabindex="-1" if the frame is meant to be reachable.',
|
|
354
|
+
i18n: {
|
|
355
|
+
summaryKey: 'iframeFocusableContent_summary_fail',
|
|
356
|
+
hintKey: 'iframeFocusableContent_hint_fail',
|
|
357
|
+
params: { element: tag }
|
|
358
|
+
},
|
|
359
|
+
data: {
|
|
360
|
+
details: { reasonCode: 'IFRAME_TABINDEX_NEGATIVE_CONTENT_FOCUSABLE', element: tag }
|
|
361
|
+
}
|
|
362
|
+
})
|
|
363
|
+
);
|
|
366
364
|
}
|
|
367
365
|
|
|
368
366
|
if (applicableCount === 0) {
|
|
@@ -88,24 +88,22 @@ function runInPage(ctx) {
|
|
|
88
88
|
})()
|
|
89
89
|
: null;
|
|
90
90
|
const tag = el.tagName.toLowerCase();
|
|
91
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
92
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
93
91
|
|
|
94
|
-
occurrences.push(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
92
|
+
occurrences.push(
|
|
93
|
+
helpers.reportOccurrence(el, {
|
|
94
|
+
summary: 'This frame has no accessible name.',
|
|
95
|
+
hint: 'Add a title attribute (or aria-label/aria-labelledby) describing the frame’s content or purpose.',
|
|
96
|
+
i18n: {
|
|
97
|
+
summaryKey: 'iframeNamePresent_summary_fail',
|
|
98
|
+
hintKey: 'iframeNamePresent_hint_fail',
|
|
99
|
+
params: { element: tag }
|
|
100
|
+
},
|
|
101
|
+
data: {
|
|
102
|
+
details: { reasonCode: 'IFRAME_NAME_MISSING', element: tag },
|
|
103
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
);
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
if (applicableCount === 0) {
|
|
@@ -89,30 +89,25 @@ function runInPage(ctx) {
|
|
|
89
89
|
|
|
90
90
|
for (const el of els) {
|
|
91
91
|
const tag = el.tagName.toLowerCase();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
details: {
|
|
109
|
-
reasonCode: 'IFRAME_TITLE_DUPLICATE',
|
|
110
|
-
element: tag,
|
|
111
|
-
title,
|
|
112
|
-
duplicateCount: els.length
|
|
92
|
+
occurrences.push(
|
|
93
|
+
helpers.reportOccurrence(el, {
|
|
94
|
+
summary: 'This frame’s title is not unique among the frames on this page.',
|
|
95
|
+
hint: 'Give each frame a distinct title describing its specific content or purpose.',
|
|
96
|
+
i18n: {
|
|
97
|
+
summaryKey: 'iframeTitleUnique_summary_fail',
|
|
98
|
+
hintKey: 'iframeTitleUnique_hint_fail',
|
|
99
|
+
params: { element: tag, title }
|
|
100
|
+
},
|
|
101
|
+
data: {
|
|
102
|
+
details: {
|
|
103
|
+
reasonCode: 'IFRAME_TITLE_DUPLICATE',
|
|
104
|
+
element: tag,
|
|
105
|
+
title,
|
|
106
|
+
duplicateCount: els.length
|
|
107
|
+
}
|
|
113
108
|
}
|
|
114
|
-
}
|
|
115
|
-
|
|
109
|
+
})
|
|
110
|
+
);
|
|
116
111
|
}
|
|
117
112
|
}
|
|
118
113
|
|
|
@@ -366,43 +366,38 @@ function runInPage(ctx) {
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
if (!contains) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
369
|
+
occurrences.push(
|
|
370
|
+
helpers.reportOccurrence(el, {
|
|
371
|
+
...(uncertainty ? { outcome: 'cantTell' } : null),
|
|
372
|
+
summary: uncertainty
|
|
373
|
+
? 'Accessible name may not contain the visible label text.'
|
|
374
|
+
: 'Accessible name does not contain the visible label text.',
|
|
375
|
+
hint: uncertainty
|
|
376
|
+
? 'Check by hand: the two differ only by an abbreviation or by hyphenation, which markup cannot settle.'
|
|
377
|
+
: 'Ensure the accessible name includes the visible text label (e.g., update aria-label/aria-labelledby to include the visible wording).',
|
|
378
|
+
i18n: {
|
|
379
|
+
summaryKey: uncertainty ? 'labelInName_summary_cantTell' : 'labelInName_summary_fail',
|
|
380
|
+
hintKey: uncertainty ? 'labelInName_hint_cantTell' : 'labelInName_hint_fail',
|
|
381
|
+
params: {
|
|
382
|
+
element: getElementDescriptor(el),
|
|
383
|
+
visibleLabel: clipForSummary(visibleLabel),
|
|
384
|
+
labelSource: labelInfo && labelInfo.source ? labelInfo.source : 'none',
|
|
385
|
+
nameMechanism: acc && acc.mechanism ? acc.mechanism : 'none'
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
data: {
|
|
389
|
+
details: {
|
|
390
|
+
reasonCode: uncertainty || 'VISIBLE_LABEL_NOT_IN_ACCESSIBLE_NAME',
|
|
391
|
+
visibleLabel,
|
|
392
|
+
accessibleName: accName,
|
|
393
|
+
normalized: { visibleLabel: visibleNorm, accessibleName: accNorm },
|
|
394
|
+
tokenized: { visibleLabel: labelTokens, accessibleName: nameTokens },
|
|
395
|
+
labelSource: labelInfo && labelInfo.source ? labelInfo.source : 'none',
|
|
396
|
+
nameMechanism: acc && acc.mechanism ? acc.mechanism : 'none'
|
|
397
|
+
}
|
|
392
398
|
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
details: {
|
|
396
|
-
reasonCode: uncertainty || 'VISIBLE_LABEL_NOT_IN_ACCESSIBLE_NAME',
|
|
397
|
-
visibleLabel,
|
|
398
|
-
accessibleName: accName,
|
|
399
|
-
normalized: { visibleLabel: visibleNorm, accessibleName: accNorm },
|
|
400
|
-
tokenized: { visibleLabel: labelTokens, accessibleName: nameTokens },
|
|
401
|
-
labelSource: labelInfo && labelInfo.source ? labelInfo.source : 'none',
|
|
402
|
-
nameMechanism: acc && acc.mechanism ? acc.mechanism : 'none'
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
});
|
|
399
|
+
})
|
|
400
|
+
);
|
|
406
401
|
}
|
|
407
402
|
}
|
|
408
403
|
|
|
@@ -215,31 +215,29 @@ function runInPage(ctx) {
|
|
|
215
215
|
const eligInfo = helpers.getEligibilityInfo
|
|
216
216
|
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
217
217
|
: null;
|
|
218
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
219
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
220
218
|
const tag = (el.tagName || '').toLowerCase();
|
|
221
219
|
const ratioStr = c.round2 ? c.round2(ratio) : String(ratio);
|
|
222
220
|
|
|
223
|
-
occurrences.push(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
'
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
221
|
+
occurrences.push(
|
|
222
|
+
helpers.reportOccurrence(el, {
|
|
223
|
+
summary:
|
|
224
|
+
'This link in a block of text relies on color alone to be distinguished from the surrounding text.',
|
|
225
|
+
hint: 'Add an underline, a font-weight/style difference, or increase the color contrast between the link and surrounding text to at least 3:1.',
|
|
226
|
+
i18n: {
|
|
227
|
+
summaryKey: 'linkInTextBlock_summary_fail',
|
|
228
|
+
hintKey: 'linkInTextBlock_hint_fail',
|
|
229
|
+
params: { element: tag, ratio: String(ratioStr), threshold: '3' }
|
|
230
|
+
},
|
|
231
|
+
data: {
|
|
232
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
233
|
+
details: {
|
|
234
|
+
reasonCode: 'COLOR_ONLY_DIFFERENTIATION',
|
|
235
|
+
metrics: { ratio, threshold: 3 },
|
|
236
|
+
colors: { linkForegroundHex: fgLinkHex, surroundingTextForegroundHex: fgParentHex }
|
|
237
|
+
}
|
|
240
238
|
}
|
|
241
|
-
}
|
|
242
|
-
|
|
239
|
+
})
|
|
240
|
+
);
|
|
243
241
|
}
|
|
244
242
|
|
|
245
243
|
if (applicableCount === 0) {
|
|
@@ -139,39 +139,34 @@ function runInPage(ctx) {
|
|
|
139
139
|
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
140
140
|
: null;
|
|
141
141
|
|
|
142
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
143
|
-
const html = helpers.getOuterHtmlSnippet
|
|
144
|
-
? helpers.getOuterHtmlSnippet(el)
|
|
145
|
-
: el.outerHTML || '';
|
|
146
142
|
const tag = (el.tagName || '').toLowerCase();
|
|
147
143
|
|
|
148
|
-
occurrences.push(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
refs: { accessibleName: nameInfo || null }
|
|
144
|
+
occurrences.push(
|
|
145
|
+
helpers.reportOccurrence(el, {
|
|
146
|
+
// Human fallbacks (allowed)
|
|
147
|
+
summary: 'This link has no accessible name.',
|
|
148
|
+
hint: 'Provide link text or an accessible-name mechanism (for example aria-label) so assistive technologies can identify the link.',
|
|
149
|
+
|
|
150
|
+
// Validator requires these keys to exist in the English dictionary
|
|
151
|
+
i18n: {
|
|
152
|
+
summaryKey: 'linkNamePresent_summary_fail',
|
|
153
|
+
hintKey: 'linkNamePresent_hint_fail',
|
|
154
|
+
params: { element: tag }
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
data: {
|
|
158
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
159
|
+
details: {
|
|
160
|
+
reasonCode: 'name_missing',
|
|
161
|
+
metrics: {
|
|
162
|
+
programmaticNameLength: programmaticName.trim().length,
|
|
163
|
+
contentNameLength: contentName.trim().length
|
|
164
|
+
},
|
|
165
|
+
refs: { accessibleName: nameInfo || null }
|
|
166
|
+
}
|
|
172
167
|
}
|
|
173
|
-
}
|
|
174
|
-
|
|
168
|
+
})
|
|
169
|
+
);
|
|
175
170
|
}
|
|
176
171
|
}
|
|
177
172
|
|
|
@@ -122,23 +122,21 @@ function runInPage(ctx) {
|
|
|
122
122
|
const dedupedInvalidTags = [...new Set(invalidTags)];
|
|
123
123
|
|
|
124
124
|
const tag = el.tagName.toLowerCase();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
});
|
|
125
|
+
|
|
126
|
+
occurrences.push(
|
|
127
|
+
helpers.reportOccurrence(el, {
|
|
128
|
+
summary: 'This list contains a direct child that is not a list item.',
|
|
129
|
+
hint: 'Only use <li> (or <script>/<template>) as direct children of <ul>/<ol>; move other markup inside an <li>.',
|
|
130
|
+
i18n: {
|
|
131
|
+
summaryKey: 'listChildrenValid_summary_fail',
|
|
132
|
+
hintKey: 'listChildrenValid_hint_fail',
|
|
133
|
+
params: { element: tag, invalidChildren: dedupedInvalidTags.join(', ') }
|
|
134
|
+
},
|
|
135
|
+
data: {
|
|
136
|
+
details: { reasonCode: 'LIST_INVALID_CHILD', element: tag, invalidChildren: invalidTags }
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
);
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
if (applicableCount === 0) {
|
|
@@ -227,24 +227,22 @@ function runInPage(ctx) {
|
|
|
227
227
|
}
|
|
228
228
|
})()
|
|
229
229
|
: null;
|
|
230
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
231
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
232
230
|
|
|
233
|
-
occurrences.push(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
231
|
+
occurrences.push(
|
|
232
|
+
helpers.reportOccurrence(el, {
|
|
233
|
+
summary: 'This element has no accessible name.',
|
|
234
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this listbox's accessible name.",
|
|
235
|
+
i18n: {
|
|
236
|
+
summaryKey: 'listboxNamePresent_summary_fail',
|
|
237
|
+
hintKey: 'listboxNamePresent_hint_fail',
|
|
238
|
+
params: { controlType: 'listbox' }
|
|
239
|
+
},
|
|
240
|
+
data: {
|
|
241
|
+
details: { reasonCode: 'name_missing', controlType: 'listbox', methodTried: res.method },
|
|
242
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
);
|
|
248
246
|
}
|
|
249
247
|
|
|
250
248
|
if (applicableCount === 0) {
|
|
@@ -116,23 +116,20 @@ function runInPage(ctx) {
|
|
|
116
116
|
|
|
117
117
|
if (valid) continue;
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
details: { reasonCode: 'LISTITEM_INVALID_PARENT', parentElement: parentTag }
|
|
134
|
-
}
|
|
135
|
-
});
|
|
119
|
+
occurrences.push(
|
|
120
|
+
helpers.reportOccurrence(el, {
|
|
121
|
+
summary: 'This list item is not contained by a list container.',
|
|
122
|
+
hint: 'Place this <li> inside a <ul>/<ol>, or give its parent role="list".',
|
|
123
|
+
i18n: {
|
|
124
|
+
summaryKey: 'listitemParentValid_summary_fail',
|
|
125
|
+
hintKey: 'listitemParentValid_hint_fail',
|
|
126
|
+
params: { parentElement: parentTag }
|
|
127
|
+
},
|
|
128
|
+
data: {
|
|
129
|
+
details: { reasonCode: 'LISTITEM_INVALID_PARENT', parentElement: parentTag }
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
);
|
|
136
133
|
}
|
|
137
134
|
|
|
138
135
|
if (applicableCount === 0) {
|