@surea11y/core 1.3.0 → 1.4.1
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 +87 -2
- package/README.md +109 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +26 -0
- package/docs/ARIA_DEPRECATION.md +95 -0
- package/docs/CI_INTEGRATIONS.md +7 -7
- package/docs/ENGINE_OPTIONS.md +1 -1
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +1 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +6 -6
- package/package.json +52 -16
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +4 -6
- package/src/checks/automatic/aria-allowed-attr.js +663 -134
- package/src/checks/automatic/aria-allowed-role.js +2 -0
- package/src/checks/automatic/aria-braille-equivalent.js +2 -0
- package/src/checks/automatic/aria-conditional-attr.js +8 -7
- package/src/checks/automatic/aria-deprecated-role.js +107 -38
- package/src/checks/automatic/aria-hidden-body.js +6 -4
- package/src/checks/automatic/aria-hidden-focus.js +12 -13
- package/src/checks/automatic/aria-prohibited-attr.js +98 -105
- package/src/checks/automatic/aria-prohibited-children.js +56 -87
- package/src/checks/automatic/aria-required-attr.js +6 -7
- package/src/checks/automatic/aria-required-children.js +7 -10
- package/src/checks/automatic/aria-required-parent.js +20 -25
- package/src/checks/automatic/aria-role-name-present.js +2 -0
- package/src/checks/automatic/aria-roles-valid.js +33 -6
- package/src/checks/automatic/aria-valid-attr-value.js +18 -15
- package/src/checks/automatic/aria-valid-attr.js +2 -0
- package/src/checks/automatic/autocomplete-valid.js +39 -1
- package/src/checks/automatic/avoid-inline-spacing.js +181 -20
- package/src/checks/automatic/binary-control-name-present.js +13 -3
- package/src/checks/automatic/button-name-present.js +54 -21
- package/src/checks/automatic/canvas-text-alternative-present.js +17 -8
- package/src/checks/automatic/combobox-name-present.js +10 -1
- package/src/checks/automatic/contrast-computable.js +2 -0
- package/src/checks/automatic/contrast-enhanced.js +2 -0
- package/src/checks/automatic/contrast-minimum.js +2 -0
- package/src/checks/automatic/css-orientation-lock.js +21 -27
- package/src/checks/automatic/definition-list-children-valid.js +6 -6
- package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
- package/src/checks/automatic/dialog-name-present.js +18 -11
- package/src/checks/automatic/dlitem-parent-valid.js +2 -0
- package/src/checks/automatic/duplicate-id-aria.js +4 -3
- package/src/checks/automatic/embed-text-alternative-present.js +2 -0
- package/src/checks/automatic/form-control-programmatic-label-present.js +50 -4
- package/src/checks/automatic/form-control-single-label.js +110 -43
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
- package/src/checks/automatic/iframe-focusable-content.js +246 -18
- package/src/checks/automatic/iframe-name-present.js +2 -0
- package/src/checks/automatic/iframe-title-unique.js +3 -1
- package/src/checks/automatic/img-alt-present.js +23 -18
- package/src/checks/automatic/input-image-alt-present.js +101 -52
- package/src/checks/automatic/label-in-name.js +97 -27
- package/src/checks/automatic/language-page-present.js +7 -1
- package/src/checks/automatic/link-in-text-block.js +2 -0
- package/src/checks/automatic/link-name-present.js +52 -17
- package/src/checks/automatic/list-children-valid.js +14 -24
- package/src/checks/automatic/listbox-name-present.js +10 -1
- package/src/checks/automatic/listitem-parent-valid.js +30 -7
- package/src/checks/automatic/menuitem-name-present.js +10 -1
- package/src/checks/automatic/meta-refresh-no-exceptions.js +33 -4
- package/src/checks/automatic/meta-refresh-timing-absent.js +32 -4
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +39 -15
- package/src/checks/automatic/meter-name-present.js +12 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +177 -25
- package/src/checks/automatic/object-text-alternative-present.js +16 -7
- package/src/checks/automatic/option-name-present.js +10 -1
- package/src/checks/automatic/page-title-present.js +2 -0
- package/src/checks/automatic/progressbar-name-present.js +16 -11
- package/src/checks/automatic/role-img-alt-present.js +4 -4
- package/src/checks/automatic/searchbox-name-present.js +10 -1
- package/src/checks/automatic/server-side-image-map-absent.js +4 -3
- package/src/checks/automatic/slider-name-present.js +13 -2
- package/src/checks/automatic/spinbutton-name-present.js +10 -1
- package/src/checks/automatic/summary-name-present.js +10 -1
- package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
- package/src/checks/automatic/svg-text-alternative-present.js +17 -5
- package/src/checks/automatic/tab-name-present.js +10 -1
- package/src/checks/automatic/table-headers-attr-valid.js +3 -2
- package/src/checks/automatic/table-th-has-data-cells.js +69 -6
- package/src/checks/automatic/target-size-minimum.js +5 -0
- package/src/checks/automatic/td-has-header.js +24 -1
- package/src/checks/automatic/textbox-name-present.js +10 -1
- package/src/checks/automatic/tooltip-name-present.js +10 -1
- package/src/checks/automatic/treeitem-name-present.js +10 -1
- package/src/checks/automatic/valid-lang.js +18 -3
- package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
- package/src/checks/manual/accesskeys-manual.js +3 -1
- package/src/checks/manual/area-alt-decorative-manual.js +2 -0
- package/src/checks/manual/area-alt-quality-manual.js +2 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
- package/src/checks/manual/aria-text-manual.js +6 -5
- package/src/checks/manual/bypass-blocks-present-manual.js +279 -0
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/css-hidden-focus.js +184 -9
- package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
- package/src/checks/manual/empty-heading-manual.js +17 -17
- package/src/checks/manual/empty-table-header-manual.js +52 -25
- package/src/checks/manual/focus-order-semantics-manual.js +16 -4
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
- package/src/checks/manual/heading-order-manual.js +28 -1
- package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
- package/src/checks/manual/image-redundant-alt-manual.js +21 -1
- package/src/checks/manual/img-alt-decorative-manual.js +2 -0
- package/src/checks/manual/img-alt-quality-manual.js +2 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +26 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
- package/src/checks/manual/label-title-only-manual.js +29 -22
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +51 -55
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +44 -31
- package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
- package/src/checks/manual/landmark-one-main-manual.js +12 -23
- package/src/checks/manual/landmark-unique-manual.js +37 -52
- package/src/checks/manual/link-name-quality-manual.js +2 -0
- package/src/checks/manual/media-transcript-present-manual.js +2 -0
- package/src/checks/manual/meta-viewport-large-manual.js +3 -1
- package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
- package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
- package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/p-as-heading-manual.js +2 -0
- package/src/checks/manual/page-has-heading-one-manual.js +12 -11
- package/src/checks/manual/page-title-patterns-manual.js +2 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
- package/src/checks/manual/region-manual.js +27 -36
- package/src/checks/manual/scope-attr-valid-manual.js +3 -1
- package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
- package/src/checks/manual/skip-link-manual.js +7 -6
- package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/tabindex-manual.js +3 -1
- package/src/checks/manual/table-duplicate-name-manual.js +5 -4
- package/src/checks/manual/table-fake-caption-manual.js +24 -3
- package/src/checks/manual/video-caption-manual.js +2 -0
- package/src/checks/manual-review.js +2 -0
- package/src/core.js +11820 -3317
- package/src/index.js +2 -0
- package/src/report.js +51 -9
- package/src/sarif.js +20 -5
- package/surea11y.browser.js +4943 -1388
- package/bin/core.js +0 -473
- package/docs/CLI.md +0 -128
- package/src/catalogs/composites.wcag.js +0 -454
- package/src/checks/automatic/bypass-blocks-present.js +0 -215
- package/src/checks/rules-and-tags.full.csv +0 -19
- package/src/checks/rules-and-tags.full.json +0 -259
- package/src/core/aria-helpers.js +0 -1211
- package/src/core/contrast-helpers.js +0 -1302
- package/src/core/dom-helpers.js +0 -4493
- package/src/core/dom-runner.js +0 -787
- package/src/core/frame-messaging.js +0 -261
- package/src/core/frame-scan.js +0 -190
- package/src/core/rollup-composites.js +0 -127
- package/src/core/rule-meta.js +0 -176
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -1228
- package/src/i18n/fr.js +0 -1185
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -59
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'dialog-name-present';
|
|
@@ -55,16 +57,14 @@ function runInPage(ctx) {
|
|
|
55
57
|
// Resolve via the shared getTextFromIdRefs helper — computes each
|
|
56
58
|
// referenced element's own ACCESSIBLE NAME (aria-label, then
|
|
57
59
|
// aria-labelledby, then a value-like name, then content, then title),
|
|
58
|
-
// not just its content text.
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// title attribute
|
|
62
|
-
// HTML-AAM, so "name from content" is always
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// pattern was hand-copied into 15 other *-name-present rules; all
|
|
67
|
-
// fixed the same way.
|
|
60
|
+
// not just its content text. This matters when aria-labelledby points
|
|
61
|
+
// at an <iframe title="..."> (e.g. BBC News' cookie-consent dialog,
|
|
62
|
+
// pointing at an `<iframe title="SP Consent Message">`), whose only
|
|
63
|
+
// name source is its title attribute — an iframe's content is
|
|
64
|
+
// opaque/cross-origin per HTML-AAM, so "name from content" is always
|
|
65
|
+
// empty. Computing only name-from-content of the referenced node would
|
|
66
|
+
// silently miss the title fallback and report no accessible name at
|
|
67
|
+
// all, the same pattern every other *-name-present rule guards against.
|
|
68
68
|
if (helpers.getTextFromIdRefs) {
|
|
69
69
|
try {
|
|
70
70
|
const r = helpers.getTextFromIdRefs(raw, ctx, { maxRefs: maxRefs || 8 });
|
|
@@ -74,9 +74,16 @@ function runInPage(ctx) {
|
|
|
74
74
|
return '';
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
78
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
79
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
77
80
|
function isEligibleAcc(helpers, el, ctx) {
|
|
78
81
|
const fn =
|
|
79
|
-
helpers && typeof helpers.
|
|
82
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
83
|
+
? helpers.isIncludedInAccessibilityTree
|
|
84
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
85
|
+
? helpers.isAccTreeEligible
|
|
86
|
+
: null;
|
|
80
87
|
if (!fn) return true;
|
|
81
88
|
try {
|
|
82
89
|
const r = fn(el, ctx);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -18,9 +20,8 @@
|
|
|
18
20
|
* which element the reference resolves to (typically the first, silently
|
|
19
21
|
* dropping the others).
|
|
20
22
|
* @implementation-notes
|
|
21
|
-
* - Scoped deliberately to ids referenced by ARIA
|
|
22
|
-
*
|
|
23
|
-
* check — see ROADMAP.md's "Skip" list).
|
|
23
|
+
* - Scoped deliberately to ids referenced by ARIA, not the broader/
|
|
24
|
+
* deprecated page-wide duplicate-id check (see ROADMAP.md's "Skip" list).
|
|
24
25
|
* - Document-wide by design: id uniqueness and ARIA id references are a
|
|
25
26
|
* whole-document property, not scoped to a sub-root.
|
|
26
27
|
*/
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -71,8 +73,15 @@ const meta = {
|
|
|
71
73
|
function runInPage(ctx) {
|
|
72
74
|
const { helpers, rule } = ctx;
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
// ACT e086e5 applies only to controls included in the accessibility tree, so
|
|
77
|
+
// a focusable control inside aria-hidden is out of scope; aria-hidden-focus
|
|
78
|
+
// reports that markup instead.
|
|
79
|
+
const isEligibleHelper =
|
|
80
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
81
|
+
? helpers.isIncludedInAccessibilityTree
|
|
82
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
83
|
+
? helpers.isAccTreeEligible
|
|
84
|
+
: null;
|
|
76
85
|
const getEligibilityInfo =
|
|
77
86
|
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
78
87
|
|
|
@@ -83,6 +92,41 @@ function runInPage(ctx) {
|
|
|
83
92
|
|
|
84
93
|
const trim = (v) => (v == null ? '' : String(v)).trim();
|
|
85
94
|
|
|
95
|
+
// Roles with their own *-name-present rule; a control carrying one
|
|
96
|
+
// explicitly is skipped here so it is reported once.
|
|
97
|
+
const ROLE_OWNED_ELSEWHERE = [
|
|
98
|
+
'alertdialog',
|
|
99
|
+
'button',
|
|
100
|
+
'checkbox',
|
|
101
|
+
'combobox',
|
|
102
|
+
'dialog',
|
|
103
|
+
'grid',
|
|
104
|
+
'link',
|
|
105
|
+
'listbox',
|
|
106
|
+
'menu',
|
|
107
|
+
'menubar',
|
|
108
|
+
'menuitem',
|
|
109
|
+
'menuitemcheckbox',
|
|
110
|
+
'menuitemradio',
|
|
111
|
+
'meter',
|
|
112
|
+
'option',
|
|
113
|
+
'progressbar',
|
|
114
|
+
'radio',
|
|
115
|
+
'radiogroup',
|
|
116
|
+
'scrollbar',
|
|
117
|
+
'searchbox',
|
|
118
|
+
'slider',
|
|
119
|
+
'spinbutton',
|
|
120
|
+
'switch',
|
|
121
|
+
'tab',
|
|
122
|
+
'tablist',
|
|
123
|
+
'textbox',
|
|
124
|
+
'toolbar',
|
|
125
|
+
'tooltip',
|
|
126
|
+
'tree',
|
|
127
|
+
'treeitem'
|
|
128
|
+
];
|
|
129
|
+
|
|
86
130
|
const metrics = {
|
|
87
131
|
applicableCount: 0,
|
|
88
132
|
passCount: 0,
|
|
@@ -106,9 +150,9 @@ function runInPage(ctx) {
|
|
|
106
150
|
}
|
|
107
151
|
|
|
108
152
|
function isEligibleAcc(el) {
|
|
109
|
-
if (!
|
|
153
|
+
if (!isEligibleHelper) return true;
|
|
110
154
|
try {
|
|
111
|
-
const r =
|
|
155
|
+
const r = isEligibleHelper(el, ctx);
|
|
112
156
|
if (typeof r === 'boolean') return r;
|
|
113
157
|
return !!(r && r.eligible);
|
|
114
158
|
} catch {
|
|
@@ -191,6 +235,8 @@ function runInPage(ctx) {
|
|
|
191
235
|
role = '';
|
|
192
236
|
}
|
|
193
237
|
|
|
238
|
+
if (role && ROLE_OWNED_ELSEWHERE.indexOf(role) !== -1) continue;
|
|
239
|
+
|
|
194
240
|
if (role === 'presentation' || role === 'none') {
|
|
195
241
|
const fi = getFocusableInfo
|
|
196
242
|
? (() => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -10,23 +12,29 @@
|
|
|
10
12
|
* Applies to labelable form controls (input, excluding
|
|
11
13
|
* hidden/submit/reset/button/image; select; textarea).
|
|
12
14
|
* @expectation
|
|
13
|
-
* At most one <label>
|
|
14
|
-
* wrapping it, or by a <label for="...">
|
|
15
|
-
* (
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* At most one <label> that can contribute to the control's accessible name
|
|
16
|
+
* is associated with it — by wrapping it, or by a <label for="..."> on its
|
|
17
|
+
* id (a label that both wraps and self-references via for counts once).
|
|
18
|
+
* Graded by whether the surplus labels actually compete for the name:
|
|
19
|
+
* - PASS when an override (aria-labelledby / aria-label) supersedes every
|
|
20
|
+
* native <label>: the labels then contribute nothing to the name, so
|
|
21
|
+
* they cannot be ambiguous. A visible-label-vs-name mismatch is SC 2.5.3
|
|
22
|
+
* Label in Name's concern, not this rule's.
|
|
23
|
+
* - FAIL when two or more non-empty labels compete and there is no
|
|
24
|
+
* override: screen readers announce a non-deterministic subset.
|
|
25
|
+
* - CANTTELL when one non-empty label is joined by empty label
|
|
26
|
+
* association(s) and there is no override: the name usually resolves to
|
|
27
|
+
* the real label, but handling of the empty association is not
|
|
28
|
+
* guaranteed across user agents.
|
|
29
|
+
* All-empty associations with no override are a missing-name case (the
|
|
30
|
+
* sibling rule below), not an ambiguity, so this rule stays silent.
|
|
19
31
|
* @implementation-notes
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* -
|
|
25
|
-
*
|
|
26
|
-
* the control's accessible name, so it can't create the ambiguity this
|
|
27
|
-
* rule exists to catch. Matches the reference engine's own
|
|
28
|
-
* multiple-label check, which filters hidden labels the same way before
|
|
29
|
-
* deciding whether there's more than one.
|
|
32
|
+
* - Distinct, atomic decision from form-control-programmatic-label-present
|
|
33
|
+
* (that rule checks a label exists at all; this one checks at most one
|
|
34
|
+
* matters). Whether a label contributes a name is decided by the shared
|
|
35
|
+
* helpers.labelContributesAccessibleName, so the two rules agree.
|
|
36
|
+
* - Accessibility-tree-ineligible labels (display:none, aria-hidden, etc.)
|
|
37
|
+
* are excluded before counting: they can't contribute to the name.
|
|
30
38
|
*/
|
|
31
39
|
|
|
32
40
|
const id = 'form-control-single-label';
|
|
@@ -63,6 +71,12 @@ function runInPage(ctx) {
|
|
|
63
71
|
|
|
64
72
|
const isAccTreeEligible =
|
|
65
73
|
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
74
|
+
const getAriaNameInfo =
|
|
75
|
+
helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
|
|
76
|
+
const labelContributesName =
|
|
77
|
+
helpers && typeof helpers.labelContributesAccessibleName === 'function'
|
|
78
|
+
? helpers.labelContributesAccessibleName
|
|
79
|
+
: null;
|
|
66
80
|
|
|
67
81
|
const selector =
|
|
68
82
|
'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
|
|
@@ -83,7 +97,8 @@ function runInPage(ctx) {
|
|
|
83
97
|
labelsByFor.get(forValue).push(lab);
|
|
84
98
|
}
|
|
85
99
|
|
|
86
|
-
const
|
|
100
|
+
const failOccurrences = [];
|
|
101
|
+
const cantTellOccurrences = [];
|
|
87
102
|
let applicableCount = 0;
|
|
88
103
|
|
|
89
104
|
for (const el of nodes) {
|
|
@@ -112,42 +127,94 @@ function runInPage(ctx) {
|
|
|
112
127
|
|
|
113
128
|
if (eligibleLabels.size <= 1) continue;
|
|
114
129
|
|
|
130
|
+
// An override (aria-labelledby / aria-label) supersedes every native
|
|
131
|
+
// <label>, so the labels contribute nothing to the accessible name and
|
|
132
|
+
// cannot compete.
|
|
133
|
+
const override = getAriaNameInfo ? getAriaNameInfo(el, ctx) : null;
|
|
134
|
+
if (override && override.present && override.value) continue;
|
|
135
|
+
|
|
136
|
+
// Without an override the labels feed the name; only labels with their
|
|
137
|
+
// own text compete for it.
|
|
138
|
+
const contributing = labelContributesName
|
|
139
|
+
? [...eligibleLabels].filter((lab) => labelContributesName(lab))
|
|
140
|
+
: [...eligibleLabels];
|
|
141
|
+
|
|
115
142
|
const tag = el.tagName.toLowerCase();
|
|
116
143
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
117
144
|
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
118
145
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
if (contributing.length >= 2) {
|
|
147
|
+
failOccurrences.push({
|
|
148
|
+
selector: stableSelector,
|
|
149
|
+
html,
|
|
150
|
+
summary: 'This form control is associated with more than one <label>.',
|
|
151
|
+
hint: 'Keep only one <label> per form control (either wrapping it or referencing it via for/id).',
|
|
152
|
+
occurrenceOutcome: 'fail',
|
|
153
|
+
i18n: {
|
|
154
|
+
summaryKey: 'formControlSingleLabel_summary_fail',
|
|
155
|
+
hintKey: 'formControlSingleLabel_hint_fail',
|
|
156
|
+
params: { element: tag, labelCount: String(contributing.length) }
|
|
157
|
+
},
|
|
158
|
+
data: {
|
|
159
|
+
details: {
|
|
160
|
+
reasonCode: 'FORM_FIELD_MULTIPLE_LABELS',
|
|
161
|
+
element: tag,
|
|
162
|
+
labelCount: contributing.length
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
} else if (contributing.length === 1) {
|
|
167
|
+
cantTellOccurrences.push({
|
|
168
|
+
selector: stableSelector,
|
|
169
|
+
html,
|
|
170
|
+
summary:
|
|
171
|
+
'This form control has one labelling <label> plus an extra empty <label> association.',
|
|
172
|
+
hint: 'Remove the redundant empty <label> so exactly one <label> is associated with the control.',
|
|
173
|
+
occurrenceOutcome: 'cantTell',
|
|
174
|
+
i18n: {
|
|
175
|
+
summaryKey: 'formControlSingleLabel_summary_cantTell',
|
|
176
|
+
hintKey: 'formControlSingleLabel_hint_cantTell',
|
|
177
|
+
params: { element: tag, labelCount: String(eligibleLabels.size) }
|
|
178
|
+
},
|
|
179
|
+
data: {
|
|
180
|
+
details: {
|
|
181
|
+
reasonCode: 'FORM_FIELD_EXTRA_EMPTY_LABEL',
|
|
182
|
+
element: tag,
|
|
183
|
+
labelCount: eligibleLabels.size,
|
|
184
|
+
contributingLabelCount: contributing.length
|
|
185
|
+
}
|
|
134
186
|
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// contributing.length === 0: no label carries text, so nothing competes
|
|
190
|
+
// for the name. A control left unnamed is form-control-programmatic-
|
|
191
|
+
// label-present's concern.
|
|
137
192
|
}
|
|
138
193
|
|
|
139
194
|
if (applicableCount === 0) {
|
|
140
195
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
141
196
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
197
|
+
|
|
198
|
+
const resolved = helpers.resolveTieredOutcome
|
|
199
|
+
? helpers.resolveTieredOutcome(
|
|
200
|
+
failOccurrences,
|
|
201
|
+
cantTellOccurrences,
|
|
202
|
+
rule.defaultSeverity || 'moderate'
|
|
203
|
+
)
|
|
204
|
+
: failOccurrences.length
|
|
205
|
+
? {
|
|
206
|
+
outcome: 'fail',
|
|
207
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
208
|
+
occurrences: failOccurrences
|
|
209
|
+
}
|
|
210
|
+
: cantTellOccurrences.length
|
|
211
|
+
? {
|
|
212
|
+
outcome: 'cantTell',
|
|
213
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
214
|
+
occurrences: cantTellOccurrences
|
|
215
|
+
}
|
|
216
|
+
: { outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
217
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
151
218
|
}
|
|
152
219
|
|
|
153
220
|
module.exports = { id, meta, runInPage };
|