@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
|
@@ -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,26 @@ 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 searchbox's accessible name.",
|
|
235
|
+
i18n: {
|
|
236
|
+
summaryKey: 'searchboxNamePresent_summary_fail',
|
|
237
|
+
hintKey: 'searchboxNamePresent_hint_fail',
|
|
238
|
+
params: { controlType: 'searchbox' }
|
|
239
|
+
},
|
|
240
|
+
data: {
|
|
241
|
+
details: {
|
|
242
|
+
reasonCode: 'name_missing',
|
|
243
|
+
controlType: 'searchbox',
|
|
244
|
+
methodTried: res.method
|
|
245
|
+
},
|
|
246
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
);
|
|
241
250
|
}
|
|
242
251
|
|
|
243
252
|
if (applicableCount === 0) {
|
|
@@ -66,24 +66,21 @@ function runInPage(ctx) {
|
|
|
66
66
|
|
|
67
67
|
applicableCount += 1;
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
details: { reasonCode: 'SERVER_SIDE_IMAGE_MAP' }
|
|
85
|
-
}
|
|
86
|
-
});
|
|
69
|
+
occurrences.push(
|
|
70
|
+
helpers.reportOccurrence(el, {
|
|
71
|
+
summary:
|
|
72
|
+
'This image uses a server-side image map, which has no keyboard-operable equivalent.',
|
|
73
|
+
hint: 'Replace the server-side image map (ismap) with a client-side image map (<map>/<area>) or separate accessible links/buttons.',
|
|
74
|
+
i18n: {
|
|
75
|
+
summaryKey: 'serverSideImageMapAbsent_summary_fail',
|
|
76
|
+
hintKey: 'serverSideImageMapAbsent_hint_fail',
|
|
77
|
+
params: {}
|
|
78
|
+
},
|
|
79
|
+
data: {
|
|
80
|
+
details: { reasonCode: 'SERVER_SIDE_IMAGE_MAP' }
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
);
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
if (applicableCount === 0) {
|
|
@@ -125,9 +125,16 @@ function runInPage(ctx) {
|
|
|
125
125
|
return '';
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
129
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
130
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
128
131
|
function isEligibleAcc(helpers, el, ctx) {
|
|
129
132
|
const fn =
|
|
130
|
-
helpers && typeof helpers.
|
|
133
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
134
|
+
? helpers.isIncludedInAccessibilityTree
|
|
135
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
136
|
+
? helpers.isAccTreeEligible
|
|
137
|
+
: null;
|
|
131
138
|
if (!fn) return true;
|
|
132
139
|
try {
|
|
133
140
|
const r = fn(el, ctx);
|
|
@@ -141,7 +148,9 @@ function runInPage(ctx) {
|
|
|
141
148
|
const occurrences = [];
|
|
142
149
|
let applicableCount = 0;
|
|
143
150
|
|
|
144
|
-
|
|
151
|
+
// Native input[type=range] belongs to
|
|
152
|
+
// form-control-programmatic-label-present.
|
|
153
|
+
const selector = '[role="slider"]';
|
|
145
154
|
const nodes = helpers.queryAllSmart
|
|
146
155
|
? helpers.queryAllSmart(selector)
|
|
147
156
|
: helpers.queryAll(selector);
|
|
@@ -233,24 +242,22 @@ function runInPage(ctx) {
|
|
|
233
242
|
}
|
|
234
243
|
})()
|
|
235
244
|
: null;
|
|
236
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
237
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
238
245
|
|
|
239
|
-
occurrences.push(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
|
|
246
|
+
occurrences.push(
|
|
247
|
+
helpers.reportOccurrence(el, {
|
|
248
|
+
summary: 'This slider has no accessible name.',
|
|
249
|
+
hint: 'Provide a label, aria-label, or aria-labelledby so assistive technologies can identify the slider.',
|
|
250
|
+
i18n: {
|
|
251
|
+
summaryKey: 'sliderNamePresent_summary_fail',
|
|
252
|
+
hintKey: 'sliderNamePresent_hint_fail',
|
|
253
|
+
params: { kind }
|
|
254
|
+
},
|
|
255
|
+
data: {
|
|
256
|
+
details: { reasonCode: 'name_missing', controlType: kind, methodTried: res.method },
|
|
257
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
);
|
|
254
261
|
}
|
|
255
262
|
|
|
256
263
|
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,26 @@ 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 spinbutton's accessible name.",
|
|
235
|
+
i18n: {
|
|
236
|
+
summaryKey: 'spinbuttonNamePresent_summary_fail',
|
|
237
|
+
hintKey: 'spinbuttonNamePresent_hint_fail',
|
|
238
|
+
params: { controlType: 'spinbutton' }
|
|
239
|
+
},
|
|
240
|
+
data: {
|
|
241
|
+
details: {
|
|
242
|
+
reasonCode: 'name_missing',
|
|
243
|
+
controlType: 'spinbutton',
|
|
244
|
+
methodTried: res.method
|
|
245
|
+
},
|
|
246
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
);
|
|
241
250
|
}
|
|
242
251
|
|
|
243
252
|
if (applicableCount === 0) {
|
|
@@ -83,9 +83,16 @@ function runInPage(ctx) {
|
|
|
83
83
|
return '';
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
87
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
88
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
86
89
|
function isEligibleAcc(helpers, el, ctx) {
|
|
87
90
|
const fn =
|
|
88
|
-
helpers && typeof helpers.
|
|
91
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
92
|
+
? helpers.isIncludedInAccessibilityTree
|
|
93
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
94
|
+
? helpers.isAccTreeEligible
|
|
95
|
+
: null;
|
|
89
96
|
if (!fn) return true;
|
|
90
97
|
try {
|
|
91
98
|
const r = fn(el, ctx);
|
|
@@ -137,24 +144,22 @@ function runInPage(ctx) {
|
|
|
137
144
|
}
|
|
138
145
|
})()
|
|
139
146
|
: null;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
});
|
|
147
|
+
|
|
148
|
+
occurrences.push(
|
|
149
|
+
helpers.reportOccurrence(el, {
|
|
150
|
+
summary: 'This summary has no accessible name.',
|
|
151
|
+
hint: 'Provide summary text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
|
|
152
|
+
i18n: {
|
|
153
|
+
summaryKey: 'summaryNamePresent_summary_fail',
|
|
154
|
+
hintKey: 'summaryNamePresent_hint_fail',
|
|
155
|
+
params: {}
|
|
156
|
+
},
|
|
157
|
+
data: {
|
|
158
|
+
details: { reasonCode: 'name_missing', controlType: 'summary', methodTried: res.method },
|
|
159
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
);
|
|
158
163
|
}
|
|
159
164
|
|
|
160
165
|
if (applicableCount === 0) {
|
|
@@ -83,9 +83,16 @@ function runInPage(ctx) {
|
|
|
83
83
|
return '';
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// Naming rules apply only to elements included in the accessibility tree
|
|
87
|
+
// (ACT c487ae and siblings), which excludes focusable aria-hidden content.
|
|
88
|
+
// aria-hidden-focus (ACT 6cfa84) covers that markup instead.
|
|
86
89
|
function isEligibleAcc(helpers, el, ctx) {
|
|
87
90
|
const fn =
|
|
88
|
-
helpers && typeof helpers.
|
|
91
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
92
|
+
? helpers.isIncludedInAccessibilityTree
|
|
93
|
+
: helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
94
|
+
? helpers.isAccTreeEligible
|
|
95
|
+
: null;
|
|
89
96
|
if (!fn) return true;
|
|
90
97
|
try {
|
|
91
98
|
const r = fn(el, ctx);
|
|
@@ -141,24 +148,22 @@ function runInPage(ctx) {
|
|
|
141
148
|
}
|
|
142
149
|
})()
|
|
143
150
|
: null;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
});
|
|
151
|
+
|
|
152
|
+
occurrences.push(
|
|
153
|
+
helpers.reportOccurrence(el, {
|
|
154
|
+
summary: 'This tab has no accessible name.',
|
|
155
|
+
hint: 'Provide tab text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
|
|
156
|
+
i18n: {
|
|
157
|
+
summaryKey: 'tabNamePresent_summary_fail',
|
|
158
|
+
hintKey: 'tabNamePresent_hint_fail',
|
|
159
|
+
params: {}
|
|
160
|
+
},
|
|
161
|
+
data: {
|
|
162
|
+
details: { reasonCode: 'name_missing', controlType: 'tab', methodTried: res.method },
|
|
163
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
);
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
if (applicableCount === 0) {
|
|
@@ -104,23 +104,21 @@ function runInPage(ctx) {
|
|
|
104
104
|
const dedupedInvalidIds = [...new Set(invalid.map((i) => i.id))];
|
|
105
105
|
|
|
106
106
|
const tag = el.tagName.toLowerCase();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
});
|
|
107
|
+
|
|
108
|
+
occurrences.push(
|
|
109
|
+
helpers.reportOccurrence(el, {
|
|
110
|
+
summary: 'This cell’s headers attribute references one or more invalid header cells.',
|
|
111
|
+
hint: 'Update the headers attribute so every id refers to a <th> element within the same table.',
|
|
112
|
+
i18n: {
|
|
113
|
+
summaryKey: 'tableHeadersAttrValid_summary_fail',
|
|
114
|
+
hintKey: 'tableHeadersAttrValid_hint_fail',
|
|
115
|
+
params: { element: tag, invalidIds: dedupedInvalidIds.join(', ') }
|
|
116
|
+
},
|
|
117
|
+
data: {
|
|
118
|
+
details: { reasonCode: 'TABLE_HEADERS_ATTR_INVALID', element: tag, invalid }
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
);
|
|
124
122
|
}
|
|
125
123
|
|
|
126
124
|
if (applicableCount === 0) {
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* @standard WCAG 2.2
|
|
10
10
|
* @sc 1.3.1
|
|
11
11
|
* @applicability
|
|
12
|
-
* Applies to <table> elements that
|
|
12
|
+
* Applies to <table> elements that keep their table semantics and are included
|
|
13
|
+
* in the accessibility tree, and that contain at least one <th> which is
|
|
14
|
+
* visible, included in the accessibility tree, and not overridden by an
|
|
15
|
+
* explicit role other than rowheader/columnheader.
|
|
13
16
|
* @expectation
|
|
14
17
|
* The table also contains at least one <td> somewhere in it.
|
|
15
18
|
* @implementation-notes
|
|
@@ -30,9 +33,10 @@
|
|
|
30
33
|
* if some particular <th> in it doesn't actually describe any cell
|
|
31
34
|
* (false negative, not a false positive — acceptable under this
|
|
32
35
|
* engine's philosophy).
|
|
33
|
-
* -
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
+
* - Applicability IS gated, because the zero-<td> case above turns any
|
|
37
|
+
* over-broad scope straight into a false fail: a layout table marked
|
|
38
|
+
* role="presentation" with a stray <th> and no <td> was being failed,
|
|
39
|
+
* as were tables whose only header was hidden or carried role="cell".
|
|
36
40
|
*/
|
|
37
41
|
|
|
38
42
|
const id = 'table-th-has-data-cells';
|
|
@@ -72,16 +76,73 @@ function runInPage(ctx) {
|
|
|
72
76
|
const occurrences = [];
|
|
73
77
|
let applicableCount = 0;
|
|
74
78
|
|
|
79
|
+
function explicitRole(el) {
|
|
80
|
+
try {
|
|
81
|
+
return String((el && el.getAttribute && el.getAttribute('role')) || '')
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.split(/\s+/)[0];
|
|
85
|
+
} catch {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isIncludedInTree(el) {
|
|
91
|
+
const fn =
|
|
92
|
+
helpers && typeof helpers.isIncludedInAccessibilityTree === 'function'
|
|
93
|
+
? helpers.isIncludedInAccessibilityTree
|
|
94
|
+
: null;
|
|
95
|
+
if (!fn) return true;
|
|
96
|
+
try {
|
|
97
|
+
const r = fn(el, ctx);
|
|
98
|
+
return typeof r === 'boolean' ? r : !!(r && r.eligible);
|
|
99
|
+
} catch {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// role="presentation"/"none" removes the table role, and with it every
|
|
105
|
+
// header cell the role would have implied. A table is not focusable, so
|
|
106
|
+
// presentational role conflict resolution does not restore it.
|
|
107
|
+
function hasTableSemantics(table) {
|
|
108
|
+
const role = explicitRole(table);
|
|
109
|
+
if (role === 'presentation' || role === 'none') return false;
|
|
110
|
+
if (role && role !== 'table' && role !== 'grid' && role !== 'treegrid') return false;
|
|
111
|
+
return isIncludedInTree(table);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A <th> only carries a rowheader/columnheader role while nothing overrides
|
|
115
|
+
// it, and a hidden header describes nothing to a screen reader either way.
|
|
116
|
+
function isHeaderCellInScope(th) {
|
|
117
|
+
if (!th) return false;
|
|
118
|
+
const role = explicitRole(th);
|
|
119
|
+
if (role && role !== 'rowheader' && role !== 'columnheader') return false;
|
|
120
|
+
if (helpers.isDomVisibleEligible) {
|
|
121
|
+
try {
|
|
122
|
+
if (!helpers.isDomVisibleEligible(th, ctx, { targetSet: 'dom' }).eligible) return false;
|
|
123
|
+
} catch {
|
|
124
|
+
// fall through to the tree check
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return isIncludedInTree(th);
|
|
128
|
+
}
|
|
129
|
+
|
|
75
130
|
for (const table of tables) {
|
|
76
131
|
if (!table || !table.querySelectorAll) continue;
|
|
77
132
|
|
|
133
|
+
// A table stripped of its semantics has no header cells to describe
|
|
134
|
+
// anything, so nothing in it is in scope.
|
|
135
|
+
if (!hasTableSemantics(table)) continue;
|
|
136
|
+
|
|
78
137
|
let ths;
|
|
79
138
|
try {
|
|
80
139
|
ths = table.querySelectorAll('th');
|
|
81
140
|
} catch {
|
|
82
141
|
ths = [];
|
|
83
142
|
}
|
|
84
|
-
|
|
143
|
+
|
|
144
|
+
const headers = Array.from(ths).filter(isHeaderCellInScope);
|
|
145
|
+
if (!headers.length) continue;
|
|
85
146
|
|
|
86
147
|
applicableCount += 1;
|
|
87
148
|
|
|
@@ -93,27 +154,23 @@ function runInPage(ctx) {
|
|
|
93
154
|
}
|
|
94
155
|
if (hasDataCell) continue;
|
|
95
156
|
|
|
96
|
-
for (const th of
|
|
157
|
+
for (const th of headers) {
|
|
97
158
|
if (!th) continue;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
data: {
|
|
114
|
-
details: { reasonCode: 'TABLE_TH_NO_DATA_CELLS' }
|
|
115
|
-
}
|
|
116
|
-
});
|
|
159
|
+
|
|
160
|
+
occurrences.push(
|
|
161
|
+
helpers.reportOccurrence(th, {
|
|
162
|
+
summary: 'This table has header cells but no data cells for them to describe.',
|
|
163
|
+
hint: 'Add data cells (<td>) to the table, or remove the header cells if the table has no data.',
|
|
164
|
+
i18n: {
|
|
165
|
+
summaryKey: 'tableThHasDataCells_summary_fail',
|
|
166
|
+
hintKey: 'tableThHasDataCells_hint_fail',
|
|
167
|
+
params: {}
|
|
168
|
+
},
|
|
169
|
+
data: {
|
|
170
|
+
details: { reasonCode: 'TABLE_TH_NO_DATA_CELLS' }
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
);
|
|
117
174
|
}
|
|
118
175
|
}
|
|
119
176
|
|