@surea11y/core 1.1.2 → 1.3.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 +48 -4
- package/LICENSE +373 -21
- package/README.md +70 -1
- package/bin/core.js +240 -11
- package/docs/API_STABILITY.md +61 -0
- package/docs/BASELINE.md +66 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +80 -1
- package/docs/ENGINE_OPTIONS.md +4 -0
- package/docs/INTEGRATION.md +19 -1
- package/docs/OUTPUT_SCHEMA.md +2 -2
- package/docs/REPORT.md +33 -0
- package/docs/RULE_AUTHORING.md +31 -0
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +50 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +125 -29
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +50 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +39 -9
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +117 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
- package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +26 -7
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -12
- package/src/checks/manual/empty-table-header-manual.js +21 -14
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -12
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
- package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +35 -21
- package/src/checks/manual/landmark-unique-manual.js +73 -45
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +25 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +40 -23
- package/src/checks/manual/page-title-patterns-manual.js +87 -51
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +275 -62
- package/src/checks/manual/scope-attr-valid-manual.js +11 -9
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +11 -9
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1128 -823
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4176 -3886
- package/src/core/dom-runner.js +720 -596
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +71 -35
- package/src/core.js +37939 -29121
- package/src/i18n/en.js +1194 -887
- package/src/i18n/fr.js +1136 -793
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +502 -0
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -13,7 +13,13 @@ const meta = {
|
|
|
13
13
|
tags: ['wcag2a', 'wcag111', 'nontext', 'atomic', 'automatic', 'name'],
|
|
14
14
|
wcagSc: ['1.1.1'],
|
|
15
15
|
normativeMappings: [
|
|
16
|
-
{
|
|
16
|
+
{
|
|
17
|
+
standard: 'WCAG',
|
|
18
|
+
version: '2.2',
|
|
19
|
+
requirement: '1.1.1',
|
|
20
|
+
title: 'Non-text Content',
|
|
21
|
+
conformanceLevel: 'A'
|
|
22
|
+
}
|
|
17
23
|
],
|
|
18
24
|
defaultSeverity: 'serious',
|
|
19
25
|
category: 'perceivable',
|
|
@@ -24,36 +30,22 @@ const meta = {
|
|
|
24
30
|
|
|
25
31
|
function runInPage(ctx) {
|
|
26
32
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
33
|
+
const getEligibilityInfo =
|
|
34
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
35
|
|
|
32
36
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
37
|
+
return String(s || '')
|
|
38
|
+
.replace(/\s+/g, ' ')
|
|
39
|
+
.trim();
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
function getAttr(el, name) {
|
|
37
43
|
try {
|
|
38
44
|
if (!el || !el.getAttribute) return '';
|
|
39
45
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
function getConservativeSubtreeText(document, container) {
|
|
44
|
-
// "Name from content" — recurses into descendants and uses each one's
|
|
45
|
-
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
46
|
-
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
47
|
-
// header comment in src/core/dom-helpers.js for the full rationale
|
|
48
|
-
// (this replaced a text-node-only TreeWalker that missed the common
|
|
49
|
-
// "<a><img alt='...'></a>" logo-link / "<button><img alt='...'></button>"
|
|
50
|
-
// icon-button pattern).
|
|
51
|
-
if (helpers.getContentNameInfo) {
|
|
52
|
-
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
|
-
return info && info.present ? info.value : '';
|
|
46
|
+
} catch {
|
|
47
|
+
return '';
|
|
54
48
|
}
|
|
55
|
-
const t = (container && container.textContent) ? String(container.textContent) : '';
|
|
56
|
-
return t.replace(/\s+/g, ' ').trim();
|
|
57
49
|
}
|
|
58
50
|
|
|
59
51
|
function resolveAriaLabelledbyText(document, el, maxRefs) {
|
|
@@ -74,7 +66,8 @@ function runInPage(ctx) {
|
|
|
74
66
|
}
|
|
75
67
|
|
|
76
68
|
function isEligibleAcc(helpers, el, ctx) {
|
|
77
|
-
const fn =
|
|
69
|
+
const fn =
|
|
70
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
78
71
|
if (!fn) return true;
|
|
79
72
|
try {
|
|
80
73
|
const r = fn(el, ctx);
|
|
@@ -85,13 +78,13 @@ function runInPage(ctx) {
|
|
|
85
78
|
}
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
|
|
89
81
|
const occurrences = [];
|
|
90
82
|
let applicableCount = 0;
|
|
91
83
|
|
|
92
84
|
const selector = '[role="meter"]';
|
|
93
|
-
const nodes = helpers.queryAllSmart
|
|
94
|
-
|
|
85
|
+
const nodes = helpers.queryAllSmart
|
|
86
|
+
? helpers.queryAllSmart(selector)
|
|
87
|
+
: helpers.queryAll(selector);
|
|
95
88
|
|
|
96
89
|
function evaluate(el) {
|
|
97
90
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -124,16 +117,22 @@ function runInPage(ctx) {
|
|
|
124
117
|
if (res.ok) continue;
|
|
125
118
|
|
|
126
119
|
const eligInfo = getEligibilityInfo
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
? (() => {
|
|
121
|
+
try {
|
|
122
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
})()
|
|
127
|
+
: null;
|
|
129
128
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
130
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
129
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
131
130
|
|
|
132
131
|
occurrences.push({
|
|
133
132
|
selector: stableSelector,
|
|
134
133
|
html,
|
|
135
134
|
summary: 'This meter has no accessible name.',
|
|
136
|
-
hint:
|
|
135
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this meter's accessible name.",
|
|
137
136
|
i18n: {
|
|
138
137
|
summaryKey: 'meterNamePresent_summary_fail',
|
|
139
138
|
hintKey: 'meterNamePresent_hint_fail',
|
|
@@ -146,14 +145,18 @@ function runInPage(ctx) {
|
|
|
146
145
|
});
|
|
147
146
|
}
|
|
148
147
|
|
|
149
|
-
|
|
150
148
|
if (applicableCount === 0) {
|
|
151
149
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
152
150
|
}
|
|
153
151
|
if (occurrences.length) {
|
|
154
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
ruleId: rule.ruleId,
|
|
154
|
+
outcome: 'fail',
|
|
155
|
+
severity: rule.defaultSeverity || 'minor',
|
|
156
|
+
occurrences
|
|
157
|
+
};
|
|
155
158
|
}
|
|
156
159
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
157
160
|
}
|
|
158
161
|
|
|
159
|
-
module.exports = { id, meta, runInPage };
|
|
162
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -34,7 +34,8 @@ const id = 'nested-interactive-controls-absent';
|
|
|
34
34
|
|
|
35
35
|
const meta = {
|
|
36
36
|
title: 'Interactive controls must not be nested',
|
|
37
|
-
description:
|
|
37
|
+
description:
|
|
38
|
+
'Checks that an interactive control (link, button, form control, or ARIA widget role) does not contain another interactive control.',
|
|
38
39
|
i18n: {
|
|
39
40
|
titleKey: 'nestedInteractiveControlsAbsent_title',
|
|
40
41
|
descriptionKey: 'nestedInteractiveControlsAbsent_description'
|
|
@@ -43,7 +44,13 @@ const meta = {
|
|
|
43
44
|
tags: ['wcag2a', 'wcag412', 'structure', 'atomic', 'automatic'],
|
|
44
45
|
wcagSc: ['4.1.2'],
|
|
45
46
|
normativeMappings: [
|
|
46
|
-
{
|
|
47
|
+
{
|
|
48
|
+
standard: 'WCAG',
|
|
49
|
+
version: '2.2',
|
|
50
|
+
requirement: '4.1.2',
|
|
51
|
+
title: 'Name, Role, Value',
|
|
52
|
+
conformanceLevel: 'A'
|
|
53
|
+
}
|
|
47
54
|
],
|
|
48
55
|
defaultSeverity: 'serious',
|
|
49
56
|
category: 'robust',
|
|
@@ -53,7 +60,7 @@ const meta = {
|
|
|
53
60
|
};
|
|
54
61
|
|
|
55
62
|
function runInPage(ctx) {
|
|
56
|
-
const {
|
|
63
|
+
const { helpers, rule } = ctx;
|
|
57
64
|
|
|
58
65
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
59
66
|
// ("runInPage MUST be self-contained").
|
|
@@ -82,7 +89,9 @@ function runInPage(ctx) {
|
|
|
82
89
|
'[role="treeitem"]'
|
|
83
90
|
].join(', ');
|
|
84
91
|
|
|
85
|
-
const nodes = helpers.queryAllSmart
|
|
92
|
+
const nodes = helpers.queryAllSmart
|
|
93
|
+
? helpers.queryAllSmart(INTERACTIVE_SELECTOR)
|
|
94
|
+
: helpers.queryAll(INTERACTIVE_SELECTOR);
|
|
86
95
|
|
|
87
96
|
const occurrences = [];
|
|
88
97
|
let applicableCount = 0;
|
|
@@ -92,7 +101,7 @@ function runInPage(ctx) {
|
|
|
92
101
|
|
|
93
102
|
applicableCount += 1;
|
|
94
103
|
|
|
95
|
-
let nested
|
|
104
|
+
let nested;
|
|
96
105
|
try {
|
|
97
106
|
nested = Array.from(el.querySelectorAll(INTERACTIVE_SELECTOR));
|
|
98
107
|
} catch {
|
|
@@ -101,10 +110,13 @@ function runInPage(ctx) {
|
|
|
101
110
|
if (!nested.length) continue;
|
|
102
111
|
|
|
103
112
|
const nestedTags = nested.map((n) => (n && n.tagName ? n.tagName.toLowerCase() : 'unknown'));
|
|
113
|
+
const dedupedNestedTags = [
|
|
114
|
+
...new Set(nested.map((n) => (n && n.tagName ? n.tagName.toLowerCase() : 'unknown')))
|
|
115
|
+
];
|
|
104
116
|
|
|
105
117
|
const tag = el.tagName.toLowerCase();
|
|
106
118
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
107
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
119
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
108
120
|
|
|
109
121
|
occurrences.push({
|
|
110
122
|
selector: stableSelector,
|
|
@@ -114,10 +126,14 @@ function runInPage(ctx) {
|
|
|
114
126
|
i18n: {
|
|
115
127
|
summaryKey: 'nestedInteractiveControlsAbsent_summary_fail',
|
|
116
128
|
hintKey: 'nestedInteractiveControlsAbsent_hint_fail',
|
|
117
|
-
params: { element: tag, nestedElements:
|
|
129
|
+
params: { element: tag, nestedElements: dedupedNestedTags.join(', ') }
|
|
118
130
|
},
|
|
119
131
|
data: {
|
|
120
|
-
details: {
|
|
132
|
+
details: {
|
|
133
|
+
reasonCode: 'NESTED_INTERACTIVE_CONTROL',
|
|
134
|
+
element: tag,
|
|
135
|
+
nestedElements: nestedTags
|
|
136
|
+
}
|
|
121
137
|
}
|
|
122
138
|
});
|
|
123
139
|
}
|
|
@@ -126,9 +142,14 @@ function runInPage(ctx) {
|
|
|
126
142
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
127
143
|
}
|
|
128
144
|
if (occurrences.length) {
|
|
129
|
-
return {
|
|
145
|
+
return {
|
|
146
|
+
ruleId: rule.ruleId,
|
|
147
|
+
outcome: 'fail',
|
|
148
|
+
severity: rule.defaultSeverity || 'serious',
|
|
149
|
+
occurrences
|
|
150
|
+
};
|
|
130
151
|
}
|
|
131
152
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
132
153
|
}
|
|
133
154
|
|
|
134
|
-
module.exports = { id, meta, runInPage };
|
|
155
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -22,7 +22,8 @@ const id = 'object-text-alternative-present';
|
|
|
22
22
|
|
|
23
23
|
const meta = {
|
|
24
24
|
title: '<object> must provide a text alternative',
|
|
25
|
-
description:
|
|
25
|
+
description:
|
|
26
|
+
'Checks that <object> elements provide a text alternative via fallback content or an accessible name.',
|
|
26
27
|
i18n: {
|
|
27
28
|
titleKey: 'object_textAltPresent_title',
|
|
28
29
|
descriptionKey: 'object_textAltPresent_description'
|
|
@@ -31,7 +32,13 @@ const meta = {
|
|
|
31
32
|
tags: ['wcag2a', 'wcag111', 'nontext', 'object', 'atomic', 'automatic'],
|
|
32
33
|
wcagSc: ['1.1.1'],
|
|
33
34
|
normativeMappings: [
|
|
34
|
-
{
|
|
35
|
+
{
|
|
36
|
+
standard: 'WCAG',
|
|
37
|
+
version: '2.2',
|
|
38
|
+
requirement: '1.1.1',
|
|
39
|
+
title: 'Non-text Content',
|
|
40
|
+
conformanceLevel: 'A'
|
|
41
|
+
}
|
|
35
42
|
],
|
|
36
43
|
defaultSeverity: 'serious',
|
|
37
44
|
category: 'perceivable',
|
|
@@ -44,33 +51,39 @@ function runInPage(ctx) {
|
|
|
44
51
|
const { document, root, helpers, rule } = ctx;
|
|
45
52
|
const safeRoot = root || document;
|
|
46
53
|
|
|
47
|
-
const queryAllSmart =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
: null;
|
|
54
|
+
const queryAllSmart =
|
|
55
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
56
|
+
const queryAll =
|
|
57
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
58
|
+
? helpers.queryAll
|
|
59
|
+
: (sel) => {
|
|
60
|
+
try {
|
|
61
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
62
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
63
|
+
: [];
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const getEligibilityInfo =
|
|
70
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
71
|
+
|
|
72
|
+
const isAccTreeEligible =
|
|
73
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
74
|
+
|
|
75
|
+
const getFocusableInfo =
|
|
76
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
77
|
+
|
|
78
|
+
const getAriaNameInfo =
|
|
79
|
+
helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
|
|
70
80
|
|
|
71
81
|
const objects = (() => {
|
|
72
|
-
try {
|
|
73
|
-
|
|
82
|
+
try {
|
|
83
|
+
return Array.from((queryAllSmart ? queryAllSmart('object') : queryAll('object')) || []);
|
|
84
|
+
} catch {
|
|
85
|
+
return queryAll('object');
|
|
86
|
+
}
|
|
74
87
|
})();
|
|
75
88
|
|
|
76
89
|
if (!objects.length) {
|
|
@@ -88,11 +101,22 @@ function runInPage(ctx) {
|
|
|
88
101
|
let aria = null;
|
|
89
102
|
|
|
90
103
|
if (getAriaNameInfo) {
|
|
91
|
-
aria = (() => {
|
|
104
|
+
aria = (() => {
|
|
105
|
+
try {
|
|
106
|
+
return getAriaNameInfo(el, ctx);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
92
111
|
}
|
|
93
112
|
|
|
94
113
|
if (aria && aria.present && trim(aria.value)) {
|
|
95
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
present: true,
|
|
116
|
+
value: trim(aria.value),
|
|
117
|
+
mechanism: aria.mechanism || 'aria',
|
|
118
|
+
flags: (aria.flags || []).slice(0)
|
|
119
|
+
};
|
|
96
120
|
}
|
|
97
121
|
|
|
98
122
|
const title = trim(el.getAttribute && el.getAttribute('title'));
|
|
@@ -123,20 +147,43 @@ function runInPage(ctx) {
|
|
|
123
147
|
if (!el || !el.getAttribute) continue;
|
|
124
148
|
|
|
125
149
|
if (isAccTreeEligible) {
|
|
126
|
-
const elig = (() => {
|
|
150
|
+
const elig = (() => {
|
|
151
|
+
try {
|
|
152
|
+
return isAccTreeEligible(el, ctx);
|
|
153
|
+
} catch {
|
|
154
|
+
return { eligible: true, reasons: [] };
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
127
157
|
if (elig && elig.eligible === false) continue;
|
|
128
158
|
}
|
|
129
159
|
|
|
130
160
|
// role presentation/none exclusion only when not focusable
|
|
131
|
-
const role = (() => {
|
|
161
|
+
const role = (() => {
|
|
162
|
+
try {
|
|
163
|
+
return String(el.getAttribute('role') || '')
|
|
164
|
+
.trim()
|
|
165
|
+
.toLowerCase();
|
|
166
|
+
} catch {
|
|
167
|
+
return '';
|
|
168
|
+
}
|
|
169
|
+
})();
|
|
132
170
|
if (role === 'presentation' || role === 'none') {
|
|
133
|
-
let focusable
|
|
171
|
+
let focusable;
|
|
134
172
|
if (getFocusableInfo) {
|
|
135
|
-
const fi = (() => {
|
|
173
|
+
const fi = (() => {
|
|
174
|
+
try {
|
|
175
|
+
return getFocusableInfo(el, ctx);
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
})();
|
|
136
180
|
focusable = !!(fi && fi.focusable);
|
|
137
181
|
} else {
|
|
138
182
|
const tabindex = el.getAttribute('tabindex');
|
|
139
|
-
focusable =
|
|
183
|
+
focusable =
|
|
184
|
+
tabindex != null &&
|
|
185
|
+
String(tabindex).trim() !== '' &&
|
|
186
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
140
187
|
}
|
|
141
188
|
if (!focusable) continue;
|
|
142
189
|
}
|
|
@@ -147,18 +194,18 @@ function runInPage(ctx) {
|
|
|
147
194
|
|
|
148
195
|
// Only compute fallback text if there is no name.
|
|
149
196
|
// (textContent can be expensive; avoid when not needed)
|
|
150
|
-
const fb = name.present
|
|
151
|
-
|
|
197
|
+
const fb = name.present
|
|
198
|
+
? { present: false, value: '', mechanism: 'fallback', flags: ['skipped-name-present'] }
|
|
199
|
+
: computeFallbackText(el);
|
|
152
200
|
|
|
153
201
|
const hasTextAlt = !!(name.present || fb.present);
|
|
154
202
|
if (hasTextAlt) continue;
|
|
155
203
|
|
|
156
|
-
|
|
157
204
|
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
158
205
|
|
|
159
206
|
const baseOccurrence = {
|
|
160
207
|
summary: 'Missing text alternative for <object>.',
|
|
161
|
-
hint: 'Provide meaningful fallback content inside <object>,
|
|
208
|
+
hint: 'Provide meaningful fallback content inside <object>, add an accessible name (aria-label/aria-labelledby), or use a title attribute as a best-effort fallback.',
|
|
162
209
|
i18n: {
|
|
163
210
|
summaryKey: 'object_textAltPresent_summary_fail',
|
|
164
211
|
hintKey: 'object_textAltPresent_hint_fail',
|
|
@@ -187,7 +234,12 @@ function runInPage(ctx) {
|
|
|
187
234
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
188
235
|
}
|
|
189
236
|
|
|
190
|
-
return {
|
|
237
|
+
return {
|
|
238
|
+
ruleId: rule.ruleId,
|
|
239
|
+
outcome: 'fail',
|
|
240
|
+
severity: rule.defaultSeverity || 'minor',
|
|
241
|
+
occurrences
|
|
242
|
+
};
|
|
191
243
|
}
|
|
192
244
|
|
|
193
245
|
module.exports = { id, meta, runInPage };
|
|
@@ -10,10 +10,16 @@ const meta = {
|
|
|
10
10
|
descriptionKey: 'optionNamePresent_description'
|
|
11
11
|
},
|
|
12
12
|
helpUrl: null,
|
|
13
|
-
tags: [
|
|
13
|
+
tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'option'],
|
|
14
14
|
wcagSc: ['4.1.2'],
|
|
15
15
|
normativeMappings: [
|
|
16
|
-
{
|
|
16
|
+
{
|
|
17
|
+
standard: 'WCAG',
|
|
18
|
+
version: '2.2',
|
|
19
|
+
requirement: '4.1.2',
|
|
20
|
+
title: 'Name, Role, Value',
|
|
21
|
+
conformanceLevel: 'A'
|
|
22
|
+
}
|
|
17
23
|
],
|
|
18
24
|
defaultSeverity: 'serious',
|
|
19
25
|
category: 'robust',
|
|
@@ -24,20 +30,22 @@ const meta = {
|
|
|
24
30
|
|
|
25
31
|
function runInPage(ctx) {
|
|
26
32
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
33
|
+
const getEligibilityInfo =
|
|
34
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
35
|
|
|
32
36
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
37
|
+
return String(s || '')
|
|
38
|
+
.replace(/\s+/g, ' ')
|
|
39
|
+
.trim();
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
function getAttr(el, name) {
|
|
37
43
|
try {
|
|
38
44
|
if (!el || !el.getAttribute) return '';
|
|
39
45
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
46
|
+
} catch {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -52,7 +60,7 @@ function runInPage(ctx) {
|
|
|
52
60
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
61
|
return info && info.present ? info.value : '';
|
|
54
62
|
}
|
|
55
|
-
const t =
|
|
63
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
56
64
|
return t.replace(/\s+/g, ' ').trim();
|
|
57
65
|
}
|
|
58
66
|
|
|
@@ -74,7 +82,8 @@ function runInPage(ctx) {
|
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
function isEligibleAcc(helpers, el, ctx) {
|
|
77
|
-
const fn =
|
|
85
|
+
const fn =
|
|
86
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
78
87
|
if (!fn) return true;
|
|
79
88
|
try {
|
|
80
89
|
const r = fn(el, ctx);
|
|
@@ -85,13 +94,13 @@ function runInPage(ctx) {
|
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
96
|
|
|
88
|
-
|
|
89
97
|
const occurrences = [];
|
|
90
98
|
let applicableCount = 0;
|
|
91
99
|
|
|
92
|
-
const selector =
|
|
93
|
-
const nodes = helpers.queryAllSmart
|
|
94
|
-
|
|
100
|
+
const selector = '[role="option"]';
|
|
101
|
+
const nodes = helpers.queryAllSmart
|
|
102
|
+
? helpers.queryAllSmart(selector)
|
|
103
|
+
: helpers.queryAll(selector);
|
|
95
104
|
|
|
96
105
|
function hasName(el) {
|
|
97
106
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -103,11 +112,9 @@ function runInPage(ctx) {
|
|
|
103
112
|
const title = getAttr(el, 'title');
|
|
104
113
|
if (title) return { ok: true, method: 'title' };
|
|
105
114
|
|
|
106
|
-
|
|
107
115
|
const t = getConservativeSubtreeText(document, el);
|
|
108
116
|
if (t) return { ok: true, method: 'content' };
|
|
109
117
|
|
|
110
|
-
|
|
111
118
|
return { ok: false, method: 'none' };
|
|
112
119
|
}
|
|
113
120
|
|
|
@@ -121,10 +128,16 @@ function runInPage(ctx) {
|
|
|
121
128
|
if (res.ok) continue;
|
|
122
129
|
|
|
123
130
|
const eligInfo = getEligibilityInfo
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
? (() => {
|
|
132
|
+
try {
|
|
133
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
})()
|
|
138
|
+
: null;
|
|
126
139
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
127
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
140
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
128
141
|
|
|
129
142
|
occurrences.push({
|
|
130
143
|
selector: stableSelector,
|
|
@@ -143,12 +156,16 @@ function runInPage(ctx) {
|
|
|
143
156
|
});
|
|
144
157
|
}
|
|
145
158
|
|
|
146
|
-
|
|
147
159
|
if (applicableCount === 0) {
|
|
148
160
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
149
161
|
}
|
|
150
162
|
if (occurrences.length) {
|
|
151
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
ruleId: rule.ruleId,
|
|
165
|
+
outcome: 'fail',
|
|
166
|
+
severity: rule.defaultSeverity || 'minor',
|
|
167
|
+
occurrences
|
|
168
|
+
};
|
|
152
169
|
}
|
|
153
170
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
154
171
|
}
|
|
@@ -13,7 +13,13 @@ const meta = {
|
|
|
13
13
|
tags: ['wcag2a', 'wcag242', 'titles', 'atomic', 'navigation', 'automatic'],
|
|
14
14
|
wcagSc: ['2.4.2'],
|
|
15
15
|
normativeMappings: [
|
|
16
|
-
{
|
|
16
|
+
{
|
|
17
|
+
standard: 'WCAG',
|
|
18
|
+
version: '2.2',
|
|
19
|
+
requirement: '2.4.2',
|
|
20
|
+
title: 'Page Titled',
|
|
21
|
+
conformanceLevel: 'A'
|
|
22
|
+
}
|
|
17
23
|
],
|
|
18
24
|
defaultSeverity: 'serious',
|
|
19
25
|
category: 'operable',
|
|
@@ -22,6 +28,14 @@ const meta = {
|
|
|
22
28
|
coverage: { facetsBySc: { '2.4.2': ['page-title-present'] } }
|
|
23
29
|
};
|
|
24
30
|
|
|
31
|
+
// This check is inherently whole-document (does the PAGE have this
|
|
32
|
+
// property?), not evaluable per-subtree -- notApplicable when contextSelector
|
|
33
|
+
// scoped this run narrower than the whole document, or when
|
|
34
|
+
// engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
|
|
35
|
+
function applicability(ctx) {
|
|
36
|
+
return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
|
|
37
|
+
}
|
|
38
|
+
|
|
25
39
|
function runInPage(ctx) {
|
|
26
40
|
const { document, helpers, rule } = ctx;
|
|
27
41
|
|
|
@@ -46,13 +60,13 @@ function runInPage(ctx) {
|
|
|
46
60
|
selector: 'head > title',
|
|
47
61
|
html: '',
|
|
48
62
|
summary: missingTitleEl
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
? 'The page is missing a <title> element.'
|
|
64
|
+
: 'The page has an empty <title>.',
|
|
51
65
|
hint: 'Provide a descriptive, non-empty <title> that identifies the page topic or purpose.',
|
|
52
66
|
i18n: {
|
|
53
67
|
summaryKey: missingTitleEl
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
? 'pageTitlePresent_summary_fail_missing'
|
|
69
|
+
: 'pageTitlePresent_summary_fail_empty',
|
|
56
70
|
hintKey: 'pageTitlePresent_hint_fail',
|
|
57
71
|
params: {}
|
|
58
72
|
},
|
|
@@ -78,9 +92,14 @@ function runInPage(ctx) {
|
|
|
78
92
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
79
93
|
}
|
|
80
94
|
if (occurrences.length) {
|
|
81
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
ruleId: rule.ruleId,
|
|
97
|
+
outcome: 'fail',
|
|
98
|
+
severity: rule.defaultSeverity || 'minor',
|
|
99
|
+
occurrences
|
|
100
|
+
};
|
|
82
101
|
}
|
|
83
102
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
84
103
|
}
|
|
85
104
|
|
|
86
|
-
module.exports = { id, meta, runInPage };
|
|
105
|
+
module.exports = { id, meta, runInPage, applicability };
|