@surea11y/core 1.2.0 → 1.4.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 +81 -7
- package/LICENSE +373 -21
- package/README.md +175 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +27 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +19 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +63 -18
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +63 -31
- package/src/checks/automatic/aria-allowed-attr.js +204 -80
- package/src/checks/automatic/aria-allowed-role.js +23 -7
- package/src/checks/automatic/aria-braille-equivalent.js +34 -10
- package/src/checks/automatic/aria-conditional-attr.js +32 -14
- package/src/checks/automatic/aria-deprecated-role.js +26 -11
- package/src/checks/automatic/aria-hidden-body.js +48 -23
- package/src/checks/automatic/aria-hidden-focus.js +420 -66
- package/src/checks/automatic/aria-prohibited-attr.js +327 -60
- package/src/checks/automatic/aria-prohibited-children.js +111 -103
- package/src/checks/automatic/aria-required-attr.js +29 -15
- package/src/checks/automatic/aria-required-children.js +44 -24
- package/src/checks/automatic/aria-required-parent.js +64 -35
- package/src/checks/automatic/aria-role-name-present.js +49 -21
- package/src/checks/automatic/aria-roles-valid.js +24 -12
- package/src/checks/automatic/aria-valid-attr-value.js +46 -22
- package/src/checks/automatic/aria-valid-attr.js +19 -5
- package/src/checks/automatic/autocomplete-valid.js +76 -16
- package/src/checks/automatic/avoid-inline-spacing.js +23 -8
- package/src/checks/automatic/binary-control-name-present.js +62 -50
- package/src/checks/automatic/button-name-present.js +54 -24
- package/src/checks/automatic/bypass-blocks-present.js +51 -32
- package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
- package/src/checks/automatic/combobox-name-present.js +40 -45
- package/src/checks/automatic/contrast-computable.js +363 -341
- package/src/checks/automatic/contrast-enhanced.js +489 -466
- package/src/checks/automatic/contrast-minimum.js +488 -465
- package/src/checks/automatic/css-orientation-lock.js +51 -35
- package/src/checks/automatic/definition-list-children-valid.js +46 -25
- package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
- package/src/checks/automatic/dialog-name-present.js +47 -85
- package/src/checks/automatic/dlitem-parent-valid.js +25 -8
- package/src/checks/automatic/duplicate-id-aria.js +28 -9
- package/src/checks/automatic/embed-text-alternative-present.js +88 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
- package/src/checks/automatic/form-control-single-label.js +50 -14
- package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
- package/src/checks/automatic/iframe-focusable-content.js +265 -22
- package/src/checks/automatic/iframe-name-present.js +33 -9
- package/src/checks/automatic/iframe-title-unique.js +32 -9
- package/src/checks/automatic/img-alt-present.js +54 -52
- package/src/checks/automatic/input-image-alt-present.js +141 -112
- package/src/checks/automatic/label-in-name.js +65 -41
- package/src/checks/automatic/language-page-present.js +111 -109
- package/src/checks/automatic/link-in-text-block.js +61 -19
- package/src/checks/automatic/link-name-present.js +47 -14
- package/src/checks/automatic/list-children-valid.js +40 -33
- package/src/checks/automatic/listbox-name-present.js +41 -19
- package/src/checks/automatic/listitem-parent-valid.js +48 -13
- package/src/checks/automatic/menuitem-name-present.js +41 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
- package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
- package/src/checks/automatic/meter-name-present.js +40 -36
- package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
- package/src/checks/automatic/object-text-alternative-present.js +93 -39
- package/src/checks/automatic/option-name-present.js +40 -21
- package/src/checks/automatic/page-title-present.js +19 -6
- package/src/checks/automatic/progressbar-name-present.js +49 -44
- package/src/checks/automatic/role-img-alt-present.js +211 -159
- package/src/checks/automatic/searchbox-name-present.js +41 -19
- package/src/checks/automatic/server-side-image-map-absent.js +27 -11
- package/src/checks/automatic/slider-name-present.js +42 -47
- package/src/checks/automatic/spinbutton-name-present.js +41 -19
- package/src/checks/automatic/summary-name-present.js +39 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
- package/src/checks/automatic/svg-text-alternative-present.js +262 -230
- package/src/checks/automatic/tab-name-present.js +39 -60
- package/src/checks/automatic/table-headers-attr-valid.js +27 -10
- package/src/checks/automatic/table-th-has-data-cells.js +24 -8
- package/src/checks/automatic/target-size-minimum.js +123 -48
- package/src/checks/automatic/td-has-header.js +53 -12
- package/src/checks/automatic/textbox-name-present.js +41 -19
- package/src/checks/automatic/tooltip-name-present.js +39 -18
- package/src/checks/automatic/treeitem-name-present.js +40 -21
- package/src/checks/automatic/valid-lang.js +22 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
- package/src/checks/manual/accesskeys-manual.js +17 -6
- package/src/checks/manual/area-alt-decorative-manual.js +194 -193
- package/src/checks/manual/area-alt-quality-manual.js +184 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
- package/src/checks/manual/aria-text-manual.js +20 -11
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
- package/src/checks/manual/css-hidden-focus.js +375 -169
- package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
- package/src/checks/manual/empty-heading-manual.js +41 -24
- package/src/checks/manual/empty-table-header-manual.js +69 -31
- package/src/checks/manual/focus-order-semantics-manual.js +60 -13
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
- package/src/checks/manual/heading-order-manual.js +50 -8
- package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
- package/src/checks/manual/image-redundant-alt-manual.js +38 -8
- package/src/checks/manual/img-alt-decorative-manual.js +133 -96
- package/src/checks/manual/img-alt-quality-manual.js +178 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
- package/src/checks/manual/label-title-only-manual.js +44 -28
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
- package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
- package/src/checks/manual/landmark-one-main-manual.js +38 -43
- package/src/checks/manual/landmark-unique-manual.js +78 -67
- package/src/checks/manual/link-name-quality-manual.js +45 -12
- package/src/checks/manual/media-transcript-present-manual.js +37 -22
- package/src/checks/manual/meta-viewport-large-manual.js +19 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
- package/src/checks/manual/p-as-heading-manual.js +24 -7
- package/src/checks/manual/page-has-heading-one-manual.js +42 -32
- package/src/checks/manual/page-title-patterns-manual.js +80 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
- package/src/checks/manual/region-manual.js +244 -60
- package/src/checks/manual/scope-attr-valid-manual.js +13 -4
- package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
- package/src/checks/manual/skip-link-manual.js +42 -18
- package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
- package/src/checks/manual/tabindex-manual.js +13 -4
- package/src/checks/manual/table-duplicate-name-manual.js +22 -11
- package/src/checks/manual/table-fake-caption-manual.js +48 -10
- package/src/checks/manual/video-caption-manual.js +17 -4
- package/src/checks/manual-review.js +58 -12
- package/src/core.js +41705 -29650
- package/src/index.js +2 -0
- package/src/report.js +109 -47
- package/src/sarif.js +190 -0
- package/surea11y.browser.js +37774 -0
- package/bin/core.js +0 -348
- package/docs/CLI.md +0 -75
- package/src/catalogs/composites.wcag.js +0 -490
- 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 -970
- package/src/core/contrast-helpers.js +0 -1147
- package/src/core/dom-helpers.js +0 -4235
- package/src/core/dom-runner.js +0 -671
- package/src/core/frame-messaging.js +0 -210
- package/src/core/frame-scan.js +0 -178
- package/src/core/rollup-composites.js +0 -135
- package/src/core/rule-meta.js +0 -159
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -923
- package/src/i18n/fr.js +0 -844
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -55
- 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 = 'label-in-name';
|
|
@@ -5,7 +7,7 @@ const id = 'label-in-name';
|
|
|
5
7
|
const meta = {
|
|
6
8
|
title: 'Label in Name: accessible name contains visible text',
|
|
7
9
|
description:
|
|
8
|
-
|
|
10
|
+
'Checks that when a control has a visible text label, the accessible name (from aria-label/aria-labelledby) contains that visible label text (WCAG 2.5.3).',
|
|
9
11
|
i18n: {
|
|
10
12
|
titleKey: 'labelInName_title',
|
|
11
13
|
descriptionKey: 'labelInName_description'
|
|
@@ -37,20 +39,27 @@ function runInPage(ctx) {
|
|
|
37
39
|
|
|
38
40
|
// Applicability: focus/activation controls with explicit ARIA naming.
|
|
39
41
|
// NOTE: aria-hidden is intentionally NOT excluded here; it does not affect visual rendering.
|
|
40
|
-
const selector =
|
|
42
|
+
const selector =
|
|
43
|
+
':is(button, a[href], summary, input:not([type="hidden"]), textarea, select, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="searchbox"], [role="tab"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], [role="treeitem"], [role="gridcell"]):not([hidden]):not([disabled]):not([aria-disabled="true"]):is([aria-label], [aria-labelledby])';
|
|
41
44
|
|
|
42
|
-
const nodes = helpers.queryAllSmart
|
|
45
|
+
const nodes = helpers.queryAllSmart
|
|
46
|
+
? helpers.queryAllSmart(selector)
|
|
47
|
+
: helpers.queryAll(selector);
|
|
43
48
|
|
|
44
49
|
function norm(s) {
|
|
45
|
-
const v =
|
|
50
|
+
const v = s == null ? '' : String(s);
|
|
46
51
|
// deterministic normalization: trim + collapse whitespace + case-fold
|
|
47
52
|
return v.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
function getElementDescriptor(el) {
|
|
51
|
-
const tag =
|
|
52
|
-
let role
|
|
53
|
-
try {
|
|
56
|
+
const tag = el && el.tagName ? String(el.tagName).toLowerCase() : 'element';
|
|
57
|
+
let role;
|
|
58
|
+
try {
|
|
59
|
+
role = el && el.getAttribute ? el.getAttribute('role') || '' : '';
|
|
60
|
+
} catch {
|
|
61
|
+
role = '';
|
|
62
|
+
}
|
|
54
63
|
const r = String(role || '').trim();
|
|
55
64
|
return r ? `${tag}[role="${r}"]` : tag;
|
|
56
65
|
}
|
|
@@ -62,35 +71,38 @@ function runInPage(ctx) {
|
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
function isNonRenderedTag(el) {
|
|
65
|
-
const tn =
|
|
66
|
-
return
|
|
74
|
+
const tn = el && el.tagName ? String(el.tagName).toLowerCase() : '';
|
|
75
|
+
return (
|
|
76
|
+
tn === 'script' ||
|
|
77
|
+
tn === 'style' ||
|
|
78
|
+
tn === 'template' ||
|
|
79
|
+
tn === 'noscript' ||
|
|
80
|
+
tn === 'meta' ||
|
|
81
|
+
tn === 'link'
|
|
82
|
+
);
|
|
67
83
|
}
|
|
68
84
|
|
|
69
85
|
function isDomVisible(el) {
|
|
70
86
|
if (!el) return false;
|
|
71
|
-
if (helpers.isDomVisibleEligible)
|
|
72
|
-
|
|
87
|
+
if (helpers.isDomVisibleEligible)
|
|
88
|
+
return !!helpers.isDomVisibleEligible(el, ctx, { targetSet: 'dom' }).eligible;
|
|
89
|
+
if (helpers.getEligibilityInfo)
|
|
90
|
+
return !!helpers.getEligibilityInfo(el, ctx, { targetSet: 'dom' }).eligible;
|
|
73
91
|
return true;
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
// Unlike isDomVisible above, this also excludes aria-hidden subtrees.
|
|
77
|
-
// Needed
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// (measuring rendered glyph shapes), not replicable here since this rule
|
|
85
|
-
// runs against static markup with no real canvas/font rendering
|
|
86
|
-
// available; excluding aria-hidden content is a cheaper, static-markup
|
|
87
|
-
// signal that gets the common case (decorative icon fonts) right without
|
|
88
|
-
// needing one. Confirmed via a live-DOM cross-engine run 2026-07-21
|
|
89
|
-
// (Angular Material's theme-picker button, aria-label="Select a theme",
|
|
90
|
-
// aria-hidden icon rendering literally as "format_color_fill").
|
|
95
|
+
// Needed for collectVisibleTextUnder's per-text-node check below: an
|
|
96
|
+
// aria-hidden icon-font glyph (e.g. an <i aria-hidden="true"> ligature
|
|
97
|
+
// rendering as "format_color_fill") is DOM-visible pixels but is never
|
|
98
|
+
// perceived as literal readable words the way real visible text is.
|
|
99
|
+
// Excluding aria-hidden content is a cheap static-markup signal that gets
|
|
100
|
+
// the common case (decorative icon fonts) right — an icon-only button
|
|
101
|
+
// named via aria-label shouldn't have its glyph name counted as text.
|
|
91
102
|
function isAccEligible(el) {
|
|
92
103
|
if (!el) return false;
|
|
93
|
-
const fn =
|
|
104
|
+
const fn =
|
|
105
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
94
106
|
if (!fn) return isDomVisible(el);
|
|
95
107
|
try {
|
|
96
108
|
const r = fn(el, ctx);
|
|
@@ -105,11 +117,12 @@ function runInPage(ctx) {
|
|
|
105
117
|
// than referencing the global NodeFilter object directly: runInPage must
|
|
106
118
|
// have zero free vars (see docs/RULE_AUTHORING.md's free-var footgun) and
|
|
107
119
|
// NodeFilter is not itself present in the execution realm this function
|
|
108
|
-
// actually runs in, unlike window/document.
|
|
109
|
-
// silently
|
|
110
|
-
// raw container.textContent (which respects none of
|
|
111
|
-
// isDomVisible/isAccEligible below, since that whole
|
|
112
|
-
// skipped in the fallback path). Same pattern already
|
|
120
|
+
// actually runs in, unlike window/document. Referencing the global
|
|
121
|
+
// directly would silently make createTreeWalker throw on every call,
|
|
122
|
+
// falling back to raw container.textContent (which respects none of
|
|
123
|
+
// isNonRenderedTag/isDomVisible/isAccEligible below, since that whole
|
|
124
|
+
// per-node loop is skipped in the fallback path). Same pattern already
|
|
125
|
+
// used correctly in
|
|
113
126
|
// region-manual.js's own createTreeWalker call.
|
|
114
127
|
const SHOW_TEXT = 4;
|
|
115
128
|
|
|
@@ -118,7 +131,7 @@ function runInPage(ctx) {
|
|
|
118
131
|
if (!isDomVisible(container)) return '';
|
|
119
132
|
|
|
120
133
|
// TreeWalker is deterministic in document order.
|
|
121
|
-
let walker
|
|
134
|
+
let walker;
|
|
122
135
|
try {
|
|
123
136
|
walker = document.createTreeWalker(container, SHOW_TEXT, null);
|
|
124
137
|
} catch {
|
|
@@ -134,7 +147,7 @@ function runInPage(ctx) {
|
|
|
134
147
|
}
|
|
135
148
|
|
|
136
149
|
const parts = [];
|
|
137
|
-
let n
|
|
150
|
+
let n;
|
|
138
151
|
// eslint-disable-next-line no-cond-assign
|
|
139
152
|
while ((n = walker.nextNode())) {
|
|
140
153
|
try {
|
|
@@ -183,7 +196,7 @@ function runInPage(ctx) {
|
|
|
183
196
|
// contextSelector support), not a single element with its own
|
|
184
197
|
// .querySelector to call directly.
|
|
185
198
|
try {
|
|
186
|
-
const idAttribute = control && control.getAttribute ?
|
|
199
|
+
const idAttribute = control && control.getAttribute ? control.getAttribute('id') || '' : '';
|
|
187
200
|
const key = String(idAttribute || '').trim();
|
|
188
201
|
if (key && document && document.querySelector) {
|
|
189
202
|
const l = document.querySelector('label[for="' + CSS.escape(key) + '"]');
|
|
@@ -215,10 +228,10 @@ function runInPage(ctx) {
|
|
|
215
228
|
// 1) <label> association for form controls
|
|
216
229
|
// 2) visible text inside the element
|
|
217
230
|
// 3) visible text from aria-labelledby referenced elements
|
|
218
|
-
let text
|
|
231
|
+
let text;
|
|
219
232
|
let source = 'none';
|
|
220
233
|
|
|
221
|
-
const tn =
|
|
234
|
+
const tn = el && el.tagName ? String(el.tagName).toLowerCase() : '';
|
|
222
235
|
|
|
223
236
|
// 1) Label association (native form controls)
|
|
224
237
|
const isFormControl = tn === 'input' || tn === 'select' || tn === 'textarea';
|
|
@@ -244,7 +257,7 @@ function runInPage(ctx) {
|
|
|
244
257
|
if (idrefs && helpers.resolveIdRefs) {
|
|
245
258
|
const r = helpers.resolveIdRefs(idrefs, ctx, { maxRefs: 8 });
|
|
246
259
|
const parts = [];
|
|
247
|
-
for (const ref of
|
|
260
|
+
for (const ref of r && Array.isArray(r.refs) ? r.refs : []) {
|
|
248
261
|
if (!ref || !ref.tagName) continue;
|
|
249
262
|
if (!isDomVisible(ref)) continue;
|
|
250
263
|
const t = collectVisibleTextUnder(ref);
|
|
@@ -275,7 +288,9 @@ function runInPage(ctx) {
|
|
|
275
288
|
|
|
276
289
|
let acc = { present: false, value: '', mechanism: 'none', flags: [] };
|
|
277
290
|
try {
|
|
278
|
-
acc = helpers.getAccessibleNameInfo
|
|
291
|
+
acc = helpers.getAccessibleNameInfo
|
|
292
|
+
? helpers.getAccessibleNameInfo(el, ctx, { maxRefs: 8 })
|
|
293
|
+
: acc;
|
|
279
294
|
} catch {
|
|
280
295
|
acc = { present: false, value: '', mechanism: 'none', flags: ['exception'] };
|
|
281
296
|
}
|
|
@@ -286,7 +301,9 @@ function runInPage(ctx) {
|
|
|
286
301
|
|
|
287
302
|
if (!contains) {
|
|
288
303
|
const selectorOut = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
289
|
-
const html = helpers.getOuterHtmlSnippet
|
|
304
|
+
const html = helpers.getOuterHtmlSnippet
|
|
305
|
+
? helpers.getOuterHtmlSnippet(el)
|
|
306
|
+
: el.outerHTML || '';
|
|
290
307
|
|
|
291
308
|
occurrences.push({
|
|
292
309
|
selector: selectorOut,
|
|
@@ -317,8 +334,15 @@ function runInPage(ctx) {
|
|
|
317
334
|
}
|
|
318
335
|
}
|
|
319
336
|
|
|
320
|
-
if (applicableCount === 0)
|
|
321
|
-
|
|
337
|
+
if (applicableCount === 0)
|
|
338
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
339
|
+
if (occurrences.length)
|
|
340
|
+
return {
|
|
341
|
+
ruleId: rule.ruleId,
|
|
342
|
+
outcome: 'fail',
|
|
343
|
+
severity: rule.defaultSeverity || 'minor',
|
|
344
|
+
occurrences
|
|
345
|
+
};
|
|
322
346
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
323
347
|
}
|
|
324
348
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -34,33 +36,33 @@
|
|
|
34
36
|
const id = 'html-lang-attr-present';
|
|
35
37
|
|
|
36
38
|
const meta = {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
39
|
+
title: 'Page language is declared',
|
|
40
|
+
description: 'Checks that the default language of the page is programmatically declared.',
|
|
41
|
+
i18n: {
|
|
42
|
+
titleKey: 'html_lang_attr_title',
|
|
43
|
+
descriptionKey: 'html_lang_attr_description'
|
|
44
|
+
},
|
|
45
|
+
helpUrl: null,
|
|
46
|
+
tags: ['wcag2a', 'wcag311', 'structure', 'language', 'automatic', 'atomic'],
|
|
47
|
+
wcagSc: ['3.1.1'],
|
|
48
|
+
normativeMappings: [
|
|
49
|
+
{
|
|
50
|
+
standard: 'WCAG',
|
|
51
|
+
version: '2.2',
|
|
52
|
+
requirement: '3.1.1',
|
|
53
|
+
title: 'Language of Page',
|
|
54
|
+
conformanceLevel: 'A'
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
defaultSeverity: 'serious',
|
|
58
|
+
category: 'understandable',
|
|
59
|
+
type: 'automatic',
|
|
60
|
+
defaultConfidence: 'high',
|
|
61
|
+
coverage: {
|
|
62
|
+
facetsBySc: {
|
|
63
|
+
'3.1.1': ['html-lang-attr-present']
|
|
63
64
|
}
|
|
65
|
+
}
|
|
64
66
|
};
|
|
65
67
|
|
|
66
68
|
// This check is inherently whole-document (does the PAGE have this
|
|
@@ -72,96 +74,96 @@ function applicability(ctx) {
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
function runInPage(ctx) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const tag = (html && html.tagName) ? String(html.tagName).toLowerCase() : '';
|
|
79
|
-
if (!html || tag !== 'html') {
|
|
80
|
-
return {
|
|
81
|
-
ruleId: rule.ruleId,
|
|
82
|
-
outcome: 'notApplicable',
|
|
83
|
-
severity: 'minor',
|
|
84
|
-
occurrences: []
|
|
85
|
-
};
|
|
86
|
-
}
|
|
77
|
+
const { document, rule, helpers } = ctx;
|
|
78
|
+
const html = document && document.documentElement;
|
|
87
79
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
80
|
+
const tag = html && html.tagName ? String(html.tagName).toLowerCase() : '';
|
|
81
|
+
if (!html || tag !== 'html') {
|
|
82
|
+
return {
|
|
83
|
+
ruleId: rule.ruleId,
|
|
84
|
+
outcome: 'notApplicable',
|
|
85
|
+
severity: 'minor',
|
|
86
|
+
occurrences: []
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const visibilityFilter = { targetSet: 'acc', accEligible: true, reasons: [] };
|
|
91
|
+
|
|
92
|
+
function pushFail(summaryKey, hintKey, params, details) {
|
|
93
|
+
const baseOccurrence = {
|
|
94
|
+
summary: '',
|
|
95
|
+
hint: '',
|
|
96
|
+
i18n: {
|
|
97
|
+
summaryKey,
|
|
98
|
+
hintKey,
|
|
99
|
+
params: params && typeof params === 'object' ? params : {}
|
|
100
|
+
},
|
|
101
|
+
data: {
|
|
102
|
+
visibilityFilter,
|
|
103
|
+
details: details && typeof details === 'object' ? details : {}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
112
106
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (rawLang === null) {
|
|
117
|
-
return {
|
|
118
|
-
ruleId: rule.ruleId,
|
|
119
|
-
outcome: 'fail',
|
|
120
|
-
severity: rule.defaultSeverity,
|
|
121
|
-
occurrences: pushFail(
|
|
122
|
-
'html_lang_attr_missing_absent',
|
|
123
|
-
'html_lang_attr_hint_missing_absent',
|
|
124
|
-
{},
|
|
125
|
-
{ reasonCode: 'lang-missing', location: 'html' }
|
|
126
|
-
)
|
|
127
|
-
};
|
|
107
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
108
|
+
return [helpers.reportOccurrence(html, baseOccurrence)];
|
|
128
109
|
}
|
|
129
110
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
outcome: 'fail',
|
|
134
|
-
severity: rule.defaultSeverity,
|
|
135
|
-
occurrences: pushFail(
|
|
136
|
-
'html_lang_attr_missing_empty',
|
|
137
|
-
'html_lang_attr_hint_missing_empty',
|
|
138
|
-
{},
|
|
139
|
-
{ reasonCode: 'lang-empty', location: 'html' }
|
|
140
|
-
)
|
|
141
|
-
};
|
|
142
|
-
}
|
|
111
|
+
// Never compute selector/snippet in the rule.
|
|
112
|
+
return [{ selector: '', html: '', ...baseOccurrence }];
|
|
113
|
+
}
|
|
143
114
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
115
|
+
const rawLang = html.getAttribute('lang'); // null if missing
|
|
116
|
+
const lang = (rawLang || '').trim();
|
|
117
|
+
|
|
118
|
+
if (rawLang === null) {
|
|
119
|
+
return {
|
|
120
|
+
ruleId: rule.ruleId,
|
|
121
|
+
outcome: 'fail',
|
|
122
|
+
severity: rule.defaultSeverity,
|
|
123
|
+
occurrences: pushFail(
|
|
124
|
+
'html_lang_attr_missing_absent',
|
|
125
|
+
'html_lang_attr_hint_missing_absent',
|
|
126
|
+
{},
|
|
127
|
+
{ reasonCode: 'lang-missing', location: 'html' }
|
|
128
|
+
)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (lang.length === 0) {
|
|
133
|
+
return {
|
|
134
|
+
ruleId: rule.ruleId,
|
|
135
|
+
outcome: 'fail',
|
|
136
|
+
severity: rule.defaultSeverity,
|
|
137
|
+
occurrences: pushFail(
|
|
138
|
+
'html_lang_attr_missing_empty',
|
|
139
|
+
'html_lang_attr_hint_missing_empty',
|
|
140
|
+
{},
|
|
141
|
+
{ reasonCode: 'lang-empty', location: 'html' }
|
|
142
|
+
)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
158
145
|
|
|
146
|
+
// Minimal BCP47 primary subtag check
|
|
147
|
+
if (!/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/.test(lang)) {
|
|
159
148
|
return {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
149
|
+
ruleId: rule.ruleId,
|
|
150
|
+
outcome: 'fail',
|
|
151
|
+
severity: rule.defaultSeverity,
|
|
152
|
+
occurrences: pushFail(
|
|
153
|
+
'html_lang_attr_invalid',
|
|
154
|
+
'html_lang_attr_hint_invalid',
|
|
155
|
+
{ lang },
|
|
156
|
+
{ reasonCode: 'lang-invalid-bcp47', lang }
|
|
157
|
+
)
|
|
164
158
|
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
ruleId: rule.ruleId,
|
|
163
|
+
outcome: 'pass',
|
|
164
|
+
severity: 'minor',
|
|
165
|
+
occurrences: []
|
|
166
|
+
};
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
module.exports = { id, meta, runInPage, applicability };
|
|
169
|
+
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -47,7 +49,8 @@
|
|
|
47
49
|
const id = 'link-in-text-block';
|
|
48
50
|
|
|
49
51
|
const meta = {
|
|
50
|
-
title:
|
|
52
|
+
title:
|
|
53
|
+
'Links in text blocks must be distinguishable from surrounding text without relying on color alone',
|
|
51
54
|
description:
|
|
52
55
|
'Checks that a link inside a run of text is visually distinguishable from the surrounding text by underline, a font-weight/style difference, or a sufficient (>=3:1) color-contrast difference — not by color alone.',
|
|
53
56
|
i18n: {
|
|
@@ -55,10 +58,26 @@ const meta = {
|
|
|
55
58
|
descriptionKey: 'linkInTextBlock_description'
|
|
56
59
|
},
|
|
57
60
|
helpUrl: null,
|
|
58
|
-
tags: [
|
|
61
|
+
tags: [
|
|
62
|
+
'wcag2a',
|
|
63
|
+
'wcag141',
|
|
64
|
+
'navigation',
|
|
65
|
+
'color',
|
|
66
|
+
'links',
|
|
67
|
+
'contrast',
|
|
68
|
+
'atomic',
|
|
69
|
+
'automatic',
|
|
70
|
+
'dom'
|
|
71
|
+
],
|
|
59
72
|
wcagSc: ['1.4.1'],
|
|
60
73
|
normativeMappings: [
|
|
61
|
-
{
|
|
74
|
+
{
|
|
75
|
+
standard: 'WCAG',
|
|
76
|
+
version: '2.2',
|
|
77
|
+
requirement: '1.4.1',
|
|
78
|
+
title: 'Use of Color',
|
|
79
|
+
conformanceLevel: 'A'
|
|
80
|
+
}
|
|
62
81
|
],
|
|
63
82
|
defaultSeverity: 'serious',
|
|
64
83
|
category: 'perceivable',
|
|
@@ -68,7 +87,7 @@ const meta = {
|
|
|
68
87
|
};
|
|
69
88
|
|
|
70
89
|
function runInPage(ctx) {
|
|
71
|
-
const {
|
|
90
|
+
const { helpers, rule, engineOptions } = ctx;
|
|
72
91
|
|
|
73
92
|
function safeComputedStyle(el) {
|
|
74
93
|
try {
|
|
@@ -77,14 +96,16 @@ function runInPage(ctx) {
|
|
|
77
96
|
const cs = helpers.computedStyle(el);
|
|
78
97
|
if (cs) return cs;
|
|
79
98
|
}
|
|
80
|
-
const view =
|
|
99
|
+
const view =
|
|
100
|
+
el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : null;
|
|
81
101
|
if (view && typeof view.getComputedStyle === 'function') return view.getComputedStyle(el);
|
|
82
102
|
} catch {}
|
|
83
103
|
return null;
|
|
84
104
|
}
|
|
85
105
|
|
|
86
106
|
function decorationTokens(cs) {
|
|
87
|
-
const raw =
|
|
107
|
+
const raw =
|
|
108
|
+
`${(cs && cs.textDecorationLine) || ''} ${(cs && cs.textDecoration) || ''}`.toLowerCase();
|
|
88
109
|
return raw.split(/\s+/).filter(Boolean);
|
|
89
110
|
}
|
|
90
111
|
|
|
@@ -102,9 +123,10 @@ function runInPage(ctx) {
|
|
|
102
123
|
return false;
|
|
103
124
|
}
|
|
104
125
|
|
|
105
|
-
const contrastOpts =
|
|
106
|
-
|
|
107
|
-
|
|
126
|
+
const contrastOpts =
|
|
127
|
+
engineOptions && typeof engineOptions.contrast === 'object' && engineOptions.contrast
|
|
128
|
+
? engineOptions.contrast
|
|
129
|
+
: {};
|
|
108
130
|
const mode = contrastOpts.mode === 'auditorAssist' ? 'auditorAssist' : 'strictConformance';
|
|
109
131
|
const rootCanvasFallback =
|
|
110
132
|
typeof contrastOpts.rootCanvasFallback === 'string' && contrastOpts.rootCanvasFallback.trim()
|
|
@@ -114,7 +136,9 @@ function runInPage(ctx) {
|
|
|
114
136
|
const c = helpers && helpers.contrast ? helpers.contrast : null;
|
|
115
137
|
|
|
116
138
|
const selector = 'a[href]';
|
|
117
|
-
const nodes = helpers.queryAllSmart
|
|
139
|
+
const nodes = helpers.queryAllSmart
|
|
140
|
+
? helpers.queryAllSmart(selector)
|
|
141
|
+
: helpers.queryAll(selector);
|
|
118
142
|
|
|
119
143
|
const occurrences = [];
|
|
120
144
|
let applicableCount = 0;
|
|
@@ -123,7 +147,8 @@ function runInPage(ctx) {
|
|
|
123
147
|
if (!el || !el.getAttribute) continue;
|
|
124
148
|
|
|
125
149
|
const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
|
|
126
|
-
const eligible =
|
|
150
|
+
const eligible =
|
|
151
|
+
typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
|
|
127
152
|
if (!eligible) continue;
|
|
128
153
|
|
|
129
154
|
const parent = el.parentElement;
|
|
@@ -156,13 +181,22 @@ function runInPage(ctx) {
|
|
|
156
181
|
if (blocker && blocker.ok === false) {
|
|
157
182
|
// Not confidently computable — skip (benefit of the doubt).
|
|
158
183
|
} else {
|
|
159
|
-
const bg = c.computeEffectiveBackground(el, {
|
|
184
|
+
const bg = c.computeEffectiveBackground(el, {
|
|
185
|
+
contrast: { mode, rootCanvasFallback },
|
|
186
|
+
collectStack: false
|
|
187
|
+
});
|
|
160
188
|
const fgLink = c.computeEffectiveForeground(el);
|
|
161
189
|
const fgParent = c.computeEffectiveForeground(parent);
|
|
162
190
|
|
|
163
191
|
if (bg && bg.ok && bg.rgba && fgLink && fgLink.rgba && fgParent && fgParent.rgba) {
|
|
164
|
-
const fgLinkOpaque =
|
|
165
|
-
|
|
192
|
+
const fgLinkOpaque =
|
|
193
|
+
fgLink.rgba.a < 1
|
|
194
|
+
? c.compositeRgba(fgLink.rgba, bg.rgba)
|
|
195
|
+
: { r: fgLink.rgba.r, g: fgLink.rgba.g, b: fgLink.rgba.b, a: 1 };
|
|
196
|
+
const fgParentOpaque =
|
|
197
|
+
fgParent.rgba.a < 1
|
|
198
|
+
? c.compositeRgba(fgParent.rgba, bg.rgba)
|
|
199
|
+
: { r: fgParent.rgba.r, g: fgParent.rgba.g, b: fgParent.rgba.b, a: 1 };
|
|
166
200
|
|
|
167
201
|
ratio = c.contrastRatio(fgLinkOpaque, fgParentOpaque);
|
|
168
202
|
fgLinkHex = c.rgbToHex ? c.rgbToHex(fgLinkOpaque) : '';
|
|
@@ -178,16 +212,19 @@ function runInPage(ctx) {
|
|
|
178
212
|
|
|
179
213
|
if (!flagged) continue;
|
|
180
214
|
|
|
181
|
-
const eligInfo = helpers.getEligibilityInfo
|
|
215
|
+
const eligInfo = helpers.getEligibilityInfo
|
|
216
|
+
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
217
|
+
: null;
|
|
182
218
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
183
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
219
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
184
220
|
const tag = (el.tagName || '').toLowerCase();
|
|
185
221
|
const ratioStr = c.round2 ? c.round2(ratio) : String(ratio);
|
|
186
222
|
|
|
187
223
|
occurrences.push({
|
|
188
224
|
selector: stableSelector,
|
|
189
225
|
html,
|
|
190
|
-
summary:
|
|
226
|
+
summary:
|
|
227
|
+
'This link in a block of text relies on color alone to be distinguished from the surrounding text.',
|
|
191
228
|
hint: 'Add an underline, a font-weight/style difference, or increase the color contrast between the link and surrounding text to at least 3:1.',
|
|
192
229
|
i18n: {
|
|
193
230
|
summaryKey: 'linkInTextBlock_summary_fail',
|
|
@@ -209,9 +246,14 @@ function runInPage(ctx) {
|
|
|
209
246
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
210
247
|
}
|
|
211
248
|
if (occurrences.length) {
|
|
212
|
-
return {
|
|
249
|
+
return {
|
|
250
|
+
ruleId: rule.ruleId,
|
|
251
|
+
outcome: 'fail',
|
|
252
|
+
severity: rule.defaultSeverity || 'serious',
|
|
253
|
+
occurrences
|
|
254
|
+
};
|
|
213
255
|
}
|
|
214
256
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
215
257
|
}
|
|
216
258
|
|
|
217
|
-
module.exports = { id, meta, runInPage };
|
|
259
|
+
module.exports = { id, meta, runInPage };
|