@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,20 +1,21 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @check skip-link
|
|
5
7
|
* @atomic true
|
|
6
8
|
* @summary A "skip" link must resolve to a real, usable target
|
|
7
|
-
* @standard Best Practices (
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
|
|
8
10
|
* @applicability
|
|
9
11
|
* Applies to <a href="#fragment"> elements whose accessible name
|
|
10
12
|
* matches a common "skip to ..." / "jump to ..." authoring convention
|
|
11
13
|
* (case-insensitive "skip" or "jump to" in the name) — the recognizable
|
|
12
14
|
* pattern for a skip-navigation link, not every same-page anchor link
|
|
13
|
-
* on the page. "jump to"
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* caught it. Text-pattern matching itself stays deliberate (see
|
|
15
|
+
* on the page. "jump to" is included alongside "skip" since real skip
|
|
16
|
+
* links use both conventions (e.g. a "Jump to section" link, which a
|
|
17
|
+
* purely positional match would catch but a "skip"-only text pattern
|
|
18
|
+
* would miss). Text-pattern matching itself stays deliberate (see
|
|
18
19
|
* implementation-notes) — this only widens the known-convention list.
|
|
19
20
|
* @expectation
|
|
20
21
|
* The link's fragment resolves to a real element in the document
|
|
@@ -37,7 +38,8 @@ const id = 'skip-link';
|
|
|
37
38
|
|
|
38
39
|
const meta = {
|
|
39
40
|
title: 'Skip link must have a resolvable, usable target',
|
|
40
|
-
description:
|
|
41
|
+
description:
|
|
42
|
+
'Checks that a "skip to ..." link\'s href fragment resolves to a real, currently usable element in the document.',
|
|
41
43
|
i18n: {
|
|
42
44
|
titleKey: 'skipLink_title',
|
|
43
45
|
descriptionKey: 'skipLink_description'
|
|
@@ -57,7 +59,9 @@ function runInPage(ctx) {
|
|
|
57
59
|
const { document, helpers, rule } = ctx;
|
|
58
60
|
|
|
59
61
|
function normalizeWs(s) {
|
|
60
|
-
return String(s || '')
|
|
62
|
+
return String(s || '')
|
|
63
|
+
.replace(/\s+/g, ' ')
|
|
64
|
+
.trim();
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
function getAccessibleNameText(el) {
|
|
@@ -105,7 +109,9 @@ function runInPage(ctx) {
|
|
|
105
109
|
|
|
106
110
|
const geometrySupported = hasReliableGeometrySupport();
|
|
107
111
|
|
|
108
|
-
const nodes = helpers.queryAllSmart
|
|
112
|
+
const nodes = helpers.queryAllSmart
|
|
113
|
+
? helpers.queryAllSmart('a[href]')
|
|
114
|
+
: helpers.queryAll('a[href]');
|
|
109
115
|
|
|
110
116
|
const occurrences = [];
|
|
111
117
|
let applicableCount = 0;
|
|
@@ -147,18 +153,29 @@ function runInPage(ctx) {
|
|
|
147
153
|
const accEligibility = toEligibility(
|
|
148
154
|
helpers.getEligibilityInfo
|
|
149
155
|
? helpers.getEligibilityInfo(target, ctx, { targetSet: 'acc' })
|
|
150
|
-
:
|
|
156
|
+
: helpers.isAccTreeEligible
|
|
157
|
+
? helpers.isAccTreeEligible(target, ctx)
|
|
158
|
+
: { eligible: true, reasons: [] }
|
|
151
159
|
);
|
|
152
160
|
|
|
153
161
|
let geometryEligibility = null;
|
|
154
162
|
let geometryReasonCode = null;
|
|
155
163
|
if (geometrySupported && helpers.isDomVisibleEligible) {
|
|
156
164
|
geometryEligibility = toEligibility(
|
|
157
|
-
helpers.isDomVisibleEligible(target, ctx, {
|
|
165
|
+
helpers.isDomVisibleEligible(target, ctx, {
|
|
166
|
+
visibilityMode: 'styleAndGeometry',
|
|
167
|
+
ignoreOpacity: true
|
|
168
|
+
})
|
|
158
169
|
);
|
|
159
|
-
if (
|
|
170
|
+
if (
|
|
171
|
+
!geometryEligibility.eligible &&
|
|
172
|
+
geometryEligibility.reasons.includes('noClientRects')
|
|
173
|
+
) {
|
|
160
174
|
geometryReasonCode = 'NO_CLIENT_RECTS';
|
|
161
|
-
} else if (
|
|
175
|
+
} else if (
|
|
176
|
+
!geometryEligibility.eligible &&
|
|
177
|
+
geometryEligibility.reasons.includes('zeroArea')
|
|
178
|
+
) {
|
|
162
179
|
geometryReasonCode = 'ZERO_AREA_TARGET';
|
|
163
180
|
}
|
|
164
181
|
}
|
|
@@ -168,7 +185,9 @@ function runInPage(ctx) {
|
|
|
168
185
|
if (!unusableByAcc && !unusableByGeometry) continue;
|
|
169
186
|
|
|
170
187
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
171
|
-
const html = helpers.getOuterHtmlSnippet
|
|
188
|
+
const html = helpers.getOuterHtmlSnippet
|
|
189
|
+
? helpers.getOuterHtmlSnippet(el)
|
|
190
|
+
: el.outerHTML || '';
|
|
172
191
|
|
|
173
192
|
occurrences.push({
|
|
174
193
|
selector: stableSelector,
|
|
@@ -202,13 +221,13 @@ function runInPage(ctx) {
|
|
|
202
221
|
}
|
|
203
222
|
|
|
204
223
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
205
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
224
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
206
225
|
|
|
207
226
|
occurrences.push({
|
|
208
227
|
selector: stableSelector,
|
|
209
228
|
html,
|
|
210
|
-
summary:
|
|
211
|
-
hint:
|
|
229
|
+
summary: "This skip link's target does not exist.",
|
|
230
|
+
hint: "Point the skip link's href at an id that exists in the document, or add the missing target element.",
|
|
212
231
|
i18n: {
|
|
213
232
|
summaryKey: 'skipLink_summary_cantTell',
|
|
214
233
|
hintKey: 'skipLink_hint_cantTell',
|
|
@@ -224,7 +243,12 @@ function runInPage(ctx) {
|
|
|
224
243
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
225
244
|
}
|
|
226
245
|
if (occurrences.length) {
|
|
227
|
-
return {
|
|
246
|
+
return {
|
|
247
|
+
ruleId: rule.ruleId,
|
|
248
|
+
outcome: 'cantTell',
|
|
249
|
+
severity: rule.defaultSeverity || 'minor',
|
|
250
|
+
occurrences
|
|
251
|
+
};
|
|
228
252
|
}
|
|
229
253
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
230
254
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -11,199 +13,240 @@
|
|
|
11
13
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
const id =
|
|
16
|
+
const id = 'svg-text-alternative-quality';
|
|
15
17
|
|
|
16
18
|
const meta = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
facetsBySc: {
|
|
35
|
-
'1.1.1': ['text-alternative-quality']
|
|
36
|
-
}
|
|
19
|
+
title: '<svg> text alternative must be appropriate (manual review)',
|
|
20
|
+
description:
|
|
21
|
+
'Flags applicable <svg> graphics with a detected text alternative for human review of appropriateness.',
|
|
22
|
+
i18n: {
|
|
23
|
+
titleKey: 'svg_textAltQuality_title',
|
|
24
|
+
descriptionKey: 'svg_textAltQuality_description'
|
|
25
|
+
},
|
|
26
|
+
helpUrl: null,
|
|
27
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'svg', 'manual', 'atomic'],
|
|
28
|
+
wcagSc: ['1.1.1'],
|
|
29
|
+
normativeMappings: [
|
|
30
|
+
{
|
|
31
|
+
standard: 'WCAG',
|
|
32
|
+
version: '2.2',
|
|
33
|
+
requirement: '1.1.1',
|
|
34
|
+
title: 'Non-text Content',
|
|
35
|
+
conformanceLevel: 'A'
|
|
37
36
|
}
|
|
37
|
+
],
|
|
38
|
+
defaultSeverity: 'minor',
|
|
39
|
+
category: 'perceivable',
|
|
40
|
+
type: 'manual',
|
|
41
|
+
defaultConfidence: 'medium',
|
|
42
|
+
coverage: {
|
|
43
|
+
facetsBySc: {
|
|
44
|
+
'1.1.1': ['text-alternative-quality']
|
|
45
|
+
}
|
|
46
|
+
}
|
|
38
47
|
};
|
|
39
48
|
|
|
40
49
|
function runInPage(ctx) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
const { document, root, helpers, rule } = ctx;
|
|
51
|
+
const safeRoot = root || document;
|
|
52
|
+
|
|
53
|
+
const queryAllSmart =
|
|
54
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
55
|
+
const queryAll =
|
|
56
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
57
|
+
? helpers.queryAll
|
|
58
|
+
: (sel) => {
|
|
59
|
+
try {
|
|
60
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
61
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
62
|
+
: [];
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
50
66
|
};
|
|
51
67
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
const buildSelector =
|
|
69
|
+
helpers && typeof helpers.buildSelector === 'function'
|
|
70
|
+
? helpers.buildSelector
|
|
71
|
+
: (el) => {
|
|
72
|
+
try {
|
|
73
|
+
if (!el || !el.tagName) return 'html';
|
|
74
|
+
const tag = (el.tagName || 'html').toLowerCase();
|
|
75
|
+
return el.id ? `${tag}#${el.id}` : tag;
|
|
76
|
+
} catch {
|
|
77
|
+
return 'html';
|
|
78
|
+
}
|
|
62
79
|
};
|
|
63
80
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
? helpers.getFocusableInfo
|
|
78
|
-
: null;
|
|
79
|
-
|
|
80
|
-
function isRolePresentationExcluded(el) {
|
|
81
|
-
const role = (() => {
|
|
82
|
-
try {
|
|
83
|
-
return String(el.getAttribute('role') || '').trim().toLowerCase();
|
|
84
|
-
} catch {
|
|
85
|
-
return '';
|
|
86
|
-
}
|
|
87
|
-
})();
|
|
88
|
-
if (role !== 'presentation' && role !== 'none') return false;
|
|
89
|
-
|
|
90
|
-
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
91
|
-
let focusable = false;
|
|
92
|
-
if (getFocusableInfo) {
|
|
93
|
-
const fi = (() => {
|
|
94
|
-
try {
|
|
95
|
-
return getFocusableInfo(el, ctx);
|
|
96
|
-
} catch {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
})();
|
|
100
|
-
focusable = !!(fi && fi.focusable);
|
|
101
|
-
} else {
|
|
102
|
-
const tabindex = el.getAttribute('tabindex');
|
|
103
|
-
focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
|
|
104
|
-
}
|
|
105
|
-
return !focusable;
|
|
106
|
-
}
|
|
81
|
+
const getOuterHtmlSnippet =
|
|
82
|
+
helpers && typeof helpers.getOuterHtmlSnippet === 'function'
|
|
83
|
+
? helpers.getOuterHtmlSnippet
|
|
84
|
+
: (el) => {
|
|
85
|
+
try {
|
|
86
|
+
return el && el.outerHTML ? String(el.outerHTML).slice(0, 2000) : '';
|
|
87
|
+
} catch {
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const getEligibilityInfo =
|
|
93
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
107
94
|
|
|
95
|
+
const isAccTreeEligible =
|
|
96
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
108
97
|
|
|
109
|
-
|
|
98
|
+
const getFocusableInfo =
|
|
99
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
100
|
+
|
|
101
|
+
function isRolePresentationExcluded(el) {
|
|
102
|
+
const role = (() => {
|
|
103
|
+
try {
|
|
104
|
+
return String(el.getAttribute('role') || '')
|
|
105
|
+
.trim()
|
|
106
|
+
.toLowerCase();
|
|
107
|
+
} catch {
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
if (role !== 'presentation' && role !== 'none') return false;
|
|
112
|
+
|
|
113
|
+
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
114
|
+
let focusable;
|
|
115
|
+
if (getFocusableInfo) {
|
|
116
|
+
const fi = (() => {
|
|
110
117
|
try {
|
|
111
|
-
|
|
118
|
+
return getFocusableInfo(el, ctx);
|
|
112
119
|
} catch {
|
|
113
|
-
|
|
120
|
+
return null;
|
|
114
121
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
})();
|
|
123
|
+
focusable = !!(fi && fi.focusable);
|
|
124
|
+
} else {
|
|
125
|
+
const tabindex = el.getAttribute('tabindex');
|
|
126
|
+
focusable =
|
|
127
|
+
tabindex != null &&
|
|
128
|
+
String(tabindex).trim() !== '' &&
|
|
129
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
119
130
|
}
|
|
131
|
+
return !focusable;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const els = (() => {
|
|
135
|
+
try {
|
|
136
|
+
return Array.from((queryAllSmart ? queryAllSmart('svg') : queryAll('svg')) || []);
|
|
137
|
+
} catch {
|
|
138
|
+
return queryAll('svg');
|
|
139
|
+
}
|
|
140
|
+
})();
|
|
120
141
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const trim = (v) => (v == null ? '' : String(v)).trim();
|
|
125
|
-
|
|
126
|
-
for (const el of els) {
|
|
127
|
-
if (!el || !el.getAttribute) continue;
|
|
142
|
+
if (!els.length) {
|
|
143
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
144
|
+
}
|
|
128
145
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const elig = (() => { try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; } })();
|
|
132
|
-
if (elig && elig.eligible === false) continue;
|
|
133
|
-
}
|
|
146
|
+
const occurrences = [];
|
|
147
|
+
let applicableCount = 0;
|
|
134
148
|
|
|
135
|
-
|
|
149
|
+
const trim = (v) => (v == null ? '' : String(v)).trim();
|
|
136
150
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
let descText = '';
|
|
140
|
-
let ariaLabel = '';
|
|
141
|
-
let ariaLabelledBy = '';
|
|
142
|
-
let labelledByText = '';
|
|
151
|
+
for (const el of els) {
|
|
152
|
+
if (!el || !el.getAttribute) continue;
|
|
143
153
|
|
|
154
|
+
// acc eligibility
|
|
155
|
+
if (isAccTreeEligible) {
|
|
156
|
+
const elig = (() => {
|
|
144
157
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
descText = trim(descEl && descEl.textContent);
|
|
149
|
-
|
|
150
|
-
ariaLabel = trim(el.getAttribute('aria-label'));
|
|
151
|
-
ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
|
|
152
|
-
} catch {}
|
|
153
|
-
|
|
154
|
-
if (!ariaLabel && ariaLabelledBy && helpers && typeof helpers.getTextFromIdRefs === 'function') {
|
|
155
|
-
try {
|
|
156
|
-
const t = helpers.getTextFromIdRefs(ariaLabelledBy, ctx);
|
|
157
|
-
labelledByText = trim(t && t.text);
|
|
158
|
-
} catch {}
|
|
158
|
+
return isAccTreeEligible(el, ctx);
|
|
159
|
+
} catch {
|
|
160
|
+
return { eligible: true, reasons: [] };
|
|
159
161
|
}
|
|
162
|
+
})();
|
|
163
|
+
if (elig && elig.eligible === false) continue;
|
|
164
|
+
}
|
|
160
165
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
};
|
|
166
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
167
|
+
|
|
168
|
+
// Detect mechanisms once
|
|
169
|
+
let titleText = '';
|
|
170
|
+
let descText = '';
|
|
171
|
+
let ariaLabel = '';
|
|
172
|
+
let ariaLabelledBy = '';
|
|
173
|
+
let labelledByText = '';
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const titleEl = el.querySelector ? el.querySelector('title') : null;
|
|
177
|
+
const descEl = el.querySelector ? el.querySelector('desc') : null;
|
|
178
|
+
titleText = trim(titleEl && titleEl.textContent);
|
|
179
|
+
descText = trim(descEl && descEl.textContent);
|
|
180
|
+
|
|
181
|
+
ariaLabel = trim(el.getAttribute('aria-label'));
|
|
182
|
+
ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
|
|
183
|
+
} catch {}
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
!ariaLabel &&
|
|
187
|
+
ariaLabelledBy &&
|
|
188
|
+
helpers &&
|
|
189
|
+
typeof helpers.getTextFromIdRefs === 'function'
|
|
190
|
+
) {
|
|
191
|
+
try {
|
|
192
|
+
const t = helpers.getTextFromIdRefs(ariaLabelledBy, ctx);
|
|
193
|
+
labelledByText = trim(t && t.text);
|
|
194
|
+
} catch {}
|
|
195
|
+
}
|
|
192
196
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
const hasNonEmptyTitle = !!titleText;
|
|
198
|
+
const hasNonEmptyDesc = !!descText;
|
|
199
|
+
const hasAriaLabel = !!ariaLabel;
|
|
200
|
+
const hasResolvedLabelledBy = !!labelledByText;
|
|
201
|
+
|
|
202
|
+
const hasMechanism =
|
|
203
|
+
hasNonEmptyTitle || hasNonEmptyDesc || hasAriaLabel || hasResolvedLabelledBy;
|
|
204
|
+
if (!hasMechanism) continue;
|
|
205
|
+
|
|
206
|
+
applicableCount += 1;
|
|
207
|
+
|
|
208
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
209
|
+
|
|
210
|
+
const baseOccurrence = {
|
|
211
|
+
summary: 'Review text alternative for <svg> for accuracy and appropriateness.',
|
|
212
|
+
hint: 'Confirm the <title>/<desc> or ARIA name conveys the meaning/purpose of the graphic in context.',
|
|
213
|
+
i18n: {
|
|
214
|
+
summaryKey: 'svg_textAltQuality_summary_cantTell',
|
|
215
|
+
hintKey: 'svg_textAltQuality_hint_cantTell',
|
|
216
|
+
params: { element: 'svg' }
|
|
217
|
+
},
|
|
218
|
+
data: {
|
|
219
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
220
|
+
details: {
|
|
221
|
+
hasNonEmptyTitle,
|
|
222
|
+
hasNonEmptyDesc,
|
|
223
|
+
ariaLabel: ariaLabel || null,
|
|
224
|
+
ariaLabelledBy: ariaLabelledBy || null,
|
|
225
|
+
ariaLabelledByText: labelledByText ? labelledByText.slice(0, 120) : null // deterministic truncation
|
|
199
226
|
}
|
|
200
|
-
|
|
227
|
+
}
|
|
228
|
+
};
|
|
201
229
|
|
|
202
|
-
if (
|
|
203
|
-
|
|
230
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
231
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
232
|
+
} else {
|
|
233
|
+
const selectorStr = (() => {
|
|
234
|
+
try {
|
|
235
|
+
return buildSelector(el);
|
|
236
|
+
} catch {
|
|
237
|
+
return 'html';
|
|
238
|
+
}
|
|
239
|
+
})();
|
|
240
|
+
const html = getOuterHtmlSnippet(el);
|
|
241
|
+
occurrences.push({ selector: selectorStr, html, ...baseOccurrence });
|
|
204
242
|
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (applicableCount === 0) {
|
|
246
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
247
|
+
}
|
|
205
248
|
|
|
206
|
-
|
|
249
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
207
250
|
}
|
|
208
251
|
|
|
209
|
-
module.exports = {id, meta, runInPage};
|
|
252
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @check tabindex
|
|
5
7
|
* @atomic true
|
|
6
8
|
* @summary tabindex should not be greater than 0
|
|
7
|
-
* @standard Best Practices (
|
|
9
|
+
* @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
|
|
8
10
|
* @applicability
|
|
9
11
|
* Applies to elements with a tabindex attribute whose value parses as
|
|
10
12
|
* a valid integer.
|
|
@@ -42,7 +44,9 @@ const meta = {
|
|
|
42
44
|
function runInPage(ctx) {
|
|
43
45
|
const { helpers, rule } = ctx;
|
|
44
46
|
|
|
45
|
-
const nodes = helpers.queryAllSmart
|
|
47
|
+
const nodes = helpers.queryAllSmart
|
|
48
|
+
? helpers.queryAllSmart('[tabindex]')
|
|
49
|
+
: helpers.queryAll('[tabindex]');
|
|
46
50
|
|
|
47
51
|
const occurrences = [];
|
|
48
52
|
let applicableCount = 0;
|
|
@@ -59,7 +63,7 @@ function runInPage(ctx) {
|
|
|
59
63
|
if (n <= 0) continue;
|
|
60
64
|
|
|
61
65
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
62
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
66
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
63
67
|
|
|
64
68
|
occurrences.push({
|
|
65
69
|
selector: stableSelector,
|
|
@@ -81,7 +85,12 @@ function runInPage(ctx) {
|
|
|
81
85
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
82
86
|
}
|
|
83
87
|
if (occurrences.length) {
|
|
84
|
-
return {
|
|
88
|
+
return {
|
|
89
|
+
ruleId: rule.ruleId,
|
|
90
|
+
outcome: 'cantTell',
|
|
91
|
+
severity: rule.defaultSeverity || 'minor',
|
|
92
|
+
occurrences
|
|
93
|
+
};
|
|
85
94
|
}
|
|
86
95
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
87
96
|
}
|