@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.
@@ -0,0 +1,30 @@
1
+ import { global } from "@storybook/global";
2
+ import { PANEL_ID } from "./constants.js";
3
+ const { document: a11yRunnerUtils_document } = global;
4
+ const withLinkPaths = (results, storyId)=>{
5
+ const pathname = a11yRunnerUtils_document.location.pathname.replace(/iframe\.html$/, '');
6
+ const enhancedResults = {
7
+ ...results
8
+ };
9
+ const propertiesToAugment = [
10
+ 'incomplete',
11
+ 'passes',
12
+ 'violations'
13
+ ];
14
+ propertiesToAugment.forEach((key)=>{
15
+ if (Array.isArray(results[key])) enhancedResults[key] = results[key].map((result)=>({
16
+ ...result,
17
+ nodes: result.nodes.map((node, index)=>{
18
+ const id = `${key}.${result.id}.${index + 1}`;
19
+ const linkPath = `${pathname}?path=/story/${storyId}&addonPanel=${PANEL_ID}&a11ySelection=${id}`;
20
+ return {
21
+ id,
22
+ ...node,
23
+ linkPath
24
+ };
25
+ })
26
+ }));
27
+ });
28
+ return enhancedResults;
29
+ };
30
+ export { withLinkPaths };
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { withLinkPaths } from "./a11yRunnerUtils.js";
3
+ describe('a11yRunnerUtils', ()=>{
4
+ describe('withLinkPaths', ()=>{
5
+ it('should add link paths to the axe results', ()=>{
6
+ const axeResults = {
7
+ violations: [
8
+ {
9
+ id: 'color-contrast',
10
+ nodes: [
11
+ {
12
+ html: '<button>Click me</button>',
13
+ target: [
14
+ '.button'
15
+ ]
16
+ },
17
+ {
18
+ html: '<a href="#">Link</a>',
19
+ target: [
20
+ '.link'
21
+ ]
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ passes: [
27
+ {
28
+ id: 'button-name',
29
+ nodes: [
30
+ {
31
+ html: '<button>Valid Button</button>',
32
+ target: [
33
+ '.valid-button'
34
+ ]
35
+ }
36
+ ]
37
+ }
38
+ ],
39
+ incomplete: [
40
+ {
41
+ id: 'aria-valid',
42
+ nodes: [
43
+ {
44
+ html: '<div aria-label="test">Test</div>',
45
+ target: [
46
+ '.aria-test'
47
+ ]
48
+ }
49
+ ]
50
+ }
51
+ ],
52
+ inapplicable: []
53
+ };
54
+ const result = withLinkPaths(axeResults, 'test-story-id');
55
+ expect(result.violations[0].nodes[0].linkPath).toBe('/?path=/story/test-story-id&addonPanel=storybook/a11y/panel&a11ySelection=violations.color-contrast.1');
56
+ expect(result.violations[0].nodes[1].linkPath).toBe('/?path=/story/test-story-id&addonPanel=storybook/a11y/panel&a11ySelection=violations.color-contrast.2');
57
+ expect(result.passes[0].nodes[0].linkPath).toBe('/?path=/story/test-story-id&addonPanel=storybook/a11y/panel&a11ySelection=passes.button-name.1');
58
+ expect(result.incomplete[0].nodes[0].linkPath).toBe('/?path=/story/test-story-id&addonPanel=storybook/a11y/panel&a11ySelection=incomplete.aria-valid.1');
59
+ });
60
+ });
61
+ });
@@ -0,0 +1,288 @@
1
+ import { global } from "@storybook/global";
2
+ const { document: apcaChecker_document } = global;
3
+ const DEFAULT_APCA_OPTIONS = {
4
+ level: 'bronze',
5
+ useCase: 'body'
6
+ };
7
+ const APCA_LC_STEPS = [
8
+ 15,
9
+ 20,
10
+ 25,
11
+ 30,
12
+ 35,
13
+ 40,
14
+ 45,
15
+ 50,
16
+ 55,
17
+ 60,
18
+ 65,
19
+ 70,
20
+ 75,
21
+ 80,
22
+ 85,
23
+ 90,
24
+ 95,
25
+ 100,
26
+ 105,
27
+ 110,
28
+ 115,
29
+ 120,
30
+ 125
31
+ ];
32
+ const APCA_MAX_CONTRAST_LC = 90;
33
+ function getComputedColor(element, property) {
34
+ const computed = global.getComputedStyle(element);
35
+ return computed[property] || '';
36
+ }
37
+ function getEffectiveBackgroundColor(element) {
38
+ let current = element;
39
+ while(current && current !== apcaChecker_document.body){
40
+ const bgColor = getComputedColor(current, 'backgroundColor');
41
+ if (bgColor && 'rgba(0, 0, 0, 0)' !== bgColor && 'transparent' !== bgColor) return bgColor;
42
+ current = current.parentElement;
43
+ }
44
+ return 'rgb(255, 255, 255)';
45
+ }
46
+ function parseColor(color) {
47
+ const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
48
+ if (!match) return null;
49
+ return [
50
+ parseInt(match[1], 10),
51
+ parseInt(match[2], 10),
52
+ parseInt(match[3], 10)
53
+ ];
54
+ }
55
+ function normalizeUseCase(value, fallback) {
56
+ if (!value) return fallback;
57
+ const normalized = value.toLowerCase().replace(/\s+/g, '-');
58
+ if (normalized.includes('body')) return 'body';
59
+ if (normalized.includes('sub') || normalized.includes('logo')) return 'sub-fluent';
60
+ if (normalized.includes('non') || normalized.includes('incidental') || normalized.includes('spot')) return 'non-fluent';
61
+ if (normalized.includes('fluent')) return 'fluent';
62
+ return fallback;
63
+ }
64
+ function getUseCaseForElement(element, fallback) {
65
+ const attr = element.getAttribute('data-apca-usecase') ?? element.getAttribute('data-apca-use-case') ?? element.getAttribute('data-apca-usage');
66
+ return normalizeUseCase(attr, fallback);
67
+ }
68
+ function normalizeWeightBucket(fontWeight) {
69
+ const weight = Number.isFinite(fontWeight) ? fontWeight : 400;
70
+ const clamped = Math.max(100, Math.min(900, weight));
71
+ return 100 * Math.round(clamped / 100);
72
+ }
73
+ function getBronzeThreshold(useCase, fontSize) {
74
+ if ('body' === useCase) return {
75
+ threshold: 75,
76
+ preferred: 90
77
+ };
78
+ if ('fluent' === useCase) {
79
+ if (fontSize > 32) return {
80
+ threshold: 45
81
+ };
82
+ if (fontSize >= 16) return {
83
+ threshold: 60
84
+ };
85
+ return {
86
+ threshold: 75
87
+ };
88
+ }
89
+ return null;
90
+ }
91
+ function getMinFontSize(level, useCase) {
92
+ if ('bronze' === level) return;
93
+ if ('sub-fluent' === useCase) return 'gold' === level ? 12 : 10;
94
+ if ('fluent' === useCase || 'body' === useCase) return 'gold' === level ? 16 : 14;
95
+ }
96
+ function getMaxContrast(level, useCase, fontSize, fontWeight) {
97
+ if ('bronze' === level) {
98
+ if ('fluent' === useCase && fontSize > 32 && fontWeight >= 700) return APCA_MAX_CONTRAST_LC;
99
+ return;
100
+ }
101
+ if (('body' === useCase || 'fluent' === useCase) && fontSize > 36) return APCA_MAX_CONTRAST_LC;
102
+ }
103
+ function getBaseThresholdFromLookup(fontSize, fontWeight, allowNonContent, fontLookupAPCA) {
104
+ const weightBucket = normalizeWeightBucket(fontWeight);
105
+ const weightIndex = Math.round(weightBucket / 100);
106
+ for (const lc of APCA_LC_STEPS){
107
+ const row = fontLookupAPCA(lc, 2);
108
+ const requiredSize = Number(row[weightIndex]);
109
+ if (!Number.isFinite(requiredSize)) continue;
110
+ if (999 === requiredSize) continue;
111
+ if (777 === requiredSize && !allowNonContent) continue;
112
+ const minSize = 777 === requiredSize ? 0 : requiredSize;
113
+ if (fontSize >= minSize) return lc;
114
+ }
115
+ return null;
116
+ }
117
+ function getApcaThreshold(level, useCase, fontSize, fontWeight, fontLookupAPCA) {
118
+ if ('bronze' === level) {
119
+ const bronzeThreshold = getBronzeThreshold(useCase, fontSize);
120
+ if (!bronzeThreshold) return {
121
+ threshold: null,
122
+ skip: true
123
+ };
124
+ return {
125
+ threshold: bronzeThreshold.threshold,
126
+ maxContrast: getMaxContrast(level, useCase, fontSize, fontWeight)
127
+ };
128
+ }
129
+ const minFontSize = getMinFontSize(level, useCase);
130
+ const baseThreshold = getBaseThresholdFromLookup(fontSize, fontWeight, 'non-fluent' === useCase, fontLookupAPCA);
131
+ if (null === baseThreshold) return {
132
+ threshold: null,
133
+ minFontSize,
134
+ note: 'Font size/weight is below the minimums in the APCA lookup table for this use case.'
135
+ };
136
+ let threshold = baseThreshold;
137
+ if ('sub-fluent' === useCase) threshold = Math.max(threshold - 15, 'silver' === level ? 40 : 45);
138
+ else if ('non-fluent' === useCase) threshold = Math.max(threshold - ('silver' === level ? 30 : 20), 30);
139
+ if ('body' === useCase && 'gold' === level && threshold < 75) threshold += 15;
140
+ return {
141
+ threshold,
142
+ minFontSize,
143
+ maxContrast: getMaxContrast(level, useCase, fontSize, fontWeight)
144
+ };
145
+ }
146
+ function hasReadableText(element) {
147
+ const text = element.textContent?.trim() || '';
148
+ return text.length > 0 && !element.hasAttribute('aria-hidden');
149
+ }
150
+ function isVisible(element) {
151
+ const computed = global.getComputedStyle(element);
152
+ return 'none' !== computed.display && 'hidden' !== computed.visibility && '0' !== computed.opacity;
153
+ }
154
+ async function runAPCACheck(context = apcaChecker_document, options = DEFAULT_APCA_OPTIONS, excludeSelectors = []) {
155
+ const { APCAcontrast, sRGBtoY, fontLookupAPCA } = await import("apca-w3");
156
+ const apcaOptions = {
157
+ ...DEFAULT_APCA_OPTIONS,
158
+ ...options
159
+ };
160
+ const violations = [];
161
+ const root = context instanceof Document ? context.body : context;
162
+ const textElements = root.querySelectorAll('p, span, div, h1, h2, h3, h4, h5, h6, a, button, label, td, th, li, input, textarea');
163
+ textElements.forEach((element)=>{
164
+ if (excludeSelectors.length > 0) {
165
+ const isExcluded = excludeSelectors.some((selector)=>{
166
+ try {
167
+ return null !== element.closest(selector);
168
+ } catch {
169
+ return false;
170
+ }
171
+ });
172
+ if (isExcluded) return;
173
+ }
174
+ if (!isVisible(element) || !hasReadableText(element)) return;
175
+ const foreground = getComputedColor(element, 'color');
176
+ const background = getEffectiveBackgroundColor(element);
177
+ const fgColor = parseColor(foreground);
178
+ const bgColor = parseColor(background);
179
+ if (!fgColor || !bgColor) return;
180
+ const computed = global.getComputedStyle(element);
181
+ const fontSize = parseFloat(computed.fontSize);
182
+ const fontWeight = parseInt(computed.fontWeight, 10);
183
+ try {
184
+ const fgLuminance = sRGBtoY(fgColor);
185
+ const bgLuminance = sRGBtoY(bgColor);
186
+ const contrastValue = Math.abs(APCAcontrast(fgLuminance, bgLuminance));
187
+ const useCase = getUseCaseForElement(element, apcaOptions.useCase);
188
+ const level = apcaOptions.level;
189
+ const { threshold, minFontSize, maxContrast, note, skip } = getApcaThreshold(level, useCase, fontSize, fontWeight, fontLookupAPCA);
190
+ if (skip) return;
191
+ const messages = [];
192
+ if (null === threshold) {
193
+ if (note) messages.push(note);
194
+ } else {
195
+ if (contrastValue < threshold) messages.push(`APCA contrast of ${contrastValue.toFixed(1)} Lc is below the minimum of ${threshold} Lc for ${level} ${useCase} text.`);
196
+ if (void 0 !== maxContrast && contrastValue > maxContrast) messages.push(`APCA contrast of ${contrastValue.toFixed(1)} Lc exceeds the maximum of ${maxContrast} Lc for ${level} ${useCase} text at ${fontSize.toFixed(1)}px.`);
197
+ }
198
+ if (minFontSize && fontSize < minFontSize) messages.push(`Font size ${fontSize.toFixed(1)}px is below the minimum ${minFontSize}px for ${level} ${useCase} text.`);
199
+ if (messages.length > 0) violations.push({
200
+ element,
201
+ foreground,
202
+ background,
203
+ contrastValue,
204
+ fontSize,
205
+ fontWeight,
206
+ threshold,
207
+ maxContrast,
208
+ useCase,
209
+ level,
210
+ minFontSize,
211
+ note
212
+ });
213
+ } catch (error) {
214
+ console.warn('APCA calculation error:', error);
215
+ }
216
+ });
217
+ const nodes = violations.map((violation)=>{
218
+ const impact = getImpact(violation.contrastValue, violation.threshold, violation.maxContrast);
219
+ const messages = [];
220
+ if (violation.note) messages.push(violation.note);
221
+ else if (null !== violation.threshold && violation.contrastValue < violation.threshold) messages.push(`APCA contrast of ${violation.contrastValue.toFixed(1)} Lc is below the minimum of ${violation.threshold} Lc for ${violation.level} ${violation.useCase} text.`);
222
+ if (void 0 !== violation.maxContrast && violation.contrastValue > violation.maxContrast) messages.push(`APCA contrast of ${violation.contrastValue.toFixed(1)} Lc exceeds the maximum of ${violation.maxContrast} Lc for ${violation.level} ${violation.useCase} text at ${violation.fontSize.toFixed(1)}px.`);
223
+ if (violation.minFontSize && violation.fontSize < violation.minFontSize) messages.push(`Font size ${violation.fontSize.toFixed(1)}px is below the minimum ${violation.minFontSize}px for ${violation.level} ${violation.useCase} text.`);
224
+ const rules = messages.map((message)=>({
225
+ id: 'apca-contrast',
226
+ impact,
227
+ message,
228
+ data: null,
229
+ relatedNodes: []
230
+ }));
231
+ const failureSummary = `Fix any of the following:\n ${messages.join('\n ')}`;
232
+ return {
233
+ html: violation.element.outerHTML,
234
+ target: [
235
+ getSelector(violation.element)
236
+ ],
237
+ any: rules,
238
+ all: [],
239
+ none: [],
240
+ impact,
241
+ failureSummary
242
+ };
243
+ });
244
+ return {
245
+ id: 'apca-contrast',
246
+ impact: nodes.length > 0 ? 'serious' : null,
247
+ tags: [
248
+ 'wcag3',
249
+ 'wcag30',
250
+ 'apca',
251
+ 'contrast'
252
+ ],
253
+ description: 'Ensures text has sufficient contrast using APCA (WCAG 3.0 method)',
254
+ help: 'Elements must have sufficient color contrast using APCA',
255
+ helpUrl: 'https://git.apcacontrast.com/',
256
+ nodes
257
+ };
258
+ }
259
+ function getSelector(element) {
260
+ if (element.id) return `#${element.id}`;
261
+ const path = [];
262
+ let current = element;
263
+ while(current && current !== apcaChecker_document.body){
264
+ let selector = current.tagName.toLowerCase();
265
+ if (current.className) {
266
+ const classes = current.className.split(' ').filter(Boolean);
267
+ if (classes.length > 0) selector += `.${classes[0]}`;
268
+ }
269
+ const parent = current.parentElement;
270
+ if (parent) {
271
+ const siblings = Array.from(parent.children);
272
+ const index = siblings.indexOf(current) + 1;
273
+ selector += `:nth-child(${index})`;
274
+ }
275
+ path.unshift(selector);
276
+ current = parent;
277
+ }
278
+ return path.join(' > ');
279
+ }
280
+ function getImpact(contrastValue, threshold, maxContrast) {
281
+ if (null === threshold && void 0 === maxContrast) return 'serious';
282
+ const difference = void 0 !== maxContrast && contrastValue > maxContrast ? contrastValue - maxContrast : (threshold ?? 0) - contrastValue;
283
+ if (difference > 30) return 'critical';
284
+ if (difference > 20) return 'serious';
285
+ if (difference > 10) return 'moderate';
286
+ return 'minor';
287
+ }
288
+ export { runAPCACheck };
@@ -0,0 +1,124 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { runAPCACheck } from "./apcaChecker.js";
3
+ describe('apcaChecker', ()=>{
4
+ let container;
5
+ beforeEach(()=>{
6
+ container = document.createElement('div');
7
+ document.body.appendChild(container);
8
+ });
9
+ afterEach(()=>{
10
+ if (container.parentNode) container.parentNode.removeChild(container);
11
+ });
12
+ it('should detect low contrast text violations', async ()=>{
13
+ const textElement = document.createElement('p');
14
+ textElement.textContent = 'This is test text';
15
+ textElement.style.color = 'rgb(170, 170, 170)';
16
+ textElement.style.backgroundColor = 'rgb(255, 255, 255)';
17
+ textElement.style.fontSize = '16px';
18
+ textElement.style.fontWeight = '400';
19
+ container.appendChild(textElement);
20
+ const result = await runAPCACheck(container);
21
+ expect(result.id).toBe('apca-contrast');
22
+ expect(result.nodes.length).toBeGreaterThan(0);
23
+ expect(result.nodes[0].failureSummary).toContain('Fix any of the following');
24
+ expect(result.nodes[0].failureSummary).toContain('APCA contrast');
25
+ });
26
+ it('should pass for high contrast text', async ()=>{
27
+ const textElement = document.createElement('p');
28
+ textElement.textContent = 'This is test text';
29
+ textElement.style.color = 'rgb(0, 0, 0)';
30
+ textElement.style.backgroundColor = 'rgb(255, 255, 255)';
31
+ textElement.style.fontSize = '16px';
32
+ textElement.style.fontWeight = '400';
33
+ container.appendChild(textElement);
34
+ const result = await runAPCACheck(container);
35
+ expect(result.id).toBe('apca-contrast');
36
+ expect(result.nodes.length).toBe(0);
37
+ });
38
+ it('should skip hidden elements', async ()=>{
39
+ const textElement = document.createElement('p');
40
+ textElement.textContent = 'This is test text';
41
+ textElement.style.color = 'rgb(170, 170, 170)';
42
+ textElement.style.backgroundColor = 'rgb(255, 255, 255)';
43
+ textElement.style.display = 'none';
44
+ container.appendChild(textElement);
45
+ const result = await runAPCACheck(container);
46
+ expect(result.nodes.length).toBe(0);
47
+ });
48
+ it('should skip aria-hidden elements', async ()=>{
49
+ const textElement = document.createElement('p');
50
+ textElement.textContent = 'This is test text';
51
+ textElement.style.color = 'rgb(170, 170, 170)';
52
+ textElement.style.backgroundColor = 'rgb(255, 255, 255)';
53
+ textElement.setAttribute('aria-hidden', 'true');
54
+ container.appendChild(textElement);
55
+ const result = await runAPCACheck(container);
56
+ expect(result.nodes.length).toBe(0);
57
+ });
58
+ it('should use appropriate thresholds for large text', async ()=>{
59
+ const textElement = document.createElement('h1');
60
+ textElement.textContent = 'Large Heading';
61
+ textElement.style.color = 'rgb(100, 100, 100)';
62
+ textElement.style.backgroundColor = 'rgb(255, 255, 255)';
63
+ textElement.style.fontSize = '32px';
64
+ textElement.style.fontWeight = '400';
65
+ container.appendChild(textElement);
66
+ const result = await runAPCACheck(container);
67
+ expect(result.id).toBe('apca-contrast');
68
+ });
69
+ it('should handle elements with inherited background colors', async ()=>{
70
+ const parent = document.createElement('div');
71
+ parent.style.backgroundColor = 'rgb(200, 200, 200)';
72
+ container.appendChild(parent);
73
+ const textElement = document.createElement('p');
74
+ textElement.textContent = 'Nested text';
75
+ textElement.style.color = 'rgb(180, 180, 180)';
76
+ textElement.style.fontSize = '16px';
77
+ parent.appendChild(textElement);
78
+ const result = await runAPCACheck(container);
79
+ expect(result.id).toBe('apca-contrast');
80
+ });
81
+ it('should enforce gold minimum font size for body text', async ()=>{
82
+ const textElement = document.createElement('p');
83
+ textElement.textContent = 'Small body text';
84
+ textElement.style.color = 'rgb(0, 0, 0)';
85
+ textElement.style.backgroundColor = 'rgb(255, 255, 255)';
86
+ textElement.style.fontSize = '12px';
87
+ textElement.style.fontWeight = '400';
88
+ container.appendChild(textElement);
89
+ const result = await runAPCACheck(container, {
90
+ level: 'gold',
91
+ useCase: 'body'
92
+ });
93
+ expect(result.nodes.length).toBeGreaterThan(0);
94
+ expect(result.nodes[0].failureSummary).toContain('minimum 16px');
95
+ });
96
+ it('should flag excessive contrast for large text at silver', async ()=>{
97
+ const textElement = document.createElement('h1');
98
+ textElement.textContent = 'Large heading';
99
+ textElement.style.color = 'rgb(0, 0, 0)';
100
+ textElement.style.backgroundColor = 'rgb(255, 255, 255)';
101
+ textElement.style.fontSize = '40px';
102
+ textElement.style.fontWeight = '400';
103
+ container.appendChild(textElement);
104
+ const result = await runAPCACheck(container, {
105
+ level: 'silver',
106
+ useCase: 'body'
107
+ });
108
+ expect(result.nodes.length).toBeGreaterThan(0);
109
+ expect(result.nodes[0].failureSummary).toContain('exceeds the maximum');
110
+ });
111
+ it('should include proper metadata in results', async ()=>{
112
+ const textElement = document.createElement('p');
113
+ textElement.textContent = 'Test';
114
+ textElement.style.color = 'rgb(170, 170, 170)';
115
+ textElement.style.backgroundColor = 'rgb(255, 255, 255)';
116
+ container.appendChild(textElement);
117
+ const result = await runAPCACheck(container);
118
+ expect(result.id).toBe('apca-contrast');
119
+ expect(result.tags).toContain('wcag3');
120
+ expect(result.tags).toContain('apca');
121
+ expect(result.description).toContain('APCA');
122
+ expect(result.helpUrl).toBeTruthy();
123
+ });
124
+ });
@@ -0,0 +1,4 @@
1
+ import { combinedRulesMap } from "./AccessibilityRuleMaps.js";
2
+ const getTitleForAxeResult = (axeResult)=>combinedRulesMap[axeResult.id]?.title || axeResult.id;
3
+ const getFriendlySummaryForAxeResult = (axeResult)=>combinedRulesMap[axeResult.id]?.friendlySummary || axeResult.description;
4
+ export { getFriendlySummaryForAxeResult, getTitleForAxeResult };
@@ -0,0 +1,140 @@
1
+ import react, { useMemo } from "react";
2
+ import { Badge, Button } from "storybook/internal/components";
3
+ import { SyncIcon } from "@storybook/icons";
4
+ import { styled } from "storybook/theming";
5
+ import { RuleType } from "../types.js";
6
+ import { useA11yContext } from "./A11yContext.js";
7
+ import { Report } from "./Report/Report.js";
8
+ import { Tabs } from "./Tabs.js";
9
+ import { TestDiscrepancyMessage } from "./TestDiscrepancyMessage.js";
10
+ const RotatingIcon = styled(SyncIcon)(({ theme })=>({
11
+ animation: `${theme.animation.rotate360} 1s linear infinite;`,
12
+ margin: 4
13
+ }));
14
+ const Tab = styled.div({
15
+ display: 'flex',
16
+ alignItems: 'center',
17
+ gap: 6
18
+ });
19
+ const Centered = styled.span(({ theme })=>({
20
+ display: 'flex',
21
+ flexDirection: 'column',
22
+ alignItems: 'center',
23
+ justifyContent: 'center',
24
+ textAlign: 'center',
25
+ fontSize: theme.typography.size.s2,
26
+ height: '100%',
27
+ gap: 24,
28
+ div: {
29
+ display: 'flex',
30
+ flexDirection: 'column',
31
+ alignItems: 'center',
32
+ gap: 8
33
+ },
34
+ p: {
35
+ margin: 0,
36
+ color: theme.textMutedColor
37
+ },
38
+ code: {
39
+ display: 'inline-block',
40
+ fontSize: theme.typography.size.s2 - 1,
41
+ backgroundColor: theme.background.app,
42
+ border: `1px solid ${theme.color.border}`,
43
+ borderRadius: 4,
44
+ padding: '2px 3px'
45
+ }
46
+ }));
47
+ const A11YPanel = ()=>{
48
+ const { parameters, tab, results, status, handleManual, error, discrepancy, handleSelectionChange, selectedItems, toggleOpen } = useA11yContext();
49
+ const tabs = useMemo(()=>{
50
+ const { passes, incomplete, violations } = results ?? {
51
+ passes: [],
52
+ incomplete: [],
53
+ violations: []
54
+ };
55
+ return [
56
+ {
57
+ label: /*#__PURE__*/ react.createElement(Tab, null, "Violations", /*#__PURE__*/ react.createElement(Badge, {
58
+ compact: true,
59
+ status: 'violations' === tab ? 'active' : 'neutral'
60
+ }, violations.length)),
61
+ panel: /*#__PURE__*/ react.createElement(Report, {
62
+ items: violations,
63
+ type: RuleType.VIOLATION,
64
+ empty: "No accessibility violations found.",
65
+ handleSelectionChange: handleSelectionChange,
66
+ selectedItems: selectedItems,
67
+ toggleOpen: toggleOpen
68
+ }),
69
+ items: violations,
70
+ type: RuleType.VIOLATION
71
+ },
72
+ {
73
+ label: /*#__PURE__*/ react.createElement(Tab, null, "Passes", /*#__PURE__*/ react.createElement(Badge, {
74
+ compact: true,
75
+ status: 'passes' === tab ? 'active' : 'neutral'
76
+ }, passes.length)),
77
+ panel: /*#__PURE__*/ react.createElement(Report, {
78
+ items: passes,
79
+ type: RuleType.PASS,
80
+ empty: "No passing accessibility checks found.",
81
+ handleSelectionChange: handleSelectionChange,
82
+ selectedItems: selectedItems,
83
+ toggleOpen: toggleOpen
84
+ }),
85
+ items: passes,
86
+ type: RuleType.PASS
87
+ },
88
+ {
89
+ label: /*#__PURE__*/ react.createElement(Tab, null, "Inconclusive", /*#__PURE__*/ react.createElement(Badge, {
90
+ compact: true,
91
+ status: 'incomplete' === tab ? 'active' : 'neutral'
92
+ }, incomplete.length)),
93
+ panel: /*#__PURE__*/ react.createElement(Report, {
94
+ items: incomplete,
95
+ type: RuleType.INCOMPLETION,
96
+ empty: "No inconclusive accessibility checks found.",
97
+ handleSelectionChange: handleSelectionChange,
98
+ selectedItems: selectedItems,
99
+ toggleOpen: toggleOpen
100
+ }),
101
+ items: incomplete,
102
+ type: RuleType.INCOMPLETION
103
+ }
104
+ ];
105
+ }, [
106
+ tab,
107
+ results,
108
+ handleSelectionChange,
109
+ selectedItems,
110
+ toggleOpen
111
+ ]);
112
+ if (parameters.disable || 'off' === parameters.test) return /*#__PURE__*/ react.createElement(Centered, null, /*#__PURE__*/ react.createElement("div", null, /*#__PURE__*/ react.createElement("strong", null, "Accessibility tests are disabled for this story"), /*#__PURE__*/ react.createElement("p", null, "Update", ' ', /*#__PURE__*/ react.createElement("code", null, parameters.disable ? 'parameters.a11y.disable' : 'parameters.a11y.test'), ' ', "to enable accessibility tests.")));
113
+ return /*#__PURE__*/ react.createElement(react.Fragment, null, discrepancy && /*#__PURE__*/ react.createElement(TestDiscrepancyMessage, {
114
+ discrepancy: discrepancy
115
+ }), 'ready' === status || 'ran' === status ? /*#__PURE__*/ react.createElement(Tabs, {
116
+ key: "tabs",
117
+ tabs: tabs
118
+ }) : /*#__PURE__*/ react.createElement(Centered, {
119
+ style: {
120
+ marginTop: discrepancy ? '1em' : 0
121
+ }
122
+ }, 'initial' === status && /*#__PURE__*/ react.createElement("div", null, /*#__PURE__*/ react.createElement(RotatingIcon, {
123
+ size: 12
124
+ }), /*#__PURE__*/ react.createElement("strong", null, "Preparing accessibility scan"), /*#__PURE__*/ react.createElement("p", null, "Please wait while the addon is initializing...")), 'manual' === status && /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement("div", null, /*#__PURE__*/ react.createElement("strong", null, "Accessibility tests run manually for this story"), /*#__PURE__*/ react.createElement("p", null, "Results will not show when using the testing module. You can still run accessibility tests manually.")), /*#__PURE__*/ react.createElement(Button, {
125
+ ariaLabel: false,
126
+ size: "medium",
127
+ onClick: handleManual
128
+ }, "Run accessibility scan"), /*#__PURE__*/ react.createElement("p", null, "Update ", /*#__PURE__*/ react.createElement("code", null, "globals.a11y.manual"), " to disable manual mode.")), 'running' === status && /*#__PURE__*/ react.createElement("div", null, /*#__PURE__*/ react.createElement(RotatingIcon, {
129
+ size: 12
130
+ }), /*#__PURE__*/ react.createElement("strong", null, "Accessibility scan in progress"), /*#__PURE__*/ react.createElement("p", null, "Please wait while the accessibility scan is running...")), 'error' === status && /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement("div", null, /*#__PURE__*/ react.createElement("strong", null, "The accessibility scan encountered an error"), /*#__PURE__*/ react.createElement("p", null, 'string' == typeof error ? error : error instanceof Error ? error.toString() : JSON.stringify(error, null, 2))), /*#__PURE__*/ react.createElement(Button, {
131
+ ariaLabel: false,
132
+ size: "medium",
133
+ onClick: handleManual
134
+ }, "Rerun accessibility scan")), 'component-test-error' === status && /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement("div", null, /*#__PURE__*/ react.createElement("strong", null, "This story's component tests failed"), /*#__PURE__*/ react.createElement("p", null, "Automated accessibility tests will not run until this is resolved. You can still test manually.")), /*#__PURE__*/ react.createElement(Button, {
135
+ ariaLabel: false,
136
+ size: "medium",
137
+ onClick: handleManual
138
+ }, "Run accessibility scan"))));
139
+ };
140
+ export { A11YPanel };