diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md
index 13aba8e45642..825656ad4a91 100644
--- a/CHANGELOG.prerelease.md
+++ b/CHANGELOG.prerelease.md
@@ -1,3 +1,8 @@
+## 10.2.0-alpha.13
+
+- Core: Fix onboarding visual bugs, survey telemetry and modal dismissal - [#33326](https://github.com/storybookjs/storybook/pull/33326), thanks @ghengeveld!
+- Core: Track vision simulator state through globals and apply styles in preview - [#33418](https://github.com/storybookjs/storybook/pull/33418), thanks @ghengeveld!
+
## 10.2.0-alpha.12
- Addon-docs: Add MDX manifest generation - [#33408](https://github.com/storybookjs/storybook/pull/33408), thanks @copilot-swe-agent!
diff --git a/code/addons/a11y/src/components/A11YPanel.test.tsx b/code/addons/a11y/src/components/A11YPanel.test.tsx
index b39be8e1712a..9352981fc613 100644
--- a/code/addons/a11y/src/components/A11YPanel.test.tsx
+++ b/code/addons/a11y/src/components/A11YPanel.test.tsx
@@ -1,4 +1,5 @@
// @vitest-environment happy-dom
+///
import { fireEvent, render } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
diff --git a/code/addons/a11y/src/components/ColorFilters.tsx b/code/addons/a11y/src/components/ColorFilters.tsx
deleted file mode 100644
index 24c82e9f95cb..000000000000
--- a/code/addons/a11y/src/components/ColorFilters.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import * as React from 'react';
-
-export const Filters: React.FC> = (props) => (
-
-);
diff --git a/code/addons/a11y/src/components/VisionSimulator.stories.tsx b/code/addons/a11y/src/components/VisionSimulator.stories.tsx
new file mode 100644
index 000000000000..ccbf09fbe583
--- /dev/null
+++ b/code/addons/a11y/src/components/VisionSimulator.stories.tsx
@@ -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) => (
+
+
+
+ ),
+ ],
+});
+
+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();
+ },
+});
diff --git a/code/addons/a11y/src/components/VisionSimulator.test.tsx b/code/addons/a11y/src/components/VisionSimulator.test.tsx
deleted file mode 100644
index 7b0ef88c9290..000000000000
--- a/code/addons/a11y/src/components/VisionSimulator.test.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-// @vitest-environment happy-dom
-/// ;
-import { fireEvent, render, screen, waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { describe, expect, it } from 'vitest';
-
-import React from 'react';
-
-import { ThemeProvider, convert, themes } from 'storybook/theming';
-
-import { VisionSimulator, baseList } from './VisionSimulator';
-
-const getOptionByNameAndPercentage = (option: string, percentage?: number) =>
- screen.getByText(
- (content, element) =>
- content !== '' &&
- // @ts-expect-error (TODO)
- element.textContent === option &&
- // @ts-expect-error (TODO)
- (percentage === undefined || element.nextSibling.textContent === `${percentage}% of users`)
- );
-
-function ThemedVisionSimulator() {
- return (
-
-
-
- );
-}
-
-describe('Vision Simulator', () => {
- // TODO: there are issues with the ThemeProvider from lib/theming for some reason
- // which are causing rendering issues in the component for all these tests
- it.skip('should render tool button', async () => {
- // when
- render();
-
- // then
- // waitFor because WithTooltip is a lazy component
- await waitFor(() => expect(screen.getByTitle('Vision simulator')).toBeInTheDocument());
- });
-
- it.skip('should display tooltip on click', async () => {
- // given
- render();
- await waitFor(() => expect(screen.getByTitle('Vision simulator')).toBeInTheDocument());
-
- // when
- userEvent.click(screen.getByRole('button', { name: 'Vision simulator' }));
-
- // then
- await waitFor(() => expect(screen.getByText('blurred vision')).toBeInTheDocument());
- baseList.forEach(({ name, percentage }) =>
- expect(getOptionByNameAndPercentage(name, percentage)).toBeInTheDocument()
- );
- });
-
- it.skip('should set filter', async () => {
- // given
- render();
- await waitFor(() => expect(screen.getByTitle('Vision simulator')).toBeInTheDocument());
- userEvent.click(screen.getByRole('button', { name: 'Vision simulator' }));
- await waitFor(() => expect(screen.getByText('blurred vision')).toBeInTheDocument());
-
- // when
- fireEvent.click(screen.getByText('blurred vision'));
-
- // then
- const rule = Object.values(document.styleSheets)
- .filter(({ cssRules }) => cssRules)
- .map(({ cssRules }) => Object.values(cssRules))
- .flat()
- // @ts-expect-error (TODO)
- .find((cssRule: CSSRule) => cssRule.selectorText === '#storybook-preview-iframe');
-
- expect(rule).toBeDefined();
- // @ts-expect-error (TODO)
- expect(rule.style.filter).toBe('blur(2px)');
- });
-});
diff --git a/code/addons/a11y/src/components/VisionSimulator.tsx b/code/addons/a11y/src/components/VisionSimulator.tsx
index c029abe20332..4783a556ed57 100644
--- a/code/addons/a11y/src/components/VisionSimulator.tsx
+++ b/code/addons/a11y/src/components/VisionSimulator.tsx
@@ -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': {
@@ -59,7 +27,7 @@ const ColorIcon = styled.span<{ $filter: string }>(
width: '1rem',
},
({ $filter }) => ({
- filter: getFilter($filter),
+ filter: filters[$filter as keyof typeof filters].filter || 'none',
}),
({ theme }) => ({
boxShadow: `${theme.appBorderColor} 0 0 0 1px inset`,
@@ -67,41 +35,28 @@ const ColorIcon = styled.span<{ $filter: string }>(
);
export const VisionSimulator = () => {
- const [filter, setFilter] = useState(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: ,
- value: name,
- };
- });
+ const options = Object.entries(filters).map(([key, { label, percentage }]) => ({
+ title: label,
+ description: percentage ? `${percentage}% of users` : undefined,
+ icon: ,
+ value: key,
+ }));
return (
<>
- {filter && (
-
- )}