@techsio/storybook-better-a11y 0.0.4 → 0.0.6
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/dist/AccessibilityRuleMaps.js +532 -0
- package/dist/a11yRunner.js +105 -0
- package/dist/a11yRunner.test.js +21 -0
- package/dist/a11yRunnerUtils.js +30 -0
- package/dist/a11yRunnerUtils.test.js +61 -0
- package/dist/{699.js → apcaChecker.js} +1 -255
- package/dist/apcaChecker.test.js +124 -0
- package/dist/axeRuleMappingHelper.js +4 -0
- package/dist/components/A11YPanel.js +140 -0
- package/dist/components/A11YPanel.stories.js +198 -0
- package/dist/components/A11YPanel.test.js +110 -0
- package/dist/components/A11yContext.js +438 -0
- package/dist/components/A11yContext.test.js +277 -0
- package/dist/components/Report/Details.js +169 -0
- package/dist/components/Report/Report.js +106 -0
- package/dist/components/Report/Report.stories.js +86 -0
- package/dist/components/Tabs.js +54 -0
- package/dist/components/TestDiscrepancyMessage.js +55 -0
- package/dist/components/TestDiscrepancyMessage.stories.js +40 -0
- package/dist/components/VisionSimulator.js +83 -0
- package/dist/components/VisionSimulator.stories.js +56 -0
- package/dist/constants.js +25 -0
- package/dist/index.js +5 -6
- package/dist/manager.js +11 -1540
- package/dist/manager.test.js +86 -0
- package/dist/params.js +0 -0
- package/dist/postinstall.js +1 -1
- package/dist/preview.js +68 -1
- package/dist/preview.test.js +215 -0
- package/dist/results.mock.js +874 -0
- package/dist/types.js +6 -0
- package/dist/utils.js +21 -0
- package/dist/{100.js → visionSimulatorFilters.js} +1 -23
- package/dist/withVisionSimulator.js +41 -0
- package/package.json +1 -1
- package/dist/212.js +0 -1965
- package/dist/212.js.LICENSE.txt +0 -19
- package/dist/rslib-runtime.js +0 -37
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
const axeRuleMapping_wcag_2_0_a_aa = {
|
|
2
|
+
'area-alt': {
|
|
3
|
+
title: '<area> alt text',
|
|
4
|
+
axeSummary: 'Ensure <area> elements of image maps have alternative text',
|
|
5
|
+
friendlySummary: 'Add alt text to all <area> elements of image maps.'
|
|
6
|
+
},
|
|
7
|
+
'aria-allowed-attr': {
|
|
8
|
+
title: 'Supported ARIA attributes',
|
|
9
|
+
axeSummary: "Ensure an element's role supports its ARIA attributes",
|
|
10
|
+
friendlySummary: "Only use ARIA attributes that are permitted for the element's role."
|
|
11
|
+
},
|
|
12
|
+
'aria-braille-equivalent': {
|
|
13
|
+
title: 'Braille equivalent',
|
|
14
|
+
axeSummary: "Ensure aria-braillelabel and aria-brailleroledescription have a non-braille equivalent",
|
|
15
|
+
friendlySummary: 'If you use braille ARIA labels, also provide a matching non-braille label.'
|
|
16
|
+
},
|
|
17
|
+
'aria-command-name': {
|
|
18
|
+
title: 'ARIA command name',
|
|
19
|
+
axeSummary: 'Ensure every ARIA button, link and menuitem has an accessible name',
|
|
20
|
+
friendlySummary: 'Every ARIA button, link, or menuitem needs a label or accessible name.'
|
|
21
|
+
},
|
|
22
|
+
'aria-conditional-attr': {
|
|
23
|
+
title: 'ARIA attribute valid for role',
|
|
24
|
+
axeSummary: "Ensure ARIA attributes are used as described in the specification of the element's role",
|
|
25
|
+
friendlySummary: "Follow the element role's specification when using ARIA attributes."
|
|
26
|
+
},
|
|
27
|
+
'aria-deprecated-role': {
|
|
28
|
+
title: 'Deprecated ARIA role',
|
|
29
|
+
axeSummary: 'Ensure elements do not use deprecated roles',
|
|
30
|
+
friendlySummary: "Don't use deprecated ARIA roles on elements."
|
|
31
|
+
},
|
|
32
|
+
'aria-hidden-body': {
|
|
33
|
+
title: 'Hidden body',
|
|
34
|
+
axeSummary: 'Ensure aria-hidden="true" is not present on the document <body>',
|
|
35
|
+
friendlySummary: 'Never set aria-hidden="true" on the <body> element.'
|
|
36
|
+
},
|
|
37
|
+
'aria-hidden-focus': {
|
|
38
|
+
title: 'Hidden element focus',
|
|
39
|
+
axeSummary: 'Ensure aria-hidden elements are not focusable nor contain focusable elements',
|
|
40
|
+
friendlySummary: 'Elements marked hidden (aria-hidden) should not be focusable or contain focusable items.'
|
|
41
|
+
},
|
|
42
|
+
'aria-input-field-name': {
|
|
43
|
+
title: 'ARIA input field name',
|
|
44
|
+
axeSummary: 'Ensure every ARIA input field has an accessible name',
|
|
45
|
+
friendlySummary: 'Give each ARIA text input or field a label or accessible name.'
|
|
46
|
+
},
|
|
47
|
+
'aria-meter-name': {
|
|
48
|
+
title: 'ARIA meter name',
|
|
49
|
+
axeSummary: 'Ensure every ARIA meter node has an accessible name',
|
|
50
|
+
friendlySummary: 'Give each element with role="meter" a label or accessible name.'
|
|
51
|
+
},
|
|
52
|
+
'aria-progressbar-name': {
|
|
53
|
+
title: 'ARIA progressbar name',
|
|
54
|
+
axeSummary: 'Ensure every ARIA progressbar node has an accessible name',
|
|
55
|
+
friendlySummary: 'Give each element with role="progressbar" a label or accessible name.'
|
|
56
|
+
},
|
|
57
|
+
'aria-prohibited-attr': {
|
|
58
|
+
title: 'ARIA prohibited attributes',
|
|
59
|
+
axeSummary: "Ensure ARIA attributes are not prohibited for an element's role",
|
|
60
|
+
friendlySummary: "Don't use ARIA attributes that are forbidden for that element's role."
|
|
61
|
+
},
|
|
62
|
+
'aria-required-attr': {
|
|
63
|
+
title: 'ARIA required attributes',
|
|
64
|
+
axeSummary: 'Ensure elements with ARIA roles have all required ARIA attributes',
|
|
65
|
+
friendlySummary: 'Include all required ARIA attributes for elements with that ARIA role.'
|
|
66
|
+
},
|
|
67
|
+
'aria-required-children': {
|
|
68
|
+
title: 'ARIA required children',
|
|
69
|
+
axeSummary: 'Ensure elements with an ARIA role that require child roles contain them',
|
|
70
|
+
friendlySummary: 'If an ARIA role requires specific child roles, include those child elements.'
|
|
71
|
+
},
|
|
72
|
+
'aria-required-parent': {
|
|
73
|
+
title: 'ARIA required parent',
|
|
74
|
+
axeSummary: 'Ensure elements with an ARIA role that require parent roles are contained by them',
|
|
75
|
+
friendlySummary: 'Place elements with certain ARIA roles inside the required parent role element.'
|
|
76
|
+
},
|
|
77
|
+
'aria-roles': {
|
|
78
|
+
title: 'ARIA role value',
|
|
79
|
+
axeSummary: 'Ensure all elements with a role attribute use a valid value',
|
|
80
|
+
friendlySummary: 'Use only valid values in the role attribute (no typos or invalid roles).'
|
|
81
|
+
},
|
|
82
|
+
'aria-toggle-field-name': {
|
|
83
|
+
title: 'ARIA toggle field name',
|
|
84
|
+
axeSummary: 'Ensure every ARIA toggle field has an accessible name',
|
|
85
|
+
friendlySummary: 'Every ARIA toggle field (elements with the checkbox, radio, or switch roles) needs an accessible name.'
|
|
86
|
+
},
|
|
87
|
+
'aria-tooltip-name': {
|
|
88
|
+
title: 'ARIA tooltip name',
|
|
89
|
+
axeSummary: 'Ensure every ARIA tooltip node has an accessible name',
|
|
90
|
+
friendlySummary: 'Give each element with role="tooltip" a descriptive accessible name.'
|
|
91
|
+
},
|
|
92
|
+
'aria-valid-attr-value': {
|
|
93
|
+
title: 'ARIA attribute values valid',
|
|
94
|
+
axeSummary: 'Ensure all ARIA attributes have valid values',
|
|
95
|
+
friendlySummary: 'Use only valid values for ARIA attributes (no typos or invalid values).'
|
|
96
|
+
},
|
|
97
|
+
'aria-valid-attr': {
|
|
98
|
+
title: 'ARIA attribute valid',
|
|
99
|
+
axeSummary: 'Ensure attributes that begin with aria- are valid ARIA attributes',
|
|
100
|
+
friendlySummary: 'Use only valid aria-* attributes (make sure the attribute name is correct).'
|
|
101
|
+
},
|
|
102
|
+
blink: {
|
|
103
|
+
title: '<blink> element',
|
|
104
|
+
axeSummary: 'Ensure <blink> elements are not used',
|
|
105
|
+
friendlySummary: "Don't use the deprecated <blink> element."
|
|
106
|
+
},
|
|
107
|
+
'button-name': {
|
|
108
|
+
title: 'Button name',
|
|
109
|
+
axeSummary: 'Ensure buttons have discernible text',
|
|
110
|
+
friendlySummary: 'Every <button> needs a visible label or accessible name.'
|
|
111
|
+
},
|
|
112
|
+
bypass: {
|
|
113
|
+
title: 'Navigation bypass',
|
|
114
|
+
axeSummary: 'Ensure each page has at least one mechanism to bypass navigation and jump to content',
|
|
115
|
+
friendlySummary: 'Provide a way to skip repetitive navigation (e.g. a "Skip to content" link).'
|
|
116
|
+
},
|
|
117
|
+
'color-contrast': {
|
|
118
|
+
title: 'Color contrast',
|
|
119
|
+
axeSummary: 'Ensure the contrast between foreground and background text meets WCAG 2 AA minimum thresholds',
|
|
120
|
+
friendlySummary: 'The color contrast between text and its background meets WCAG AA contrast ratio.'
|
|
121
|
+
},
|
|
122
|
+
'definition-list': {
|
|
123
|
+
title: 'Definition list structure',
|
|
124
|
+
axeSummary: 'Ensure <dl> elements are structured correctly',
|
|
125
|
+
friendlySummary: 'Definition lists (<dl>) should directly contain <dt> and <dd> elements in order.'
|
|
126
|
+
},
|
|
127
|
+
dlitem: {
|
|
128
|
+
title: 'Definition list items',
|
|
129
|
+
axeSummary: 'Ensure <dt> and <dd> elements are contained by a <dl>',
|
|
130
|
+
friendlySummary: 'Ensure <dt> and <dd> elements are contained by a <dl>'
|
|
131
|
+
},
|
|
132
|
+
'document-title': {
|
|
133
|
+
title: 'Document title',
|
|
134
|
+
axeSummary: 'Ensure each HTML document contains a non-empty <title> element',
|
|
135
|
+
friendlySummary: 'Include a non-empty <title> element for every page.'
|
|
136
|
+
},
|
|
137
|
+
'duplicate-id-aria': {
|
|
138
|
+
title: 'Unique id',
|
|
139
|
+
axeSummary: 'Ensure every id attribute value used in ARIA and in labels is unique',
|
|
140
|
+
friendlySummary: 'Every id used for ARIA or form labels should be unique on the page.'
|
|
141
|
+
},
|
|
142
|
+
'form-field-multiple-labels': {
|
|
143
|
+
title: 'Multiple form field labels',
|
|
144
|
+
axeSummary: 'Ensure a form field does not have multiple <label> elements',
|
|
145
|
+
friendlySummary: "Don't give a single form field more than one <label>."
|
|
146
|
+
},
|
|
147
|
+
'frame-focusable-content': {
|
|
148
|
+
title: 'Focusable frames',
|
|
149
|
+
axeSummary: 'Ensure <frame> and <iframe> with focusable content do not have tabindex="-1"',
|
|
150
|
+
friendlySummary: 'Don\'t set tabindex="-1" on a <frame> or <iframe> that contains focusable elements.'
|
|
151
|
+
},
|
|
152
|
+
'frame-title-unique': {
|
|
153
|
+
title: 'Unique frame title',
|
|
154
|
+
axeSummary: 'Ensure <iframe> and <frame> elements contain a unique title attribute',
|
|
155
|
+
friendlySummary: 'Use a unique title attribute for each <frame> or <iframe> on the page.'
|
|
156
|
+
},
|
|
157
|
+
'frame-title': {
|
|
158
|
+
title: 'Frame title',
|
|
159
|
+
axeSummary: 'Ensure <iframe> and <frame> elements have an accessible name',
|
|
160
|
+
friendlySummary: 'Every <frame> and <iframe> needs a title or accessible name.'
|
|
161
|
+
},
|
|
162
|
+
'html-has-lang': {
|
|
163
|
+
title: '<html> has lang',
|
|
164
|
+
axeSummary: 'Ensure every HTML document has a lang attribute',
|
|
165
|
+
friendlySummary: 'Add a lang attribute to the <html> element.'
|
|
166
|
+
},
|
|
167
|
+
'html-lang-valid': {
|
|
168
|
+
title: '<html> lang valid',
|
|
169
|
+
axeSummary: 'Ensure the <html lang> attribute has a valid value',
|
|
170
|
+
friendlySummary: 'Use a valid language code in the <html lang> attribute.'
|
|
171
|
+
},
|
|
172
|
+
'html-xml-lang-mismatch': {
|
|
173
|
+
title: 'HTML and XML lang mismatch',
|
|
174
|
+
axeSummary: "Ensure that HTML elements with both lang and xml:lang agree on the page's language",
|
|
175
|
+
friendlySummary: 'If using both lang and xml:lang on <html>, make sure they are the same language.'
|
|
176
|
+
},
|
|
177
|
+
'image-alt': {
|
|
178
|
+
title: 'Image alt text',
|
|
179
|
+
axeSummary: 'Ensure <img> elements have alternative text or a role of none/presentation',
|
|
180
|
+
friendlySummary: 'Give every image alt text or mark it as decorative with alt="".'
|
|
181
|
+
},
|
|
182
|
+
'input-button-name': {
|
|
183
|
+
title: 'Input button name',
|
|
184
|
+
axeSummary: 'Ensure input buttons have discernible text',
|
|
185
|
+
friendlySummary: 'Give each <input type="button"> or similar a clear label (text or aria-label).'
|
|
186
|
+
},
|
|
187
|
+
'input-image-alt': {
|
|
188
|
+
title: 'Input image alt',
|
|
189
|
+
axeSummary: 'Ensure <input type="image"> elements have alternative text',
|
|
190
|
+
friendlySummary: '<input type="image"> must have alt text describing its image.'
|
|
191
|
+
},
|
|
192
|
+
label: {
|
|
193
|
+
title: 'Form label',
|
|
194
|
+
axeSummary: 'Ensure every form element has a label',
|
|
195
|
+
friendlySummary: 'Every form field needs an associated label.'
|
|
196
|
+
},
|
|
197
|
+
'link-in-text-block': {
|
|
198
|
+
title: 'Identifiable links',
|
|
199
|
+
axeSummary: 'Ensure links are distinguishable from surrounding text without relying on color',
|
|
200
|
+
friendlySummary: 'Make sure links are obviously identifiable without relying only on color.'
|
|
201
|
+
},
|
|
202
|
+
'link-name': {
|
|
203
|
+
title: 'Link name',
|
|
204
|
+
axeSummary: 'Ensure links have discernible text',
|
|
205
|
+
friendlySummary: 'Give each link meaningful text or an aria-label so its purpose is clear.'
|
|
206
|
+
},
|
|
207
|
+
list: {
|
|
208
|
+
title: 'List structure',
|
|
209
|
+
axeSummary: 'Ensure that lists are structured correctly',
|
|
210
|
+
friendlySummary: 'Use proper list structure. Only use <li> inside <ul> or <ol>.'
|
|
211
|
+
},
|
|
212
|
+
listitem: {
|
|
213
|
+
title: 'List item',
|
|
214
|
+
axeSummary: 'Ensure <li> elements are used semantically',
|
|
215
|
+
friendlySummary: 'Only use <li> tags inside <ul> or <ol> lists.'
|
|
216
|
+
},
|
|
217
|
+
marquee: {
|
|
218
|
+
title: '<marquee> element',
|
|
219
|
+
axeSummary: 'Ensure <marquee> elements are not used',
|
|
220
|
+
friendlySummary: "Don't use the deprecated <marquee> element."
|
|
221
|
+
},
|
|
222
|
+
'meta-refresh': {
|
|
223
|
+
title: '<meta> refresh',
|
|
224
|
+
axeSummary: 'Ensure <meta http-equiv="refresh"> is not used for delayed refresh',
|
|
225
|
+
friendlySummary: 'Avoid auto-refreshing or redirecting pages using <meta http-equiv="refresh">.'
|
|
226
|
+
},
|
|
227
|
+
'meta-viewport': {
|
|
228
|
+
title: '<meta> viewport scaling',
|
|
229
|
+
axeSummary: 'Ensure <meta name="viewport"> does not disable text scaling and zooming',
|
|
230
|
+
friendlySummary: 'Don\'t disable user zooming in <meta name="viewport"> to allow scaling.'
|
|
231
|
+
},
|
|
232
|
+
'nested-interactive': {
|
|
233
|
+
title: 'Nested interactive controls',
|
|
234
|
+
axeSummary: 'Ensure interactive controls are not nested (nesting causes screen reader/focus issues)',
|
|
235
|
+
friendlySummary: 'Do not nest interactive elements; it can confuse screen readers and keyboard focus.'
|
|
236
|
+
},
|
|
237
|
+
'no-autoplay-audio': {
|
|
238
|
+
title: 'Autoplaying video',
|
|
239
|
+
axeSummary: 'Ensure <video> or <audio> do not autoplay audio > 3 seconds without a control to stop/mute',
|
|
240
|
+
friendlySummary: "Don't autoplay audio for more than 3 seconds without providing a way to stop or mute it."
|
|
241
|
+
},
|
|
242
|
+
'object-alt': {
|
|
243
|
+
title: '<object> alt text',
|
|
244
|
+
axeSummary: 'Ensure <object> elements have alternative text',
|
|
245
|
+
friendlySummary: 'Provide alternative text or content for <object> elements.'
|
|
246
|
+
},
|
|
247
|
+
'role-img-alt': {
|
|
248
|
+
title: 'role="img" alt text',
|
|
249
|
+
axeSummary: 'Ensure elements with role="img" have alternative text',
|
|
250
|
+
friendlySummary: 'Any element with role="img" needs alt text.'
|
|
251
|
+
},
|
|
252
|
+
'scrollable-region-focusable': {
|
|
253
|
+
title: 'Scrollable element focusable',
|
|
254
|
+
axeSummary: 'Ensure elements with scrollable content are keyboard-accessible',
|
|
255
|
+
friendlySummary: 'If an area can scroll, ensure it can be focused and scrolled via keyboard.'
|
|
256
|
+
},
|
|
257
|
+
'select-name': {
|
|
258
|
+
title: '<select> name',
|
|
259
|
+
axeSummary: 'Ensure <select> elements have an accessible name',
|
|
260
|
+
friendlySummary: 'Give each <select> field a label or other accessible name.'
|
|
261
|
+
},
|
|
262
|
+
'server-side-image-map': {
|
|
263
|
+
title: 'Server-side image map',
|
|
264
|
+
axeSummary: 'Ensure that server-side image maps are not used',
|
|
265
|
+
friendlySummary: "Don't use server-side image maps."
|
|
266
|
+
},
|
|
267
|
+
'svg-img-alt': {
|
|
268
|
+
title: 'SVG image alt text',
|
|
269
|
+
axeSummary: 'Ensure <svg> images/graphics have accessible text',
|
|
270
|
+
friendlySummary: 'SVG images with role="img" or similar need a text description.'
|
|
271
|
+
},
|
|
272
|
+
'td-headers-attr': {
|
|
273
|
+
title: 'Table headers attribute',
|
|
274
|
+
axeSummary: 'Ensure each cell in a table using headers only refers to <th> in that table',
|
|
275
|
+
friendlySummary: 'In tables using the headers attribute, only reference other cells in the same table.'
|
|
276
|
+
},
|
|
277
|
+
'th-has-data-cells': {
|
|
278
|
+
title: '<th> has data cell',
|
|
279
|
+
axeSummary: 'Ensure <th> (or header role) elements have data cells they describe',
|
|
280
|
+
friendlySummary: 'Every table header (<th> or header role) should correspond to at least one data cell.'
|
|
281
|
+
},
|
|
282
|
+
'valid-lang': {
|
|
283
|
+
title: 'Valid lang',
|
|
284
|
+
axeSummary: 'Ensure lang attributes have valid values',
|
|
285
|
+
friendlySummary: 'Use valid language codes in all lang attributes.'
|
|
286
|
+
},
|
|
287
|
+
'video-caption': {
|
|
288
|
+
title: '<video> captions',
|
|
289
|
+
axeSummary: 'Ensure <video> elements have captions',
|
|
290
|
+
friendlySummary: 'Provide captions for all <video> content.'
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const axeRuleMapping_wcag_2_1_a_aa = {
|
|
294
|
+
'autocomplete-valid': {
|
|
295
|
+
title: 'autocomplete attribute valid',
|
|
296
|
+
axeSummary: 'Ensure the autocomplete attribute is correct and suitable for the form field',
|
|
297
|
+
friendlySummary: "Use valid autocomplete values that match the form field's purpose."
|
|
298
|
+
},
|
|
299
|
+
'avoid-inline-spacing': {
|
|
300
|
+
title: 'Forced inline spacing',
|
|
301
|
+
axeSummary: 'Ensure that text spacing set via inline styles can be adjusted with custom CSS',
|
|
302
|
+
friendlySummary: "Don't lock in text spacing with forced (!important) inline styles—allow user CSS to adjust text spacing."
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const axeRuleMapping_wcag_2_2_a_aa = {
|
|
306
|
+
'target-size': {
|
|
307
|
+
title: 'Touch target size',
|
|
308
|
+
axeSummary: 'Ensure touch targets have sufficient size and space',
|
|
309
|
+
friendlySummary: 'Make sure interactive elements are big enough and not too close together for touch.'
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const axeRuleMapping_best_practices = {
|
|
313
|
+
accesskeys: {
|
|
314
|
+
title: 'Unique accesskey',
|
|
315
|
+
axeSummary: 'Ensure every accesskey attribute value is unique',
|
|
316
|
+
friendlySummary: 'Use unique values for all accesskey attributes.'
|
|
317
|
+
},
|
|
318
|
+
'aria-allowed-role': {
|
|
319
|
+
title: 'Appropriate role value',
|
|
320
|
+
axeSummary: 'Ensure the role attribute has an appropriate value for the element',
|
|
321
|
+
friendlySummary: 'ARIA roles should have a valid value for the element.'
|
|
322
|
+
},
|
|
323
|
+
'aria-dialog-name': {
|
|
324
|
+
title: 'ARIA dialog name',
|
|
325
|
+
axeSummary: 'Ensure every ARIA dialog and alertdialog has an accessible name',
|
|
326
|
+
friendlySummary: 'Give each ARIA dialog or alertdialog a title or accessible name.'
|
|
327
|
+
},
|
|
328
|
+
'aria-text': {
|
|
329
|
+
title: 'ARIA role="text"',
|
|
330
|
+
axeSummary: 'Ensure role="text" is used on elements with no focusable descendants',
|
|
331
|
+
friendlySummary: 'Only use role="text" on elements that don\'t contain focusable elements.'
|
|
332
|
+
},
|
|
333
|
+
'aria-treeitem-name': {
|
|
334
|
+
title: 'ARIA treeitem name',
|
|
335
|
+
axeSummary: 'Ensure every ARIA treeitem node has an accessible name',
|
|
336
|
+
friendlySummary: 'Give each ARIA treeitem a label or accessible name.'
|
|
337
|
+
},
|
|
338
|
+
'empty-heading': {
|
|
339
|
+
title: 'Empty heading',
|
|
340
|
+
axeSummary: 'Ensure headings have discernible text',
|
|
341
|
+
friendlySummary: "Don't leave heading elements empty or hide them."
|
|
342
|
+
},
|
|
343
|
+
'empty-table-header': {
|
|
344
|
+
title: 'Empty table header',
|
|
345
|
+
axeSummary: 'Ensure table headers have discernible text',
|
|
346
|
+
friendlySummary: 'Make sure table header cells have visible text.'
|
|
347
|
+
},
|
|
348
|
+
'frame-tested': {
|
|
349
|
+
title: 'Test all frames',
|
|
350
|
+
axeSummary: "Ensure <iframe> and <frame> elements contain the axe-core script",
|
|
351
|
+
friendlySummary: 'Make sure axe-core is injected into all frames or iframes so they are tested.'
|
|
352
|
+
},
|
|
353
|
+
'heading-order': {
|
|
354
|
+
title: 'Heading order',
|
|
355
|
+
axeSummary: 'Ensure the order of headings is semantically correct (no skipping levels)',
|
|
356
|
+
friendlySummary: "Use proper heading order (don't skip heading levels)."
|
|
357
|
+
},
|
|
358
|
+
'image-redundant-alt': {
|
|
359
|
+
title: 'Redundant image alt text',
|
|
360
|
+
axeSummary: 'Ensure image alternative text is not repeated as nearby text',
|
|
361
|
+
friendlySummary: "Avoid repeating the same information in both an image's alt text and nearby text."
|
|
362
|
+
},
|
|
363
|
+
'label-title-only': {
|
|
364
|
+
title: 'Visible form element label',
|
|
365
|
+
axeSummary: 'Ensure each form element has a visible label (not only title/ARIA)',
|
|
366
|
+
friendlySummary: 'Every form input needs a visible label (not only a title attribute or hidden text).'
|
|
367
|
+
},
|
|
368
|
+
'landmark-banner-is-top-level': {
|
|
369
|
+
title: 'Top-level landmark banner',
|
|
370
|
+
axeSummary: 'Ensure the banner landmark is at top level (not nested)',
|
|
371
|
+
friendlySummary: 'Use the banner landmark (e.g. site header) only at the top level of the page, not inside another landmark.'
|
|
372
|
+
},
|
|
373
|
+
'landmark-complementary-is-top-level': {
|
|
374
|
+
title: 'Top-level <aside>',
|
|
375
|
+
axeSummary: 'Ensure the complementary landmark (<aside>) is top level',
|
|
376
|
+
friendlySummary: 'The complementary landmark <aside> or role="complementary" should be a top-level region, not nested in another landmark.'
|
|
377
|
+
},
|
|
378
|
+
'landmark-contentinfo-is-top-level': {
|
|
379
|
+
title: 'Top-level contentinfo',
|
|
380
|
+
axeSummary: 'Ensure the contentinfo landmark (footer) is top level',
|
|
381
|
+
friendlySummary: 'Make sure the contentinfo landmark (footer) is at the top level of the page and not contained in another landmark.'
|
|
382
|
+
},
|
|
383
|
+
'landmark-main-is-top-level': {
|
|
384
|
+
title: 'Top-level main',
|
|
385
|
+
axeSummary: 'Ensure the main landmark is at top level',
|
|
386
|
+
friendlySummary: 'The main landmark should be a top-level element and not nested inside another landmark.'
|
|
387
|
+
},
|
|
388
|
+
'landmark-no-duplicate-banner': {
|
|
389
|
+
title: 'Duplicate banner landmark',
|
|
390
|
+
axeSummary: 'Ensure the document has at most one banner landmark',
|
|
391
|
+
friendlySummary: 'Have only one role="banner" or <header> on a page.'
|
|
392
|
+
},
|
|
393
|
+
'landmark-no-duplicate-contentinfo': {
|
|
394
|
+
title: 'Duplicate contentinfo',
|
|
395
|
+
axeSummary: 'Ensure the document has at most one contentinfo landmark',
|
|
396
|
+
friendlySummary: 'Have only one role="contentinfo" or <footer> on a page.'
|
|
397
|
+
},
|
|
398
|
+
'landmark-no-duplicate-main': {
|
|
399
|
+
title: 'Duplicate main',
|
|
400
|
+
axeSummary: 'Ensure the document has at most one main landmark',
|
|
401
|
+
friendlySummary: 'Have only one role="main" or <main> on a page.'
|
|
402
|
+
},
|
|
403
|
+
'landmark-one-main': {
|
|
404
|
+
title: 'main landmark',
|
|
405
|
+
axeSummary: 'Ensure the document has a main landmark',
|
|
406
|
+
friendlySummary: 'Include a main landmark on each page using a <main> region or role="main".'
|
|
407
|
+
},
|
|
408
|
+
'landmark-unique': {
|
|
409
|
+
title: 'Unique landmark',
|
|
410
|
+
axeSummary: 'Ensure landmarks have a unique role or role/label combination',
|
|
411
|
+
friendlySummary: 'If you use multiple landmarks of the same type, give them unique labels (names).'
|
|
412
|
+
},
|
|
413
|
+
'meta-viewport-large': {
|
|
414
|
+
title: 'Significant viewport scaling',
|
|
415
|
+
axeSummary: 'Ensure <meta name="viewport"> can scale a significant amount (e.g. 500%)',
|
|
416
|
+
friendlySummary: '<meta name="viewport"> should allow users to significantly scale content.'
|
|
417
|
+
},
|
|
418
|
+
'page-has-heading-one': {
|
|
419
|
+
title: 'Has <h1>',
|
|
420
|
+
axeSummary: 'Ensure the page (or at least one frame) contains a level-one heading',
|
|
421
|
+
friendlySummary: 'Every page or frame should have at least one <h1> heading.'
|
|
422
|
+
},
|
|
423
|
+
'presentation-role-conflict': {
|
|
424
|
+
title: 'Presentational content',
|
|
425
|
+
axeSummary: 'Ensure elements with role="presentation"/"none" have no ARIA or tabindex',
|
|
426
|
+
friendlySummary: 'Don\'t give elements with role="none"/"presentation" any ARIA attributes or a tabindex.'
|
|
427
|
+
},
|
|
428
|
+
region: {
|
|
429
|
+
title: 'Landmark regions',
|
|
430
|
+
axeSummary: 'Ensure all page content is contained by landmarks',
|
|
431
|
+
friendlySummary: 'Wrap all page content in appropriate landmark regions (<header>, <main>, <footer>, etc.).'
|
|
432
|
+
},
|
|
433
|
+
'scope-attr-valid': {
|
|
434
|
+
title: 'scope attribute',
|
|
435
|
+
axeSummary: 'Ensure the scope attribute is used correctly on tables',
|
|
436
|
+
friendlySummary: 'Use the scope attribute only on <th> elements, with proper values (col, row, etc.).'
|
|
437
|
+
},
|
|
438
|
+
'skip-link': {
|
|
439
|
+
title: 'Skip link',
|
|
440
|
+
axeSummary: 'Ensure all "skip" links have a focusable target',
|
|
441
|
+
friendlySummary: 'Make sure any "skip to content" link targets an existing, focusable element.'
|
|
442
|
+
},
|
|
443
|
+
tabindex: {
|
|
444
|
+
title: 'tabindex values',
|
|
445
|
+
axeSummary: 'Ensure tabindex attribute values are not greater than 0',
|
|
446
|
+
friendlySummary: "Don't use tabindex values greater than 0."
|
|
447
|
+
},
|
|
448
|
+
'table-duplicate-name': {
|
|
449
|
+
title: 'Duplicate names for table',
|
|
450
|
+
axeSummary: 'Ensure the <caption> does not duplicate the summary attribute text',
|
|
451
|
+
friendlySummary: "Don't use the same text in both a table's <caption> and its summary attribute."
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
const axeRuleMapping_wcag_2_x_aaa = {
|
|
455
|
+
'color-contrast-enhanced': {
|
|
456
|
+
title: 'Enhanced color contrast',
|
|
457
|
+
axeSummary: 'Ensure contrast between text and background meets WCAG 2 AAA enhanced contrast thresholds',
|
|
458
|
+
friendlySummary: 'Use extra-high contrast for text and background to meet WCAG AAA level.'
|
|
459
|
+
},
|
|
460
|
+
'identical-links-same-purpose': {
|
|
461
|
+
title: 'Same link name, same purpose',
|
|
462
|
+
axeSummary: 'Ensure links with the same accessible name serve a similar purpose',
|
|
463
|
+
friendlySummary: 'If two links have the same text, they should do the same thing (lead to the same content).'
|
|
464
|
+
},
|
|
465
|
+
'meta-refresh-no-exceptions': {
|
|
466
|
+
title: 'No <meta http-equiv="refresh">',
|
|
467
|
+
axeSummary: 'Ensure <meta http-equiv="refresh"> is not used for delayed refresh (no exceptions)',
|
|
468
|
+
friendlySummary: 'Don\'t auto-refresh or redirect pages using <meta http-equiv="refresh"> even with a delay.'
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
const axeRuleMapping_experimental = {
|
|
472
|
+
'css-orientation-lock': {
|
|
473
|
+
title: 'CSS orientation lock',
|
|
474
|
+
axeSummary: 'Ensure content is not locked to a specific display orientation (works in all orientations)',
|
|
475
|
+
friendlySummary: "Don't lock content to one screen orientation; support both portrait and landscape modes."
|
|
476
|
+
},
|
|
477
|
+
'focus-order-semantics': {
|
|
478
|
+
title: 'Focus order semantic role',
|
|
479
|
+
axeSummary: 'Ensure elements in the tab order have a role appropriate for interactive content',
|
|
480
|
+
friendlySummary: 'Ensure elements in the tab order have a role appropriate for interactive content'
|
|
481
|
+
},
|
|
482
|
+
'hidden-content': {
|
|
483
|
+
title: 'Hidden content',
|
|
484
|
+
axeSummary: 'Informs users about hidden content',
|
|
485
|
+
friendlySummary: 'Display hidden content on the page for test analysis.'
|
|
486
|
+
},
|
|
487
|
+
'label-content-name-mismatch': {
|
|
488
|
+
title: 'Content name mismatch',
|
|
489
|
+
axeSummary: 'Ensure elements labeled by their content include that text in their accessible name',
|
|
490
|
+
friendlySummary: "If an element's visible text serves as its label, include that text in its accessible name."
|
|
491
|
+
},
|
|
492
|
+
'p-as-heading': {
|
|
493
|
+
title: 'No <p> headings',
|
|
494
|
+
axeSummary: "Ensure <p> elements aren't styled to look like headings (use real headings)",
|
|
495
|
+
friendlySummary: "Don't just style a <p> to look like a heading – use an actual heading tag for headings."
|
|
496
|
+
},
|
|
497
|
+
'table-fake-caption': {
|
|
498
|
+
title: 'Table caption',
|
|
499
|
+
axeSummary: 'Ensure that tables with a caption use the <caption> element',
|
|
500
|
+
friendlySummary: 'Use a <caption> element for table captions instead of just styled text.'
|
|
501
|
+
},
|
|
502
|
+
'td-has-header': {
|
|
503
|
+
title: '<td> has header',
|
|
504
|
+
axeSummary: 'Ensure each non-empty data cell in large tables (3×3+) has one or more headers',
|
|
505
|
+
friendlySummary: 'Every data cell in large tables should be associated with at least one header cell.'
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const axeRuleMapping_deprecated = {
|
|
509
|
+
"aria-roledescription": {
|
|
510
|
+
title: "aria-roledescription",
|
|
511
|
+
axeSummary: "Ensure aria-roledescription is only used on elements with an implicit or explicit role",
|
|
512
|
+
friendlySummary: "Only use aria-roledescription on elements that already have a defined role."
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
const axeRuleMapping_wcag_3_0 = {
|
|
516
|
+
'apca-contrast': {
|
|
517
|
+
title: 'APCA color contrast',
|
|
518
|
+
axeSummary: 'Ensure the contrast between foreground and background text meets WCAG 3.0 APCA thresholds for the configured conformance level and use case',
|
|
519
|
+
friendlySummary: 'Text has sufficient perceptual contrast using the APCA method (WCAG 3.0). Thresholds vary by conformance level (bronze, silver, gold) and use case (body, fluent, sub-fluent, non-fluent).'
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
const combinedRulesMap = {
|
|
523
|
+
...axeRuleMapping_wcag_2_0_a_aa,
|
|
524
|
+
...axeRuleMapping_wcag_2_1_a_aa,
|
|
525
|
+
...axeRuleMapping_wcag_2_2_a_aa,
|
|
526
|
+
...axeRuleMapping_wcag_2_x_aaa,
|
|
527
|
+
...axeRuleMapping_wcag_3_0,
|
|
528
|
+
...axeRuleMapping_best_practices,
|
|
529
|
+
...axeRuleMapping_experimental,
|
|
530
|
+
...axeRuleMapping_deprecated
|
|
531
|
+
};
|
|
532
|
+
export { combinedRulesMap };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ElementA11yParameterError } from "storybook/internal/preview-errors";
|
|
2
|
+
import { global } from "@storybook/global";
|
|
3
|
+
import { addons, waitForAnimations } from "storybook/preview-api";
|
|
4
|
+
import { withLinkPaths } from "./a11yRunnerUtils.js";
|
|
5
|
+
import { runAPCACheck } from "./apcaChecker.js";
|
|
6
|
+
import { EVENTS } from "./constants.js";
|
|
7
|
+
const { document: a11yRunner_document } = global;
|
|
8
|
+
const channel = addons.getChannel();
|
|
9
|
+
const DEFAULT_PARAMETERS = {
|
|
10
|
+
config: {},
|
|
11
|
+
options: {}
|
|
12
|
+
};
|
|
13
|
+
const DISABLED_RULES = [
|
|
14
|
+
'region'
|
|
15
|
+
];
|
|
16
|
+
const queue = [];
|
|
17
|
+
let isRunning = false;
|
|
18
|
+
const runNext = async ()=>{
|
|
19
|
+
if (0 === queue.length) {
|
|
20
|
+
isRunning = false;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
isRunning = true;
|
|
24
|
+
const next = queue.shift();
|
|
25
|
+
if (next) await next();
|
|
26
|
+
runNext();
|
|
27
|
+
};
|
|
28
|
+
const run = async (input = DEFAULT_PARAMETERS, storyId)=>{
|
|
29
|
+
const axeCore = await import("axe-core");
|
|
30
|
+
const axe = axeCore?.default || globalThis.axe;
|
|
31
|
+
const { config = {}, options = {} } = input;
|
|
32
|
+
if (input.element) throw new ElementA11yParameterError();
|
|
33
|
+
const context = {
|
|
34
|
+
include: a11yRunner_document?.body,
|
|
35
|
+
exclude: [
|
|
36
|
+
'.sb-wrapper',
|
|
37
|
+
'#storybook-docs',
|
|
38
|
+
'#storybook-highlights-root'
|
|
39
|
+
]
|
|
40
|
+
};
|
|
41
|
+
if (input.context) {
|
|
42
|
+
const hasInclude = 'object' == typeof input.context && 'include' in input.context && void 0 !== input.context.include;
|
|
43
|
+
const hasExclude = 'object' == typeof input.context && 'exclude' in input.context && void 0 !== input.context.exclude;
|
|
44
|
+
if (hasInclude) context.include = input.context.include;
|
|
45
|
+
else if (!hasInclude && !hasExclude) context.include = input.context;
|
|
46
|
+
if (hasExclude) context.exclude = context.exclude.concat(input.context.exclude);
|
|
47
|
+
}
|
|
48
|
+
axe.reset();
|
|
49
|
+
const configWithDefault = {
|
|
50
|
+
...config,
|
|
51
|
+
rules: [
|
|
52
|
+
...DISABLED_RULES.map((id)=>({
|
|
53
|
+
id,
|
|
54
|
+
enabled: false
|
|
55
|
+
})),
|
|
56
|
+
...config?.rules ?? []
|
|
57
|
+
]
|
|
58
|
+
};
|
|
59
|
+
axe.configure(configWithDefault);
|
|
60
|
+
return new Promise((resolve, reject)=>{
|
|
61
|
+
const highlightsRoot = a11yRunner_document?.getElementById('storybook-highlights-root');
|
|
62
|
+
if (highlightsRoot) highlightsRoot.style.display = 'none';
|
|
63
|
+
const task = async ()=>{
|
|
64
|
+
try {
|
|
65
|
+
const result = await axe.run(context, options);
|
|
66
|
+
let contextElement = a11yRunner_document;
|
|
67
|
+
if (context.include instanceof Element) contextElement = context.include;
|
|
68
|
+
else if (Array.isArray(context.include)) {
|
|
69
|
+
const first = context.include[0];
|
|
70
|
+
if (first instanceof Element) contextElement = first;
|
|
71
|
+
else if ('string' == typeof first) contextElement = a11yRunner_document.querySelector(first) || a11yRunner_document;
|
|
72
|
+
} else if ('string' == typeof context.include) contextElement = a11yRunner_document.querySelector(context.include) || a11yRunner_document;
|
|
73
|
+
const excludeSelectors = Array.isArray(context.exclude) ? context.exclude.filter((value)=>'string' == typeof value) : 'string' == typeof context.exclude ? [
|
|
74
|
+
context.exclude
|
|
75
|
+
] : [];
|
|
76
|
+
const apcaResult = await runAPCACheck(contextElement, input.apca, excludeSelectors);
|
|
77
|
+
if (apcaResult.nodes.length > 0) result.violations.push(apcaResult);
|
|
78
|
+
else result.passes.push(apcaResult);
|
|
79
|
+
const resultWithLinks = withLinkPaths(result, storyId);
|
|
80
|
+
globalThis.__TECHSIO_A11Y_RESULTS__ = {
|
|
81
|
+
storyId,
|
|
82
|
+
results: resultWithLinks,
|
|
83
|
+
timestamp: Date.now()
|
|
84
|
+
};
|
|
85
|
+
resolve(resultWithLinks);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
reject(error);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
queue.push(task);
|
|
91
|
+
if (!isRunning) runNext();
|
|
92
|
+
if (highlightsRoot) highlightsRoot.style.display = '';
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
channel.on(EVENTS.MANUAL, async (storyId, input = DEFAULT_PARAMETERS)=>{
|
|
96
|
+
try {
|
|
97
|
+
await waitForAnimations();
|
|
98
|
+
const result = await run(input, storyId);
|
|
99
|
+
const resultJson = JSON.parse(JSON.stringify(result));
|
|
100
|
+
channel.emit(EVENTS.RESULT, resultJson, storyId);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
channel.emit(EVENTS.ERROR, error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
export { run };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { addons } from "storybook/preview-api";
|
|
3
|
+
import { EVENTS } from "./constants.js";
|
|
4
|
+
vi.mock('storybook/preview-api');
|
|
5
|
+
const mockedAddons = vi.mocked(addons);
|
|
6
|
+
describe('a11yRunner', ()=>{
|
|
7
|
+
let mockChannel;
|
|
8
|
+
beforeEach(()=>{
|
|
9
|
+
mockedAddons.getChannel.mockReset();
|
|
10
|
+
mockChannel = {
|
|
11
|
+
on: vi.fn(),
|
|
12
|
+
emit: vi.fn()
|
|
13
|
+
};
|
|
14
|
+
mockedAddons.getChannel.mockReturnValue(mockChannel);
|
|
15
|
+
});
|
|
16
|
+
it('should listen to events', async ()=>{
|
|
17
|
+
await import("./a11yRunner.js");
|
|
18
|
+
expect(mockedAddons.getChannel).toHaveBeenCalled();
|
|
19
|
+
expect(mockChannel.on).toHaveBeenCalledWith(EVENTS.MANUAL, expect.any(Function));
|
|
20
|
+
});
|
|
21
|
+
});
|