Skip to content

Commit 1165845

Browse files
authored
Merge pull request #26654 from storybookjs/tom/23347-story-globals
CSF: Allow overridding globals at the story level
2 parents d419bc6 + 49d0155 commit 1165845

File tree

145 files changed

+2756
-1185
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

145 files changed

+2756
-1185
lines changed

.git-blame-ignore-revs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
34e364a0ca1d93555d36a7367d78e8e229493de8
22
c0896915fb7fb9a8dd416b9aebca17abd909d1c1
33
a41c227037e7e7249b8b376f838f4f8bcc3e3e59
4-
13c46e6c0b7f3dd8cf4ba42d1cfd6714f4777d54
4+
13c46e6c0b7f3dd8cf4ba42d1cfd6714f4777d54
5+
0a4522a3f84773f39daec4820c49b8a92e9f9d11

code/.storybook/main.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ const config: StorybookConfig = {
6868
directory: '../addons/toolbars/template/stories',
6969
titlePrefix: 'addons/toolbars',
7070
},
71+
{
72+
directory: '../addons/themes/template/stories',
73+
titlePrefix: 'addons/themes',
74+
},
7175
{
7276
directory: '../addons/onboarding/src',
7377
titlePrefix: 'addons/onboarding',
@@ -83,6 +87,7 @@ const config: StorybookConfig = {
8387
],
8488
addons: [
8589
'@storybook/addon-links',
90+
'@storybook/addon-themes',
8691
'@storybook/addon-essentials',
8792
'@storybook/addon-interactions',
8893
'@storybook/addon-storysource',
@@ -119,7 +124,6 @@ const config: StorybookConfig = {
119124
},
120125
features: {
121126
viewportStoryGlobals: true,
122-
themesStoryGlobals: true,
123127
backgroundsStoryGlobals: true,
124128
},
125129
viteFinal: (viteConfig, { configType }) =>

code/.storybook/preview.tsx

+80-24
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@ import {
1212
} from 'storybook/internal/theming';
1313
import { useArgs, DocsContext as DocsContextProps } from 'storybook/internal/preview-api';
1414
import type { PreviewWeb } from 'storybook/internal/preview-api';
15-
import type { ReactRenderer } from '@storybook/react';
15+
import type { ReactRenderer, Decorator } from '@storybook/react';
1616
import type { Channel } from 'storybook/internal/channels';
1717

1818
import { DocsContext } from '@storybook/blocks';
19+
import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport';
1920

2021
import { DocsPageWrapper } from '../lib/blocks/src/components';
2122

2223
const { document } = global;
2324

24-
const ThemeBlock = styled.div<{ side: 'left' | 'right' }>(
25+
const ThemeBlock = styled.div<{ side: 'left' | 'right'; layout: string }>(
2526
{
2627
position: 'absolute',
2728
top: 0,
@@ -31,8 +32,10 @@ const ThemeBlock = styled.div<{ side: 'left' | 'right' }>(
3132
height: '100vh',
3233
bottom: 0,
3334
overflow: 'auto',
34-
padding: 10,
3535
},
36+
({ layout }) => ({
37+
padding: layout === 'fullscreen' ? 0 : '1rem',
38+
}),
3639
({ theme }) => ({
3740
background: theme.background.content,
3841
color: theme.color.defaultText,
@@ -49,14 +52,17 @@ const ThemeBlock = styled.div<{ side: 'left' | 'right' }>(
4952
}
5053
);
5154

52-
const ThemeStack = styled.div(
55+
const ThemeStack = styled.div<{ layout: string }>(
5356
{
5457
position: 'relative',
55-
minHeight: 'calc(50vh - 15px)',
58+
flex: 1,
5659
},
5760
({ theme }) => ({
5861
background: theme.background.content,
5962
color: theme.color.defaultText,
63+
}),
64+
({ layout }) => ({
65+
padding: layout === 'fullscreen' ? 0 : '1rem',
6066
})
6167
);
6268

@@ -80,6 +86,25 @@ const PlayFnNotice = styled.div(
8086
})
8187
);
8288

89+
const StackContainer = ({ children, layout }) => (
90+
<div
91+
style={{
92+
height: '100%',
93+
display: 'flex',
94+
flexDirection: 'column',
95+
// margin: layout === 'fullscreen' ? 0 : '-1rem',
96+
}}
97+
>
98+
<style dangerouslySetInnerHTML={{ __html: 'html, body, #storybook-root { height: 100%; }' }} />
99+
{layout === 'fullscreen' ? null : (
100+
<style
101+
dangerouslySetInnerHTML={{ __html: 'html, body { padding: 0!important; margin: 0; }' }}
102+
/>
103+
)}
104+
{children}
105+
</div>
106+
);
107+
83108
const ThemedSetRoot = () => {
84109
const theme = useTheme();
85110

@@ -159,10 +184,20 @@ export const decorators = [
159184
/**
160185
* This decorator renders the stories side-by-side, stacked or default based on the theme switcher in the toolbar
161186
*/
162-
(StoryFn, { globals, parameters, playFunction, args }) => {
163-
const defaultTheme =
164-
isChromatic() && !playFunction && args.autoplay !== true ? 'stacked' : 'light';
165-
const theme = globals.theme || parameters.theme || defaultTheme;
187+
(StoryFn, { globals, playFunction, args, storyGlobals, parameters }) => {
188+
let theme = globals.sb_theme;
189+
let showPlayFnNotice = false;
190+
191+
// this makes the decorator be out of 'phase' with the actually selected theme in the toolbar
192+
// but this is acceptable, I guess
193+
// we need to ensure only a single rendering in chromatic
194+
// a more 'correct' approach would be to set a specific theme global on every story that has a playFunction
195+
if (playFunction && args.autoplay !== false && !(theme === 'light' || theme === 'dark')) {
196+
theme = 'light';
197+
showPlayFnNotice = true;
198+
} else if (isChromatic() && !storyGlobals.sb_theme && !playFunction) {
199+
theme = 'stacked';
200+
}
166201

167202
switch (theme) {
168203
case 'side-by-side': {
@@ -172,12 +207,12 @@ export const decorators = [
172207
<Global styles={createReset} />
173208
</ThemeProvider>
174209
<ThemeProvider theme={convert(themes.light)}>
175-
<ThemeBlock side="left" data-side="left">
210+
<ThemeBlock side="left" data-side="left" layout={parameters.layout}>
176211
<StoryFn />
177212
</ThemeBlock>
178213
</ThemeProvider>
179214
<ThemeProvider theme={convert(themes.dark)}>
180-
<ThemeBlock side="right" data-side="right">
215+
<ThemeBlock side="right" data-side="right" layout={parameters.layout}>
181216
<StoryFn />
182217
</ThemeBlock>
183218
</ThemeProvider>
@@ -190,16 +225,18 @@ export const decorators = [
190225
<ThemeProvider theme={convert(themes.light)}>
191226
<Global styles={createReset} />
192227
</ThemeProvider>
193-
<ThemeProvider theme={convert(themes.light)}>
194-
<ThemeStack data-side="left">
195-
<StoryFn />
196-
</ThemeStack>
197-
</ThemeProvider>
198-
<ThemeProvider theme={convert(themes.dark)}>
199-
<ThemeStack data-side="right">
200-
<StoryFn />
201-
</ThemeStack>
202-
</ThemeProvider>
228+
<StackContainer layout={parameters.layout}>
229+
<ThemeProvider theme={convert(themes.light)}>
230+
<ThemeStack data-side="left" layout={parameters.layout}>
231+
<StoryFn />
232+
</ThemeStack>
233+
</ThemeProvider>
234+
<ThemeProvider theme={convert(themes.dark)}>
235+
<ThemeStack data-side="right" layout={parameters.layout}>
236+
<StoryFn />
237+
</ThemeStack>
238+
</ThemeProvider>
239+
</StackContainer>
203240
</Fragment>
204241
);
205242
}
@@ -209,7 +246,7 @@ export const decorators = [
209246
<ThemeProvider theme={convert(themes[theme])}>
210247
<Global styles={createReset} />
211248
<ThemedSetRoot />
212-
{!parameters.theme && isChromatic() && playFunction && (
249+
{showPlayFnNotice && (
213250
<>
214251
<PlayFnNotice>
215252
<span>
@@ -233,7 +270,7 @@ export const decorators = [
233270
*
234271
* If parameters.withRawArg is not set, this decorator will do nothing
235272
*/
236-
(StoryFn, { parameters, args, hooks }) => {
273+
(StoryFn, { parameters, args }) => {
237274
const [, updateArgs] = useArgs();
238275
if (!parameters.withRawArg) {
239276
return <StoryFn />;
@@ -246,6 +283,7 @@ export const decorators = [
246283
...args,
247284
onChange: (newValue) => {
248285
updateArgs({ [parameters.withRawArg]: newValue });
286+
// @ts-expect-error onChange is not a valid arg
249287
args.onChange?.(newValue);
250288
},
251289
}}
@@ -257,7 +295,7 @@ export const decorators = [
257295
</>
258296
);
259297
},
260-
];
298+
] satisfies Decorator[];
261299

262300
export const parameters = {
263301
options: {
@@ -295,4 +333,22 @@ export const parameters = {
295333
'slategray',
296334
],
297335
},
336+
viewport: {
337+
options: MINIMAL_VIEWPORTS,
338+
},
339+
themes: {
340+
disable: true,
341+
},
342+
backgrounds: {
343+
options: {
344+
light: { name: 'light', value: '#edecec' },
345+
dark: { name: 'dark', value: '#262424' },
346+
blue: { name: 'blue', value: '#1b1a2c' },
347+
},
348+
grid: {
349+
cellSize: 15,
350+
cellAmount: 10,
351+
opacity: 0.4,
352+
},
353+
},
298354
};

code/addons/backgrounds/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
"./src/manager.tsx"
8181
],
8282
"previewEntries": [
83-
"./src/preview.tsx"
83+
"./src/preview.ts"
8484
]
8585
},
8686
"gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import React, { useState, memo, Fragment, useCallback } from 'react';
2+
3+
import { useGlobals, useParameter } from 'storybook/internal/manager-api';
4+
import { IconButton, WithTooltip, TooltipLinkList } from 'storybook/internal/components';
5+
6+
import { CircleIcon, GridIcon, PhotoIcon, RefreshIcon } from '@storybook/icons';
7+
import { PARAM_KEY as KEY } from '../constants';
8+
import type { Background, BackgroundMap, Config, GlobalStateUpdate } from '../types';
9+
10+
type Link = Parameters<typeof TooltipLinkList>['0']['links'][0];
11+
12+
const emptyBackgroundMap: BackgroundMap = {};
13+
14+
export const BackgroundTool = memo(function BackgroundSelector() {
15+
const config = useParameter<Config>(KEY);
16+
const [globals, updateGlobals, storyGlobals] = useGlobals();
17+
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
18+
19+
const { options = emptyBackgroundMap, disable = true } = config || {};
20+
if (disable) {
21+
return null;
22+
}
23+
24+
const data = globals[KEY] || {};
25+
const backgroundName: string = data.value;
26+
const isGridActive = data.grid || false;
27+
28+
const item = options[backgroundName];
29+
const isLocked = !!storyGlobals?.[KEY];
30+
const length = Object.keys(options).length;
31+
32+
return (
33+
<Pure
34+
{...{
35+
length,
36+
backgroundMap: options,
37+
item,
38+
updateGlobals,
39+
backgroundName,
40+
setIsTooltipVisible,
41+
isLocked,
42+
isGridActive,
43+
isTooltipVisible,
44+
}}
45+
/>
46+
);
47+
});
48+
49+
interface PureProps {
50+
length: number;
51+
backgroundMap: BackgroundMap;
52+
item: Background | undefined;
53+
updateGlobals: ReturnType<typeof useGlobals>['1'];
54+
backgroundName: string | undefined;
55+
setIsTooltipVisible: React.Dispatch<React.SetStateAction<boolean>>;
56+
isLocked: boolean;
57+
isGridActive: boolean;
58+
isTooltipVisible: boolean;
59+
}
60+
61+
const Pure = memo(function PureTool(props: PureProps) {
62+
const {
63+
item,
64+
length,
65+
updateGlobals,
66+
setIsTooltipVisible,
67+
backgroundMap,
68+
backgroundName,
69+
isLocked,
70+
isGridActive: isGrid,
71+
isTooltipVisible,
72+
} = props;
73+
74+
const update = useCallback(
75+
(input: GlobalStateUpdate) => {
76+
updateGlobals({
77+
[KEY]: input,
78+
});
79+
},
80+
[updateGlobals]
81+
);
82+
83+
return (
84+
<Fragment>
85+
<IconButton
86+
key="grid"
87+
active={isGrid}
88+
disabled={isLocked}
89+
title="Apply a grid to the preview"
90+
onClick={() => update({ value: backgroundName, grid: !isGrid })}
91+
>
92+
<GridIcon />
93+
</IconButton>
94+
95+
{length > 0 ? (
96+
<WithTooltip
97+
key="background"
98+
placement="top"
99+
closeOnOutsideClick
100+
tooltip={({ onHide }) => {
101+
return (
102+
<TooltipLinkList
103+
links={[
104+
...(!!item
105+
? [
106+
{
107+
id: 'reset',
108+
title: 'Reset background',
109+
icon: <RefreshIcon />,
110+
onClick: () => {
111+
update({ value: undefined, grid: isGrid });
112+
onHide();
113+
},
114+
},
115+
]
116+
: []),
117+
...Object.entries(backgroundMap).map<Link>(([k, value]) => ({
118+
id: k,
119+
title: value.name,
120+
icon: <CircleIcon color={value?.value || 'grey'} />,
121+
active: k === backgroundName,
122+
onClick: () => {
123+
update({ value: k, grid: isGrid });
124+
onHide();
125+
},
126+
})),
127+
]}
128+
/>
129+
);
130+
}}
131+
onVisibleChange={setIsTooltipVisible}
132+
>
133+
<IconButton
134+
disabled={isLocked}
135+
key="background"
136+
title="Change the background of the preview"
137+
active={!!item || isTooltipVisible}
138+
>
139+
<PhotoIcon />
140+
</IconButton>
141+
</WithTooltip>
142+
) : null}
143+
</Fragment>
144+
);
145+
});

0 commit comments

Comments
 (0)