@surea11y/core 1.5.0 → 1.6.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 +193 -149
- package/README.md +27 -6
- package/docs/ACT_RULE_MAPPING.md +243 -0
- package/docs/API_STABILITY.md +2 -2
- package/docs/BINDING_AUTHORS_GUIDE.md +3 -3
- package/docs/DESIGN_CHALLENGES.md +301 -0
- package/docs/ENGINE_OPTIONS.md +16 -4
- package/docs/I18N.md +4 -4
- package/docs/INTEGRATION.md +1 -1
- package/docs/LIMITATIONS.md +6 -4
- package/docs/REPORT.md +1 -1
- package/docs/RULE_AUTHORING.md +53 -25
- package/docs/RULE_CATALOG.md +1878 -169
- package/docs/RULE_TAXONOMY.md +2 -2
- package/docs/TROUBLESHOOTING.md +2 -2
- package/docs/WCAG_CONFORMANCE.md +25 -9
- package/package.json +3 -7
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +68 -10
- package/src/checks/automatic/aria-allowed-role.js +2 -2
- package/src/checks/automatic/aria-braille-equivalent.js +3 -3
- package/src/checks/automatic/aria-conditional-attr.js +5 -5
- package/src/checks/automatic/aria-deprecated-role.js +1 -1
- package/src/checks/automatic/aria-hidden-body.js +2 -2
- package/src/checks/automatic/aria-hidden-focus.js +5 -5
- package/src/checks/automatic/aria-prohibited-attr.js +18 -18
- package/src/checks/automatic/aria-prohibited-children.js +130 -37
- package/src/checks/automatic/aria-required-attr.js +60 -12
- package/src/checks/automatic/aria-required-children.js +21 -14
- package/src/checks/automatic/aria-required-parent.js +61 -9
- package/src/checks/automatic/aria-role-name-present.js +36 -22
- package/src/checks/automatic/aria-valid-attr-value.js +15 -12
- package/src/checks/automatic/aria-valid-attr.js +1 -1
- package/src/checks/automatic/autocomplete-valid.js +2 -2
- 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 +32 -0
- package/src/checks/automatic/contrast-enhanced.js +21 -1
- package/src/checks/automatic/contrast-minimum.js +21 -1
- package/src/checks/automatic/css-orientation-lock.js +96 -19
- 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 +5 -3
- package/src/checks/automatic/duplicate-id.js +198 -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 +1 -1
- package/src/checks/automatic/iframe-focusable-content.js +63 -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 +172 -18
- package/src/checks/automatic/link-in-text-block.js +10 -10
- 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 +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 +71 -53
- package/src/checks/automatic/td-has-header.js +5 -5
- 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 +453 -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-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/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 +8965 -1647
- package/src/report.js +2 -2
- package/surea11y.browser.js +3768 -611
- package/surea11y.i18n.de.js +1 -1
- package/surea11y.i18n.es.js +1 -1
- package/surea11y.i18n.fr.js +1 -1
- package/bin/surea11y-core.js +0 -20
|
@@ -8,6 +8,24 @@
|
|
|
8
8
|
* @summary Pointer-operable targets should be at least 24×24 CSS px (or meet an exception)
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 2.5.8
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to <button>, <summary>, <a href>, <area href>, <input>,
|
|
13
|
+
* <select>, <textarea> and elements with role="button"/"link" that are
|
|
14
|
+
* pointer-reachable: rendered, not suppressed by pointer-events:none, and
|
|
15
|
+
* with a measurable box of non-zero size. Accessibility-tree exclusion isn't
|
|
16
|
+
* a filter here: an aria-hidden control is still a target a pointer can hit.
|
|
17
|
+
* <area> is matched but never actually evaluated, for the reason given below.
|
|
18
|
+
* @expectation
|
|
19
|
+
* Each target is at least 24 by 24 CSS pixels, or meets one of the SC
|
|
20
|
+
* 2.5.8 exceptions this rule can establish from geometry: spacing (a
|
|
21
|
+
* 24px-diameter circle centred on the target reaches no unrelated target),
|
|
22
|
+
* the inline exception for a link inside a run of text, or user-agent
|
|
23
|
+
* sizing (an unstyled native checkbox or radio, detected by appearance not
|
|
24
|
+
* having been reset to none). An undersized target too close to a
|
|
25
|
+
* neighbour fails. Where an exception may apply but geometry cannot
|
|
26
|
+
* confirm it (two inline links in one run of text, or a target inside an
|
|
27
|
+
* SVG, canvas or image map that may be essential), the result is cantTell
|
|
28
|
+
* rather than a guess.
|
|
11
29
|
*
|
|
12
30
|
* Notes (engine intent):
|
|
13
31
|
* - This rule is DOM-based and measures pointer hit regions available to sighted pointer users.
|
|
@@ -19,42 +37,43 @@
|
|
|
19
37
|
* - Spacing: a 24px-diameter circle centered on an undersized target must not
|
|
20
38
|
* intersect another (unrelated) target's box or another undersized
|
|
21
39
|
* target's own circle. Two passes: a fast center-distance check (exact for
|
|
22
|
-
* undersized-vs-undersized
|
|
40
|
+
* undersized-vs-undersized, a reasonable proxy otherwise) and a 16-point
|
|
23
41
|
* perimeter sample via elementFromPoint as a more precise fallback for
|
|
24
42
|
* cases the distance check under-detects (e.g. a small target adjacent to
|
|
25
43
|
* a large, elongated neighbor). Ancestor/descendant relationships between
|
|
26
|
-
* the target and the "other" element are never treated as a conflict
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
44
|
+
* the target and the "other" element are never treated as a conflict (see
|
|
45
|
+
* isRelated): a nested-interactive shape, a small control inside its own
|
|
46
|
+
* wrapping link/button, is one visual region, not two independent targets.
|
|
47
|
+
* That pattern is nested-interactive-controls-absent's concern, not a
|
|
48
|
+
* spacing one.
|
|
31
49
|
* - Inline: a link inside a text-block container passes outright
|
|
32
50
|
* (isInlineTextExceptionTarget). An inline link whose only spacing conflict
|
|
33
51
|
* is another inline link in the same run is reported as cantTell
|
|
34
|
-
* (isInlineLinkTarget)
|
|
35
|
-
* can't confirm that.
|
|
52
|
+
* (isInlineLinkTarget), since the inline exception may cover it but
|
|
53
|
+
* geometry can't confirm that.
|
|
36
54
|
* - User Agent Control: an unstyled native checkbox/radio, detected via
|
|
37
|
-
* `appearance` not being reset to `none` (see isUserAgentSizedControl)
|
|
38
|
-
*
|
|
55
|
+
* `appearance` not being reset to `none` (see isUserAgentSizedControl).
|
|
56
|
+
* Scoped narrowly to checkbox/radio specifically, not every form control,
|
|
39
57
|
* since those are the only types with unambiguous native rendering.
|
|
40
58
|
* - Essential/Equivalent: only a narrow, high-confidence subset is asserted
|
|
41
|
-
* (SVG/canvas/map-embedded controls
|
|
59
|
+
* (SVG/canvas/map-embedded controls, see isPlausiblyEssentialOrEquivalent);
|
|
42
60
|
* anything else defers to cantTell rather than guessing "essential" from a
|
|
43
61
|
* layout container.
|
|
44
62
|
*
|
|
45
|
-
* Known,
|
|
63
|
+
* Known gap, left unimplemented on purpose: `<area>` (image-map hotspot)
|
|
46
64
|
* elements are not evaluated at all. `area[href]` is in CANDIDATE_SELECTOR
|
|
47
|
-
* for forward-compatibility, but it's currently a no-op
|
|
65
|
+
* for forward-compatibility, but it's currently a no-op. `<area>` has no
|
|
48
66
|
* CSS box of its own (`display: none` by the HTML spec's default UA
|
|
49
|
-
* stylesheet
|
|
50
|
-
* always reports zero geometry and
|
|
51
|
-
* `display:none` check rejects it before any
|
|
52
|
-
* real `<area>` hit-region is computed by the
|
|
53
|
-
* `coords` attributes against the associated
|
|
54
|
-
* an entirely different measurement path than
|
|
55
|
-
* Implementing that properly (parsing `coords`,
|
|
56
|
-
* `<img>` via its `usemap`, accounting for the image's
|
|
57
|
-
* size) is a separate, larger feature, not attempted in
|
|
67
|
+
* stylesheet, confirmed against the spec rather than a jsdom quirk), so
|
|
68
|
+
* `getBoundingClientRect()` always reports zero geometry and
|
|
69
|
+
* `isPointerReachable`'s existing `display:none` check rejects it before any
|
|
70
|
+
* size/exception logic runs. A real `<area>` hit-region is computed by the
|
|
71
|
+
* browser from its `shape`/`coords` attributes against the associated
|
|
72
|
+
* `<img>`'s *rendered* size, an entirely different measurement path than
|
|
73
|
+
* every other candidate here. Implementing that properly (parsing `coords`,
|
|
74
|
+
* resolving the owning `<img>` via its `usemap`, accounting for the image's
|
|
75
|
+
* CSS-scaled render size) is a separate, larger feature, not attempted in
|
|
76
|
+
* this pass.
|
|
58
77
|
*
|
|
59
78
|
* This is an automatic, deterministic approximation intended to be:
|
|
60
79
|
* - strict on clear failures,
|
|
@@ -64,7 +83,8 @@
|
|
|
64
83
|
const id = 'target-size-minimum';
|
|
65
84
|
|
|
66
85
|
const meta = {
|
|
67
|
-
title:
|
|
86
|
+
title:
|
|
87
|
+
'Pointer targets must be at least 24x24px large, or leave sufficient distance to other targets',
|
|
68
88
|
description:
|
|
69
89
|
'Checks that pointer-operable targets have an effective hit region of at least 24 by 24 CSS pixels, or meet an allowed exception (e.g. sufficient spacing).',
|
|
70
90
|
i18n: {
|
|
@@ -354,11 +374,11 @@ function runInPage(ctx) {
|
|
|
354
374
|
// element, or either one is an ancestor of the other. A nested-interactive
|
|
355
375
|
// pattern (e.g. a small <button> inside a wrapping <a href>, or vice
|
|
356
376
|
// versa) is a single visual/interactive region, not two independently
|
|
357
|
-
// placed targets
|
|
377
|
+
// placed targets. The spacing exception's "does the circle intersect
|
|
358
378
|
// ANOTHER target" language is about separate targets, not an element and
|
|
359
379
|
// its own container. (Nested interactive controls are their own,
|
|
360
|
-
// separately-flagged anti-pattern
|
|
361
|
-
//
|
|
380
|
+
// separately-flagged anti-pattern, nested-interactive-controls-absent,
|
|
381
|
+
// not a target-size spacing concern.)
|
|
362
382
|
function isRelated(a, b) {
|
|
363
383
|
try {
|
|
364
384
|
if (!a || !b) return false;
|
|
@@ -415,7 +435,7 @@ function runInPage(ctx) {
|
|
|
415
435
|
function hasSpacingConflict(target) {
|
|
416
436
|
// 0) Pure geometry: deterministic center-distance check against ANY
|
|
417
437
|
// nearby target, not just other undersized ones. Per WCAG 2.5.8, the
|
|
418
|
-
// spacing exception depends on proximity to any adjacent target
|
|
438
|
+
// spacing exception depends on proximity to any adjacent target, so an
|
|
419
439
|
// undersized target sitting flush against an adequately-sized one still
|
|
420
440
|
// fails the exception, which an undersized-only comparison would miss.
|
|
421
441
|
for (const other of items) {
|
|
@@ -439,8 +459,8 @@ function runInPage(ctx) {
|
|
|
439
459
|
// as a confident conflict. Perimeter sampling is an approximation
|
|
440
460
|
// (rounded corners, border-radius, and sub-pixel geometry can shift a
|
|
441
461
|
// sample point in or out of a neighboring element), so a result that
|
|
442
|
-
// merely reaches HIT_THRESHOLD is not asserted as a deterministic
|
|
443
|
-
//
|
|
462
|
+
// merely reaches HIT_THRESHOLD is not asserted as a deterministic fail.
|
|
463
|
+
// See the ambiguous band below.
|
|
444
464
|
const CONFIDENT_THRESHOLD = 5;
|
|
445
465
|
let hitCount = 0;
|
|
446
466
|
let firstConflictEl = null;
|
|
@@ -486,20 +506,20 @@ function runInPage(ctx) {
|
|
|
486
506
|
|
|
487
507
|
// WCAG 2.5.8 "User Agent Control" exception: the target's size requirement
|
|
488
508
|
// does not apply at all when its size is determined by the user agent and
|
|
489
|
-
// not modified by the author
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
// input[type=checkbox]/[type=radio] specifically
|
|
493
|
-
// types with a universally-recognized, unambiguous native rendering
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
509
|
+
// not modified by the author. The canonical example is an unstyled native
|
|
510
|
+
// checkbox/radio (browsers render these well under 24px by default, and
|
|
511
|
+
// that's not the author's choice). Scoped narrowly to
|
|
512
|
+
// input[type=checkbox]/[type=radio] specifically, the only form-control
|
|
513
|
+
// types with a universally-recognized, unambiguous native rendering, and
|
|
514
|
+
// not extended to select/range/color/file, whose "default" sizing varies
|
|
515
|
+
// enough across browsers/OSes that a wrong exemption there risks masking a
|
|
516
|
+
// real author-introduced undersized target.
|
|
497
517
|
//
|
|
498
518
|
// Detection signal: `appearance` (or the legacy `-webkit-appearance`)
|
|
499
519
|
// computed as `none` is the near-universal first step of custom
|
|
500
|
-
// checkbox/radio styling across every CSS framework/design system
|
|
501
|
-
//
|
|
502
|
-
//
|
|
520
|
+
// checkbox/radio styling across every CSS framework/design system. If the
|
|
521
|
+
// author hasn't reset it, the browser is still rendering its own default
|
|
522
|
+
// control chrome, so the size is UA-determined rather than authored.
|
|
503
523
|
function isUserAgentSizedControl(el) {
|
|
504
524
|
try {
|
|
505
525
|
if (!el || el.nodeType !== 1) return false;
|
|
@@ -545,8 +565,8 @@ function runInPage(ctx) {
|
|
|
545
565
|
// Image map targets are often constrained by the underlying image.
|
|
546
566
|
// Currently unreachable in practice: <area> never becomes a
|
|
547
567
|
// measurable candidate at all (see the file header's "Known,
|
|
548
|
-
//
|
|
549
|
-
//
|
|
568
|
+
// unimplemented gap" note). Kept for forward compatibility if that
|
|
569
|
+
// gap is closed later.
|
|
550
570
|
if (tag === 'area') return true;
|
|
551
571
|
|
|
552
572
|
// Graphics / spatial interaction regions are commonly essential by design.
|
|
@@ -570,7 +590,7 @@ function runInPage(ctx) {
|
|
|
570
590
|
|
|
571
591
|
// User Agent Control exception: size isn't the author's choice, so the
|
|
572
592
|
// size requirement (and therefore any spacing conflict stemming from
|
|
573
|
-
// it) doesn't apply at all
|
|
593
|
+
// it) doesn't apply at all. Skip straight to pass, no need to even
|
|
574
594
|
// evaluate spacing.
|
|
575
595
|
if (isUserAgentSizedControl(it.el)) {
|
|
576
596
|
continue;
|
|
@@ -579,12 +599,10 @@ function runInPage(ctx) {
|
|
|
579
599
|
const info = hasSpacingConflict(it);
|
|
580
600
|
|
|
581
601
|
if (!info.conflict && info.confident === false) {
|
|
582
|
-
// Ambiguous perimeter-sampling result near the decision threshold
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
// fail (see helpers.resolveTieredOutcome's header comment). Now
|
|
587
|
-
// reported as its own cantTell-tier occurrence instead.
|
|
602
|
+
// Ambiguous perimeter-sampling result near the decision threshold: report
|
|
603
|
+
// it as its own cantTell-tier occurrence for this target so it isn't lost
|
|
604
|
+
// once any other target on the page has a confident fail (see
|
|
605
|
+
// helpers.resolveTieredOutcome's header comment).
|
|
588
606
|
cantTellOccurrences.push(
|
|
589
607
|
helpers.reportOccurrence(it.el, {
|
|
590
608
|
occurrenceOutcome: 'cantTell',
|
|
@@ -612,14 +630,14 @@ function runInPage(ctx) {
|
|
|
612
630
|
if (info.conflict) {
|
|
613
631
|
if (isPlausiblyEssentialOrEquivalent(it.el)) {
|
|
614
632
|
// Confident spacing conflict, but the target may be exempt as part
|
|
615
|
-
// of an essential graphic/image-map region
|
|
616
|
-
//
|
|
633
|
+
// of an essential graphic/image-map region, so report it as
|
|
634
|
+
// cantTell rather than dropping it.
|
|
617
635
|
cantTellOccurrences.push(
|
|
618
636
|
helpers.reportOccurrence(it.el, {
|
|
619
637
|
occurrenceOutcome: 'cantTell',
|
|
620
638
|
summary:
|
|
621
639
|
'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
|
|
640
|
+
hint: 'Verify whether this target’s size is essential to its function (e.g. part of an SVG/canvas/image map); if not, increase target size or spacing.',
|
|
623
641
|
i18n: {
|
|
624
642
|
summaryKey: 'targetSizeMinimum_summary_cantTell_plausiblyEssential',
|
|
625
643
|
hintKey: 'targetSizeMinimum_hint_cantTell_plausiblyEssential',
|
|
@@ -691,7 +709,7 @@ function runInPage(ctx) {
|
|
|
691
709
|
|
|
692
710
|
// See helpers.resolveTieredOutcome's own header comment (src/core/dom-helpers.js):
|
|
693
711
|
// a fail-tier finding never silently discards cantTell-tier findings from
|
|
694
|
-
// the same run
|
|
712
|
+
// the same run. Both are returned together when the outcome is 'fail'.
|
|
695
713
|
const resolved = helpers.resolveTieredOutcome(
|
|
696
714
|
failOccurrences,
|
|
697
715
|
cantTellOccurrences,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @applicability
|
|
12
12
|
* `<table>` elements with at least 4 rows and at least 4 columns
|
|
13
13
|
* (a "large" table, where implicit row/column header association is
|
|
14
|
-
*
|
|
14
|
+
* useful; small tables are usually self-evident), and with
|
|
15
15
|
* NO `colspan`/`rowspan` anywhere in the table.
|
|
16
16
|
* @expectation
|
|
17
17
|
* Every `<td>` has an associated header, via one of:
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
* earlier row, OR
|
|
23
23
|
* - an implicit row header: some `<th>` earlier in the same row.
|
|
24
24
|
* @implementation-notes
|
|
25
|
-
* - Closes the gap `table-th-has-data-cells`
|
|
25
|
+
* - Closes the gap `table-th-has-data-cells` deferred (see
|
|
26
26
|
* that rule's own implementation notes): this is the fuller positional
|
|
27
|
-
* header-association algorithm, but still intentionally scoped
|
|
27
|
+
* header-association algorithm, but still intentionally scoped,
|
|
28
28
|
* tables with any `colspan`/`rowspan` are skipped entirely (marked
|
|
29
29
|
* `notApplicable`) rather than risk a wrong column-index computation
|
|
30
30
|
* producing a false `fail`.
|
|
31
31
|
* - The `headers`-attribute branch does not itself validate that the
|
|
32
|
-
* referenced ids exist or point at `<th>` elements
|
|
32
|
+
* referenced ids exist or point at `<th>` elements, that's already
|
|
33
33
|
* `table-headers-attr-valid`'s job.
|
|
34
34
|
*/
|
|
35
35
|
|
|
@@ -38,7 +38,7 @@ const id = 'td-has-header';
|
|
|
38
38
|
const meta = {
|
|
39
39
|
title: 'Data cells in large tables must have an associated header',
|
|
40
40
|
description:
|
|
41
|
-
'Checks that every <td> in a large, simple (no colspan/rowspan) table has an associated header
|
|
41
|
+
'Checks that every <td> in a large, simple (no colspan/rowspan) table has an associated header, via a headers attribute, an implicit column <th> above it, or an implicit row <th> to its left.',
|
|
42
42
|
i18n: {
|
|
43
43
|
titleKey: 'tdHasHeader_title',
|
|
44
44
|
descriptionKey: 'tdHasHeader_description'
|
|
@@ -2,11 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check textbox-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="textbox" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="textbox" (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="textbox" is name-from-author-only, so subtree text is never
|
|
20
|
+
* accepted: text sitting inside a custom textbox widget is not reliably
|
|
21
|
+
* exposed as its name. On a labelable element (<input role="textbox">) an
|
|
22
|
+
* associated <label> counts as well.
|
|
23
|
+
*/
|
|
24
|
+
|
|
5
25
|
const id = 'textbox-name-present';
|
|
6
26
|
|
|
7
27
|
const meta = {
|
|
8
|
-
title: '
|
|
9
|
-
description: 'Checks that elements expose a non-empty accessible name.',
|
|
28
|
+
title: 'Textboxes have an accessible name',
|
|
29
|
+
description: 'Checks that elements with role="textbox" expose a non-empty accessible name.',
|
|
10
30
|
i18n: {
|
|
11
31
|
titleKey: 'textboxNamePresent_title',
|
|
12
32
|
descriptionKey: 'textboxNamePresent_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 textbox's accessible name.",
|
|
235
255
|
i18n: {
|
|
236
256
|
summaryKey: 'textboxNamePresent_summary_fail',
|
|
237
257
|
hintKey: 'textboxNamePresent_hint_fail',
|
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check tooltip-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="tooltip" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="tooltip" (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, from title, or,
|
|
19
|
+
* role="tooltip" being name-from-content, from its own subtree text,
|
|
20
|
+
* where a descendant's own name (an <img alt>, aria-label or title) counts
|
|
21
|
+
* as that descendant's contribution rather than only its text nodes.
|
|
22
|
+
*/
|
|
23
|
+
|
|
5
24
|
const id = 'tooltip-name-present';
|
|
6
25
|
|
|
7
26
|
const meta = {
|
|
@@ -51,7 +70,7 @@ function runInPage(ctx) {
|
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
function getConservativeSubtreeText(document, container) {
|
|
54
|
-
// "Name from content"
|
|
73
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
55
74
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
56
75
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
57
76
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -70,7 +89,7 @@ function runInPage(ctx) {
|
|
|
70
89
|
const raw = getAttr(el, 'aria-labelledby');
|
|
71
90
|
if (!raw) return '';
|
|
72
91
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
73
|
-
// name-from-content of the referenced element
|
|
92
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
74
93
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
75
94
|
// aria-labelledby target's only name source is its title attribute,
|
|
76
95
|
// which name-from-content alone can never see).
|
|
@@ -2,11 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @check treeitem-name-present
|
|
7
|
+
* @atomic true
|
|
8
|
+
* @summary Elements with role="treeitem" must have an accessible name
|
|
9
|
+
* @standard WCAG 2.2
|
|
10
|
+
* @sc 4.1.2
|
|
11
|
+
* @applicability
|
|
12
|
+
* Applies to elements carrying role="treeitem" (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, from title, or,
|
|
19
|
+
* role="treeitem" being name-from-content, from its own subtree text,
|
|
20
|
+
* where a descendant's own name (an <img alt>, aria-label or title) counts
|
|
21
|
+
* as that descendant's contribution rather than only its text nodes.
|
|
22
|
+
*/
|
|
23
|
+
|
|
5
24
|
const id = 'treeitem-name-present';
|
|
6
25
|
|
|
7
26
|
const meta = {
|
|
8
|
-
title: '
|
|
9
|
-
description: 'Checks that elements expose a non-empty accessible name.',
|
|
27
|
+
title: 'Tree items have an accessible name',
|
|
28
|
+
description: 'Checks that elements with role="treeitem" expose a non-empty accessible name.',
|
|
10
29
|
i18n: {
|
|
11
30
|
titleKey: 'treeitemNamePresent_title',
|
|
12
31
|
descriptionKey: 'treeitemNamePresent_description'
|
|
@@ -51,7 +70,7 @@ function runInPage(ctx) {
|
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
function getConservativeSubtreeText(document, container) {
|
|
54
|
-
// "Name from content"
|
|
73
|
+
// "Name from content", recurses into descendants and uses each one's
|
|
55
74
|
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
56
75
|
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
57
76
|
// header comment in src/core/dom-helpers.js for the full rationale
|
|
@@ -70,7 +89,7 @@ function runInPage(ctx) {
|
|
|
70
89
|
const raw = getAttr(el, 'aria-labelledby');
|
|
71
90
|
if (!raw) return '';
|
|
72
91
|
// Delegates to the shared getTextFromIdRefs helper instead of computing
|
|
73
|
-
// name-from-content of the referenced element
|
|
92
|
+
// name-from-content of the referenced element, see dialog-name-
|
|
74
93
|
// present.js's identical fix for the full rationale (an <iframe>
|
|
75
94
|
// aria-labelledby target's only name source is its title attribute,
|
|
76
95
|
// which name-from-content alone can never see).
|
|
@@ -10,11 +10,23 @@
|
|
|
10
10
|
* @sc 3.1.2
|
|
11
11
|
* @applicability
|
|
12
12
|
* Applies to any element other than the root <html> with a non-empty
|
|
13
|
-
* lang attribute
|
|
13
|
+
* lang attribute AND at least some non-whitespace "governed text" that
|
|
14
|
+
* actually inherits its language from that element, per ACT de46e4:
|
|
15
|
+
* - Descendant text/alt is governed by the nearest lang-carrying
|
|
16
|
+
* ancestor only, a nested descendant with its own non-empty lang
|
|
17
|
+
* re-scopes everything inside it, so that subtree no longer counts
|
|
18
|
+
* toward the outer element's applicability (it counts toward the
|
|
19
|
+
* nested element's own, if that one is also being evaluated).
|
|
20
|
+
* - A non-empty alt attribute on img/area/input[type=image] counts as
|
|
21
|
+
* governed text, the same as a text node.
|
|
22
|
+
* - Text (or alt) that CSS keeps out of the render tree (display:none,
|
|
23
|
+
* the hidden attribute, ...) does not count. aria-hidden and
|
|
24
|
+
* offscreen positioning do NOT exempt text, per ACT's own failed
|
|
25
|
+
* examples for both, only actual non-rendering does.
|
|
14
26
|
* @expectation
|
|
15
27
|
* The lang value matches a valid BCP47 language-tag syntax. WCAG 3.1.2
|
|
16
28
|
* (Language of Parts) requires that when a passage's language differs
|
|
17
|
-
* from the page's default, it is identified programmatically
|
|
29
|
+
* from the page's default, it is identified programmatically. An
|
|
18
30
|
* invalid tag fails to identify a real language at all.
|
|
19
31
|
* @implementation-notes
|
|
20
32
|
* - Distinct, atomic decision from html-lang-attr-present (that
|
|
@@ -22,7 +34,7 @@
|
|
|
22
34
|
* covers every other element, for SC 3.1.2.
|
|
23
35
|
* - Same minimal BCP47 *syntax* check as html-lang-attr-present (primary
|
|
24
36
|
* subtag + optional subtags), not IANA Language Subtag Registry
|
|
25
|
-
* validation
|
|
37
|
+
* validation, same documented scope limitation (syntactically
|
|
26
38
|
* well-formed but unregistered tags like "xx-ZZ" are not flagged).
|
|
27
39
|
*/
|
|
28
40
|
|
|
@@ -66,6 +78,78 @@ function runInPage(ctx) {
|
|
|
66
78
|
? helpers.isValidLanguageTag
|
|
67
79
|
: (v) => BCP47_RE.test(String(v || ''));
|
|
68
80
|
|
|
81
|
+
function isDomVisible(node) {
|
|
82
|
+
if (!node) return false;
|
|
83
|
+
if (helpers.isDomVisibleEligible)
|
|
84
|
+
return !!helpers.isDomVisibleEligible(node, ctx, { targetSet: 'dom' }).eligible;
|
|
85
|
+
if (helpers.getEligibilityInfo)
|
|
86
|
+
return !!helpers.getEligibilityInfo(node, ctx, { targetSet: 'dom' }).eligible;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasOwnNonEmptyLang(node) {
|
|
91
|
+
try {
|
|
92
|
+
const v = node.getAttribute ? node.getAttribute('lang') : null;
|
|
93
|
+
return v != null && v.trim() !== '';
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isAltBearing(node) {
|
|
100
|
+
const tag = (node.tagName || '').toLowerCase();
|
|
101
|
+
if (tag === 'img' || tag === 'area') return true;
|
|
102
|
+
if (tag !== 'input') return false;
|
|
103
|
+
try {
|
|
104
|
+
return (node.getAttribute('type') || '').toLowerCase() === 'image';
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// "Governed text": non-whitespace text (or alt) that inherits its
|
|
111
|
+
// language from `root`, per ACT de46e4, see @applicability above. Walks
|
|
112
|
+
// the flat subtree, stopping at any descendant carrying its own non-empty
|
|
113
|
+
// lang (that subtree governs itself, not `root`), and treating
|
|
114
|
+
// display:none/hidden content as absent. Bails out as soon as any
|
|
115
|
+
// qualifying text is found; a node-visit budget guards pathological
|
|
116
|
+
// markup the same way other subtree walks in this engine do.
|
|
117
|
+
const MAX_VISITS = 5000;
|
|
118
|
+
function hasGovernedText(root) {
|
|
119
|
+
let found = false;
|
|
120
|
+
let visits = 0;
|
|
121
|
+
|
|
122
|
+
function walk(node, isRoot) {
|
|
123
|
+
if (found || visits++ > MAX_VISITS) return;
|
|
124
|
+
if (!node || node.nodeType !== 1) return;
|
|
125
|
+
if (!isRoot && hasOwnNonEmptyLang(node)) return; // re-scoped to itself
|
|
126
|
+
|
|
127
|
+
if (isAltBearing(node)) {
|
|
128
|
+
const alt = node.getAttribute ? node.getAttribute('alt') : null;
|
|
129
|
+
if (alt != null && alt.trim() !== '' && isDomVisible(node)) found = true;
|
|
130
|
+
return; // alt-bearing elements have no other text to walk into
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!isDomVisible(node)) return;
|
|
134
|
+
|
|
135
|
+
const kids = node.childNodes ? Array.from(node.childNodes) : [];
|
|
136
|
+
for (const kid of kids) {
|
|
137
|
+
if (found) return;
|
|
138
|
+
if (kid.nodeType === 3) {
|
|
139
|
+
if (String(kid.nodeValue || '').trim()) {
|
|
140
|
+
found = true;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
} else if (kid.nodeType === 1) {
|
|
144
|
+
walk(kid, false);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
walk(root, true);
|
|
150
|
+
return found;
|
|
151
|
+
}
|
|
152
|
+
|
|
69
153
|
const nodes = helpers.queryAllSmart
|
|
70
154
|
? helpers.queryAllSmart('[lang]')
|
|
71
155
|
: helpers.queryAll('[lang]');
|
|
@@ -80,10 +164,11 @@ function runInPage(ctx) {
|
|
|
80
164
|
const rawAttr = el.getAttribute('lang');
|
|
81
165
|
if (rawAttr === null || rawAttr === '') continue; // ACT de46e4: empty is out of scope
|
|
82
166
|
|
|
83
|
-
// The rule applies only where text actually inherits the language
|
|
84
|
-
// element
|
|
85
|
-
//
|
|
86
|
-
|
|
167
|
+
// The rule applies only where text actually inherits the language from
|
|
168
|
+
// THIS element specifically, not merely where the subtree has any text
|
|
169
|
+
// at all, which could all belong to a nested element's own (possibly
|
|
170
|
+
// valid) lang instead. See hasGovernedText's doc comment.
|
|
171
|
+
if (!hasGovernedText(el)) continue;
|
|
87
172
|
|
|
88
173
|
applicableCount += 1;
|
|
89
174
|
|
|
@@ -87,7 +87,7 @@ function runInPage(ctx) {
|
|
|
87
87
|
|
|
88
88
|
function computeNameInfo(el) {
|
|
89
89
|
// <video> is not a labelable element (no browser computes an accessible
|
|
90
|
-
// name from <label for="...">), so only ARIA naming and title count
|
|
90
|
+
// name from <label for="...">), so only ARIA naming and title count,
|
|
91
91
|
// do not accept native <label> associations.
|
|
92
92
|
const flags = [];
|
|
93
93
|
let aria = null;
|
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
* @check accesskeys
|
|
7
7
|
* @atomic true
|
|
8
8
|
* @summary accesskey values must be unique on the page
|
|
9
|
-
* @standard Best Practices (no formal WCAG Success Criterion
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion)
|
|
10
10
|
* @applicability
|
|
11
11
|
* Applies whenever two or more elements share the same non-empty
|
|
12
12
|
* accesskey attribute value (case-insensitive).
|
|
13
13
|
* @expectation
|
|
14
14
|
* Every accesskey value on the page is unique. Duplicate accesskeys
|
|
15
|
-
* make keyboard-shortcut activation ambiguous
|
|
15
|
+
* make keyboard-shortcut activation ambiguous: only one of the
|
|
16
16
|
* elements sharing the key can actually be reached by it, and which
|
|
17
17
|
* one is browser/platform-dependent.
|
|
18
18
|
* @implementation-notes
|
|
19
|
-
* - Not WCAG-normative
|
|
19
|
+
* - Not WCAG-normative, authored as an advisory, cantTell-capped
|
|
20
20
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
21
21
|
* header comment for the shared rationale/precedent.
|
|
22
22
|
*/
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.1.1
|
|
11
11
|
* @type manual
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to <area> elements whose alt attribute is present but empty once
|
|
14
|
+
* trimmed, the markup that declares a hotspot decorative. The <area> must
|
|
15
|
+
* belong to a <map> that an <img usemap> actually references, and both
|
|
16
|
+
* that <img> and the <area> itself must be included in the accessibility
|
|
17
|
+
* tree; an <area> in an unused map is out of scope.
|
|
18
|
+
* role="presentation"/"none" takes an element out unless it is focusable.
|
|
12
19
|
* @expectation
|
|
13
20
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
14
21
|
*/
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.1.1
|
|
11
11
|
* @type manual
|
|
12
|
+
* @applicability
|
|
13
|
+
* Applies to <area> elements whose alt attribute is present and non-empty.
|
|
14
|
+
* The <area> must belong to a <map> that an <img usemap> actually
|
|
15
|
+
* references, and both that <img> and the <area> itself must be included
|
|
16
|
+
* in the accessibility tree; an <area> in an unused map is out of scope.
|
|
17
|
+
* role="presentation"/"none" takes an element out unless it is focusable.
|
|
12
18
|
* @expectation
|
|
13
19
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
14
20
|
*/
|