@surea11y/core 1.4.0 → 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 +88 -7
- package/README.md +19 -3
- package/bin/surea11y-core.js +0 -0
- package/docs/API_STABILITY.md +2 -2
- package/docs/ARIA_DEPRECATION.md +95 -0
- 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/RULE_CATALOG.md +6 -6
- package/docs/TROUBLESHOOTING.md +2 -2
- package/package.json +8 -1
- package/src/checks/automatic/aria-allowed-attr.js +661 -99
- 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 +122 -41
- 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 +66 -27
- 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 +52 -18
- package/src/checks/automatic/avoid-inline-spacing.js +192 -34
- package/src/checks/automatic/binary-control-name-present.js +30 -24
- package/src/checks/automatic/button-name-present.js +73 -45
- package/src/checks/automatic/canvas-text-alternative-present.js +15 -8
- package/src/checks/automatic/combobox-name-present.js +23 -18
- 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 +24 -19
- 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-programmatic-label-present.js +48 -4
- package/src/checks/automatic/form-control-single-label.js +109 -43
- 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/img-alt-present.js +16 -9
- package/src/checks/automatic/input-image-alt-present.js +99 -48
- package/src/checks/automatic/label-in-name.js +102 -33
- package/src/checks/automatic/language-page-present.js +5 -1
- package/src/checks/automatic/link-in-text-block.js +19 -21
- package/src/checks/automatic/link-name-present.js +75 -47
- package/src/checks/automatic/list-children-valid.js +15 -17
- package/src/checks/automatic/listbox-name-present.js +23 -18
- package/src/checks/automatic/listitem-parent-valid.js +14 -17
- package/src/checks/automatic/menuitem-name-present.js +24 -19
- package/src/checks/automatic/meta-refresh-no-exceptions.js +44 -18
- package/src/checks/automatic/meta-refresh-timing-absent.js +44 -21
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +51 -32
- package/src/checks/automatic/meter-name-present.js +24 -19
- package/src/checks/automatic/nested-interactive-controls-absent.js +179 -51
- package/src/checks/automatic/object-text-alternative-present.js +14 -7
- package/src/checks/automatic/option-name-present.js +24 -19
- package/src/checks/automatic/progressbar-name-present.js +27 -22
- package/src/checks/automatic/searchbox-name-present.js +27 -18
- package/src/checks/automatic/server-side-image-map-absent.js +15 -18
- package/src/checks/automatic/slider-name-present.js +26 -19
- package/src/checks/automatic/spinbutton-name-present.js +27 -18
- package/src/checks/automatic/summary-name-present.js +24 -19
- package/src/checks/automatic/tab-name-present.js +24 -19
- package/src/checks/automatic/table-headers-attr-valid.js +15 -17
- package/src/checks/automatic/table-th-has-data-cells.js +82 -25
- 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 +23 -18
- package/src/checks/automatic/tooltip-name-present.js +24 -19
- package/src/checks/automatic/treeitem-name-present.js +24 -19
- package/src/checks/automatic/valid-lang.js +31 -20
- package/src/checks/manual/accesskeys-manual.js +18 -19
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +19 -22
- package/src/checks/{automatic/bypass-blocks-present.js → manual/bypass-blocks-present-manual.js} +98 -44
- 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/input-image-alt-decorative-manual.js +24 -0
- package/src/checks/manual/label-title-only-manual.js +15 -17
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +25 -39
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +21 -27
- 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 +8818 -4330
- package/src/report.js +14 -0
- package/src/sarif.js +2 -2
- package/surea11y.browser.js +4023 -3911
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
|
@@ -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
|
}
|
|
@@ -109,9 +109,16 @@ function runInPage(ctx) {
|
|
|
109
109
|
return '';
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
113
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
114
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
112
115
|
function isEligibleAcc(helpers, el, ctx) {
|
|
113
116
|
const fn =
|
|
114
|
-
helpers && typeof helpers.
|
|
117
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
118
|
+
? helpers.isIncludedInAccessibilityTree
|
|
119
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
120
|
+
? helpers.isAccTreeEligible
|
|
121
|
+
: null;
|
|
115
122
|
if (!fn) return true;
|
|
116
123
|
try {
|
|
117
124
|
const r = fn(el, ctx);
|
|
@@ -220,24 +227,22 @@ function runInPage(ctx) {
|
|
|
220
227
|
}
|
|
221
228
|
})()
|
|
222
229
|
: null;
|
|
223
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
224
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
225
230
|
|
|
226
|
-
occurrences.push(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
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
|
+
);
|
|
241
246
|
}
|
|
242
247
|
|
|
243
248
|
if (applicableCount === 0) {
|
|
@@ -83,9 +83,16 @@ function runInPage(ctx) {
|
|
|
83
83
|
return '';
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
87
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
88
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
86
89
|
function isEligibleAcc(helpers, el, ctx) {
|
|
87
90
|
const fn =
|
|
88
|
-
helpers && typeof helpers.
|
|
91
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
92
|
+
? helpers.isIncludedInAccessibilityTree
|
|
93
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
94
|
+
? helpers.isAccTreeEligible
|
|
95
|
+
: null;
|
|
89
96
|
if (!fn) return true;
|
|
90
97
|
try {
|
|
91
98
|
const r = fn(el, ctx);
|
|
@@ -141,24 +148,22 @@ function runInPage(ctx) {
|
|
|
141
148
|
}
|
|
142
149
|
})()
|
|
143
150
|
: null;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
});
|
|
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
|
+
);
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
if (applicableCount === 0) {
|
|
@@ -83,9 +83,16 @@ function runInPage(ctx) {
|
|
|
83
83
|
return '';
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
87
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
88
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
86
89
|
function isEligibleAcc(helpers, el, ctx) {
|
|
87
90
|
const fn =
|
|
88
|
-
helpers && typeof helpers.
|
|
91
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
92
|
+
? helpers.isIncludedInAccessibilityTree
|
|
93
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
94
|
+
? helpers.isAccTreeEligible
|
|
95
|
+
: null;
|
|
89
96
|
if (!fn) return true;
|
|
90
97
|
try {
|
|
91
98
|
const r = fn(el, ctx);
|
|
@@ -138,24 +145,22 @@ function runInPage(ctx) {
|
|
|
138
145
|
}
|
|
139
146
|
})()
|
|
140
147
|
: null;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
});
|
|
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
|
+
);
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
if (applicableCount === 0) {
|
|
@@ -58,7 +58,13 @@ const meta = {
|
|
|
58
58
|
function runInPage(ctx) {
|
|
59
59
|
const { helpers, rule } = ctx;
|
|
60
60
|
|
|
61
|
+
// Shape alone accepts unregistered tags such as "eng" and "em-US", so the
|
|
62
|
+
// primary subtag is checked against the IANA registry via the shared helper.
|
|
61
63
|
const BCP47_RE = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
|
|
64
|
+
const isValidTag =
|
|
65
|
+
typeof helpers.isValidLanguageTag === 'function'
|
|
66
|
+
? helpers.isValidLanguageTag
|
|
67
|
+
: (v) => BCP47_RE.test(String(v || ''));
|
|
62
68
|
|
|
63
69
|
const nodes = helpers.queryAllSmart
|
|
64
70
|
? helpers.queryAllSmart('[lang]')
|
|
@@ -71,31 +77,36 @@ function runInPage(ctx) {
|
|
|
71
77
|
if (!el || !el.getAttribute) continue;
|
|
72
78
|
if (el.tagName && el.tagName.toLowerCase() === 'html') continue;
|
|
73
79
|
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
80
|
+
const rawAttr = el.getAttribute('lang');
|
|
81
|
+
if (rawAttr === null || rawAttr === '') continue; // ACT de46e4: empty is out of scope
|
|
82
|
+
|
|
83
|
+
// The rule applies only where text actually inherits the language, so an
|
|
84
|
+
// element with no text, or whose text reaches no one, has nothing to
|
|
85
|
+
// declare a language for.
|
|
86
|
+
if (!String(el.textContent || '').trim()) continue;
|
|
76
87
|
|
|
77
88
|
applicableCount += 1;
|
|
78
89
|
|
|
79
|
-
|
|
90
|
+
// A whitespace-only value is in scope and has no primary language tag.
|
|
91
|
+
const raw = String(rawAttr).trim();
|
|
92
|
+
if (isValidTag(raw)) continue;
|
|
80
93
|
|
|
81
94
|
const tag = el.tagName.toLowerCase();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
});
|
|
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
|
+
);
|
|
99
110
|
}
|
|
100
111
|
|
|
101
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) {
|