@surea11y/core 1.4.1 → 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 +47 -7
- package/README.md +19 -3
- package/docs/API_STABILITY.md +2 -2
- 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/TROUBLESHOOTING.md +2 -2
- package/package.json +6 -1
- package/src/checks/automatic/aria-allowed-attr.js +27 -30
- 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 +62 -49
- 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 +37 -23
- 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 +15 -17
- package/src/checks/automatic/avoid-inline-spacing.js +14 -16
- package/src/checks/automatic/binary-control-name-present.js +19 -21
- package/src/checks/automatic/button-name-present.js +23 -28
- package/src/checks/automatic/combobox-name-present.js +15 -17
- 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 +16 -18
- 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-single-label.js +38 -40
- 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/label-in-name.js +31 -36
- package/src/checks/automatic/link-in-text-block.js +19 -21
- package/src/checks/automatic/link-name-present.js +25 -30
- package/src/checks/automatic/list-children-valid.js +15 -17
- package/src/checks/automatic/listbox-name-present.js +15 -17
- package/src/checks/automatic/listitem-parent-valid.js +14 -17
- package/src/checks/automatic/menuitem-name-present.js +16 -18
- package/src/checks/automatic/meta-refresh-no-exceptions.js +15 -18
- package/src/checks/automatic/meta-refresh-timing-absent.js +14 -17
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
- package/src/checks/automatic/meter-name-present.js +16 -18
- package/src/checks/automatic/nested-interactive-controls-absent.js +17 -19
- package/src/checks/automatic/option-name-present.js +16 -18
- package/src/checks/automatic/progressbar-name-present.js +19 -21
- package/src/checks/automatic/searchbox-name-present.js +19 -17
- package/src/checks/automatic/server-side-image-map-absent.js +15 -18
- package/src/checks/automatic/slider-name-present.js +15 -17
- package/src/checks/automatic/spinbutton-name-present.js +19 -17
- package/src/checks/automatic/summary-name-present.js +16 -18
- package/src/checks/automatic/tab-name-present.js +16 -18
- package/src/checks/automatic/table-headers-attr-valid.js +15 -17
- package/src/checks/automatic/table-th-has-data-cells.js +15 -19
- 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 +15 -17
- package/src/checks/automatic/tooltip-name-present.js +16 -18
- package/src/checks/automatic/treeitem-name-present.js +16 -18
- package/src/checks/automatic/valid-lang.js +15 -17
- package/src/checks/manual/accesskeys-manual.js +18 -19
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +19 -22
- package/src/checks/manual/bypass-blocks-present-manual.js +4 -12
- 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/label-title-only-manual.js +15 -17
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +14 -17
- 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 +4203 -3604
- package/src/report.js +14 -0
- package/surea11y.browser.js +1960 -3671
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
|
@@ -86,24 +86,21 @@ function runInPage(ctx) {
|
|
|
86
86
|
|
|
87
87
|
if (!invalidNames || !invalidNames.length) continue;
|
|
88
88
|
|
|
89
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
90
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
91
|
-
|
|
92
89
|
for (const name of invalidNames) {
|
|
93
|
-
occurrences.push(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
90
|
+
occurrences.push(
|
|
91
|
+
helpers.reportOccurrence(el, {
|
|
92
|
+
summary: 'This element has an attribute that is not a recognized ARIA attribute.',
|
|
93
|
+
hint: 'Correct the attribute name (check for typos), or remove it if not needed.',
|
|
94
|
+
i18n: {
|
|
95
|
+
summaryKey: 'ariaValidAttr_summary_fail',
|
|
96
|
+
hintKey: 'ariaValidAttr_hint_fail',
|
|
97
|
+
params: { attr: name }
|
|
98
|
+
},
|
|
99
|
+
data: {
|
|
100
|
+
details: { reasonCode: 'ARIA_ATTR_INVALID', attr: name }
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
);
|
|
107
104
|
}
|
|
108
105
|
}
|
|
109
106
|
|
|
@@ -193,23 +193,21 @@ function runInPage(ctx) {
|
|
|
193
193
|
if (isValidAutocomplete(raw)) continue;
|
|
194
194
|
|
|
195
195
|
const tag = el.tagName.toLowerCase();
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
});
|
|
196
|
+
|
|
197
|
+
occurrences.push(
|
|
198
|
+
helpers.reportOccurrence(el, {
|
|
199
|
+
summary: 'This autocomplete attribute value is not a valid autofill value.',
|
|
200
|
+
hint: 'Use "on"/"off", or a valid autofill token list (e.g. "shipping street-address", "cc-number").',
|
|
201
|
+
i18n: {
|
|
202
|
+
summaryKey: 'autocompleteValid_summary_fail',
|
|
203
|
+
hintKey: 'autocompleteValid_hint_fail',
|
|
204
|
+
params: { element: tag, value: raw }
|
|
205
|
+
},
|
|
206
|
+
data: {
|
|
207
|
+
details: { reasonCode: 'AUTOCOMPLETE_VALUE_INVALID', element: tag, value: raw }
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
);
|
|
213
211
|
}
|
|
214
212
|
|
|
215
213
|
if (applicableCount === 0) {
|
|
@@ -245,23 +245,21 @@ function runInPage(ctx) {
|
|
|
245
245
|
if (!flagged.length) continue;
|
|
246
246
|
|
|
247
247
|
const tag = el.tagName.toLowerCase();
|
|
248
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
249
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
250
248
|
|
|
251
|
-
occurrences.push(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
249
|
+
occurrences.push(
|
|
250
|
+
helpers.reportOccurrence(el, {
|
|
251
|
+
summary: `This element's inline style forces ${flagged.join(', ')} with !important below the WCAG text-spacing metric, so the user cannot raise it.`,
|
|
252
|
+
hint: 'Remove !important from line-height/letter-spacing/word-spacing in inline styles, or set a value that already meets the metric (line-height 1.5, letter-spacing 0.12em, word-spacing 0.16em).',
|
|
253
|
+
i18n: {
|
|
254
|
+
summaryKey: 'avoidInlineSpacing_summary_fail',
|
|
255
|
+
hintKey: 'avoidInlineSpacing_hint_fail',
|
|
256
|
+
params: { element: tag, properties: flagged.join(', ') }
|
|
257
|
+
},
|
|
258
|
+
data: {
|
|
259
|
+
details: { reasonCode: 'INLINE_SPACING_IMPORTANT', element: tag, properties: flagged }
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
);
|
|
265
263
|
}
|
|
266
264
|
|
|
267
265
|
if (applicableCount === 0) {
|
|
@@ -272,28 +272,26 @@ function runInPage(ctx) {
|
|
|
272
272
|
}
|
|
273
273
|
})()
|
|
274
274
|
: null;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
summaryKey: 'binaryControlNamePresent_summary_fail',
|
|
285
|
-
hintKey: 'binaryControlNamePresent_hint_fail',
|
|
286
|
-
params: { controlType }
|
|
287
|
-
},
|
|
288
|
-
data: {
|
|
289
|
-
details: {
|
|
290
|
-
reasonCode: 'name_missing',
|
|
291
|
-
controlType,
|
|
292
|
-
methodTried: res.method
|
|
275
|
+
|
|
276
|
+
occurrences.push(
|
|
277
|
+
helpers.reportOccurrence(el, {
|
|
278
|
+
summary: 'This control has no accessible name.',
|
|
279
|
+
hint: 'Provide a label, aria-label, aria-labelledby, or other accessible-name mechanism so assistive technologies can identify the control.',
|
|
280
|
+
i18n: {
|
|
281
|
+
summaryKey: 'binaryControlNamePresent_summary_fail',
|
|
282
|
+
hintKey: 'binaryControlNamePresent_hint_fail',
|
|
283
|
+
params: { controlType }
|
|
293
284
|
},
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
285
|
+
data: {
|
|
286
|
+
details: {
|
|
287
|
+
reasonCode: 'name_missing',
|
|
288
|
+
controlType,
|
|
289
|
+
methodTried: res.method
|
|
290
|
+
},
|
|
291
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
);
|
|
297
295
|
}
|
|
298
296
|
|
|
299
297
|
if (applicableCount === 0) {
|
|
@@ -170,35 +170,30 @@ function runInPage(ctx) {
|
|
|
170
170
|
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
171
171
|
: null;
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
inputValueNameLength: inputValueName.length,
|
|
195
|
-
contentNameLength: contentName.length,
|
|
196
|
-
explicitProgrammatic: explicitProg ? 1 : 0
|
|
197
|
-
},
|
|
198
|
-
refs: { accessibleName: nameInfo || null }
|
|
173
|
+
occurrences.push(
|
|
174
|
+
helpers.reportOccurrence(el, {
|
|
175
|
+
summary: 'This button has no accessible name.',
|
|
176
|
+
hint: 'Provide visible button text or a programmatic accessible-name mechanism (for example aria-label) so assistive technologies can identify the button.',
|
|
177
|
+
i18n: {
|
|
178
|
+
summaryKey: 'buttonNamePresent_summary_fail',
|
|
179
|
+
hintKey: 'buttonNamePresent_hint_fail',
|
|
180
|
+
params: { element: tag }
|
|
181
|
+
},
|
|
182
|
+
data: {
|
|
183
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
184
|
+
details: {
|
|
185
|
+
reasonCode: 'name_missing',
|
|
186
|
+
metrics: {
|
|
187
|
+
trustedProgrammaticNameLength: trustedProgrammaticName.length,
|
|
188
|
+
inputValueNameLength: inputValueName.length,
|
|
189
|
+
contentNameLength: contentName.length,
|
|
190
|
+
explicitProgrammatic: explicitProg ? 1 : 0
|
|
191
|
+
},
|
|
192
|
+
refs: { accessibleName: nameInfo || null }
|
|
193
|
+
}
|
|
199
194
|
}
|
|
200
|
-
}
|
|
201
|
-
|
|
195
|
+
})
|
|
196
|
+
);
|
|
202
197
|
}
|
|
203
198
|
}
|
|
204
199
|
|
|
@@ -231,24 +231,22 @@ function runInPage(ctx) {
|
|
|
231
231
|
}
|
|
232
232
|
})()
|
|
233
233
|
: null;
|
|
234
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
235
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
236
234
|
|
|
237
|
-
occurrences.push(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
235
|
+
occurrences.push(
|
|
236
|
+
helpers.reportOccurrence(el, {
|
|
237
|
+
summary: 'This combobox has no accessible name.',
|
|
238
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this combobox's accessible name.",
|
|
239
|
+
i18n: {
|
|
240
|
+
summaryKey: 'comboboxNamePresent_summary_fail',
|
|
241
|
+
hintKey: 'comboboxNamePresent_hint_fail',
|
|
242
|
+
params: {}
|
|
243
|
+
},
|
|
244
|
+
data: {
|
|
245
|
+
details: { reasonCode: 'name_missing', controlType: 'combobox', methodTried: res.method },
|
|
246
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
);
|
|
252
250
|
}
|
|
253
251
|
|
|
254
252
|
if (applicableCount === 0) {
|
|
@@ -156,7 +156,7 @@ function runInPage(ctx) {
|
|
|
156
156
|
for (const r of rules) {
|
|
157
157
|
if (!r) continue;
|
|
158
158
|
if (r.type === CSS_STYLE_RULE && isLockingRotation(r.style)) {
|
|
159
|
-
findings.push({ mediaText, selectorText: trim(r.selectorText)
|
|
159
|
+
findings.push({ mediaText, selectorText: trim(r.selectorText) });
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
}
|
|
@@ -196,28 +196,28 @@ function runInPage(ctx) {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
const target = document.documentElement || document.body || null;
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
199
|
+
const occurrences = findings.map((f) =>
|
|
200
|
+
helpers.reportOccurrence(target, {
|
|
201
|
+
summary: f.selectorText
|
|
202
|
+
? `A "${f.mediaText}" media query rotates "${f.selectorText}", locking the page to one orientation.`
|
|
203
|
+
: `A "${f.mediaText}" media query rotates an element with no readable selector, locking the page to one orientation.`,
|
|
204
|
+
hint: 'Remove the rotate() transform from the orientation media query; let the page respond naturally to device orientation instead of forcing a visual rotation.',
|
|
205
|
+
i18n: {
|
|
206
|
+
summaryKey: f.selectorText
|
|
207
|
+
? 'cssOrientationLock_summary_fail'
|
|
208
|
+
: 'cssOrientationLock_summary_fail_unknownSelector',
|
|
209
|
+
hintKey: 'cssOrientationLock_hint_fail',
|
|
210
|
+
params: { mediaText: f.mediaText, selectorText: f.selectorText }
|
|
211
|
+
},
|
|
212
|
+
data: {
|
|
213
|
+
details: {
|
|
214
|
+
reasonCode: 'ORIENTATION_MEDIA_ROTATE_TRANSFORM',
|
|
215
|
+
mediaText: f.mediaText,
|
|
216
|
+
selectorText: f.selectorText
|
|
217
|
+
}
|
|
218
218
|
}
|
|
219
|
-
}
|
|
220
|
-
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
221
|
|
|
222
222
|
return {
|
|
223
223
|
ruleId: rule.ruleId,
|
|
@@ -121,9 +121,6 @@ function runInPage(ctx) {
|
|
|
121
121
|
: null;
|
|
122
122
|
if (!reasonCode) continue;
|
|
123
123
|
|
|
124
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
125
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
126
|
-
|
|
127
124
|
const summary = invalidTags.length
|
|
128
125
|
? 'This description list contains a direct or wrapped child that is not part of a dt/dd group.'
|
|
129
126
|
: 'This description list has no <dt>/<dd> term-definition group.';
|
|
@@ -131,24 +128,24 @@ function runInPage(ctx) {
|
|
|
131
128
|
? 'Only use <dt>/<dd> (optionally wrapped in one <div>), <script>, <template>, or <style> inside <dl>.'
|
|
132
129
|
: 'Add at least one <dt>/<dd> pair inside this <dl>.';
|
|
133
130
|
|
|
134
|
-
occurrences.push(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
: '
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
131
|
+
occurrences.push(
|
|
132
|
+
helpers.reportOccurrence(el, {
|
|
133
|
+
summary,
|
|
134
|
+
hint,
|
|
135
|
+
i18n: {
|
|
136
|
+
summaryKey: invalidTags.length
|
|
137
|
+
? 'definitionListChildrenValid_summary_fail_invalidChild'
|
|
138
|
+
: 'definitionListChildrenValid_summary_fail_noDtDd',
|
|
139
|
+
hintKey: invalidTags.length
|
|
140
|
+
? 'definitionListChildrenValid_hint_fail_invalidChild'
|
|
141
|
+
: 'definitionListChildrenValid_hint_fail_noDtDd',
|
|
142
|
+
params: { invalidChildren: dedupedInvalidTags.join(', ') }
|
|
143
|
+
},
|
|
144
|
+
data: {
|
|
145
|
+
details: { reasonCode, invalidChildren: invalidTags }
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
);
|
|
152
149
|
}
|
|
153
150
|
|
|
154
151
|
if (applicableCount === 0) {
|
|
@@ -72,23 +72,21 @@ function runInPage(ctx) {
|
|
|
72
72
|
applicableCount += 1;
|
|
73
73
|
|
|
74
74
|
const tag = el.tagName.toLowerCase();
|
|
75
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
76
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
77
75
|
|
|
78
|
-
occurrences.push(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
76
|
+
occurrences.push(
|
|
77
|
+
helpers.reportOccurrence(el, {
|
|
78
|
+
summary: 'This element’s content cannot be paused, stopped, or hidden by the user.',
|
|
79
|
+
hint: 'Remove this element; use static content, or an animation with a user-facing pause/stop control, instead.',
|
|
80
|
+
i18n: {
|
|
81
|
+
summaryKey: 'deprecatedElements_summary_fail',
|
|
82
|
+
hintKey: 'deprecatedElements_hint_fail',
|
|
83
|
+
params: { element: tag }
|
|
84
|
+
},
|
|
85
|
+
data: {
|
|
86
|
+
details: { reasonCode: 'DEPRECATED_NON_STOPPABLE_ELEMENT', element: tag }
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
);
|
|
92
90
|
}
|
|
93
91
|
|
|
94
92
|
if (applicableCount === 0) {
|
|
@@ -140,24 +140,22 @@ function runInPage(ctx) {
|
|
|
140
140
|
}
|
|
141
141
|
})()
|
|
142
142
|
: null;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
});
|
|
143
|
+
|
|
144
|
+
occurrences.push(
|
|
145
|
+
helpers.reportOccurrence(el, {
|
|
146
|
+
summary: 'This dialog has no accessible name.',
|
|
147
|
+
hint: 'Provide aria-labelledby (preferred) or aria-label so assistive technologies can announce the dialog.',
|
|
148
|
+
i18n: {
|
|
149
|
+
summaryKey: 'dialogNamePresent_summary_fail',
|
|
150
|
+
hintKey: 'dialogNamePresent_hint_fail',
|
|
151
|
+
params: { role }
|
|
152
|
+
},
|
|
153
|
+
data: {
|
|
154
|
+
details: { reasonCode: 'name_missing', controlType: role, methodTried: res.method },
|
|
155
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
);
|
|
161
159
|
}
|
|
162
160
|
|
|
163
161
|
if (applicableCount === 0) {
|
|
@@ -80,23 +80,21 @@ function runInPage(ctx) {
|
|
|
80
80
|
if (valid) continue;
|
|
81
81
|
|
|
82
82
|
const tag = el.tagName.toLowerCase();
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
});
|
|
83
|
+
|
|
84
|
+
occurrences.push(
|
|
85
|
+
helpers.reportOccurrence(el, {
|
|
86
|
+
summary: 'This description-list item is not contained by a <dl>.',
|
|
87
|
+
hint: 'Place this <dt>/<dd> inside a <dl>, directly or wrapped in a single <div>.',
|
|
88
|
+
i18n: {
|
|
89
|
+
summaryKey: 'dlitemParentValid_summary_fail',
|
|
90
|
+
hintKey: 'dlitemParentValid_hint_fail',
|
|
91
|
+
params: { element: tag, parentElement: parentTag }
|
|
92
|
+
},
|
|
93
|
+
data: {
|
|
94
|
+
details: { reasonCode: 'DLITEM_INVALID_PARENT', element: tag, parentElement: parentTag }
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
);
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
if (applicableCount === 0) {
|
|
@@ -15,15 +15,16 @@
|
|
|
15
15
|
* attribute (i.e. at least one ARIA ID reference exists to resolve).
|
|
16
16
|
* @expectation
|
|
17
17
|
* For every id value referenced by one of those attributes, exactly one
|
|
18
|
-
* element in the document carries that id. A
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* element in the document carries that id. A duplicate does not break the
|
|
19
|
+
* reference: it resolves to the first element in tree order, so the name is
|
|
20
|
+
* still computed. Whether that element is the intended target depends on
|
|
21
|
+
* author intent, which markup does not carry, so the outcome is cantTell.
|
|
22
22
|
* @implementation-notes
|
|
23
23
|
* - Scoped deliberately to ids referenced by ARIA, not the broader/
|
|
24
24
|
* deprecated page-wide duplicate-id check (see ROADMAP.md's "Skip" list).
|
|
25
25
|
* - Document-wide by design: id uniqueness and ARIA id references are a
|
|
26
|
-
* whole-document property, not scoped to a sub-root.
|
|
26
|
+
* whole-document property, not scoped to a sub-root. Reported occurrences are
|
|
27
|
+
* limited to the scanned scope.
|
|
27
28
|
*/
|
|
28
29
|
|
|
29
30
|
const id = 'duplicate-id-aria';
|
|
@@ -95,51 +96,58 @@ function runInPage(ctx) {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
// Detection is document-wide; occurrences are limited to the scanned scope.
|
|
100
|
+
let inScope = null;
|
|
101
|
+
if (helpers && typeof helpers.queryAllSmart === 'function') {
|
|
102
|
+
try {
|
|
103
|
+
const scoped = helpers.queryAllSmart('[id]');
|
|
104
|
+
inScope = new Set(Array.isArray(scoped) ? scoped : Array.from(scoped || []));
|
|
105
|
+
} catch {
|
|
106
|
+
inScope = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const cantTellOccurrences = [];
|
|
99
111
|
|
|
100
112
|
for (const refId of referencedIds) {
|
|
101
113
|
const els = idMap.get(refId) || [];
|
|
102
114
|
if (els.length <= 1) continue;
|
|
103
115
|
|
|
104
116
|
for (const el of els) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
id: refId,
|
|
124
|
-
duplicateCount: els.length
|
|
117
|
+
if (inScope && !inScope.has(el)) continue;
|
|
118
|
+
|
|
119
|
+
cantTellOccurrences.push(
|
|
120
|
+
helpers.reportOccurrence(el, {
|
|
121
|
+
summary:
|
|
122
|
+
'This id is referenced by an ARIA attribute but is used by more than one element; the reference resolves to the first.',
|
|
123
|
+
hint: 'Confirm the first element carrying this id is the intended target, or make the id unique.',
|
|
124
|
+
i18n: {
|
|
125
|
+
summaryKey: 'duplicateIdAria_summary_cantTell',
|
|
126
|
+
hintKey: 'duplicateIdAria_hint_cantTell',
|
|
127
|
+
params: { id: refId, duplicateCount: String(els.length) }
|
|
128
|
+
},
|
|
129
|
+
data: {
|
|
130
|
+
details: {
|
|
131
|
+
reasonCode: 'DUPLICATE_ID_ARIA_REFERENCED',
|
|
132
|
+
id: refId,
|
|
133
|
+
duplicateCount: els.length
|
|
134
|
+
}
|
|
125
135
|
}
|
|
126
|
-
}
|
|
127
|
-
|
|
136
|
+
})
|
|
137
|
+
);
|
|
128
138
|
}
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
if (referencedIds.size === 0) {
|
|
132
142
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
133
143
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
144
|
+
|
|
145
|
+
const resolved = helpers.resolveTieredOutcome(
|
|
146
|
+
[],
|
|
147
|
+
cantTellOccurrences,
|
|
148
|
+
rule.defaultSeverity || 'serious'
|
|
149
|
+
);
|
|
150
|
+
return { ruleId: rule.ruleId, ...resolved };
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
module.exports = { id, meta, runInPage };
|