@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.
Files changed (38) hide show
  1. package/dist/AccessibilityRuleMaps.js +532 -0
  2. package/dist/a11yRunner.js +105 -0
  3. package/dist/a11yRunner.test.js +21 -0
  4. package/dist/a11yRunnerUtils.js +30 -0
  5. package/dist/a11yRunnerUtils.test.js +61 -0
  6. package/dist/{699.js → apcaChecker.js} +1 -255
  7. package/dist/apcaChecker.test.js +124 -0
  8. package/dist/axeRuleMappingHelper.js +4 -0
  9. package/dist/components/A11YPanel.js +140 -0
  10. package/dist/components/A11YPanel.stories.js +198 -0
  11. package/dist/components/A11YPanel.test.js +110 -0
  12. package/dist/components/A11yContext.js +438 -0
  13. package/dist/components/A11yContext.test.js +277 -0
  14. package/dist/components/Report/Details.js +169 -0
  15. package/dist/components/Report/Report.js +106 -0
  16. package/dist/components/Report/Report.stories.js +86 -0
  17. package/dist/components/Tabs.js +54 -0
  18. package/dist/components/TestDiscrepancyMessage.js +55 -0
  19. package/dist/components/TestDiscrepancyMessage.stories.js +40 -0
  20. package/dist/components/VisionSimulator.js +83 -0
  21. package/dist/components/VisionSimulator.stories.js +56 -0
  22. package/dist/constants.js +25 -0
  23. package/dist/index.js +5 -6
  24. package/dist/manager.js +11 -1540
  25. package/dist/manager.test.js +86 -0
  26. package/dist/params.js +0 -0
  27. package/dist/postinstall.js +1 -1
  28. package/dist/preview.js +68 -1
  29. package/dist/preview.test.js +215 -0
  30. package/dist/results.mock.js +874 -0
  31. package/dist/types.js +6 -0
  32. package/dist/utils.js +21 -0
  33. package/dist/{100.js → visionSimulatorFilters.js} +1 -23
  34. package/dist/withVisionSimulator.js +41 -0
  35. package/package.json +1 -1
  36. package/dist/212.js +0 -1965
  37. package/dist/212.js.LICENSE.txt +0 -19
  38. package/dist/rslib-runtime.js +0 -37
@@ -0,0 +1,438 @@
1
+ import react, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
2
+ import { STORY_CHANGED, STORY_FINISHED, STORY_HOT_UPDATED, STORY_RENDER_PHASE_CHANGED } from "storybook/internal/core-events";
3
+ import { HIGHLIGHT, REMOVE_HIGHLIGHT, SCROLL_INTO_VIEW } from "storybook/highlight";
4
+ import { experimental_getStatusStore, experimental_useStatusStore, useAddonState, useChannel, useGlobals, useParameter, useStorybookApi, useStorybookState } from "storybook/manager-api";
5
+ import { convert, themes } from "storybook/theming";
6
+ import { getFriendlySummaryForAxeResult, getTitleForAxeResult } from "../axeRuleMappingHelper.js";
7
+ import { ADDON_ID, EVENTS, STATUS_TYPE_ID_A11Y, STATUS_TYPE_ID_COMPONENT_TEST } from "../constants.js";
8
+ import { RuleType } from "../types.js";
9
+ const unhighlightedSelectors = [
10
+ 'html',
11
+ 'body',
12
+ 'main'
13
+ ];
14
+ const theme = convert(themes.light);
15
+ const colorsByType = {
16
+ [RuleType.VIOLATION]: theme.color.negative,
17
+ [RuleType.PASS]: theme.color.positive,
18
+ [RuleType.INCOMPLETION]: theme.color.warning
19
+ };
20
+ const A11yContext = /*#__PURE__*/ createContext({
21
+ parameters: {},
22
+ results: void 0,
23
+ highlighted: false,
24
+ toggleHighlight: ()=>{},
25
+ tab: RuleType.VIOLATION,
26
+ handleCopyLink: ()=>{},
27
+ setTab: ()=>{},
28
+ setStatus: ()=>{},
29
+ status: 'initial',
30
+ error: void 0,
31
+ handleManual: ()=>{},
32
+ discrepancy: null,
33
+ selectedItems: new Map(),
34
+ allExpanded: false,
35
+ toggleOpen: ()=>{},
36
+ handleCollapseAll: ()=>{},
37
+ handleExpandAll: ()=>{},
38
+ handleJumpToElement: ()=>{},
39
+ handleSelectionChange: ()=>{}
40
+ });
41
+ const A11yContextProvider = (props)=>{
42
+ const parameters = useParameter('a11y', {});
43
+ const [globals] = useGlobals() ?? [];
44
+ const api = useStorybookApi();
45
+ const getInitialStatus = useCallback((manual = false)=>manual ? 'manual' : 'initial', []);
46
+ const manual = useMemo(()=>globals?.a11y?.manual ?? false, [
47
+ globals?.a11y?.manual
48
+ ]);
49
+ const a11ySelection = useMemo(()=>{
50
+ const value = api.getQueryParam('a11ySelection');
51
+ if (value) api.setQueryParams({
52
+ a11ySelection: ''
53
+ });
54
+ return value;
55
+ }, [
56
+ api
57
+ ]);
58
+ const [state, setState] = useAddonState(ADDON_ID, {
59
+ ui: {
60
+ highlighted: false,
61
+ tab: RuleType.VIOLATION
62
+ },
63
+ results: void 0,
64
+ error: void 0,
65
+ status: getInitialStatus(manual)
66
+ });
67
+ const { ui, results, error, status } = state;
68
+ const { storyId } = useStorybookState();
69
+ const currentStoryA11yStatusValue = experimental_useStatusStore((allStatuses)=>allStatuses[storyId]?.[STATUS_TYPE_ID_A11Y]?.value);
70
+ useEffect(()=>{
71
+ const unsubscribe = experimental_getStatusStore('storybook/component-test').onAllStatusChange((statuses, previousStatuses)=>{
72
+ const current = statuses[storyId]?.[STATUS_TYPE_ID_COMPONENT_TEST];
73
+ const previous = previousStatuses[storyId]?.[STATUS_TYPE_ID_COMPONENT_TEST];
74
+ if (current?.value === 'status-value:error' && previous?.value !== 'status-value:error') setState((prev)=>({
75
+ ...prev,
76
+ status: 'component-test-error'
77
+ }));
78
+ });
79
+ return unsubscribe;
80
+ }, [
81
+ setState,
82
+ storyId
83
+ ]);
84
+ const handleToggleHighlight = useCallback(()=>{
85
+ setState((prev)=>({
86
+ ...prev,
87
+ ui: {
88
+ ...prev.ui,
89
+ highlighted: !prev.ui.highlighted
90
+ }
91
+ }));
92
+ }, [
93
+ setState
94
+ ]);
95
+ const [selectedItems, setSelectedItems] = useState(()=>{
96
+ const initialValue = new Map();
97
+ if (a11ySelection && /^[a-z]+.[a-z-]+.[0-9]+$/.test(a11ySelection)) {
98
+ const [type, id] = a11ySelection.split('.');
99
+ initialValue.set(`${type}.${id}`, a11ySelection);
100
+ }
101
+ return initialValue;
102
+ });
103
+ const allExpanded = useMemo(()=>{
104
+ const currentResults = results?.[ui.tab];
105
+ return currentResults?.every((result)=>selectedItems.has(`${ui.tab}.${result.id}`)) ?? false;
106
+ }, [
107
+ results,
108
+ selectedItems,
109
+ ui.tab
110
+ ]);
111
+ const toggleOpen = useCallback((event, type, item)=>{
112
+ event.stopPropagation();
113
+ const key = `${type}.${item.id}`;
114
+ setSelectedItems((prev)=>new Map(prev.delete(key) ? prev : prev.set(key, `${key}.1`)));
115
+ }, []);
116
+ const handleCollapseAll = useCallback(()=>{
117
+ setSelectedItems(new Map());
118
+ }, []);
119
+ const handleExpandAll = useCallback(()=>{
120
+ setSelectedItems((prev)=>new Map(results?.[ui.tab]?.map((result)=>{
121
+ const key = `${ui.tab}.${result.id}`;
122
+ return [
123
+ key,
124
+ prev.get(key) ?? `${key}.1`
125
+ ];
126
+ }) ?? []));
127
+ }, [
128
+ results,
129
+ ui.tab
130
+ ]);
131
+ const handleSelectionChange = useCallback((key)=>{
132
+ const [type, id] = key.split('.');
133
+ setSelectedItems((prev)=>new Map(prev.set(`${type}.${id}`, key)));
134
+ }, []);
135
+ const handleError = useCallback((err)=>{
136
+ setState((prev)=>({
137
+ ...prev,
138
+ status: 'error',
139
+ error: err
140
+ }));
141
+ }, [
142
+ setState
143
+ ]);
144
+ const handleResult = useCallback((axeResults, id)=>{
145
+ if (storyId === id) {
146
+ setState((prev)=>({
147
+ ...prev,
148
+ status: 'ran',
149
+ results: axeResults
150
+ }));
151
+ setTimeout(()=>{
152
+ setState((prev)=>{
153
+ if ('ran' === prev.status) return {
154
+ ...prev,
155
+ status: 'ready'
156
+ };
157
+ return prev;
158
+ });
159
+ setSelectedItems((prev)=>{
160
+ if (1 === prev.size) {
161
+ const [key] = prev.values();
162
+ document.getElementById(key)?.scrollIntoView({
163
+ behavior: 'smooth',
164
+ block: 'center'
165
+ });
166
+ }
167
+ return prev;
168
+ });
169
+ }, 900);
170
+ }
171
+ }, [
172
+ storyId,
173
+ setState,
174
+ setSelectedItems
175
+ ]);
176
+ const handleSelect = useCallback((itemId, details)=>{
177
+ const [type, id] = itemId.split('.');
178
+ const { helpUrl, nodes } = results?.[type]?.find((r)=>r.id === id) || {};
179
+ const openedWindow = helpUrl && window.open(helpUrl, '_blank', 'noopener,noreferrer');
180
+ if (nodes && !openedWindow) {
181
+ const index = nodes.findIndex((n)=>details.selectors.some((s)=>s === String(n.target))) ?? -1;
182
+ if (-1 !== index) {
183
+ const key = `${type}.${id}.${index + 1}`;
184
+ setSelectedItems(new Map([
185
+ [
186
+ `${type}.${id}`,
187
+ key
188
+ ]
189
+ ]));
190
+ setTimeout(()=>{
191
+ document.getElementById(key)?.scrollIntoView({
192
+ behavior: 'smooth',
193
+ block: 'center'
194
+ });
195
+ }, 100);
196
+ }
197
+ }
198
+ }, [
199
+ results
200
+ ]);
201
+ const handleReport = useCallback(({ reporters })=>{
202
+ const a11yReport = reporters.find((r)=>'a11y' === r.type);
203
+ if (a11yReport) if ('error' in a11yReport.result) handleError(a11yReport.result.error);
204
+ else handleResult(a11yReport.result, storyId);
205
+ }, [
206
+ handleError,
207
+ handleResult,
208
+ storyId
209
+ ]);
210
+ const handleReset = useCallback(({ newPhase })=>{
211
+ if ('loading' === newPhase) setState((prev)=>({
212
+ ...prev,
213
+ results: void 0,
214
+ status: manual ? 'manual' : 'initial'
215
+ }));
216
+ else if ('afterEach' === newPhase && !manual) setState((prev)=>({
217
+ ...prev,
218
+ status: 'running'
219
+ }));
220
+ }, [
221
+ manual,
222
+ setState
223
+ ]);
224
+ const emit = useChannel({
225
+ [EVENTS.RESULT]: handleResult,
226
+ [EVENTS.ERROR]: handleError,
227
+ [EVENTS.SELECT]: handleSelect,
228
+ [STORY_CHANGED]: ()=>setSelectedItems(new Map()),
229
+ [STORY_RENDER_PHASE_CHANGED]: handleReset,
230
+ [STORY_FINISHED]: handleReport,
231
+ [STORY_HOT_UPDATED]: ()=>{
232
+ setState((prev)=>({
233
+ ...prev,
234
+ status: 'running'
235
+ }));
236
+ emit(EVENTS.MANUAL, storyId, parameters);
237
+ }
238
+ }, [
239
+ handleReset,
240
+ handleReport,
241
+ handleSelect,
242
+ handleError,
243
+ handleResult,
244
+ parameters,
245
+ storyId
246
+ ]);
247
+ const handleManual = useCallback(()=>{
248
+ setState((prev)=>({
249
+ ...prev,
250
+ status: 'running'
251
+ }));
252
+ emit(EVENTS.MANUAL, storyId, parameters);
253
+ }, [
254
+ emit,
255
+ parameters,
256
+ setState,
257
+ storyId
258
+ ]);
259
+ const handleCopyLink = useCallback(async (linkPath)=>{
260
+ const { createCopyToClipboardFunction } = await import("storybook/internal/components");
261
+ await createCopyToClipboardFunction()(`${window.location.origin}${linkPath}`);
262
+ }, []);
263
+ const handleJumpToElement = useCallback((target)=>emit(SCROLL_INTO_VIEW, target), [
264
+ emit
265
+ ]);
266
+ useEffect(()=>{
267
+ setState((prev)=>({
268
+ ...prev,
269
+ status: getInitialStatus(manual)
270
+ }));
271
+ }, [
272
+ getInitialStatus,
273
+ manual,
274
+ setState
275
+ ]);
276
+ const isInitial = 'initial' === status;
277
+ useEffect(()=>{
278
+ if (!a11ySelection) return;
279
+ setState((prev)=>{
280
+ const update = {
281
+ ...prev.ui,
282
+ highlighted: true
283
+ };
284
+ const [type] = a11ySelection.split('.') ?? [];
285
+ if (type && Object.values(RuleType).includes(type)) update.tab = type;
286
+ return {
287
+ ...prev,
288
+ ui: update
289
+ };
290
+ });
291
+ }, [
292
+ a11ySelection
293
+ ]);
294
+ useEffect(()=>{
295
+ emit(REMOVE_HIGHLIGHT, `${ADDON_ID}/selected`);
296
+ emit(REMOVE_HIGHLIGHT, `${ADDON_ID}/others`);
297
+ if (!ui.highlighted || isInitial) return;
298
+ const selected = Array.from(selectedItems.values()).flatMap((key)=>{
299
+ const [type, id, number] = key.split('.');
300
+ if (type !== ui.tab) return [];
301
+ const result = results?.[type]?.find((r)=>r.id === id);
302
+ const target = result?.nodes[Number(number) - 1]?.target;
303
+ return target ? [
304
+ String(target)
305
+ ] : [];
306
+ });
307
+ if (selected.length) emit(HIGHLIGHT, {
308
+ id: `${ADDON_ID}/selected`,
309
+ priority: 1,
310
+ selectors: selected,
311
+ styles: {
312
+ outline: `1px solid color-mix(in srgb, ${colorsByType[ui.tab]}, transparent 30%)`,
313
+ backgroundColor: 'transparent'
314
+ },
315
+ hoverStyles: {
316
+ outlineWidth: '2px'
317
+ },
318
+ focusStyles: {
319
+ backgroundColor: 'transparent'
320
+ },
321
+ menu: results?.[ui.tab].map((result)=>{
322
+ const selectors = result.nodes.flatMap((n)=>n.target).map(String).filter((e)=>selected.includes(e));
323
+ return [
324
+ {
325
+ id: `${ui.tab}.${result.id}:info`,
326
+ title: getTitleForAxeResult(result),
327
+ description: getFriendlySummaryForAxeResult(result),
328
+ selectors
329
+ },
330
+ {
331
+ id: `${ui.tab}.${result.id}`,
332
+ iconLeft: 'info',
333
+ iconRight: 'shareAlt',
334
+ title: 'Learn how to resolve this violation',
335
+ clickEvent: EVENTS.SELECT,
336
+ selectors
337
+ }
338
+ ];
339
+ })
340
+ });
341
+ const others = results?.[ui.tab].flatMap((r)=>r.nodes.flatMap((n)=>n.target).map(String)).filter((e)=>![
342
+ ...unhighlightedSelectors,
343
+ ...selected
344
+ ].includes(e));
345
+ if (others?.length) emit(HIGHLIGHT, {
346
+ id: `${ADDON_ID}/others`,
347
+ selectors: others,
348
+ styles: {
349
+ outline: `1px solid color-mix(in srgb, ${colorsByType[ui.tab]}, transparent 30%)`,
350
+ backgroundColor: `color-mix(in srgb, ${colorsByType[ui.tab]}, transparent 60%)`
351
+ },
352
+ hoverStyles: {
353
+ outlineWidth: '2px'
354
+ },
355
+ focusStyles: {
356
+ backgroundColor: 'transparent'
357
+ },
358
+ menu: results?.[ui.tab].map((result)=>{
359
+ const selectors = result.nodes.flatMap((n)=>n.target).map(String).filter((e)=>!selected.includes(e));
360
+ return [
361
+ {
362
+ id: `${ui.tab}.${result.id}:info`,
363
+ title: getTitleForAxeResult(result),
364
+ description: getFriendlySummaryForAxeResult(result),
365
+ selectors
366
+ },
367
+ {
368
+ id: `${ui.tab}.${result.id}`,
369
+ iconLeft: 'info',
370
+ iconRight: 'shareAlt',
371
+ title: 'Learn how to resolve this violation',
372
+ clickEvent: EVENTS.SELECT,
373
+ selectors
374
+ }
375
+ ];
376
+ })
377
+ });
378
+ }, [
379
+ isInitial,
380
+ emit,
381
+ ui.highlighted,
382
+ results,
383
+ ui.tab,
384
+ selectedItems
385
+ ]);
386
+ const discrepancy = useMemo(()=>{
387
+ if (!currentStoryA11yStatusValue) return null;
388
+ if ('status-value:success' === currentStoryA11yStatusValue && results?.violations.length) return 'cliPassedBrowserFailed';
389
+ if ('status-value:error' === currentStoryA11yStatusValue && !results?.violations.length) {
390
+ if ('ready' === status || 'ran' === status) return 'browserPassedCliFailed';
391
+ if ('manual' === status) return 'cliFailedButModeManual';
392
+ }
393
+ return null;
394
+ }, [
395
+ results?.violations.length,
396
+ status,
397
+ currentStoryA11yStatusValue
398
+ ]);
399
+ return /*#__PURE__*/ react.createElement(A11yContext.Provider, {
400
+ value: {
401
+ parameters,
402
+ results,
403
+ highlighted: ui.highlighted,
404
+ toggleHighlight: handleToggleHighlight,
405
+ tab: ui.tab,
406
+ setTab: useCallback((type)=>setState((prev)=>({
407
+ ...prev,
408
+ ui: {
409
+ ...prev.ui,
410
+ tab: type
411
+ }
412
+ })), [
413
+ setState
414
+ ]),
415
+ handleCopyLink,
416
+ status: status,
417
+ setStatus: useCallback((status)=>setState((prev)=>({
418
+ ...prev,
419
+ status
420
+ })), [
421
+ setState
422
+ ]),
423
+ error: error,
424
+ handleManual,
425
+ discrepancy,
426
+ selectedItems,
427
+ toggleOpen,
428
+ allExpanded,
429
+ handleCollapseAll,
430
+ handleExpandAll,
431
+ handleJumpToElement,
432
+ handleSelectionChange
433
+ },
434
+ ...props
435
+ });
436
+ };
437
+ const useA11yContext = ()=>useContext(A11yContext);
438
+ export { A11yContext, A11yContextProvider, useA11yContext };