@techsio/storybook-better-a11y 0.0.5 → 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/apcaChecker.js +288 -0
- 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/manager.test.js +86 -0
- package/dist/params.js +0 -0
- 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/visionSimulatorFilters.js +100 -0
- package/dist/withVisionSimulator.js +41 -0
- package/package.json +1 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { act, cleanup, render } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { Fragment, createElement, useState } from "react";
|
|
4
|
+
import { STORY_FINISHED, STORY_RENDER_PHASE_CHANGED } from "storybook/internal/core-events";
|
|
5
|
+
import { EVENTS } from "../constants.js";
|
|
6
|
+
import { A11yContextProvider, useA11yContext } from "./A11yContext.js";
|
|
7
|
+
import * as __rspack_external_storybook_manager_api_d834c1f5 from "storybook/manager-api";
|
|
8
|
+
vi.mock('storybook/manager-api');
|
|
9
|
+
const mockedApi = vi.mocked(__rspack_external_storybook_manager_api_d834c1f5);
|
|
10
|
+
const storyId = 'button--primary';
|
|
11
|
+
const axeResult = {
|
|
12
|
+
incomplete: [
|
|
13
|
+
{
|
|
14
|
+
id: 'color-contrast',
|
|
15
|
+
impact: 'serious',
|
|
16
|
+
tags: [],
|
|
17
|
+
description: 'Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds',
|
|
18
|
+
help: 'Elements must have sufficient color contrast',
|
|
19
|
+
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/color-contrast?application=axeAPI',
|
|
20
|
+
nodes: []
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
passes: [
|
|
24
|
+
{
|
|
25
|
+
id: 'aria-allowed-attr',
|
|
26
|
+
impact: void 0,
|
|
27
|
+
tags: [],
|
|
28
|
+
description: "Ensures ARIA attributes are allowed for an element's role",
|
|
29
|
+
help: 'Elements must only use allowed ARIA attributes',
|
|
30
|
+
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/aria-allowed-attr?application=axeAPI',
|
|
31
|
+
nodes: []
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
violations: [
|
|
35
|
+
{
|
|
36
|
+
id: 'color-contrast',
|
|
37
|
+
impact: 'serious',
|
|
38
|
+
tags: [],
|
|
39
|
+
description: 'Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds',
|
|
40
|
+
help: 'Elements must have sufficient color contrast',
|
|
41
|
+
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/color-contrast?application=axeAPI',
|
|
42
|
+
nodes: []
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
describe('A11yContext', ()=>{
|
|
47
|
+
afterEach(()=>{
|
|
48
|
+
cleanup();
|
|
49
|
+
});
|
|
50
|
+
const onAllStatusChange = vi.fn();
|
|
51
|
+
const getAll = vi.fn();
|
|
52
|
+
const set = vi.fn();
|
|
53
|
+
const onSelect = vi.fn();
|
|
54
|
+
const unset = vi.fn();
|
|
55
|
+
const getCurrentStoryData = vi.fn();
|
|
56
|
+
const getParameters = vi.fn();
|
|
57
|
+
const getQueryParam = vi.fn();
|
|
58
|
+
beforeEach(()=>{
|
|
59
|
+
mockedApi.experimental_getStatusStore.mockReturnValue({
|
|
60
|
+
onAllStatusChange,
|
|
61
|
+
getAll,
|
|
62
|
+
set,
|
|
63
|
+
onSelect,
|
|
64
|
+
unset
|
|
65
|
+
});
|
|
66
|
+
mockedApi.useAddonState.mockImplementation((_, defaultState)=>useState(defaultState));
|
|
67
|
+
mockedApi.useChannel.mockReturnValue(vi.fn());
|
|
68
|
+
getCurrentStoryData.mockReturnValue({
|
|
69
|
+
id: storyId,
|
|
70
|
+
type: 'story'
|
|
71
|
+
});
|
|
72
|
+
getParameters.mockReturnValue({});
|
|
73
|
+
mockedApi.useStorybookApi.mockReturnValue({
|
|
74
|
+
getCurrentStoryData,
|
|
75
|
+
getParameters,
|
|
76
|
+
getQueryParam
|
|
77
|
+
});
|
|
78
|
+
mockedApi.useParameter.mockReturnValue({
|
|
79
|
+
manual: false
|
|
80
|
+
});
|
|
81
|
+
mockedApi.useStorybookState.mockReturnValue({
|
|
82
|
+
storyId
|
|
83
|
+
});
|
|
84
|
+
mockedApi.useGlobals.mockReturnValue([
|
|
85
|
+
{
|
|
86
|
+
a11y: {}
|
|
87
|
+
}
|
|
88
|
+
]);
|
|
89
|
+
mockedApi.useChannel.mockClear();
|
|
90
|
+
mockedApi.useStorybookApi.mockClear();
|
|
91
|
+
mockedApi.useAddonState.mockClear();
|
|
92
|
+
mockedApi.useParameter.mockClear();
|
|
93
|
+
mockedApi.useStorybookState.mockClear();
|
|
94
|
+
mockedApi.useGlobals.mockClear();
|
|
95
|
+
});
|
|
96
|
+
it('should render children', ()=>{
|
|
97
|
+
const { getByTestId } = render(/*#__PURE__*/ createElement(A11yContextProvider, null, /*#__PURE__*/ createElement("div", {
|
|
98
|
+
"data-testid": "child"
|
|
99
|
+
})));
|
|
100
|
+
expect(getByTestId('child')).toBeTruthy();
|
|
101
|
+
});
|
|
102
|
+
it('should handle STORY_FINISHED event correctly', ()=>{
|
|
103
|
+
const emit = vi.fn();
|
|
104
|
+
mockedApi.useChannel.mockReturnValue(emit);
|
|
105
|
+
const Component = ()=>{
|
|
106
|
+
const { results } = useA11yContext();
|
|
107
|
+
return /*#__PURE__*/ createElement(Fragment, null, !!results?.passes.length && /*#__PURE__*/ createElement("div", {
|
|
108
|
+
"data-testid": "anyPassesResults"
|
|
109
|
+
}, JSON.stringify(results.passes)), !!results?.incomplete.length && /*#__PURE__*/ createElement("div", {
|
|
110
|
+
"data-testid": "anyIncompleteResults"
|
|
111
|
+
}, JSON.stringify(results.incomplete)), !!results?.violations.length && /*#__PURE__*/ createElement("div", {
|
|
112
|
+
"data-testid": "anyViolationsResults"
|
|
113
|
+
}, JSON.stringify(results.violations)));
|
|
114
|
+
};
|
|
115
|
+
const { queryByTestId } = render(/*#__PURE__*/ createElement(A11yContextProvider, null, /*#__PURE__*/ createElement(Component, null)));
|
|
116
|
+
expect(queryByTestId('anyPassesResults')).toBeFalsy();
|
|
117
|
+
expect(queryByTestId('anyIncompleteResults')).toBeFalsy();
|
|
118
|
+
expect(queryByTestId('anyViolationsResults')).toBeFalsy();
|
|
119
|
+
const useChannelArgs = mockedApi.useChannel.mock.calls[0][0];
|
|
120
|
+
const storyFinishedPayload = {
|
|
121
|
+
storyId,
|
|
122
|
+
status: 'error',
|
|
123
|
+
reporters: [
|
|
124
|
+
{
|
|
125
|
+
type: 'a11y',
|
|
126
|
+
result: axeResult,
|
|
127
|
+
status: 'failed',
|
|
128
|
+
version: 1
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
};
|
|
132
|
+
act(()=>useChannelArgs[STORY_FINISHED](storyFinishedPayload));
|
|
133
|
+
expect(queryByTestId('anyPassesResults')).toHaveTextContent(JSON.stringify(axeResult.passes));
|
|
134
|
+
expect(queryByTestId('anyIncompleteResults')).toHaveTextContent(JSON.stringify(axeResult.incomplete));
|
|
135
|
+
expect(queryByTestId('anyViolationsResults')).toHaveTextContent(JSON.stringify(axeResult.violations));
|
|
136
|
+
});
|
|
137
|
+
it('should set discrepancy to cliFailedButModeManual when in manual mode (set via globals)', ()=>{
|
|
138
|
+
mockedApi.useGlobals.mockReturnValue([
|
|
139
|
+
{
|
|
140
|
+
a11y: {
|
|
141
|
+
manual: true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
]);
|
|
145
|
+
mockedApi.experimental_useStatusStore.mockReturnValue('status-value:error');
|
|
146
|
+
const Component = ()=>{
|
|
147
|
+
const { discrepancy } = useA11yContext();
|
|
148
|
+
return /*#__PURE__*/ createElement("div", {
|
|
149
|
+
"data-testid": "discrepancy"
|
|
150
|
+
}, discrepancy);
|
|
151
|
+
};
|
|
152
|
+
const { getByTestId } = render(/*#__PURE__*/ createElement(A11yContextProvider, null, /*#__PURE__*/ createElement(Component, null)));
|
|
153
|
+
expect(getByTestId('discrepancy').textContent).toBe('cliFailedButModeManual');
|
|
154
|
+
});
|
|
155
|
+
it('should set discrepancy to cliPassedBrowserFailed', ()=>{
|
|
156
|
+
mockedApi.useParameter.mockReturnValue({
|
|
157
|
+
manual: true
|
|
158
|
+
});
|
|
159
|
+
mockedApi.experimental_useStatusStore.mockReturnValue('status-value:success');
|
|
160
|
+
const Component = ()=>{
|
|
161
|
+
const { discrepancy } = useA11yContext();
|
|
162
|
+
return /*#__PURE__*/ createElement("div", {
|
|
163
|
+
"data-testid": "discrepancy"
|
|
164
|
+
}, discrepancy);
|
|
165
|
+
};
|
|
166
|
+
const { getByTestId } = render(/*#__PURE__*/ createElement(A11yContextProvider, null, /*#__PURE__*/ createElement(Component, null)));
|
|
167
|
+
const storyFinishedPayload = {
|
|
168
|
+
storyId,
|
|
169
|
+
status: 'error',
|
|
170
|
+
reporters: [
|
|
171
|
+
{
|
|
172
|
+
type: 'a11y',
|
|
173
|
+
result: axeResult,
|
|
174
|
+
status: 'failed',
|
|
175
|
+
version: 1
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
};
|
|
179
|
+
const useChannelArgs = mockedApi.useChannel.mock.calls[0][0];
|
|
180
|
+
act(()=>useChannelArgs[STORY_FINISHED](storyFinishedPayload));
|
|
181
|
+
expect(getByTestId('discrepancy').textContent).toBe('cliPassedBrowserFailed');
|
|
182
|
+
});
|
|
183
|
+
it('should handle STORY_RENDER_PHASE_CHANGED event correctly', ()=>{
|
|
184
|
+
const emit = vi.fn();
|
|
185
|
+
mockedApi.useChannel.mockReturnValue(emit);
|
|
186
|
+
const Component = ()=>{
|
|
187
|
+
const { status } = useA11yContext();
|
|
188
|
+
return /*#__PURE__*/ createElement("div", {
|
|
189
|
+
"data-testid": "status"
|
|
190
|
+
}, status);
|
|
191
|
+
};
|
|
192
|
+
const { queryByTestId } = render(/*#__PURE__*/ createElement(A11yContextProvider, null, /*#__PURE__*/ createElement(Component, null)));
|
|
193
|
+
expect(queryByTestId('status')).toHaveTextContent('initial');
|
|
194
|
+
const useChannelArgs = mockedApi.useChannel.mock.calls[0][0];
|
|
195
|
+
act(()=>useChannelArgs[STORY_RENDER_PHASE_CHANGED]({
|
|
196
|
+
newPhase: 'loading'
|
|
197
|
+
}));
|
|
198
|
+
expect(queryByTestId('status')).toHaveTextContent('initial');
|
|
199
|
+
act(()=>useChannelArgs[STORY_RENDER_PHASE_CHANGED]({
|
|
200
|
+
newPhase: 'afterEach'
|
|
201
|
+
}));
|
|
202
|
+
expect(queryByTestId('status')).toHaveTextContent('running');
|
|
203
|
+
});
|
|
204
|
+
it('should handle STORY_RENDER_PHASE_CHANGED event correctly when in manual mode (set via globals)', ()=>{
|
|
205
|
+
mockedApi.useGlobals.mockReturnValue([
|
|
206
|
+
{
|
|
207
|
+
a11y: {
|
|
208
|
+
manual: true
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
]);
|
|
212
|
+
const emit = vi.fn();
|
|
213
|
+
mockedApi.useChannel.mockReturnValue(emit);
|
|
214
|
+
const Component = ()=>{
|
|
215
|
+
const { status } = useA11yContext();
|
|
216
|
+
return /*#__PURE__*/ createElement("div", {
|
|
217
|
+
"data-testid": "status"
|
|
218
|
+
}, status);
|
|
219
|
+
};
|
|
220
|
+
const { queryByTestId } = render(/*#__PURE__*/ createElement(A11yContextProvider, null, /*#__PURE__*/ createElement(Component, null)));
|
|
221
|
+
expect(queryByTestId('status')).toHaveTextContent('manual');
|
|
222
|
+
const useChannelArgs = mockedApi.useChannel.mock.calls[0][0];
|
|
223
|
+
act(()=>useChannelArgs[STORY_RENDER_PHASE_CHANGED]({
|
|
224
|
+
newPhase: 'loading'
|
|
225
|
+
}));
|
|
226
|
+
expect(queryByTestId('status')).toHaveTextContent('manual');
|
|
227
|
+
act(()=>useChannelArgs[STORY_RENDER_PHASE_CHANGED]({
|
|
228
|
+
newPhase: 'afterEach'
|
|
229
|
+
}));
|
|
230
|
+
expect(queryByTestId('status')).toHaveTextContent('manual');
|
|
231
|
+
});
|
|
232
|
+
it('should handle STORY_FINISHED event with error correctly', ()=>{
|
|
233
|
+
const emit = vi.fn();
|
|
234
|
+
mockedApi.useChannel.mockReturnValue(emit);
|
|
235
|
+
const Component = ()=>{
|
|
236
|
+
const { error } = useA11yContext();
|
|
237
|
+
return /*#__PURE__*/ createElement("div", {
|
|
238
|
+
"data-testid": "error"
|
|
239
|
+
}, error ? error.message : 'No Error');
|
|
240
|
+
};
|
|
241
|
+
const { getByTestId } = render(/*#__PURE__*/ createElement(A11yContextProvider, null, /*#__PURE__*/ createElement(Component, null)));
|
|
242
|
+
expect(getByTestId('error').textContent).toBe('No Error');
|
|
243
|
+
const useChannelArgs = mockedApi.useChannel.mock.calls[0][0];
|
|
244
|
+
const storyFinishedPayload = {
|
|
245
|
+
storyId,
|
|
246
|
+
status: 'error',
|
|
247
|
+
reporters: [
|
|
248
|
+
{
|
|
249
|
+
status: 'failed',
|
|
250
|
+
version: 1,
|
|
251
|
+
type: 'a11y',
|
|
252
|
+
result: {
|
|
253
|
+
error: new Error('Test error')
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
]
|
|
257
|
+
};
|
|
258
|
+
act(()=>useChannelArgs[STORY_FINISHED](storyFinishedPayload));
|
|
259
|
+
expect(getByTestId('error').textContent).toBe('Test error');
|
|
260
|
+
});
|
|
261
|
+
it('should handle manual run correctly', ()=>{
|
|
262
|
+
const emit = vi.fn();
|
|
263
|
+
mockedApi.useChannel.mockReturnValue(emit);
|
|
264
|
+
const Component = ()=>{
|
|
265
|
+
const { handleManual } = useA11yContext();
|
|
266
|
+
return /*#__PURE__*/ createElement("button", {
|
|
267
|
+
onClick: handleManual,
|
|
268
|
+
"data-testid": "manualRunButton"
|
|
269
|
+
}, "Run Manual");
|
|
270
|
+
};
|
|
271
|
+
const { getByTestId } = render(/*#__PURE__*/ createElement(A11yContextProvider, null, /*#__PURE__*/ createElement(Component, null)));
|
|
272
|
+
act(()=>{
|
|
273
|
+
getByTestId('manualRunButton').click();
|
|
274
|
+
});
|
|
275
|
+
expect(emit).toHaveBeenCalledWith(EVENTS.MANUAL, storyId, expect.any(Object));
|
|
276
|
+
});
|
|
277
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import react, { Fragment, useCallback, useState } from "react";
|
|
2
|
+
import { Button, Link, SyntaxHighlighter } from "storybook/internal/components";
|
|
3
|
+
import { CheckIcon, CopyIcon, LocationIcon } from "@storybook/icons";
|
|
4
|
+
import { Content, List, Root, Trigger } from "@radix-ui/react-tabs";
|
|
5
|
+
import { styled } from "storybook/theming";
|
|
6
|
+
import { getFriendlySummaryForAxeResult } from "../../axeRuleMappingHelper.js";
|
|
7
|
+
import { useA11yContext } from "../A11yContext.js";
|
|
8
|
+
const StyledSyntaxHighlighter = styled(SyntaxHighlighter)(({ theme })=>({
|
|
9
|
+
fontSize: theme.typography.size.s1
|
|
10
|
+
}), ({ language })=>'css' === language && {
|
|
11
|
+
'.selector ~ span:nth-last-of-type(-n+3)': {
|
|
12
|
+
display: 'none'
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
const Info = styled.div({
|
|
16
|
+
display: 'flex',
|
|
17
|
+
flexDirection: 'column'
|
|
18
|
+
});
|
|
19
|
+
const RuleId = styled.div(({ theme })=>({
|
|
20
|
+
display: 'block',
|
|
21
|
+
color: theme.textMutedColor,
|
|
22
|
+
fontFamily: theme.typography.fonts.mono,
|
|
23
|
+
fontSize: theme.typography.size.s1,
|
|
24
|
+
marginTop: -8,
|
|
25
|
+
marginBottom: 12,
|
|
26
|
+
'@container (min-width: 800px)': {
|
|
27
|
+
display: 'none'
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
const Description = styled.p({
|
|
31
|
+
margin: 0
|
|
32
|
+
});
|
|
33
|
+
const Wrapper = styled.div({
|
|
34
|
+
display: 'flex',
|
|
35
|
+
flexDirection: 'column',
|
|
36
|
+
padding: '0 15px 20px 15px',
|
|
37
|
+
gap: 20
|
|
38
|
+
});
|
|
39
|
+
const Columns = styled.div({
|
|
40
|
+
gap: 15,
|
|
41
|
+
'@container (min-width: 800px)': {
|
|
42
|
+
display: 'grid',
|
|
43
|
+
gridTemplateColumns: '50% 50%'
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const Details_Content = styled.div(({ theme, side })=>({
|
|
47
|
+
display: 'left' === side ? 'flex' : 'none',
|
|
48
|
+
flexDirection: 'column',
|
|
49
|
+
gap: 15,
|
|
50
|
+
margin: 'left' === side ? '15px 0' : 0,
|
|
51
|
+
padding: 'left' === side ? '0 15px' : 0,
|
|
52
|
+
borderLeft: 'left' === side ? `1px solid ${theme.color.border}` : 'none',
|
|
53
|
+
'&:focus-visible': {
|
|
54
|
+
outline: 'none',
|
|
55
|
+
borderRadius: 4,
|
|
56
|
+
boxShadow: `0 0 0 1px inset ${theme.color.secondary}`
|
|
57
|
+
},
|
|
58
|
+
'@container (min-width: 800px)': {
|
|
59
|
+
display: 'left' === side ? 'none' : 'flex'
|
|
60
|
+
}
|
|
61
|
+
}));
|
|
62
|
+
const Item = styled(Button)(({ theme })=>({
|
|
63
|
+
fontFamily: theme.typography.fonts.mono,
|
|
64
|
+
fontWeight: theme.typography.weight.regular,
|
|
65
|
+
color: theme.textMutedColor,
|
|
66
|
+
height: 40,
|
|
67
|
+
overflow: 'hidden',
|
|
68
|
+
textOverflow: 'ellipsis',
|
|
69
|
+
whiteSpace: 'nowrap',
|
|
70
|
+
display: 'block',
|
|
71
|
+
width: '100%',
|
|
72
|
+
textAlign: 'left',
|
|
73
|
+
padding: '0 12px',
|
|
74
|
+
'&[data-state="active"]': {
|
|
75
|
+
color: theme.color.secondary,
|
|
76
|
+
backgroundColor: theme.background.hoverable
|
|
77
|
+
}
|
|
78
|
+
}));
|
|
79
|
+
const Messages = styled.div({
|
|
80
|
+
display: 'flex',
|
|
81
|
+
flexDirection: 'column',
|
|
82
|
+
gap: 10
|
|
83
|
+
});
|
|
84
|
+
const Actions = styled.div({
|
|
85
|
+
display: 'flex',
|
|
86
|
+
gap: 10
|
|
87
|
+
});
|
|
88
|
+
const CopyButton = ({ onClick })=>{
|
|
89
|
+
const [copied, setCopied] = useState(false);
|
|
90
|
+
const handleClick = useCallback(()=>{
|
|
91
|
+
onClick();
|
|
92
|
+
setCopied(true);
|
|
93
|
+
const timeout = setTimeout(()=>setCopied(false), 2000);
|
|
94
|
+
return ()=>clearTimeout(timeout);
|
|
95
|
+
}, [
|
|
96
|
+
onClick
|
|
97
|
+
]);
|
|
98
|
+
return /*#__PURE__*/ react.createElement(Button, {
|
|
99
|
+
ariaLabel: false,
|
|
100
|
+
onClick: handleClick
|
|
101
|
+
}, copied ? /*#__PURE__*/ react.createElement(CheckIcon, null) : /*#__PURE__*/ react.createElement(CopyIcon, null), " ", copied ? 'Copied' : 'Copy link');
|
|
102
|
+
};
|
|
103
|
+
const Details = ({ id, item, type, selection, handleSelectionChange })=>/*#__PURE__*/ react.createElement(Wrapper, {
|
|
104
|
+
id: id
|
|
105
|
+
}, /*#__PURE__*/ react.createElement(Info, null, /*#__PURE__*/ react.createElement(RuleId, null, item.id), /*#__PURE__*/ react.createElement(Description, null, getFriendlySummaryForAxeResult(item), ' ', /*#__PURE__*/ react.createElement(Link, {
|
|
106
|
+
href: item.helpUrl,
|
|
107
|
+
target: "_blank",
|
|
108
|
+
rel: "noopener noreferrer",
|
|
109
|
+
withArrow: true
|
|
110
|
+
}, "Learn how to resolve this violation"))), /*#__PURE__*/ react.createElement(Root, {
|
|
111
|
+
defaultValue: selection,
|
|
112
|
+
orientation: "vertical",
|
|
113
|
+
value: selection,
|
|
114
|
+
onValueChange: handleSelectionChange,
|
|
115
|
+
asChild: true
|
|
116
|
+
}, /*#__PURE__*/ react.createElement(Columns, null, /*#__PURE__*/ react.createElement(List, {
|
|
117
|
+
"aria-label": type
|
|
118
|
+
}, item.nodes.map((node, index)=>{
|
|
119
|
+
const key = `${type}.${item.id}.${index + 1}`;
|
|
120
|
+
return /*#__PURE__*/ react.createElement(Fragment, {
|
|
121
|
+
key: key
|
|
122
|
+
}, /*#__PURE__*/ react.createElement(Trigger, {
|
|
123
|
+
value: key,
|
|
124
|
+
asChild: true
|
|
125
|
+
}, /*#__PURE__*/ react.createElement(Item, {
|
|
126
|
+
ariaLabel: false,
|
|
127
|
+
variant: "ghost",
|
|
128
|
+
size: "medium",
|
|
129
|
+
id: key
|
|
130
|
+
}, index + 1, ". ", node.html)), /*#__PURE__*/ react.createElement(Content, {
|
|
131
|
+
value: key,
|
|
132
|
+
asChild: true
|
|
133
|
+
}, /*#__PURE__*/ react.createElement(Details_Content, {
|
|
134
|
+
side: "left"
|
|
135
|
+
}, getContent(node))));
|
|
136
|
+
})), item.nodes.map((node, index)=>{
|
|
137
|
+
const key = `${type}.${item.id}.${index + 1}`;
|
|
138
|
+
return /*#__PURE__*/ react.createElement(Content, {
|
|
139
|
+
key: key,
|
|
140
|
+
value: key,
|
|
141
|
+
asChild: true
|
|
142
|
+
}, /*#__PURE__*/ react.createElement(Details_Content, {
|
|
143
|
+
side: "right"
|
|
144
|
+
}, getContent(node)));
|
|
145
|
+
}))));
|
|
146
|
+
function getContent(node) {
|
|
147
|
+
const { handleCopyLink, handleJumpToElement } = useA11yContext();
|
|
148
|
+
const { any, all, none, html, target } = node;
|
|
149
|
+
const rules = [
|
|
150
|
+
...any,
|
|
151
|
+
...all,
|
|
152
|
+
...none
|
|
153
|
+
];
|
|
154
|
+
return /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(Messages, null, rules.map((rule)=>/*#__PURE__*/ react.createElement("div", {
|
|
155
|
+
key: rule.id
|
|
156
|
+
}, `${rule.message}${/(\.|: [^.]+\.*)$/.test(rule.message) ? '' : '.'}`))), /*#__PURE__*/ react.createElement(Actions, null, /*#__PURE__*/ react.createElement(Button, {
|
|
157
|
+
ariaLabel: false,
|
|
158
|
+
onClick: ()=>handleJumpToElement(node.target.toString())
|
|
159
|
+
}, /*#__PURE__*/ react.createElement(LocationIcon, null), " Jump to element"), /*#__PURE__*/ react.createElement(CopyButton, {
|
|
160
|
+
onClick: ()=>handleCopyLink(node.linkPath)
|
|
161
|
+
})), /*#__PURE__*/ react.createElement(StyledSyntaxHighlighter, {
|
|
162
|
+
language: "jsx",
|
|
163
|
+
wrapLongLines: true
|
|
164
|
+
}, `/* element */\n${html}`), /*#__PURE__*/ react.createElement(StyledSyntaxHighlighter, {
|
|
165
|
+
language: "css",
|
|
166
|
+
wrapLongLines: true
|
|
167
|
+
}, `/* selector */\n${target} {}`));
|
|
168
|
+
}
|
|
169
|
+
export { Details };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import react from "react";
|
|
2
|
+
import { Badge, Button, EmptyTabContent } from "storybook/internal/components";
|
|
3
|
+
import { ChevronSmallDownIcon } from "@storybook/icons";
|
|
4
|
+
import { styled } from "storybook/theming";
|
|
5
|
+
import { getTitleForAxeResult } from "../../axeRuleMappingHelper.js";
|
|
6
|
+
import { RuleType } from "../../types.js";
|
|
7
|
+
import { Details } from "./Details.js";
|
|
8
|
+
const impactStatus = {
|
|
9
|
+
minor: 'neutral',
|
|
10
|
+
moderate: 'warning',
|
|
11
|
+
serious: 'negative',
|
|
12
|
+
critical: 'critical'
|
|
13
|
+
};
|
|
14
|
+
const impactLabels = {
|
|
15
|
+
minor: 'Minor',
|
|
16
|
+
moderate: 'Moderate',
|
|
17
|
+
serious: 'Serious',
|
|
18
|
+
critical: 'Critical'
|
|
19
|
+
};
|
|
20
|
+
const Wrapper = styled.div(({ theme })=>({
|
|
21
|
+
display: 'flex',
|
|
22
|
+
flexDirection: 'column',
|
|
23
|
+
width: '100%',
|
|
24
|
+
borderBottom: `1px solid ${theme.appBorderColor}`,
|
|
25
|
+
containerType: 'inline-size',
|
|
26
|
+
fontSize: theme.typography.size.s2
|
|
27
|
+
}));
|
|
28
|
+
const Icon = styled(ChevronSmallDownIcon)({
|
|
29
|
+
transition: 'transform 0.1s ease-in-out'
|
|
30
|
+
});
|
|
31
|
+
const HeaderBar = styled.div(({ theme })=>({
|
|
32
|
+
display: 'flex',
|
|
33
|
+
justifyContent: 'space-between',
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
gap: 6,
|
|
36
|
+
padding: '6px 10px 6px 15px',
|
|
37
|
+
minHeight: 40,
|
|
38
|
+
background: 'none',
|
|
39
|
+
color: 'inherit',
|
|
40
|
+
textAlign: 'left',
|
|
41
|
+
cursor: 'pointer',
|
|
42
|
+
width: '100%',
|
|
43
|
+
'&:hover': {
|
|
44
|
+
color: theme.color.secondary
|
|
45
|
+
}
|
|
46
|
+
}));
|
|
47
|
+
const Title = styled.div(({ theme })=>({
|
|
48
|
+
display: 'flex',
|
|
49
|
+
alignItems: 'baseline',
|
|
50
|
+
flexGrow: 1,
|
|
51
|
+
fontSize: theme.typography.size.s2,
|
|
52
|
+
gap: 8
|
|
53
|
+
}));
|
|
54
|
+
const RuleId = styled.div(({ theme })=>({
|
|
55
|
+
display: 'none',
|
|
56
|
+
color: theme.textMutedColor,
|
|
57
|
+
fontFamily: theme.typography.fonts.mono,
|
|
58
|
+
fontSize: theme.typography.size.s1,
|
|
59
|
+
'@container (min-width: 800px)': {
|
|
60
|
+
display: 'block'
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
const Count = styled.div(({ theme })=>({
|
|
64
|
+
display: 'flex',
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
justifyContent: 'center',
|
|
67
|
+
color: theme.textMutedColor,
|
|
68
|
+
width: 28,
|
|
69
|
+
height: 28
|
|
70
|
+
}));
|
|
71
|
+
const Report = ({ items, empty, type, handleSelectionChange, selectedItems, toggleOpen })=>/*#__PURE__*/ react.createElement(react.Fragment, null, items && items.length ? items.map((item)=>{
|
|
72
|
+
const id = `${type}.${item.id}`;
|
|
73
|
+
const detailsId = `details:${id}`;
|
|
74
|
+
const selection = selectedItems.get(id);
|
|
75
|
+
const title = getTitleForAxeResult(item);
|
|
76
|
+
return /*#__PURE__*/ react.createElement(Wrapper, {
|
|
77
|
+
key: id
|
|
78
|
+
}, /*#__PURE__*/ react.createElement(HeaderBar, {
|
|
79
|
+
onClick: (event)=>toggleOpen(event, type, item),
|
|
80
|
+
"data-active": !!selection
|
|
81
|
+
}, /*#__PURE__*/ react.createElement(Title, null, /*#__PURE__*/ react.createElement("strong", null, title), /*#__PURE__*/ react.createElement(RuleId, null, item.id)), item.impact && /*#__PURE__*/ react.createElement(Badge, {
|
|
82
|
+
status: type === RuleType.PASS ? 'neutral' : impactStatus[item.impact]
|
|
83
|
+
}, impactLabels[item.impact]), /*#__PURE__*/ react.createElement(Count, null, item.nodes.length), /*#__PURE__*/ react.createElement(Button, {
|
|
84
|
+
onClick: (event)=>toggleOpen(event, type, item),
|
|
85
|
+
ariaLabel: `${selection ? 'Collapse' : 'Expand'} details for: ${title}`,
|
|
86
|
+
"aria-expanded": !!selection,
|
|
87
|
+
"aria-controls": detailsId,
|
|
88
|
+
variant: "ghost",
|
|
89
|
+
padding: "small"
|
|
90
|
+
}, /*#__PURE__*/ react.createElement(Icon, {
|
|
91
|
+
style: {
|
|
92
|
+
transform: `rotate(${selection ? -180 : 0}deg)`
|
|
93
|
+
}
|
|
94
|
+
}))), selection ? /*#__PURE__*/ react.createElement(Details, {
|
|
95
|
+
id: detailsId,
|
|
96
|
+
item: item,
|
|
97
|
+
type: type,
|
|
98
|
+
selection: selection,
|
|
99
|
+
handleSelectionChange: handleSelectionChange
|
|
100
|
+
}) : /*#__PURE__*/ react.createElement("div", {
|
|
101
|
+
id: detailsId
|
|
102
|
+
}));
|
|
103
|
+
}) : /*#__PURE__*/ react.createElement(EmptyTabContent, {
|
|
104
|
+
title: empty
|
|
105
|
+
}));
|
|
106
|
+
export { Report };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import react from "react";
|
|
2
|
+
import { ManagerContext } from "storybook/manager-api";
|
|
3
|
+
import { fn } from "storybook/test";
|
|
4
|
+
import { styled } from "storybook/theming";
|
|
5
|
+
import preview from "../../../../../.storybook/preview";
|
|
6
|
+
import { results } from "../../results.mock.js";
|
|
7
|
+
import { RuleType } from "../../types.js";
|
|
8
|
+
import { Report } from "./Report.js";
|
|
9
|
+
const StyledWrapper = styled.div(({ theme })=>({
|
|
10
|
+
backgroundColor: theme.background.content,
|
|
11
|
+
fontSize: theme.typography.size.s2 - 1,
|
|
12
|
+
color: theme.color.defaultText,
|
|
13
|
+
display: 'block',
|
|
14
|
+
height: '100%',
|
|
15
|
+
position: 'absolute',
|
|
16
|
+
left: 0,
|
|
17
|
+
right: 0,
|
|
18
|
+
bottom: 0,
|
|
19
|
+
overflow: 'auto'
|
|
20
|
+
}));
|
|
21
|
+
const managerContext = {
|
|
22
|
+
state: {},
|
|
23
|
+
api: {
|
|
24
|
+
getDocsUrl: fn().mockName('api::getDocsUrl')
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const meta = preview.meta({
|
|
28
|
+
title: 'Report',
|
|
29
|
+
component: Report,
|
|
30
|
+
decorators: [
|
|
31
|
+
(Story)=>/*#__PURE__*/ react.createElement(ManagerContext.Provider, {
|
|
32
|
+
value: managerContext
|
|
33
|
+
}, /*#__PURE__*/ react.createElement(StyledWrapper, {
|
|
34
|
+
id: "panel-tab-content"
|
|
35
|
+
}, /*#__PURE__*/ react.createElement(Story, null)))
|
|
36
|
+
],
|
|
37
|
+
parameters: {
|
|
38
|
+
layout: 'fullscreen'
|
|
39
|
+
},
|
|
40
|
+
args: {
|
|
41
|
+
items: [],
|
|
42
|
+
empty: 'No issues found',
|
|
43
|
+
type: RuleType.VIOLATION,
|
|
44
|
+
handleSelectionChange: fn().mockName('handleSelectionChange'),
|
|
45
|
+
selectedItems: new Map(),
|
|
46
|
+
toggleOpen: fn().mockName('toggleOpen')
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const Empty = meta.story({});
|
|
50
|
+
const Violations = meta.story({
|
|
51
|
+
args: {
|
|
52
|
+
items: results.violations,
|
|
53
|
+
type: RuleType.VIOLATION,
|
|
54
|
+
selectedItems: new Map([
|
|
55
|
+
[
|
|
56
|
+
`${RuleType.VIOLATION}.${results.violations["0"].id}`,
|
|
57
|
+
`${RuleType.VIOLATION}.${results.violations["0"].id}.3`
|
|
58
|
+
]
|
|
59
|
+
])
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const Incomplete = meta.story({
|
|
63
|
+
args: {
|
|
64
|
+
items: results.incomplete,
|
|
65
|
+
type: RuleType.INCOMPLETION,
|
|
66
|
+
selectedItems: new Map([
|
|
67
|
+
[
|
|
68
|
+
`${RuleType.INCOMPLETION}.${results.incomplete["1"].id}`,
|
|
69
|
+
`${RuleType.INCOMPLETION}.${results.incomplete["1"].id}.2`
|
|
70
|
+
]
|
|
71
|
+
])
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const Passes = meta.story({
|
|
75
|
+
args: {
|
|
76
|
+
items: results.passes,
|
|
77
|
+
type: RuleType.PASS,
|
|
78
|
+
selectedItems: new Map([
|
|
79
|
+
[
|
|
80
|
+
`${RuleType.PASS}.${results.passes["2"].id}`,
|
|
81
|
+
`${RuleType.PASS}.${results.passes["2"].id}.1`
|
|
82
|
+
]
|
|
83
|
+
])
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
export { Empty, Incomplete, Passes, Violations };
|