@surea11y/core 1.4.0 → 1.5.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 +88 -7
- package/README.md +19 -3
- package/bin/surea11y-core.js +0 -0
- package/docs/API_STABILITY.md +2 -2
- package/docs/ARIA_DEPRECATION.md +95 -0
- package/docs/ENGINE_OPTIONS.md +14 -10
- package/docs/I18N.md +176 -20
- package/docs/INTEGRATION.md +28 -6
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +13 -3
- package/docs/REPORT.md +2 -0
- package/docs/RULE_AUTHORING.md +53 -0
- package/docs/RULE_CATALOG.md +6 -6
- package/docs/TROUBLESHOOTING.md +2 -2
- package/package.json +8 -1
- package/src/checks/automatic/aria-allowed-attr.js +661 -99
- package/src/checks/automatic/aria-allowed-role.js +14 -16
- package/src/checks/automatic/aria-braille-equivalent.js +17 -19
- package/src/checks/automatic/aria-conditional-attr.js +17 -19
- package/src/checks/automatic/aria-deprecated-role.js +122 -41
- package/src/checks/automatic/aria-hidden-body.js +2 -9
- package/src/checks/automatic/aria-hidden-focus.js +99 -18
- package/src/checks/automatic/aria-prohibited-attr.js +54 -55
- package/src/checks/automatic/aria-prohibited-children.js +26 -26
- package/src/checks/automatic/aria-required-attr.js +14 -17
- package/src/checks/automatic/aria-required-children.js +17 -20
- package/src/checks/automatic/aria-required-parent.js +17 -20
- package/src/checks/automatic/aria-roles-valid.js +66 -27
- package/src/checks/automatic/aria-valid-attr-value.js +18 -21
- package/src/checks/automatic/aria-valid-attr.js +14 -17
- package/src/checks/automatic/autocomplete-valid.js +52 -18
- package/src/checks/automatic/avoid-inline-spacing.js +192 -34
- package/src/checks/automatic/binary-control-name-present.js +30 -24
- package/src/checks/automatic/button-name-present.js +73 -45
- package/src/checks/automatic/canvas-text-alternative-present.js +15 -8
- package/src/checks/automatic/combobox-name-present.js +23 -18
- package/src/checks/automatic/css-orientation-lock.js +22 -22
- package/src/checks/automatic/definition-list-children-valid.js +18 -21
- package/src/checks/automatic/deprecated-elements-not-used.js +14 -16
- package/src/checks/automatic/dialog-name-present.js +24 -19
- package/src/checks/automatic/dlitem-parent-valid.js +15 -17
- package/src/checks/automatic/duplicate-id-aria.js +45 -37
- package/src/checks/automatic/form-control-programmatic-label-present.js +48 -4
- package/src/checks/automatic/form-control-single-label.js +109 -43
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
- package/src/checks/automatic/iframe-focusable-content.js +29 -31
- package/src/checks/automatic/iframe-name-present.js +15 -17
- package/src/checks/automatic/iframe-title-unique.js +18 -23
- package/src/checks/automatic/img-alt-present.js +16 -9
- package/src/checks/automatic/input-image-alt-present.js +99 -48
- package/src/checks/automatic/label-in-name.js +102 -33
- package/src/checks/automatic/language-page-present.js +5 -1
- package/src/checks/automatic/link-in-text-block.js +19 -21
- package/src/checks/automatic/link-name-present.js +75 -47
- package/src/checks/automatic/list-children-valid.js +15 -17
- package/src/checks/automatic/listbox-name-present.js +23 -18
- package/src/checks/automatic/listitem-parent-valid.js +14 -17
- package/src/checks/automatic/menuitem-name-present.js +24 -19
- package/src/checks/automatic/meta-refresh-no-exceptions.js +44 -18
- package/src/checks/automatic/meta-refresh-timing-absent.js +44 -21
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +51 -32
- package/src/checks/automatic/meter-name-present.js +24 -19
- package/src/checks/automatic/nested-interactive-controls-absent.js +179 -51
- package/src/checks/automatic/object-text-alternative-present.js +14 -7
- package/src/checks/automatic/option-name-present.js +24 -19
- package/src/checks/automatic/progressbar-name-present.js +27 -22
- package/src/checks/automatic/searchbox-name-present.js +27 -18
- package/src/checks/automatic/server-side-image-map-absent.js +15 -18
- package/src/checks/automatic/slider-name-present.js +26 -19
- package/src/checks/automatic/spinbutton-name-present.js +27 -18
- package/src/checks/automatic/summary-name-present.js +24 -19
- package/src/checks/automatic/tab-name-present.js +24 -19
- package/src/checks/automatic/table-headers-attr-valid.js +15 -17
- package/src/checks/automatic/table-th-has-data-cells.js +82 -25
- package/src/checks/automatic/target-size-minimum.js +104 -81
- package/src/checks/automatic/td-has-header.js +15 -20
- package/src/checks/automatic/textbox-name-present.js +23 -18
- package/src/checks/automatic/tooltip-name-present.js +24 -19
- package/src/checks/automatic/treeitem-name-present.js +24 -19
- package/src/checks/automatic/valid-lang.js +31 -20
- package/src/checks/manual/accesskeys-manual.js +18 -19
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +19 -22
- package/src/checks/{automatic/bypass-blocks-present.js → manual/bypass-blocks-present-manual.js} +98 -44
- package/src/checks/manual/empty-heading-manual.js +15 -17
- package/src/checks/manual/empty-table-header-manual.js +27 -30
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -9
- package/src/checks/manual/heading-order-manual.js +17 -22
- package/src/checks/manual/image-redundant-alt-manual.js +14 -17
- package/src/checks/manual/input-image-alt-decorative-manual.js +24 -0
- package/src/checks/manual/label-title-only-manual.js +15 -17
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +25 -39
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +21 -27
- package/src/checks/manual/landmark-main-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -7
- package/src/checks/manual/landmark-one-main-manual.js +2 -9
- package/src/checks/manual/landmark-unique-manual.js +22 -27
- package/src/checks/manual/link-name-quality-manual.js +15 -17
- package/src/checks/manual/meta-viewport-large-manual.js +14 -17
- package/src/checks/manual/mouse-only-event-handlers-manual.js +17 -19
- package/src/checks/manual/page-has-heading-one-manual.js +2 -9
- package/src/checks/manual/presentation-role-conflict-manual.js +19 -21
- package/src/checks/manual/region-manual.js +13 -6
- package/src/checks/manual/scope-attr-valid-manual.js +14 -17
- package/src/checks/manual/skip-link-manual.js +39 -47
- package/src/checks/manual/tabindex-manual.js +14 -17
- package/src/checks/manual/table-duplicate-name-manual.js +14 -17
- package/src/core.js +8818 -4330
- package/src/report.js +14 -0
- package/src/sarif.js +2 -2
- package/surea11y.browser.js +4023 -3911
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
|
@@ -215,31 +215,29 @@ function runInPage(ctx) {
|
|
|
215
215
|
const eligInfo = helpers.getEligibilityInfo
|
|
216
216
|
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
217
217
|
: null;
|
|
218
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
219
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
220
218
|
const tag = (el.tagName || '').toLowerCase();
|
|
221
219
|
const ratioStr = c.round2 ? c.round2(ratio) : String(ratio);
|
|
222
220
|
|
|
223
|
-
occurrences.push(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
'
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
221
|
+
occurrences.push(
|
|
222
|
+
helpers.reportOccurrence(el, {
|
|
223
|
+
summary:
|
|
224
|
+
'This link in a block of text relies on color alone to be distinguished from the surrounding text.',
|
|
225
|
+
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.',
|
|
226
|
+
i18n: {
|
|
227
|
+
summaryKey: 'linkInTextBlock_summary_fail',
|
|
228
|
+
hintKey: 'linkInTextBlock_hint_fail',
|
|
229
|
+
params: { element: tag, ratio: String(ratioStr), threshold: '3' }
|
|
230
|
+
},
|
|
231
|
+
data: {
|
|
232
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
233
|
+
details: {
|
|
234
|
+
reasonCode: 'COLOR_ONLY_DIFFERENTIATION',
|
|
235
|
+
metrics: { ratio, threshold: 3 },
|
|
236
|
+
colors: { linkForegroundHex: fgLinkHex, surroundingTextForegroundHex: fgParentHex }
|
|
237
|
+
}
|
|
240
238
|
}
|
|
241
|
-
}
|
|
242
|
-
|
|
239
|
+
})
|
|
240
|
+
);
|
|
243
241
|
}
|
|
244
242
|
|
|
245
243
|
if (applicableCount === 0) {
|
|
@@ -59,7 +59,14 @@ function runInPage(ctx) {
|
|
|
59
59
|
|
|
60
60
|
for (const el of nodes) {
|
|
61
61
|
// isAccTreeEligible returns { eligible, reasons }, not a boolean.
|
|
62
|
-
|
|
62
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
63
|
+
// (ACT c487ae), which excludes focusable aria-hidden content;
|
|
64
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
65
|
+
const eligResult = helpers.isIncludedInAccessibilityTree
|
|
66
|
+
? helpers.isIncludedInAccessibilityTree(el, ctx)
|
|
67
|
+
: helpers.isAccTreeEligible
|
|
68
|
+
? helpers.isAccTreeEligible(el, ctx)
|
|
69
|
+
: true;
|
|
63
70
|
const eligible =
|
|
64
71
|
typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
|
|
65
72
|
if (!eligible) continue;
|
|
@@ -69,33 +76,59 @@ function runInPage(ctx) {
|
|
|
69
76
|
const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
|
|
70
77
|
const programmaticName = nameInfo && typeof nameInfo.value === 'string' ? nameInfo.value : '';
|
|
71
78
|
|
|
72
|
-
// A native <a href>/<area href> (or [role="link"]) whose role has been
|
|
73
|
-
// overridden to one of these roles is no longer semantically a link —
|
|
74
|
-
// per the WAI-ARIA Accessible Name and Description Computation spec
|
|
75
|
-
// these roles are name-from-author-only, and their rendered content
|
|
76
|
-
// represents a VALUE, not a NAME (see button-name-present.js's identical
|
|
77
|
-
// fix for a real-world example — <button role="combobox">List</button> —
|
|
78
|
-
// of the same class of bug on a different host tag).
|
|
79
79
|
const role = el.getAttribute ? el.getAttribute('role') : null;
|
|
80
80
|
const roleNorm = String(role || '')
|
|
81
81
|
.replace(/\s+/g, ' ')
|
|
82
82
|
.trim()
|
|
83
83
|
.toLowerCase();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
84
|
+
// ARIA 1.2 "Name From: author, contents". Every other known role is
|
|
85
|
+
// name-from-author-only. An unknown role falls back to the implicit role.
|
|
86
|
+
// <generated:aria-name-from-content>
|
|
87
|
+
const NAME_FROM_CONTENT_ROLES = [
|
|
88
|
+
'button',
|
|
89
|
+
'cell',
|
|
90
|
+
'checkbox',
|
|
91
|
+
'columnheader',
|
|
92
|
+
'doc-backlink',
|
|
93
|
+
'doc-biblioref',
|
|
94
|
+
'doc-glossref',
|
|
95
|
+
'doc-noteref',
|
|
96
|
+
'graphics-object',
|
|
97
|
+
'gridcell',
|
|
98
|
+
'heading',
|
|
99
|
+
'link',
|
|
100
|
+
'menuitem',
|
|
101
|
+
'menuitemcheckbox',
|
|
102
|
+
'menuitemradio',
|
|
103
|
+
'option',
|
|
104
|
+
'radio',
|
|
105
|
+
'row',
|
|
106
|
+
'rowgroup',
|
|
107
|
+
'rowheader',
|
|
108
|
+
'switch',
|
|
109
|
+
'tab',
|
|
110
|
+
'tooltip',
|
|
111
|
+
'treeitem'
|
|
92
112
|
];
|
|
93
|
-
|
|
113
|
+
// </generated:aria-name-from-content>
|
|
114
|
+
const isKnownRoleToken =
|
|
115
|
+
helpers && helpers.aria && typeof helpers.aria.isKnownRole === 'function'
|
|
116
|
+
? (() => {
|
|
117
|
+
try {
|
|
118
|
+
return !!helpers.aria.isKnownRole(roleNorm);
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
})()
|
|
123
|
+
: false;
|
|
124
|
+
const isContentNameCandidate =
|
|
125
|
+
!roleNorm || !isKnownRoleToken || NAME_FROM_CONTENT_ROLES.includes(roleNorm);
|
|
94
126
|
|
|
95
127
|
const contentName =
|
|
96
128
|
programmaticName.trim().length === 0 && isContentNameCandidate
|
|
97
129
|
? getConservativeSubtreeText(el)
|
|
98
130
|
: '';
|
|
131
|
+
|
|
99
132
|
const finalName = (programmaticName.trim().length ? programmaticName : contentName).trim();
|
|
100
133
|
|
|
101
134
|
if (finalName.length === 0) {
|
|
@@ -106,39 +139,34 @@ function runInPage(ctx) {
|
|
|
106
139
|
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
107
140
|
: null;
|
|
108
141
|
|
|
109
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
110
|
-
const html = helpers.getOuterHtmlSnippet
|
|
111
|
-
? helpers.getOuterHtmlSnippet(el)
|
|
112
|
-
: el.outerHTML || '';
|
|
113
142
|
const tag = (el.tagName || '').toLowerCase();
|
|
114
143
|
|
|
115
|
-
occurrences.push(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
refs: { accessibleName: nameInfo || null }
|
|
144
|
+
occurrences.push(
|
|
145
|
+
helpers.reportOccurrence(el, {
|
|
146
|
+
// Human fallbacks (allowed)
|
|
147
|
+
summary: 'This link has no accessible name.',
|
|
148
|
+
hint: 'Provide link text or an accessible-name mechanism (for example aria-label) so assistive technologies can identify the link.',
|
|
149
|
+
|
|
150
|
+
// Validator requires these keys to exist in the English dictionary
|
|
151
|
+
i18n: {
|
|
152
|
+
summaryKey: 'linkNamePresent_summary_fail',
|
|
153
|
+
hintKey: 'linkNamePresent_hint_fail',
|
|
154
|
+
params: { element: tag }
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
data: {
|
|
158
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
159
|
+
details: {
|
|
160
|
+
reasonCode: 'name_missing',
|
|
161
|
+
metrics: {
|
|
162
|
+
programmaticNameLength: programmaticName.trim().length,
|
|
163
|
+
contentNameLength: contentName.trim().length
|
|
164
|
+
},
|
|
165
|
+
refs: { accessibleName: nameInfo || null }
|
|
166
|
+
}
|
|
139
167
|
}
|
|
140
|
-
}
|
|
141
|
-
|
|
168
|
+
})
|
|
169
|
+
);
|
|
142
170
|
}
|
|
143
171
|
}
|
|
144
172
|
|
|
@@ -122,23 +122,21 @@ function runInPage(ctx) {
|
|
|
122
122
|
const dedupedInvalidTags = [...new Set(invalidTags)];
|
|
123
123
|
|
|
124
124
|
const tag = el.tagName.toLowerCase();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
});
|
|
125
|
+
|
|
126
|
+
occurrences.push(
|
|
127
|
+
helpers.reportOccurrence(el, {
|
|
128
|
+
summary: 'This list contains a direct child that is not a list item.',
|
|
129
|
+
hint: 'Only use <li> (or <script>/<template>) as direct children of <ul>/<ol>; move other markup inside an <li>.',
|
|
130
|
+
i18n: {
|
|
131
|
+
summaryKey: 'listChildrenValid_summary_fail',
|
|
132
|
+
hintKey: 'listChildrenValid_hint_fail',
|
|
133
|
+
params: { element: tag, invalidChildren: dedupedInvalidTags.join(', ') }
|
|
134
|
+
},
|
|
135
|
+
data: {
|
|
136
|
+
details: { reasonCode: 'LIST_INVALID_CHILD', element: tag, invalidChildren: invalidTags }
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
);
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
if (applicableCount === 0) {
|
|
@@ -109,9 +109,16 @@ function runInPage(ctx) {
|
|
|
109
109
|
return '';
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
113
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
114
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
112
115
|
function isEligibleAcc(helpers, el, ctx) {
|
|
113
116
|
const fn =
|
|
114
|
-
helpers && typeof helpers.
|
|
117
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
118
|
+
? helpers.isIncludedInAccessibilityTree
|
|
119
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
120
|
+
? helpers.isAccTreeEligible
|
|
121
|
+
: null;
|
|
115
122
|
if (!fn) return true;
|
|
116
123
|
try {
|
|
117
124
|
const r = fn(el, ctx);
|
|
@@ -220,24 +227,22 @@ function runInPage(ctx) {
|
|
|
220
227
|
}
|
|
221
228
|
})()
|
|
222
229
|
: null;
|
|
223
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
224
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
225
230
|
|
|
226
|
-
occurrences.push(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
231
|
+
occurrences.push(
|
|
232
|
+
helpers.reportOccurrence(el, {
|
|
233
|
+
summary: 'This element has no accessible name.',
|
|
234
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this listbox's accessible name.",
|
|
235
|
+
i18n: {
|
|
236
|
+
summaryKey: 'listboxNamePresent_summary_fail',
|
|
237
|
+
hintKey: 'listboxNamePresent_hint_fail',
|
|
238
|
+
params: { controlType: 'listbox' }
|
|
239
|
+
},
|
|
240
|
+
data: {
|
|
241
|
+
details: { reasonCode: 'name_missing', controlType: 'listbox', methodTried: res.method },
|
|
242
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
);
|
|
241
246
|
}
|
|
242
247
|
|
|
243
248
|
if (applicableCount === 0) {
|
|
@@ -116,23 +116,20 @@ function runInPage(ctx) {
|
|
|
116
116
|
|
|
117
117
|
if (valid) continue;
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
details: { reasonCode: 'LISTITEM_INVALID_PARENT', parentElement: parentTag }
|
|
134
|
-
}
|
|
135
|
-
});
|
|
119
|
+
occurrences.push(
|
|
120
|
+
helpers.reportOccurrence(el, {
|
|
121
|
+
summary: 'This list item is not contained by a list container.',
|
|
122
|
+
hint: 'Place this <li> inside a <ul>/<ol>, or give its parent role="list".',
|
|
123
|
+
i18n: {
|
|
124
|
+
summaryKey: 'listitemParentValid_summary_fail',
|
|
125
|
+
hintKey: 'listitemParentValid_hint_fail',
|
|
126
|
+
params: { parentElement: parentTag }
|
|
127
|
+
},
|
|
128
|
+
data: {
|
|
129
|
+
details: { reasonCode: 'LISTITEM_INVALID_PARENT', parentElement: parentTag }
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
);
|
|
136
133
|
}
|
|
137
134
|
|
|
138
135
|
if (applicableCount === 0) {
|
|
@@ -84,9 +84,16 @@ function runInPage(ctx) {
|
|
|
84
84
|
return '';
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
88
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
89
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
87
90
|
function isEligibleAcc(helpers, el, ctx) {
|
|
88
91
|
const fn =
|
|
89
|
-
helpers && typeof helpers.
|
|
92
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
93
|
+
? helpers.isIncludedInAccessibilityTree
|
|
94
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
95
|
+
? helpers.isAccTreeEligible
|
|
96
|
+
: null;
|
|
90
97
|
if (!fn) return true;
|
|
91
98
|
try {
|
|
92
99
|
const r = fn(el, ctx);
|
|
@@ -143,24 +150,22 @@ function runInPage(ctx) {
|
|
|
143
150
|
}
|
|
144
151
|
})()
|
|
145
152
|
: null;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
});
|
|
153
|
+
|
|
154
|
+
occurrences.push(
|
|
155
|
+
helpers.reportOccurrence(el, {
|
|
156
|
+
summary: 'This menu item has no accessible name.',
|
|
157
|
+
hint: 'Provide visible text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
|
|
158
|
+
i18n: {
|
|
159
|
+
summaryKey: 'menuitemNamePresent_summary_fail',
|
|
160
|
+
hintKey: 'menuitemNamePresent_hint_fail',
|
|
161
|
+
params: { role }
|
|
162
|
+
},
|
|
163
|
+
data: {
|
|
164
|
+
details: { reasonCode: 'name_missing', controlType: role, methodTried: res.method },
|
|
165
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
);
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
if (applicableCount === 0) {
|
|
@@ -87,6 +87,34 @@ function runInPage(ctx) {
|
|
|
87
87
|
: [];
|
|
88
88
|
|
|
89
89
|
const occurrences = [];
|
|
90
|
+
// HTML's shared declarative refresh steps. Returns the delay in seconds, or
|
|
91
|
+
// null when the value is not a valid refresh directive, in which case the
|
|
92
|
+
// browser never refreshes and there is nothing to report. Rejects a leading
|
|
93
|
+
// sign ("+72001"), a non-numeric time ("foo"), and a separator other than
|
|
94
|
+
// "," or ";" ("0:1").
|
|
95
|
+
function parseRefreshDelay(input) {
|
|
96
|
+
const s = String(input == null ? '' : input);
|
|
97
|
+
const isSpace = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\f' || c === '\r';
|
|
98
|
+
let i = 0;
|
|
99
|
+
while (i < s.length && isSpace(s[i])) i++;
|
|
100
|
+
let digits = '';
|
|
101
|
+
while (i < s.length && s[i] >= '0' && s[i] <= '9') digits += s[i++];
|
|
102
|
+
if (!digits) return null;
|
|
103
|
+
if (s[i] === '.') {
|
|
104
|
+
i++;
|
|
105
|
+
while (i < s.length && s[i] >= '0' && s[i] <= '9') i++;
|
|
106
|
+
}
|
|
107
|
+
if (i >= s.length) return parseInt(digits, 10);
|
|
108
|
+
let sawSpace = false;
|
|
109
|
+
while (i < s.length && isSpace(s[i])) {
|
|
110
|
+
sawSpace = true;
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
113
|
+
if (i >= s.length) return parseInt(digits, 10);
|
|
114
|
+
if (s[i] === ';' || s[i] === ',' || sawSpace) return parseInt(digits, 10);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
let applicableCount = 0;
|
|
91
119
|
|
|
92
120
|
for (const el of nodes) {
|
|
@@ -94,27 +122,25 @@ function runInPage(ctx) {
|
|
|
94
122
|
if (el.closest && el.closest('noscript')) continue; // never applies with scripting enabled
|
|
95
123
|
const raw = String(el.getAttribute('content') || '').trim();
|
|
96
124
|
if (!raw) continue;
|
|
125
|
+
if (parseRefreshDelay(raw) === null) continue;
|
|
97
126
|
|
|
98
127
|
applicableCount += 1;
|
|
99
128
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
details: { reasonCode: 'META_REFRESH_PRESENT' }
|
|
116
|
-
}
|
|
117
|
-
});
|
|
129
|
+
occurrences.push(
|
|
130
|
+
helpers.reportOccurrence(el, {
|
|
131
|
+
summary:
|
|
132
|
+
'This page uses a meta refresh, which is an automatic context change not initiated by the user.',
|
|
133
|
+
hint: 'Remove the meta refresh; trigger the redirect/refresh only in response to a user action instead.',
|
|
134
|
+
i18n: {
|
|
135
|
+
summaryKey: 'metaRefreshNoExceptions_summary_fail',
|
|
136
|
+
hintKey: 'metaRefreshNoExceptions_hint_fail',
|
|
137
|
+
params: {}
|
|
138
|
+
},
|
|
139
|
+
data: {
|
|
140
|
+
details: { reasonCode: 'META_REFRESH_PRESENT' }
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
);
|
|
118
144
|
}
|
|
119
145
|
|
|
120
146
|
if (applicableCount === 0) {
|
|
@@ -79,39 +79,62 @@ function runInPage(ctx) {
|
|
|
79
79
|
const occurrences = [];
|
|
80
80
|
let applicableCount = 0;
|
|
81
81
|
|
|
82
|
+
// HTML's shared declarative refresh steps. Returns the delay in seconds, or
|
|
83
|
+
// null when the value is not a valid refresh directive, in which case the
|
|
84
|
+
// browser never refreshes and there is nothing to report. Rejects a leading
|
|
85
|
+
// sign ("+72001"), a non-numeric time ("foo"), and a separator other than
|
|
86
|
+
// "," or ";" ("0:1").
|
|
87
|
+
function parseRefreshDelay(input) {
|
|
88
|
+
const s = String(input == null ? '' : input);
|
|
89
|
+
const isSpace = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\f' || c === '\r';
|
|
90
|
+
let i = 0;
|
|
91
|
+
while (i < s.length && isSpace(s[i])) i++;
|
|
92
|
+
let digits = '';
|
|
93
|
+
while (i < s.length && s[i] >= '0' && s[i] <= '9') digits += s[i++];
|
|
94
|
+
if (!digits) return null;
|
|
95
|
+
if (s[i] === '.') {
|
|
96
|
+
i++;
|
|
97
|
+
while (i < s.length && s[i] >= '0' && s[i] <= '9') i++;
|
|
98
|
+
}
|
|
99
|
+
if (i >= s.length) return parseInt(digits, 10);
|
|
100
|
+
let sawSpace = false;
|
|
101
|
+
while (i < s.length && isSpace(s[i])) {
|
|
102
|
+
sawSpace = true;
|
|
103
|
+
i++;
|
|
104
|
+
}
|
|
105
|
+
if (i >= s.length) return parseInt(digits, 10);
|
|
106
|
+
if (s[i] === ';' || s[i] === ',' || sawSpace) return parseInt(digits, 10);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
82
110
|
for (const el of nodes) {
|
|
83
111
|
if (!el || !el.getAttribute) continue;
|
|
84
112
|
if (el.closest && el.closest('noscript')) continue; // never applies with scripting enabled — see meta-refresh-no-exceptions.js's header comment
|
|
85
113
|
const raw = String(el.getAttribute('content') || '').trim();
|
|
86
114
|
if (!raw) continue;
|
|
87
115
|
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
const delay = parseFloat(match[1]);
|
|
91
|
-
if (Number.isNaN(delay)) continue;
|
|
116
|
+
const delay = parseRefreshDelay(raw);
|
|
117
|
+
if (delay === null) continue;
|
|
92
118
|
|
|
93
119
|
applicableCount += 1;
|
|
94
120
|
|
|
95
121
|
if (!(delay > 0)) continue;
|
|
96
122
|
if (delay > EXEMPT_DELAY_SECONDS) continue; // WCAG 2.2.1 Exception 3 (>20 hours)
|
|
97
123
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
details: { reasonCode: 'META_REFRESH_DELAYED', delay }
|
|
113
|
-
}
|
|
114
|
-
});
|
|
124
|
+
occurrences.push(
|
|
125
|
+
helpers.reportOccurrence(el, {
|
|
126
|
+
summary: 'This page refreshes itself automatically after a delay.',
|
|
127
|
+
hint: 'Remove the timed meta refresh, or provide a way for users to turn it off, extend it, or pause it before it triggers.',
|
|
128
|
+
i18n: {
|
|
129
|
+
summaryKey: 'metaRefreshTimingAbsent_summary_fail',
|
|
130
|
+
hintKey: 'metaRefreshTimingAbsent_hint_fail',
|
|
131
|
+
params: { delay: String(delay) }
|
|
132
|
+
},
|
|
133
|
+
data: {
|
|
134
|
+
details: { reasonCode: 'META_REFRESH_DELAYED', delay }
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
);
|
|
115
138
|
}
|
|
116
139
|
|
|
117
140
|
if (applicableCount === 0) {
|