@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
|
@@ -22,12 +22,9 @@
|
|
|
22
22
|
* `type: 'manual'` rule; see landmark-banner-is-top-level's
|
|
23
23
|
* header comment for the shared rationale/precedent (this rule mirrors
|
|
24
24
|
* its structure with contentinfo/footer in place of banner/header).
|
|
25
|
-
* - Candidate selection (`isContentinfoCandidate`
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* self-defeating-candidate-selection reasoning as
|
|
29
|
-
* landmark-banner-is-top-level (see that file's header comment for the
|
|
30
|
-
* full root cause).
|
|
25
|
+
* - Candidate selection (`isContentinfoCandidate`) requires the element to
|
|
26
|
+
* really carry the contentinfo role, via the suppression-aware
|
|
27
|
+
* `getLandmarkRole` — same reasoning as landmark-banner-is-top-level.
|
|
31
28
|
*/
|
|
32
29
|
|
|
33
30
|
const id = 'landmark-contentinfo-is-top-level';
|
|
@@ -135,11 +132,11 @@ function runInPage(ctx) {
|
|
|
135
132
|
// a candidate purely by tag + absence of any role attribute, independent
|
|
136
133
|
// of whether sectioning-ancestor nesting would currently suppress its
|
|
137
134
|
// implicit role; an explicit role="contentinfo" is always a candidate too.
|
|
135
|
+
// A candidate must actually have the contentinfo role — a <footer> inside
|
|
136
|
+
// article/aside/main/nav/section is not one, so flagging it as nested
|
|
137
|
+
// would report a landmark that does not exist.
|
|
138
138
|
function isContentinfoCandidate(el) {
|
|
139
|
-
|
|
140
|
-
const explicit = getExplicitRoleToken(el);
|
|
141
|
-
if (explicit) return explicit === 'contentinfo';
|
|
142
|
-
return !!(el.tagName && el.tagName.toLowerCase() === 'footer');
|
|
139
|
+
return getLandmarkRole(el) === 'contentinfo';
|
|
143
140
|
}
|
|
144
141
|
|
|
145
142
|
function hasLandmarkAncestor(el) {
|
|
@@ -205,23 +202,20 @@ function runInPage(ctx) {
|
|
|
205
202
|
for (const el of contentinfos) {
|
|
206
203
|
if (!hasLandmarkAncestor(el)) continue;
|
|
207
204
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
details: { reasonCode: 'LANDMARK_CONTENTINFO_NOT_TOP_LEVEL' }
|
|
223
|
-
}
|
|
224
|
-
});
|
|
205
|
+
occurrences.push(
|
|
206
|
+
helpers.reportOccurrence(el, {
|
|
207
|
+
summary: 'This contentinfo landmark is nested inside another landmark region.',
|
|
208
|
+
hint: 'Move the contentinfo landmark (footer/role="contentinfo") so it is not contained by another landmark; contentinfo should be a top-level region of the page.',
|
|
209
|
+
i18n: {
|
|
210
|
+
summaryKey: 'landmarkContentinfoIsTopLevel_summary_cantTell',
|
|
211
|
+
hintKey: 'landmarkContentinfoIsTopLevel_hint_cantTell',
|
|
212
|
+
params: {}
|
|
213
|
+
},
|
|
214
|
+
data: {
|
|
215
|
+
details: { reasonCode: 'LANDMARK_CONTENTINFO_NOT_TOP_LEVEL' }
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
);
|
|
225
219
|
}
|
|
226
220
|
|
|
227
221
|
if (occurrences.length) {
|
|
@@ -192,23 +192,20 @@ function runInPage(ctx) {
|
|
|
192
192
|
for (const el of mains) {
|
|
193
193
|
if (!hasLandmarkAncestor(el)) continue;
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
details: { reasonCode: 'LANDMARK_MAIN_NOT_TOP_LEVEL' }
|
|
210
|
-
}
|
|
211
|
-
});
|
|
195
|
+
occurrences.push(
|
|
196
|
+
helpers.reportOccurrence(el, {
|
|
197
|
+
summary: 'This main landmark is nested inside another landmark region.',
|
|
198
|
+
hint: 'Move the main landmark (<main>/role="main") so it is not contained by another landmark; main should be a top-level region of the page.',
|
|
199
|
+
i18n: {
|
|
200
|
+
summaryKey: 'landmarkMainIsTopLevel_summary_cantTell',
|
|
201
|
+
hintKey: 'landmarkMainIsTopLevel_hint_cantTell',
|
|
202
|
+
params: {}
|
|
203
|
+
},
|
|
204
|
+
data: {
|
|
205
|
+
details: { reasonCode: 'LANDMARK_MAIN_NOT_TOP_LEVEL' }
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
);
|
|
212
209
|
}
|
|
213
210
|
|
|
214
211
|
if (occurrences.length) {
|
|
@@ -171,12 +171,7 @@ function runInPage(ctx) {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
const occurrences = banners.map((el) => {
|
|
174
|
-
|
|
175
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
selector: stableSelector,
|
|
179
|
-
html,
|
|
174
|
+
return helpers.reportOccurrence(el, {
|
|
180
175
|
summary: 'This page has more than one banner landmark.',
|
|
181
176
|
hint: 'Keep only one banner landmark (header/role="banner") per page.',
|
|
182
177
|
i18n: {
|
|
@@ -187,7 +182,7 @@ function runInPage(ctx) {
|
|
|
187
182
|
data: {
|
|
188
183
|
details: { reasonCode: 'LANDMARK_DUPLICATE_BANNER', count: banners.length }
|
|
189
184
|
}
|
|
190
|
-
};
|
|
185
|
+
});
|
|
191
186
|
});
|
|
192
187
|
|
|
193
188
|
return {
|
|
@@ -163,12 +163,7 @@ function runInPage(ctx) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
const occurrences = contentinfos.map((el) => {
|
|
166
|
-
|
|
167
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
selector: stableSelector,
|
|
171
|
-
html,
|
|
166
|
+
return helpers.reportOccurrence(el, {
|
|
172
167
|
summary: 'This page has more than one contentinfo landmark.',
|
|
173
168
|
hint: 'Keep only one contentinfo landmark (footer/role="contentinfo") per page.',
|
|
174
169
|
i18n: {
|
|
@@ -179,7 +174,7 @@ function runInPage(ctx) {
|
|
|
179
174
|
data: {
|
|
180
175
|
details: { reasonCode: 'LANDMARK_DUPLICATE_CONTENTINFO', count: contentinfos.length }
|
|
181
176
|
}
|
|
182
|
-
};
|
|
177
|
+
});
|
|
183
178
|
});
|
|
184
179
|
|
|
185
180
|
return {
|
|
@@ -108,12 +108,7 @@ function runInPage(ctx) {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
const occurrences = mains.map((el) => {
|
|
111
|
-
|
|
112
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
selector: stableSelector,
|
|
116
|
-
html,
|
|
111
|
+
return helpers.reportOccurrence(el, {
|
|
117
112
|
summary: 'This page has more than one main landmark.',
|
|
118
113
|
hint: 'Keep only one main landmark (<main>/role="main") per page.',
|
|
119
114
|
i18n: {
|
|
@@ -124,7 +119,7 @@ function runInPage(ctx) {
|
|
|
124
119
|
data: {
|
|
125
120
|
details: { reasonCode: 'LANDMARK_DUPLICATE_MAIN', count: mains.length }
|
|
126
121
|
}
|
|
127
|
-
};
|
|
122
|
+
});
|
|
128
123
|
});
|
|
129
124
|
|
|
130
125
|
return {
|
|
@@ -123,19 +123,12 @@ function runInPage(ctx) {
|
|
|
123
123
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
|
|
127
|
-
const html = helpers.getOuterHtmlSnippet
|
|
128
|
-
? helpers.getOuterHtmlSnippet(body)
|
|
129
|
-
: (body.outerHTML || '').slice(0, 200);
|
|
130
|
-
|
|
131
126
|
return {
|
|
132
127
|
ruleId: rule.ruleId,
|
|
133
128
|
outcome: 'cantTell',
|
|
134
129
|
severity: rule.defaultSeverity || 'minor',
|
|
135
130
|
occurrences: [
|
|
136
|
-
{
|
|
137
|
-
selector: stableSelector,
|
|
138
|
-
html,
|
|
131
|
+
helpers.reportOccurrence(body, {
|
|
139
132
|
summary: 'This page has no main landmark.',
|
|
140
133
|
hint: 'Add a main landmark (<main> or role="main") around the page\'s primary content.',
|
|
141
134
|
i18n: {
|
|
@@ -146,7 +139,7 @@ function runInPage(ctx) {
|
|
|
146
139
|
data: {
|
|
147
140
|
details: { reasonCode: 'LANDMARK_MAIN_MISSING' }
|
|
148
141
|
}
|
|
149
|
-
}
|
|
142
|
+
})
|
|
150
143
|
]
|
|
151
144
|
};
|
|
152
145
|
}
|
|
@@ -219,34 +219,29 @@ function runInPage(ctx) {
|
|
|
219
219
|
if (group.length <= 1) continue;
|
|
220
220
|
|
|
221
221
|
for (const { el } of group) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
details: {
|
|
243
|
-
reasonCode: 'LANDMARK_NOT_UNIQUE',
|
|
244
|
-
role,
|
|
245
|
-
name: normalizedName,
|
|
246
|
-
groupSize: group.length
|
|
222
|
+
occurrences.push(
|
|
223
|
+
helpers.reportOccurrence(el, {
|
|
224
|
+
summary: normalizedName
|
|
225
|
+
? `This ${role} landmark shares its accessible name with another ${role} landmark.`
|
|
226
|
+
: `This ${role} landmark has no accessible name, and more than one unnamed ${role} landmark exists on this page.`,
|
|
227
|
+
hint: `Give each ${role} landmark a distinct name via aria-label or aria-labelledby.`,
|
|
228
|
+
i18n: {
|
|
229
|
+
summaryKey: normalizedName
|
|
230
|
+
? 'landmarkUnique_summary_cantTell_duplicateName'
|
|
231
|
+
: 'landmarkUnique_summary_cantTell_bothUnnamed',
|
|
232
|
+
hintKey: 'landmarkUnique_hint_cantTell',
|
|
233
|
+
params: { role }
|
|
234
|
+
},
|
|
235
|
+
data: {
|
|
236
|
+
details: {
|
|
237
|
+
reasonCode: 'LANDMARK_NOT_UNIQUE',
|
|
238
|
+
role,
|
|
239
|
+
name: normalizedName,
|
|
240
|
+
groupSize: group.length
|
|
241
|
+
}
|
|
247
242
|
}
|
|
248
|
-
}
|
|
249
|
-
|
|
243
|
+
})
|
|
244
|
+
);
|
|
250
245
|
}
|
|
251
246
|
}
|
|
252
247
|
}
|
|
@@ -133,27 +133,25 @@ function runInPage(ctx) {
|
|
|
133
133
|
|
|
134
134
|
if (!GENERIC_LINK_TEXT.has(normalized)) continue;
|
|
135
135
|
|
|
136
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
137
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
138
136
|
const eligInfo = helpers.getEligibilityInfo
|
|
139
137
|
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
140
138
|
: null;
|
|
141
139
|
|
|
142
|
-
occurrences.push(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
140
|
+
occurrences.push(
|
|
141
|
+
helpers.reportOccurrence(el, {
|
|
142
|
+
summary: `This link's accessible name ("${rawName.trim()}") is a generic, non-descriptive phrase.`,
|
|
143
|
+
hint: 'Make the link text itself describe its destination/purpose (e.g. "Download the 2026 pricing guide" instead of "Download"), or confirm the surrounding context already makes the purpose clear.',
|
|
144
|
+
i18n: {
|
|
145
|
+
summaryKey: 'linkNameQuality_summary_cantTell',
|
|
146
|
+
hintKey: 'linkNameQuality_hint_cantTell',
|
|
147
|
+
params: { name: rawName.trim() }
|
|
148
|
+
},
|
|
149
|
+
data: {
|
|
150
|
+
details: { reasonCode: 'GENERIC_LINK_TEXT', normalizedName: normalized },
|
|
151
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
);
|
|
157
155
|
}
|
|
158
156
|
|
|
159
157
|
if (applicableCount === 0) {
|
|
@@ -104,23 +104,20 @@ function runInPage(ctx) {
|
|
|
104
104
|
|
|
105
105
|
if (!reasons.length) continue;
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
details: { reasonCode: 'VIEWPORT_ZOOM_BELOW_500', reasons }
|
|
122
|
-
}
|
|
123
|
-
});
|
|
107
|
+
occurrences.push(
|
|
108
|
+
helpers.reportOccurrence(el, {
|
|
109
|
+
summary: 'This viewport meta tag restricts zoom below the 500% best-practice target.',
|
|
110
|
+
hint: 'Remove user-scalable=no and raise maximum-scale to at least 5 (500%) if possible.',
|
|
111
|
+
i18n: {
|
|
112
|
+
summaryKey: 'metaViewportLarge_summary_cantTell',
|
|
113
|
+
hintKey: 'metaViewportLarge_hint_cantTell',
|
|
114
|
+
params: { reasons: reasons.join(', ') }
|
|
115
|
+
},
|
|
116
|
+
data: {
|
|
117
|
+
details: { reasonCode: 'VIEWPORT_ZOOM_BELOW_500', reasons }
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
);
|
|
124
121
|
}
|
|
125
122
|
|
|
126
123
|
if (applicableCount === 0) {
|
|
@@ -117,30 +117,28 @@ function runInPage(ctx) {
|
|
|
117
117
|
const hasKeyboardEquiv = KEYBOARD_EQUIV_ATTRS.some((a) => trim(el.getAttribute(a)));
|
|
118
118
|
if (hasKeyboardEquiv) continue;
|
|
119
119
|
|
|
120
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
121
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
122
120
|
const eligInfo = helpers.getEligibilityInfo
|
|
123
121
|
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
124
122
|
: null;
|
|
125
123
|
|
|
126
|
-
occurrences.push(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
params: { attrs: presentMouseAttrs.join(', ') }
|
|
135
|
-
},
|
|
136
|
-
data: {
|
|
137
|
-
details: {
|
|
138
|
-
reasonCode: 'MOUSE_ONLY_HANDLER_NO_KEYBOARD_EQUIVALENT',
|
|
139
|
-
mouseAttrs: presentMouseAttrs
|
|
124
|
+
occurrences.push(
|
|
125
|
+
helpers.reportOccurrence(el, {
|
|
126
|
+
summary: `This element has ${presentMouseAttrs.join(', ')} but no keyboard-reachable equivalent handler.`,
|
|
127
|
+
hint: 'Add onkeydown/onkeyup/onkeypress (or onfocus/onblur for hover-triggered behavior) so this functionality is also reachable by keyboard.',
|
|
128
|
+
i18n: {
|
|
129
|
+
summaryKey: 'mouseOnlyEventHandlers_summary_cantTell',
|
|
130
|
+
hintKey: 'mouseOnlyEventHandlers_hint_cantTell',
|
|
131
|
+
params: { attrs: presentMouseAttrs.join(', ') }
|
|
140
132
|
},
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
133
|
+
data: {
|
|
134
|
+
details: {
|
|
135
|
+
reasonCode: 'MOUSE_ONLY_HANDLER_NO_KEYBOARD_EQUIVALENT',
|
|
136
|
+
mouseAttrs: presentMouseAttrs
|
|
137
|
+
},
|
|
138
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
);
|
|
144
142
|
}
|
|
145
143
|
|
|
146
144
|
if (applicableCount === 0) {
|
|
@@ -126,19 +126,12 @@ function runInPage(ctx) {
|
|
|
126
126
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
|
|
130
|
-
const html = helpers.getOuterHtmlSnippet
|
|
131
|
-
? helpers.getOuterHtmlSnippet(body)
|
|
132
|
-
: (body.outerHTML || '').slice(0, 200);
|
|
133
|
-
|
|
134
129
|
return {
|
|
135
130
|
ruleId: rule.ruleId,
|
|
136
131
|
outcome: 'cantTell',
|
|
137
132
|
severity: rule.defaultSeverity || 'minor',
|
|
138
133
|
occurrences: [
|
|
139
|
-
{
|
|
140
|
-
selector: stableSelector,
|
|
141
|
-
html,
|
|
134
|
+
helpers.reportOccurrence(body, {
|
|
142
135
|
summary: 'This page has no level-one heading.',
|
|
143
136
|
hint: 'Add a level-one heading (<h1> or role="heading" aria-level="1") that identifies the page\'s main content.',
|
|
144
137
|
i18n: {
|
|
@@ -149,7 +142,7 @@ function runInPage(ctx) {
|
|
|
149
142
|
data: {
|
|
150
143
|
details: { reasonCode: 'HEADING_ONE_MISSING' }
|
|
151
144
|
}
|
|
152
|
-
}
|
|
145
|
+
})
|
|
153
146
|
]
|
|
154
147
|
};
|
|
155
148
|
}
|
|
@@ -171,28 +171,26 @@ function runInPage(ctx) {
|
|
|
171
171
|
String(el.getAttribute('role') || '')
|
|
172
172
|
.trim()
|
|
173
173
|
.toLowerCase() || 'presentation';
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
conflictingAttrs: present,
|
|
192
|
-
focusable: isFocusable
|
|
174
|
+
|
|
175
|
+
occurrences.push(
|
|
176
|
+
helpers.reportOccurrence(el, {
|
|
177
|
+
summary: `This role="${role}" element also has a conflicting condition (${parts.join(', ')}), which restores its implicit role and cancels the presentational intent.`,
|
|
178
|
+
hint: 'Remove the conflicting naming attribute(s) and/or focusability (tabindex/native) if the element should stay presentational, or remove role="presentation"/"none" if it should be exposed to assistive technology.',
|
|
179
|
+
i18n: {
|
|
180
|
+
summaryKey: 'presentationRoleConflict_summary_cantTell',
|
|
181
|
+
hintKey: 'presentationRoleConflict_hint_cantTell',
|
|
182
|
+
params: { role, attrs: parts.join(', ') }
|
|
183
|
+
},
|
|
184
|
+
data: {
|
|
185
|
+
details: {
|
|
186
|
+
reasonCode: 'PRESENTATION_ROLE_CONFLICT',
|
|
187
|
+
role,
|
|
188
|
+
conflictingAttrs: present,
|
|
189
|
+
focusable: isFocusable
|
|
190
|
+
}
|
|
193
191
|
}
|
|
194
|
-
}
|
|
195
|
-
|
|
192
|
+
})
|
|
193
|
+
);
|
|
196
194
|
}
|
|
197
195
|
|
|
198
196
|
if (applicableCount === 0) {
|
|
@@ -352,14 +352,11 @@ function runInPage(ctx) {
|
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
// Report the element itself: without a node reference the engine re-finds
|
|
356
|
+
// each one with document.querySelector to build its structuralPath.
|
|
355
357
|
const occurrences = collapsed.map((el) => {
|
|
356
358
|
const tag = el.tagName ? lower(el.tagName) : '';
|
|
357
|
-
const
|
|
358
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
359
|
-
|
|
360
|
-
return {
|
|
361
|
-
selector: stableSelector,
|
|
362
|
-
html,
|
|
359
|
+
const partial = {
|
|
363
360
|
summary: 'This content is not contained within a landmark region.',
|
|
364
361
|
hint: 'Move this content inside a landmark region (main, nav, aside, a labeled section, etc.).',
|
|
365
362
|
i18n: {
|
|
@@ -371,6 +368,16 @@ function runInPage(ctx) {
|
|
|
371
368
|
details: { reasonCode: 'CONTENT_OUTSIDE_LANDMARK', element: tag }
|
|
372
369
|
}
|
|
373
370
|
};
|
|
371
|
+
|
|
372
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
373
|
+
return helpers.reportOccurrence(el, partial);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
selector: helpers.buildSelector ? helpers.buildSelector(el) : 'html',
|
|
378
|
+
html: helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '',
|
|
379
|
+
...partial
|
|
380
|
+
};
|
|
374
381
|
});
|
|
375
382
|
|
|
376
383
|
if (occurrences.length === 0) {
|
|
@@ -61,23 +61,20 @@ function runInPage(ctx) {
|
|
|
61
61
|
|
|
62
62
|
if (VALID_SCOPES.has(raw.toLowerCase())) continue;
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
details: { reasonCode: 'SCOPE_ATTR_INVALID', value: raw }
|
|
79
|
-
}
|
|
80
|
-
});
|
|
64
|
+
occurrences.push(
|
|
65
|
+
helpers.reportOccurrence(el, {
|
|
66
|
+
summary: 'This scope attribute value is not recognized.',
|
|
67
|
+
hint: 'Use one of row, col, rowgroup, or colgroup for the scope attribute.',
|
|
68
|
+
i18n: {
|
|
69
|
+
summaryKey: 'scopeAttrValid_summary_cantTell',
|
|
70
|
+
hintKey: 'scopeAttrValid_hint_cantTell',
|
|
71
|
+
params: { value: raw }
|
|
72
|
+
},
|
|
73
|
+
data: {
|
|
74
|
+
details: { reasonCode: 'SCOPE_ATTR_INVALID', value: raw }
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
);
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
if (applicableCount === 0) {
|