Skip to content
1 change: 1 addition & 0 deletions code/addons/a11y/src/components/A11YPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @vitest-environment happy-dom
/// <reference types="@testing-library/jest-dom" />
import { fireEvent, render } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

Expand Down
57 changes: 0 additions & 57 deletions code/addons/a11y/src/components/ColorFilters.tsx

This file was deleted.

57 changes: 57 additions & 0 deletions code/addons/a11y/src/components/VisionSimulator.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { PlayFunction, PlayFunctionContext } from 'storybook/internal/types';

import { ManagerContext } from 'storybook/manager-api';
import { expect, fn, screen } from 'storybook/test';

import preview from '../../../../.storybook/preview';
import { VisionSimulator } from './VisionSimulator';

const managerContext: any = {
state: {},
api: {
getGlobals: fn(() => ({ vision: undefined })),
updateGlobals: fn(),
getStoryGlobals: fn(() => ({ vision: undefined })),
getUserGlobals: fn(() => ({ vision: undefined })),
},
};

const meta = preview.meta({
title: 'Vision Simulator',
component: VisionSimulator,
decorators: [
(Story: any) => (
<ManagerContext.Provider value={managerContext}>
<Story />
</ManagerContext.Provider>
),
],
});

export default meta;

const openMenu: PlayFunction = async ({ canvas, userEvent }) => {
await userEvent.click(canvas.getByRole('button', { name: 'Vision simulator' }));
};

export const Default = meta.story({
play: openMenu,
});

export const WithFilter = meta.story({
play: openMenu,
globals: {
vision: 'achromatopsia',
},
});

export const Selection = meta.story({
play: async (context) => {
await openMenu(context);
await context.userEvent.click(await screen.findByText('Blurred vision'));
await expect(managerContext.api.updateGlobals).toHaveBeenCalledWith({ vision: 'blurred' });
await expect(
context.canvas.getByRole('button', { name: 'Vision simulator Blurred vision' })
).toBeVisible();
},
});
80 changes: 0 additions & 80 deletions code/addons/a11y/src/components/VisionSimulator.test.tsx

This file was deleted.

81 changes: 18 additions & 63 deletions code/addons/a11y/src/components/VisionSimulator.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,14 @@
import React, { useState } from 'react';
import React from 'react';

import { Select } from 'storybook/internal/components';

import { AccessibilityIcon } from '@storybook/icons';

import { Global, styled } from 'storybook/theming';
import { useGlobals } from 'storybook/manager-api';
import { styled } from 'storybook/theming';

import { Filters } from './ColorFilters';

const iframeId = 'storybook-preview-iframe';

interface Option {
name: string;
percentage?: number;
}

export const baseList = [
{ name: 'blurred vision', percentage: 22.9 },
{ name: 'deuteranomaly', percentage: 2.7 },
{ name: 'deuteranopia', percentage: 0.56 },
{ name: 'protanomaly', percentage: 0.66 },
{ name: 'protanopia', percentage: 0.59 },
{ name: 'tritanomaly', percentage: 0.01 },
{ name: 'tritanopia', percentage: 0.016 },
{ name: 'achromatopsia', percentage: 0.0001 },
{ name: 'grayscale' },
] as Option[];

type Filter = Option | null;

const getFilter = (filterName: string) => {
if (!filterName) {
return 'none';
}
if (filterName === 'blurred vision') {
return 'blur(2px)';
}
if (filterName === 'grayscale') {
return 'grayscale(100%)';
}
return `url('#${filterName}')`;
};
import { VISION_GLOBAL_KEY } from '../constants';
import { filterDefs, filters } from '../visionSimulatorFilters';

const Hidden = styled.div({
'&, & svg': {
Expand All @@ -59,49 +27,36 @@ const ColorIcon = styled.span<{ $filter: string }>(
width: '1rem',
},
({ $filter }) => ({
filter: getFilter($filter),
filter: filters[$filter as keyof typeof filters].filter || 'none',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Verify the filter key exists before accessing.

The code assumes the filter key exists in the filters object, but if $filter is invalid, this could throw at runtime.

🔎 Proposed defensive fix
  ({ $filter }) => ({
-    filter: filters[$filter as keyof typeof filters].filter || 'none',
+    filter: filters[$filter as keyof typeof filters]?.filter || 'none',
  }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
filter: filters[$filter as keyof typeof filters].filter || 'none',
({ $filter }) => ({
filter: filters[$filter as keyof typeof filters]?.filter || 'none',
}),
🤖 Prompt for AI Agents
In code/addons/a11y/src/components/VisionSimulator.tsx around line 35, the
lookup filters[$filter as keyof typeof filters].filter assumes $filter is a
valid key and may throw if it's invalid; first check that $filter exists in the
filters object (e.g. use "if ($filter in filters)" or
Object.prototype.hasOwnProperty.call(filters, $filter)) and only access .filter
when present, otherwise fall back to 'none' (or use optional chaining like
filters[$filter as keyof typeof filters]?.filter ?? 'none'); ensure the check is
typed so TypeScript accepts the access and keep the 'none' fallback.

}),
({ theme }) => ({
boxShadow: `${theme.appBorderColor} 0 0 0 1px inset`,
})
);

export const VisionSimulator = () => {
const [filter, setFilter] = useState<Filter>(null);
const [globals, updateGlobals] = useGlobals();
const value = globals[VISION_GLOBAL_KEY];

const options = baseList.map(({ name, percentage }) => {
const description = percentage !== undefined ? `${percentage}% of users` : undefined;
return {
title: name,
description,
icon: <ColorIcon $filter={name} />,
value: name,
};
});
const options = Object.entries(filters).map(([key, { label, percentage }]) => ({
title: label,
description: percentage ? `${percentage}% of users` : undefined,
icon: <ColorIcon $filter={key} />,
value: key,
}));

return (
<>
{filter && (
<Global
styles={{
[`#${iframeId}`]: {
filter: getFilter(filter.name),
},
}}
/>
)}
<Select
resetLabel="Reset color filter"
onReset={() => setFilter(null)}
onReset={() => updateGlobals({ [VISION_GLOBAL_KEY]: undefined })}
icon={<AccessibilityIcon />}
ariaLabel="Vision simulator"
defaultOptions={filter?.name}
defaultOptions={value}
options={options}
onSelect={(selected) => setFilter(() => ({ name: selected }))}
onSelect={(selected) => updateGlobals({ [VISION_GLOBAL_KEY]: selected })}
/>
<Hidden>
<Filters />
</Hidden>
<Hidden dangerouslySetInnerHTML={{ __html: filterDefs }} />
</>
);
};
1 change: 1 addition & 0 deletions code/addons/a11y/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const ADDON_ID = 'storybook/a11y';
export const PANEL_ID = `${ADDON_ID}/panel`;
export const PARAM_KEY = `a11y`;
export const VISION_GLOBAL_KEY = `vision`;
export const UI_STATE_ID = `${ADDON_ID}/ui`;
const RESULT = `${ADDON_ID}/result`;
const REQUEST = `${ADDON_ID}/request`;
Expand Down
4 changes: 4 additions & 0 deletions code/addons/a11y/src/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { expect } from 'storybook/test';
import { run } from './a11yRunner';
import type { A11yParameters } from './params';
import { getIsVitestStandaloneRun } from './utils';
import { withVisionSimulator } from './withVisionSimulator';

let vitestMatchersExtended = false;

export const decorators = [withVisionSimulator];

export const afterEach: AfterEach<any> = async ({
id: storyId,
reporting,
Expand Down Expand Up @@ -94,6 +97,7 @@ export const initialGlobals = {
a11y: {
manual: false,
},
vision: undefined,
};

export const parameters = {
Expand Down
Loading
Loading