@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
|
@@ -156,25 +156,21 @@ function runInPage(ctx) {
|
|
|
156
156
|
|
|
157
157
|
for (const th of headers) {
|
|
158
158
|
if (!th) continue;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
data: {
|
|
175
|
-
details: { reasonCode: 'TABLE_TH_NO_DATA_CELLS' }
|
|
176
|
-
}
|
|
177
|
-
});
|
|
159
|
+
|
|
160
|
+
occurrences.push(
|
|
161
|
+
helpers.reportOccurrence(th, {
|
|
162
|
+
summary: 'This table has header cells but no data cells for them to describe.',
|
|
163
|
+
hint: 'Add data cells (<td>) to the table, or remove the header cells if the table has no data.',
|
|
164
|
+
i18n: {
|
|
165
|
+
summaryKey: 'tableThHasDataCells_summary_fail',
|
|
166
|
+
hintKey: 'tableThHasDataCells_hint_fail',
|
|
167
|
+
params: {}
|
|
168
|
+
},
|
|
169
|
+
data: {
|
|
170
|
+
details: { reasonCode: 'TABLE_TH_NO_DATA_CELLS' }
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
);
|
|
178
174
|
}
|
|
179
175
|
}
|
|
180
176
|
|
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
* its own wrapping link/button) is one visual region, not two independent
|
|
29
29
|
* targets; that pattern is nested-interactive-controls-absent's
|
|
30
30
|
* concern, not a spacing one.
|
|
31
|
-
* - Inline: a link inside a
|
|
31
|
+
* - Inline: a link inside a text-block container passes outright
|
|
32
|
+
* (isInlineTextExceptionTarget). An inline link whose only spacing conflict
|
|
33
|
+
* is another inline link in the same run is reported as cantTell
|
|
34
|
+
* (isInlineLinkTarget) — the inline exception may cover it, but geometry
|
|
35
|
+
* can't confirm that.
|
|
32
36
|
* - User Agent Control: an unstyled native checkbox/radio, detected via
|
|
33
37
|
* `appearance` not being reset to `none` (see isUserAgentSizedControl) —
|
|
34
38
|
* scoped narrowly to checkbox/radio specifically, not every form control,
|
|
@@ -147,23 +151,10 @@ function runInPage(ctx) {
|
|
|
147
151
|
return 'html';
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
} catch {}
|
|
155
|
-
try {
|
|
156
|
-
return el && el.outerHTML ? String(el.outerHTML) : '';
|
|
157
|
-
} catch {}
|
|
158
|
-
return '';
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function isInlineTextExceptionTarget(el) {
|
|
162
|
-
// SC 2.5.8 exception: target is in a sentence/block of text (inline)
|
|
163
|
-
// Deterministic heuristic:
|
|
164
|
-
// - must be a link-like target
|
|
165
|
-
// - must be rendered as inline/inline-*
|
|
166
|
-
// - must be inside a typical text container
|
|
154
|
+
// A link-like target (a[href] or role="link") rendered inline/inline-* and
|
|
155
|
+
// carrying visible text. This is the shape the SC 2.5.8 inline exception is
|
|
156
|
+
// about, without the surrounding-container requirement.
|
|
157
|
+
function isInlineLinkTarget(el) {
|
|
167
158
|
try {
|
|
168
159
|
if (!el || el.nodeType !== 1) return false;
|
|
169
160
|
|
|
@@ -180,27 +171,31 @@ function runInPage(ctx) {
|
|
|
180
171
|
|
|
181
172
|
const cs = getStyle(el);
|
|
182
173
|
const display = cs && cs.display ? String(cs.display) : '';
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// Inline exception is for inline text runs; many design systems use inline-block for links,
|
|
186
|
-
// so treat inline-block variants as eligible for this exception.
|
|
174
|
+
// Many design systems use inline-block for links, so treat the
|
|
175
|
+
// inline-block variants as inline here too.
|
|
187
176
|
const isInline =
|
|
188
177
|
display === 'inline' ||
|
|
189
178
|
display === 'inline-block' ||
|
|
190
179
|
display === 'inline-flex' ||
|
|
191
180
|
display === 'inline-grid' ||
|
|
192
181
|
display === 'inline-table';
|
|
193
|
-
|
|
194
182
|
if (!isInline) return false;
|
|
195
183
|
|
|
196
|
-
|
|
197
|
-
|
|
184
|
+
return (el.textContent || '').trim().length > 0;
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isInlineTextExceptionTarget(el) {
|
|
191
|
+
// SC 2.5.8 exception: an inline link that sits inside a run of text.
|
|
192
|
+
try {
|
|
193
|
+
if (!isInlineLinkTarget(el)) return false;
|
|
198
194
|
|
|
199
|
-
// Require that it sits in a "text block" context
|
|
200
195
|
const textContainer = closest(el, 'p, li, dd, dt, blockquote, figcaption, caption, td, th');
|
|
201
196
|
if (textContainer) return true;
|
|
202
197
|
|
|
203
|
-
//
|
|
198
|
+
// Links are often wrapped in an inline element inside the text block.
|
|
204
199
|
const inlineContainer = closest(el, 'span, em, strong, small, label');
|
|
205
200
|
if (inlineContainer) {
|
|
206
201
|
const outerTextBlock = closest(
|
|
@@ -590,27 +585,27 @@ function runInPage(ctx) {
|
|
|
590
585
|
// the result once any other target on the page had a confident
|
|
591
586
|
// fail (see helpers.resolveTieredOutcome's header comment). Now
|
|
592
587
|
// reported as its own cantTell-tier occurrence instead.
|
|
593
|
-
cantTellOccurrences.push(
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
'
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
588
|
+
cantTellOccurrences.push(
|
|
589
|
+
helpers.reportOccurrence(it.el, {
|
|
590
|
+
occurrenceOutcome: 'cantTell',
|
|
591
|
+
summary:
|
|
592
|
+
'Target may be too small and too close to another target, but the overlap is near the detection threshold and could not be confidently measured.',
|
|
593
|
+
hint: 'Manually verify the effective spacing between this target and its neighbor; increase target size or spacing if the overlap is real.',
|
|
594
|
+
i18n: {
|
|
595
|
+
summaryKey: 'targetSizeMinimum_summary_cantTell_ambiguousSpacing',
|
|
596
|
+
hintKey: 'targetSizeMinimum_hint_cantTell_ambiguousSpacing',
|
|
597
|
+
params: {}
|
|
598
|
+
},
|
|
599
|
+
data: {
|
|
600
|
+
details: {
|
|
601
|
+
measured: { width: it.rect.width, height: it.rect.height },
|
|
602
|
+
reasonCode: 'undersized-ambiguous-spacing',
|
|
603
|
+
conflictHitCount: info.hitCount,
|
|
604
|
+
conflictWith: info.conflictEl ? buildSelector(info.conflictEl) : null
|
|
605
|
+
}
|
|
611
606
|
}
|
|
612
|
-
}
|
|
613
|
-
|
|
607
|
+
})
|
|
608
|
+
);
|
|
614
609
|
continue;
|
|
615
610
|
}
|
|
616
611
|
|
|
@@ -619,50 +614,78 @@ function runInPage(ctx) {
|
|
|
619
614
|
// Confident spacing conflict, but the target may be exempt as part
|
|
620
615
|
// of an essential graphic/image-map region — same "previously
|
|
621
616
|
// unrecoverable" gap as above, now reported instead of dropped.
|
|
622
|
-
cantTellOccurrences.push(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
'
|
|
628
|
-
|
|
617
|
+
cantTellOccurrences.push(
|
|
618
|
+
helpers.reportOccurrence(it.el, {
|
|
619
|
+
occurrenceOutcome: 'cantTell',
|
|
620
|
+
summary:
|
|
621
|
+
'Target is too small and too close to another target, but may be exempt as part of an essential graphic or image-map region.',
|
|
622
|
+
hint: 'Verify whether this target’s size is genuinely essential to its function (e.g. part of an SVG/canvas/image map); if not, increase target size or spacing.',
|
|
623
|
+
i18n: {
|
|
624
|
+
summaryKey: 'targetSizeMinimum_summary_cantTell_plausiblyEssential',
|
|
625
|
+
hintKey: 'targetSizeMinimum_hint_cantTell_plausiblyEssential',
|
|
626
|
+
params: {}
|
|
627
|
+
},
|
|
628
|
+
data: {
|
|
629
|
+
details: {
|
|
630
|
+
measured: { width: it.rect.width, height: it.rect.height },
|
|
631
|
+
reasonCode: 'undersized-plausibly-essential',
|
|
632
|
+
conflictHitCount: info.hitCount,
|
|
633
|
+
conflictWith: info.conflictEl ? buildSelector(info.conflictEl) : null
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
})
|
|
637
|
+
);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// This target and its conflicting neighbor are both inline links in the
|
|
642
|
+
// same text run, where the SC 2.5.8 inline exception may apply. That
|
|
643
|
+
// can't be decided from geometry alone, so defer to manual review.
|
|
644
|
+
if (isInlineLinkTarget(it.el) && info.conflictEl && isInlineLinkTarget(info.conflictEl)) {
|
|
645
|
+
cantTellOccurrences.push(
|
|
646
|
+
helpers.reportOccurrence(it.el, {
|
|
647
|
+
occurrenceOutcome: 'cantTell',
|
|
648
|
+
summary:
|
|
649
|
+
'Target is smaller than 24×24 CSS px and close to another inline link in the same run of text, where the inline exception may apply.',
|
|
650
|
+
hint: 'Confirm whether these links form a run of inline text (which is exempt); otherwise increase the target size to at least 24×24 CSS px or add spacing.',
|
|
651
|
+
i18n: {
|
|
652
|
+
summaryKey: 'targetSizeMinimum_summary_cantTell_inlineLinkRun',
|
|
653
|
+
hintKey: 'targetSizeMinimum_hint_cantTell_inlineLinkRun',
|
|
654
|
+
params: {}
|
|
655
|
+
},
|
|
656
|
+
data: {
|
|
657
|
+
details: {
|
|
658
|
+
measured: { width: it.rect.width, height: it.rect.height },
|
|
659
|
+
reasonCode: 'undersized-inline-link-run',
|
|
660
|
+
conflictHitCount: info.hitCount,
|
|
661
|
+
conflictWith: info.conflictEl ? buildSelector(info.conflictEl) : null
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
})
|
|
665
|
+
);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
failOccurrences.push(
|
|
670
|
+
helpers.reportOccurrence(it.el, {
|
|
671
|
+
occurrenceOutcome: 'fail',
|
|
672
|
+
summary: 'Target is too small and too close to another target.',
|
|
673
|
+
hint: 'Increase target size to at least 24 by 24 CSS pixels, or add sufficient spacing.',
|
|
629
674
|
i18n: {
|
|
630
|
-
summaryKey: '
|
|
631
|
-
hintKey: '
|
|
675
|
+
summaryKey: 'targetSizeMinimum_summary_fail',
|
|
676
|
+
hintKey: 'targetSizeMinimum_hint_fail',
|
|
632
677
|
params: {}
|
|
633
678
|
},
|
|
634
679
|
data: {
|
|
635
680
|
details: {
|
|
636
681
|
measured: { width: it.rect.width, height: it.rect.height },
|
|
637
|
-
reasonCode: 'undersized-
|
|
682
|
+
reasonCode: 'undersized-and-too-close',
|
|
638
683
|
conflictHitCount: info.hitCount,
|
|
639
684
|
conflictWith: info.conflictEl ? buildSelector(info.conflictEl) : null
|
|
640
685
|
}
|
|
641
686
|
}
|
|
642
|
-
})
|
|
643
|
-
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
failOccurrences.push({
|
|
647
|
-
selector: buildSelector(it.el),
|
|
648
|
-
html: htmlSnippet(it.el),
|
|
649
|
-
occurrenceOutcome: 'fail',
|
|
650
|
-
summary: 'Target is too small and too close to another target.',
|
|
651
|
-
hint: 'Increase target size to at least 24 by 24 CSS pixels, or add sufficient spacing.',
|
|
652
|
-
i18n: {
|
|
653
|
-
summaryKey: 'targetSizeMinimum_summary_fail',
|
|
654
|
-
hintKey: 'targetSizeMinimum_hint_fail',
|
|
655
|
-
params: {}
|
|
656
|
-
},
|
|
657
|
-
data: {
|
|
658
|
-
details: {
|
|
659
|
-
measured: { width: it.rect.width, height: it.rect.height },
|
|
660
|
-
reasonCode: 'undersized-and-too-close',
|
|
661
|
-
conflictHitCount: info.hitCount,
|
|
662
|
-
conflictWith: info.conflictEl ? buildSelector(info.conflictEl) : null
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
});
|
|
687
|
+
})
|
|
688
|
+
);
|
|
666
689
|
}
|
|
667
690
|
}
|
|
668
691
|
|
|
@@ -150,26 +150,21 @@ function runInPage(ctx) {
|
|
|
150
150
|
if (hasColumnHeaderAbove(r, c)) continue;
|
|
151
151
|
if (hasRowHeaderBefore(r, c)) continue;
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
},
|
|
169
|
-
data: {
|
|
170
|
-
details: { reasonCode: 'TD_NO_ASSOCIATED_HEADER', row: r, column: c }
|
|
171
|
-
}
|
|
172
|
-
});
|
|
153
|
+
occurrences.push(
|
|
154
|
+
helpers.reportOccurrence(cell, {
|
|
155
|
+
summary:
|
|
156
|
+
'This data cell has no associated header (no headers attribute, no column <th> above it, no row <th> to its left).',
|
|
157
|
+
hint: 'Add a headers attribute referencing the relevant <th> id(s), or restructure the table so this cell has an implicit row/column header.',
|
|
158
|
+
i18n: {
|
|
159
|
+
summaryKey: 'tdHasHeader_summary_fail',
|
|
160
|
+
hintKey: 'tdHasHeader_hint_fail',
|
|
161
|
+
params: { row: String(r), column: String(c) }
|
|
162
|
+
},
|
|
163
|
+
data: {
|
|
164
|
+
details: { reasonCode: 'TD_NO_ASSOCIATED_HEADER', row: r, column: c }
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
);
|
|
173
168
|
}
|
|
174
169
|
}
|
|
175
170
|
}
|
|
@@ -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 textbox's accessible name.",
|
|
235
|
+
i18n: {
|
|
236
|
+
summaryKey: 'textboxNamePresent_summary_fail',
|
|
237
|
+
hintKey: 'textboxNamePresent_hint_fail',
|
|
238
|
+
params: { controlType: 'textbox' }
|
|
239
|
+
},
|
|
240
|
+
data: {
|
|
241
|
+
details: { reasonCode: 'name_missing', controlType: 'textbox', methodTried: res.method },
|
|
242
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
);
|
|
248
246
|
}
|
|
249
247
|
|
|
250
248
|
if (applicableCount === 0) {
|
|
@@ -148,24 +148,22 @@ function runInPage(ctx) {
|
|
|
148
148
|
}
|
|
149
149
|
})()
|
|
150
150
|
: null;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
});
|
|
151
|
+
|
|
152
|
+
occurrences.push(
|
|
153
|
+
helpers.reportOccurrence(el, {
|
|
154
|
+
summary: 'This tooltip has no accessible name.',
|
|
155
|
+
hint: 'Provide tooltip text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
|
|
156
|
+
i18n: {
|
|
157
|
+
summaryKey: 'tooltipNamePresent_summary_fail',
|
|
158
|
+
hintKey: 'tooltipNamePresent_hint_fail',
|
|
159
|
+
params: {}
|
|
160
|
+
},
|
|
161
|
+
data: {
|
|
162
|
+
details: { reasonCode: 'name_missing', controlType: 'tooltip', methodTried: res.method },
|
|
163
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
);
|
|
169
167
|
}
|
|
170
168
|
|
|
171
169
|
if (applicableCount === 0) {
|
|
@@ -145,24 +145,22 @@ function runInPage(ctx) {
|
|
|
145
145
|
}
|
|
146
146
|
})()
|
|
147
147
|
: null;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
});
|
|
148
|
+
|
|
149
|
+
occurrences.push(
|
|
150
|
+
helpers.reportOccurrence(el, {
|
|
151
|
+
summary: 'This element has no accessible name.',
|
|
152
|
+
hint: 'Provide aria-label or aria-labelledby (preferred), or provide visible text that is not hidden from assistive technologies.',
|
|
153
|
+
i18n: {
|
|
154
|
+
summaryKey: 'treeitemNamePresent_summary_fail',
|
|
155
|
+
hintKey: 'treeitemNamePresent_hint_fail',
|
|
156
|
+
params: { controlType: 'treeitem' }
|
|
157
|
+
},
|
|
158
|
+
data: {
|
|
159
|
+
details: { reasonCode: 'name_missing', controlType: 'treeitem', methodTried: res.method },
|
|
160
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
);
|
|
166
164
|
}
|
|
167
165
|
|
|
168
166
|
if (applicableCount === 0) {
|
|
@@ -92,23 +92,21 @@ function runInPage(ctx) {
|
|
|
92
92
|
if (isValidTag(raw)) continue;
|
|
93
93
|
|
|
94
94
|
const tag = el.tagName.toLowerCase();
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
});
|
|
95
|
+
|
|
96
|
+
occurrences.push(
|
|
97
|
+
helpers.reportOccurrence(el, {
|
|
98
|
+
summary: `This lang attribute value ("${raw}") is not a syntactically valid language tag.`,
|
|
99
|
+
hint: 'Use a valid BCP47 language tag (e.g. "fr", "es-MX").',
|
|
100
|
+
i18n: {
|
|
101
|
+
summaryKey: 'validLang_summary_fail',
|
|
102
|
+
hintKey: 'validLang_hint_fail',
|
|
103
|
+
params: { element: tag, value: raw }
|
|
104
|
+
},
|
|
105
|
+
data: {
|
|
106
|
+
details: { reasonCode: 'ELEMENT_LANG_INVALID', element: tag, value: raw }
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
);
|
|
112
110
|
}
|
|
113
111
|
|
|
114
112
|
if (applicableCount === 0) {
|
|
@@ -62,25 +62,24 @@ function runInPage(ctx) {
|
|
|
62
62
|
for (const [key, els] of groups) {
|
|
63
63
|
if (els.length <= 1) continue;
|
|
64
64
|
for (const el of els) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
});
|
|
65
|
+
occurrences.push(
|
|
66
|
+
helpers.reportOccurrence(el, {
|
|
67
|
+
summary: "This element's accesskey is shared with another element on the page.",
|
|
68
|
+
hint: 'Make each accesskey value unique across the page.',
|
|
69
|
+
i18n: {
|
|
70
|
+
summaryKey: 'accesskeys_summary_cantTell',
|
|
71
|
+
hintKey: 'accesskeys_hint_cantTell',
|
|
72
|
+
params: { accesskey: key, duplicateCount: String(els.length) }
|
|
73
|
+
},
|
|
74
|
+
data: {
|
|
75
|
+
details: {
|
|
76
|
+
reasonCode: 'ACCESSKEY_DUPLICATE',
|
|
77
|
+
accesskey: key,
|
|
78
|
+
duplicateCount: els.length
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
);
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
|
|
@@ -114,29 +114,26 @@ function runInPage(ctx) {
|
|
|
114
114
|
|
|
115
115
|
if (normalizedAriaChecked === actualState) continue;
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
ariaChecked: normalizedAriaChecked,
|
|
135
|
-
actualState,
|
|
136
|
-
type
|
|
117
|
+
occurrences.push(
|
|
118
|
+
helpers.reportOccurrence(el, {
|
|
119
|
+
summary:
|
|
120
|
+
'This element’s aria-checked value does not match its actual checked/indeterminate state.',
|
|
121
|
+
hint: 'Set aria-checked to match the element’s real state, or remove it — a native checkbox/radio already exposes this state without it.',
|
|
122
|
+
i18n: {
|
|
123
|
+
summaryKey: 'ariaCheckedStateMismatch_summary_cantTell',
|
|
124
|
+
hintKey: 'ariaCheckedStateMismatch_hint_cantTell',
|
|
125
|
+
params: { ariaChecked: normalizedAriaChecked, actualState, type }
|
|
126
|
+
},
|
|
127
|
+
data: {
|
|
128
|
+
details: {
|
|
129
|
+
reasonCode: 'ARIA_CHECKED_STATE_MISMATCH',
|
|
130
|
+
ariaChecked: normalizedAriaChecked,
|
|
131
|
+
actualState,
|
|
132
|
+
type
|
|
133
|
+
}
|
|
137
134
|
}
|
|
138
|
-
}
|
|
139
|
-
|
|
135
|
+
})
|
|
136
|
+
);
|
|
140
137
|
}
|
|
141
138
|
|
|
142
139
|
if (applicableCount === 0) {
|
|
@@ -51,9 +51,8 @@
|
|
|
51
51
|
* unmounted SPA view).
|
|
52
52
|
* Both cases would produce false positives under a hard `fail`, which this
|
|
53
53
|
* engine reserves for high-confidence violations; `cantTell` routes them to
|
|
54
|
-
* human review instead.
|
|
55
|
-
*
|
|
56
|
-
* failing it), and why no ACT rule hard-fails 2.4.1 by presence alone.
|
|
54
|
+
* human review instead. No ACT rule hard-fails 2.4.1 by presence alone,
|
|
55
|
+
* for the same reason.
|
|
57
56
|
* - This rule intentionally checks presence, not position, for the
|
|
58
57
|
* same-page-anchor condition (b): a full bypass algorithm is heuristic
|
|
59
58
|
* (see ROADMAP.md's Tier 1a note on why this rule was
|
|
@@ -244,15 +243,8 @@ function runInPage(ctx) {
|
|
|
244
243
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
245
244
|
}
|
|
246
245
|
|
|
247
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
|
|
248
|
-
const html = helpers.getOuterHtmlSnippet
|
|
249
|
-
? helpers.getOuterHtmlSnippet(body)
|
|
250
|
-
: (body.outerHTML || '').slice(0, 200);
|
|
251
|
-
|
|
252
246
|
const occurrences = [
|
|
253
|
-
{
|
|
254
|
-
selector: stableSelector,
|
|
255
|
-
html,
|
|
247
|
+
helpers.reportOccurrence(body, {
|
|
256
248
|
summary:
|
|
257
249
|
'No recognized way to bypass repeated blocks of content was detected on this page — verify a bypass mechanism exists.',
|
|
258
250
|
hint: 'Confirm the page offers a bypass mechanism: a main landmark (<main> or role="main"), a working "skip to content" link, or heading elements that assistive technology can use to jump past repeated content. (A mechanism may be temporarily hidden — e.g. while a modal dialog makes the page inert — or provided on a per-site basis; this needs human confirmation.)',
|
|
@@ -265,7 +257,7 @@ function runInPage(ctx) {
|
|
|
265
257
|
details: { reasonCode: 'BYPASS_MECHANISM_ABSENT' },
|
|
266
258
|
visibilityFilter: { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
267
259
|
}
|
|
268
|
-
}
|
|
260
|
+
})
|
|
269
261
|
];
|
|
270
262
|
|
|
271
263
|
return {
|