Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d6f94a2
mark deprecations of yarn pnp
ndelangen Oct 6, 2025
51c0518
add runtime check warning users that pnp support is going away in fut…
ndelangen Oct 6, 2025
8158942
Merge branch 'next' into norbert/mark-pnp-deprecated
ndelangen Oct 13, 2025
9ec584f
fix(a11y): persist tab/highlight across docs navigation (#32634)
404Dealer Oct 17, 2025
a80203c
Merge branch 'next' into norbert/mark-pnp-deprecated
ndelangen Oct 20, 2025
3cd94e5
Merge branch 'next' into fix/a11y-persist-panel-state
404Dealer Oct 20, 2025
604d8a8
Merge branch 'next' into fix/a11y-persist-panel-state
404Dealer Oct 22, 2025
f5839ef
Merge branch 'next' into fix/a11y-persist-panel-state
404Dealer Oct 24, 2025
e055907
Merge branch 'next' into norbert/mark-pnp-deprecated
ndelangen Oct 26, 2025
92e078b
Merge branch 'next' into fix/a11y-persist-panel-state
404Dealer Oct 26, 2025
4ef6304
Fix: Don't add triple slash reference to vitest.config files
Copilot Oct 23, 2025
7ad957f
Fix vitest addon to extract coverage config to top-level test object
Copilot Oct 23, 2025
aba3791
Merge pull request #32645 from storybookjs/norbert/mark-pnp-deprecated
ndelangen Oct 27, 2025
65a60f3
Merge branch 'next' into fix/a11y-persist-panel-state
ndelangen Oct 27, 2025
b1be709
Core: Enhance warning for Testing Library's `screen` usage in docs mode
yannbf Oct 27, 2025
6afa95b
refactor
ndelangen Oct 27, 2025
53b41db
improve defaults
ndelangen Oct 27, 2025
4147d30
fix test
ndelangen Oct 27, 2025
676611a
Refactor A11yContextProvider to streamline state updates and improve …
ndelangen Oct 27, 2025
9e5690b
Update A11yContextProvider to use nullish coalescing for violation co…
ndelangen Oct 27, 2025
83f36d8
Update A11yContextProvider comment to clarify dependency exclusion in…
ndelangen Oct 27, 2025
0384713
support non defineConfig calls in mergeConfig
yannbf Oct 27, 2025
a66d817
improvements
yannbf Oct 27, 2025
7ac43e3
Merge pull request #32762 from 404Dealer/fix/a11y-persist-panel-state
ndelangen Oct 27, 2025
a0e584f
Merge pull request #32844 from storybookjs/yann/vitest-addon-setup-fixes
yannbf Oct 27, 2025
f40907a
fix url checking
yannbf Oct 27, 2025
74e0037
Merge pull request #32851 from storybookjs/yann/scope-tl-screen-warning
yannbf Oct 27, 2025
f034eb9
Write changelog for 10.0.0-rc.3 [skip ci]
storybook-bot Oct 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.prerelease.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 10.0.0-rc.3

- A11y: Persist tab/highlight across docs navigation - [#32762](https://github.com/storybookjs/storybook/pull/32762), thanks @404Dealer!
- Addon Vitest: Fix incorrect file modifications during setup - [#32844](https://github.com/storybookjs/storybook/pull/32844), thanks @yannbf!
- Core: Enhance warning for Testing Library's `screen` usage in docs mode - [#32851](https://github.com/storybookjs/storybook/pull/32851), thanks @yannbf!
- Core: Mark pnp support as deprecated - [#32645](https://github.com/storybookjs/storybook/pull/32645), thanks @ndelangen!

## 10.0.0-rc.2

- CLI: Fix Nextjs project creation in empty directories - [#32828](https://github.com/storybookjs/storybook/pull/32828), thanks @yannbf!
Expand Down
3 changes: 2 additions & 1 deletion code/addons/a11y/src/components/A11yContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
import type { AxeResults } from 'axe-core';
import * as api from 'storybook/manager-api';

import { EVENTS } from '../constants';
import { EVENTS, UI_STATE_ID } from '../constants';
import { RuleType } from '../types';
import { A11yContextProvider, useA11yContext } from './A11yContext';

vi.mock('storybook/manager-api');
Expand Down
173 changes: 105 additions & 68 deletions code/addons/a11y/src/components/A11yContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { convert, themes } from 'storybook/theming';
import { getFriendlySummaryForAxeResult, getTitleForAxeResult } from '../axeRuleMappingHelper';
import { ADDON_ID, EVENTS, STATUS_TYPE_ID_A11Y, STATUS_TYPE_ID_COMPONENT_TEST } from '../constants';
import type { A11yParameters } from '../params';
import type { A11YReport, EnhancedResult, EnhancedResults } from '../types';
import type { A11YReport, EnhancedResult, EnhancedResults, Status } from '../types';
import { RuleType } from '../types';
import type { TestDiscrepancy } from './TestDiscrepancyMessage';

Expand Down Expand Up @@ -86,8 +86,6 @@ export const A11yContext = createContext<A11yContextStore>({
handleSelectionChange: () => {},
});

type Status = 'initial' | 'manual' | 'running' | 'error' | 'component-test-error' | 'ran' | 'ready';

export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
const parameters = useParameter<A11yParameters>('a11y', {});

Expand All @@ -106,16 +104,22 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
return value;
}, [api]);

const [results, setResults] = useAddonState<EnhancedResults | undefined>(ADDON_ID);
const [tab, setTab] = useState(() => {
const [type] = a11ySelection?.split('.') ?? [];
return type && Object.values(RuleType).includes(type as RuleType)
? (type as RuleType)
: RuleType.VIOLATION;
const [state, setState] = useAddonState<{
ui: { highlighted: boolean; tab: RuleType };
results: EnhancedResults | undefined;
error: unknown;
status: Status;
}>(ADDON_ID, {
ui: {
highlighted: false,
tab: RuleType.VIOLATION,
},
results: undefined,
error: undefined,
status: getInitialStatus(manual),
});
const [error, setError] = useState<unknown>(undefined);
const [status, setStatus] = useState<Status>(getInitialStatus(manual));
const [highlighted, setHighlighted] = useState(!!a11ySelection);

const { ui, results, error, status } = state;

const { storyId } = useStorybookState();
const currentStoryA11yStatusValue = experimental_useStatusStore(
Expand All @@ -128,17 +132,16 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
const current = statuses[storyId]?.[STATUS_TYPE_ID_COMPONENT_TEST];
const previous = previousStatuses[storyId]?.[STATUS_TYPE_ID_COMPONENT_TEST];
if (current?.value === 'status-value:error' && previous?.value !== 'status-value:error') {
setStatus('component-test-error');
setState((prev) => ({ ...prev, status: 'component-test-error' }));
}
}
);
return unsubscribe;
}, [storyId]);
}, [setState, storyId]);

const handleToggleHighlight = useCallback(
() => setHighlighted((prevHighlighted) => !prevHighlighted),
[]
);
const handleToggleHighlight = useCallback(() => {
setState((prev) => ({ ...prev, ui: { ...prev.ui, highlighted: !prev.ui.highlighted } }));
}, [setState]);

const [selectedItems, setSelectedItems] = useState<Map<string, string>>(() => {
const initialValue = new Map();
Expand All @@ -153,9 +156,9 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {

// All items are expanded if something is selected from each result for the current tab
const allExpanded = useMemo(() => {
const currentResults = results?.[tab];
return currentResults?.every((result) => selectedItems.has(`${tab}.${result.id}`)) ?? false;
}, [results, selectedItems, tab]);
const currentResults = results?.[ui.tab];
return currentResults?.every((result) => selectedItems.has(`${ui.tab}.${result.id}`)) ?? false;
}, [results, selectedItems, ui.tab]);

const toggleOpen = useCallback(
(event: React.SyntheticEvent<Element>, type: RuleType, item: EnhancedResult) => {
Expand All @@ -174,42 +177,49 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
setSelectedItems(
(prev) =>
new Map(
results?.[tab]?.map((result) => {
const key = `${tab}.${result.id}`;
results?.[ui.tab]?.map((result) => {
const key = `${ui.tab}.${result.id}`;
return [key, prev.get(key) ?? `${key}.1`];
}) ?? []
)
);
}, [results, tab]);
}, [results, ui.tab]);

const handleSelectionChange = useCallback((key: string) => {
const [type, id] = key.split('.');
setSelectedItems((prev) => new Map(prev.set(`${type}.${id}`, key)));
}, []);

const handleError = useCallback((err: unknown) => {
setStatus('error');
setError(err);
}, []);
const handleError = useCallback(
(err: unknown) => {
setState((prev) => ({ ...prev, status: 'error', error: err }));
},
[setState]
);

const handleResult = useCallback(
(axeResults: EnhancedResults, id: string) => {
if (storyId === id) {
setStatus('ran');
setResults(axeResults);
setState((prev) => ({ ...prev, status: 'ran', results: axeResults }));

setTimeout(() => {
if (status === 'ran') {
setStatus('ready');
}
if (selectedItems.size === 1) {
const [key] = selectedItems.values();
document.getElementById(key)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
setState((prev) => {
if (prev.status === 'ran') {
return { ...prev, status: 'ready' };
}
return prev;
});
setSelectedItems((prev) => {
if (prev.size === 1) {
const [key] = prev.values();
document.getElementById(key)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
return prev;
});
}, 900);
}
},
[setResults, status, storyId, selectedItems]
[storyId, setState, setSelectedItems]
);

const handleSelect = useCallback(
Expand Down Expand Up @@ -250,14 +260,16 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
const handleReset = useCallback(
({ newPhase }: { newPhase: string }) => {
if (newPhase === 'loading') {
setResults(undefined);
setStatus(manual ? 'manual' : 'initial');
}
if (newPhase === 'afterEach' && !manual) {
setStatus('running');
setState((prev) => ({
...prev,
results: undefined,
status: manual ? 'manual' : 'initial',
}));
} else if (newPhase === 'afterEach' && !manual) {
setState((prev) => ({ ...prev, status: 'running' }));
}
},
[manual, setResults]
[manual, setState]
);

const emit = useChannel(
Expand All @@ -269,17 +281,17 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
[STORY_RENDER_PHASE_CHANGED]: handleReset,
[STORY_FINISHED]: handleReport,
[STORY_HOT_UPDATED]: () => {
setStatus('running');
setState((prev) => ({ ...prev, status: 'running' }));
emit(EVENTS.MANUAL, storyId, parameters);
},
},
[handleReset, handleReport, handleSelect, handleError, handleResult, parameters, storyId]
);

const handleManual = useCallback(() => {
setStatus('running');
setState((prev) => ({ ...prev, status: 'running' }));
emit(EVENTS.MANUAL, storyId, parameters);
}, [emit, parameters, storyId]);
}, [emit, parameters, setState, storyId]);

const handleCopyLink = useCallback(async (linkPath: string) => {
const { createCopyToClipboardFunction } = await import('storybook/internal/components');
Expand All @@ -292,22 +304,41 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
);

useEffect(() => {
setStatus(getInitialStatus(manual));
}, [getInitialStatus, manual]);
setState((prev) => ({ ...prev, status: getInitialStatus(manual) }));
}, [getInitialStatus, manual, setState]);

const isInitial = status === 'initial';

// If a deep link is provided, prefer it once on mount and persist UI state accordingly
useEffect(() => {
if (!a11ySelection) {
return;
}
setState((prev) => {
const update = { ...prev.ui, highlighted: true };

const [type] = a11ySelection.split('.') ?? [];
if (type && Object.values(RuleType).includes(type as RuleType)) {
update.tab = type as RuleType;
}
return { ...prev, ui: update };
});

// We intentionally do not include setState in deps to avoid loops
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [a11ySelection]);

useEffect(() => {
emit(REMOVE_HIGHLIGHT, `${ADDON_ID}/selected`);
emit(REMOVE_HIGHLIGHT, `${ADDON_ID}/others`);

if (!highlighted || isInitial) {
if (!ui.highlighted || isInitial) {
return;
}

const selected = Array.from(selectedItems.values()).flatMap((key) => {
const [type, id, number] = key.split('.');
if (type !== tab) {
if (type !== ui.tab) {
return [];
}
const result = results?.[type as RuleType]?.find((r) => r.id === id);
Expand All @@ -320,7 +351,7 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
priority: 1,
selectors: selected,
styles: {
outline: `1px solid color-mix(in srgb, ${colorsByType[tab]}, transparent 30%)`,
outline: `1px solid color-mix(in srgb, ${colorsByType[ui.tab]}, transparent 30%)`,
backgroundColor: 'transparent',
},
hoverStyles: {
Expand All @@ -329,20 +360,20 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
focusStyles: {
backgroundColor: 'transparent',
},
menu: results?.[tab as RuleType].map<HighlightMenuItem[]>((result) => {
menu: results?.[ui.tab as RuleType].map<HighlightMenuItem[]>((result) => {
const selectors = result.nodes
.flatMap((n) => n.target)
.map(String)
.filter((e) => selected.includes(e));
return [
{
id: `${tab}.${result.id}:info`,
id: `${ui.tab}.${result.id}:info`,
title: getTitleForAxeResult(result),
description: getFriendlySummaryForAxeResult(result),
selectors,
},
{
id: `${tab}.${result.id}`,
id: `${ui.tab}.${result.id}`,
iconLeft: 'info',
iconRight: 'shareAlt',
title: 'Learn how to resolve this violation',
Expand All @@ -354,37 +385,37 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
});
}

const others = results?.[tab as RuleType]
const others = results?.[ui.tab as RuleType]
.flatMap((r) => r.nodes.flatMap((n) => n.target).map(String))
.filter((e) => ![...unhighlightedSelectors, ...selected].includes(e));
if (others?.length) {
emit(HIGHLIGHT, {
id: `${ADDON_ID}/others`,
selectors: others,
styles: {
outline: `1px solid color-mix(in srgb, ${colorsByType[tab]}, transparent 30%)`,
backgroundColor: `color-mix(in srgb, ${colorsByType[tab]}, transparent 60%)`,
outline: `1px solid color-mix(in srgb, ${colorsByType[ui.tab]}, transparent 30%)`,
backgroundColor: `color-mix(in srgb, ${colorsByType[ui.tab]}, transparent 60%)`,
},
hoverStyles: {
outlineWidth: '2px',
},
focusStyles: {
backgroundColor: 'transparent',
},
menu: results?.[tab as RuleType].map<HighlightMenuItem[]>((result) => {
menu: results?.[ui.tab as RuleType].map<HighlightMenuItem[]>((result) => {
const selectors = result.nodes
.flatMap((n) => n.target)
.map(String)
.filter((e) => !selected.includes(e));
return [
{
id: `${tab}.${result.id}:info`,
id: `${ui.tab}.${result.id}:info`,
title: getTitleForAxeResult(result),
description: getFriendlySummaryForAxeResult(result),
selectors,
},
{
id: `${tab}.${result.id}`,
id: `${ui.tab}.${result.id}`,
iconLeft: 'info',
iconRight: 'shareAlt',
title: 'Learn how to resolve this violation',
Expand All @@ -395,7 +426,7 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
}),
});
}
}, [isInitial, emit, highlighted, results, tab, selectedItems]);
}, [isInitial, emit, ui.highlighted, results, ui.tab, selectedItems]);

const discrepancy: TestDiscrepancy = useMemo(() => {
if (!currentStoryA11yStatusValue) {
Expand All @@ -422,14 +453,20 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
value={{
parameters,
results,
highlighted,
highlighted: ui.highlighted,
toggleHighlight: handleToggleHighlight,
tab,
setTab,
tab: ui.tab,
setTab: useCallback(
(type: RuleType) => setState((prev) => ({ ...prev, ui: { ...prev.ui, tab: type } })),
[setState]
),
handleCopyLink,
status,
setStatus,
error,
status: status,
setStatus: useCallback(
(status: Status) => setState((prev) => ({ ...prev, status })),
[setState]
),
error: error,
handleManual,
discrepancy,
selectedItems,
Expand Down
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 UI_STATE_ID = `${ADDON_ID}/ui`;
const RESULT = `${ADDON_ID}/result`;
const REQUEST = `${ADDON_ID}/request`;
const RUNNING = `${ADDON_ID}/running`;
Expand Down
Loading