Skip to content

Commit

Permalink
Merge pull request #26654 from storybookjs/tom/23347-story-globals
Browse files Browse the repository at this point in the history
CSF: Allow overridding globals at the story level
  • Loading branch information
ndelangen authored Aug 2, 2024
2 parents d419bc6 + 49d0155 commit 1165845
Show file tree
Hide file tree
Showing 145 changed files with 2,756 additions and 1,185 deletions.
3 changes: 2 additions & 1 deletion .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
34e364a0ca1d93555d36a7367d78e8e229493de8
c0896915fb7fb9a8dd416b9aebca17abd909d1c1
a41c227037e7e7249b8b376f838f4f8bcc3e3e59
13c46e6c0b7f3dd8cf4ba42d1cfd6714f4777d54
13c46e6c0b7f3dd8cf4ba42d1cfd6714f4777d54
0a4522a3f84773f39daec4820c49b8a92e9f9d11
6 changes: 5 additions & 1 deletion code/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ const config: StorybookConfig = {
directory: '../addons/toolbars/template/stories',
titlePrefix: 'addons/toolbars',
},
{
directory: '../addons/themes/template/stories',
titlePrefix: 'addons/themes',
},
{
directory: '../addons/onboarding/src',
titlePrefix: 'addons/onboarding',
Expand All @@ -83,6 +87,7 @@ const config: StorybookConfig = {
],
addons: [
'@storybook/addon-links',
'@storybook/addon-themes',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-storysource',
Expand Down Expand Up @@ -119,7 +124,6 @@ const config: StorybookConfig = {
},
features: {
viewportStoryGlobals: true,
themesStoryGlobals: true,
backgroundsStoryGlobals: true,
},
viteFinal: (viteConfig, { configType }) =>
Expand Down
104 changes: 80 additions & 24 deletions code/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ import {
} from 'storybook/internal/theming';
import { useArgs, DocsContext as DocsContextProps } from 'storybook/internal/preview-api';
import type { PreviewWeb } from 'storybook/internal/preview-api';
import type { ReactRenderer } from '@storybook/react';
import type { ReactRenderer, Decorator } from '@storybook/react';
import type { Channel } from 'storybook/internal/channels';

import { DocsContext } from '@storybook/blocks';
import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport';

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

const { document } = global;

const ThemeBlock = styled.div<{ side: 'left' | 'right' }>(
const ThemeBlock = styled.div<{ side: 'left' | 'right'; layout: string }>(
{
position: 'absolute',
top: 0,
Expand All @@ -31,8 +32,10 @@ const ThemeBlock = styled.div<{ side: 'left' | 'right' }>(
height: '100vh',
bottom: 0,
overflow: 'auto',
padding: 10,
},
({ layout }) => ({
padding: layout === 'fullscreen' ? 0 : '1rem',
}),
({ theme }) => ({
background: theme.background.content,
color: theme.color.defaultText,
Expand All @@ -49,14 +52,17 @@ const ThemeBlock = styled.div<{ side: 'left' | 'right' }>(
}
);

const ThemeStack = styled.div(
const ThemeStack = styled.div<{ layout: string }>(
{
position: 'relative',
minHeight: 'calc(50vh - 15px)',
flex: 1,
},
({ theme }) => ({
background: theme.background.content,
color: theme.color.defaultText,
}),
({ layout }) => ({
padding: layout === 'fullscreen' ? 0 : '1rem',
})
);

Expand All @@ -80,6 +86,25 @@ const PlayFnNotice = styled.div(
})
);

const StackContainer = ({ children, layout }) => (
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
// margin: layout === 'fullscreen' ? 0 : '-1rem',
}}
>
<style dangerouslySetInnerHTML={{ __html: 'html, body, #storybook-root { height: 100%; }' }} />
{layout === 'fullscreen' ? null : (
<style
dangerouslySetInnerHTML={{ __html: 'html, body { padding: 0!important; margin: 0; }' }}
/>
)}
{children}
</div>
);

const ThemedSetRoot = () => {
const theme = useTheme();

Expand Down Expand Up @@ -159,10 +184,20 @@ export const decorators = [
/**
* This decorator renders the stories side-by-side, stacked or default based on the theme switcher in the toolbar
*/
(StoryFn, { globals, parameters, playFunction, args }) => {
const defaultTheme =
isChromatic() && !playFunction && args.autoplay !== true ? 'stacked' : 'light';
const theme = globals.theme || parameters.theme || defaultTheme;
(StoryFn, { globals, playFunction, args, storyGlobals, parameters }) => {
let theme = globals.sb_theme;
let showPlayFnNotice = false;

// this makes the decorator be out of 'phase' with the actually selected theme in the toolbar
// but this is acceptable, I guess
// we need to ensure only a single rendering in chromatic
// a more 'correct' approach would be to set a specific theme global on every story that has a playFunction
if (playFunction && args.autoplay !== false && !(theme === 'light' || theme === 'dark')) {
theme = 'light';
showPlayFnNotice = true;
} else if (isChromatic() && !storyGlobals.sb_theme && !playFunction) {
theme = 'stacked';
}

switch (theme) {
case 'side-by-side': {
Expand All @@ -172,12 +207,12 @@ export const decorators = [
<Global styles={createReset} />
</ThemeProvider>
<ThemeProvider theme={convert(themes.light)}>
<ThemeBlock side="left" data-side="left">
<ThemeBlock side="left" data-side="left" layout={parameters.layout}>
<StoryFn />
</ThemeBlock>
</ThemeProvider>
<ThemeProvider theme={convert(themes.dark)}>
<ThemeBlock side="right" data-side="right">
<ThemeBlock side="right" data-side="right" layout={parameters.layout}>
<StoryFn />
</ThemeBlock>
</ThemeProvider>
Expand All @@ -190,16 +225,18 @@ export const decorators = [
<ThemeProvider theme={convert(themes.light)}>
<Global styles={createReset} />
</ThemeProvider>
<ThemeProvider theme={convert(themes.light)}>
<ThemeStack data-side="left">
<StoryFn />
</ThemeStack>
</ThemeProvider>
<ThemeProvider theme={convert(themes.dark)}>
<ThemeStack data-side="right">
<StoryFn />
</ThemeStack>
</ThemeProvider>
<StackContainer layout={parameters.layout}>
<ThemeProvider theme={convert(themes.light)}>
<ThemeStack data-side="left" layout={parameters.layout}>
<StoryFn />
</ThemeStack>
</ThemeProvider>
<ThemeProvider theme={convert(themes.dark)}>
<ThemeStack data-side="right" layout={parameters.layout}>
<StoryFn />
</ThemeStack>
</ThemeProvider>
</StackContainer>
</Fragment>
);
}
Expand All @@ -209,7 +246,7 @@ export const decorators = [
<ThemeProvider theme={convert(themes[theme])}>
<Global styles={createReset} />
<ThemedSetRoot />
{!parameters.theme && isChromatic() && playFunction && (
{showPlayFnNotice && (
<>
<PlayFnNotice>
<span>
Expand All @@ -233,7 +270,7 @@ export const decorators = [
*
* If parameters.withRawArg is not set, this decorator will do nothing
*/
(StoryFn, { parameters, args, hooks }) => {
(StoryFn, { parameters, args }) => {
const [, updateArgs] = useArgs();
if (!parameters.withRawArg) {
return <StoryFn />;
Expand All @@ -246,6 +283,7 @@ export const decorators = [
...args,
onChange: (newValue) => {
updateArgs({ [parameters.withRawArg]: newValue });
// @ts-expect-error onChange is not a valid arg
args.onChange?.(newValue);
},
}}
Expand All @@ -257,7 +295,7 @@ export const decorators = [
</>
);
},
];
] satisfies Decorator[];

export const parameters = {
options: {
Expand Down Expand Up @@ -295,4 +333,22 @@ export const parameters = {
'slategray',
],
},
viewport: {
options: MINIMAL_VIEWPORTS,
},
themes: {
disable: true,
},
backgrounds: {
options: {
light: { name: 'light', value: '#edecec' },
dark: { name: 'dark', value: '#262424' },
blue: { name: 'blue', value: '#1b1a2c' },
},
grid: {
cellSize: 15,
cellAmount: 10,
opacity: 0.4,
},
},
};
2 changes: 1 addition & 1 deletion code/addons/backgrounds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"./src/manager.tsx"
],
"previewEntries": [
"./src/preview.tsx"
"./src/preview.ts"
]
},
"gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16",
Expand Down
145 changes: 145 additions & 0 deletions code/addons/backgrounds/src/components/Tool.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React, { useState, memo, Fragment, useCallback } from 'react';

import { useGlobals, useParameter } from 'storybook/internal/manager-api';
import { IconButton, WithTooltip, TooltipLinkList } from 'storybook/internal/components';

import { CircleIcon, GridIcon, PhotoIcon, RefreshIcon } from '@storybook/icons';
import { PARAM_KEY as KEY } from '../constants';
import type { Background, BackgroundMap, Config, GlobalStateUpdate } from '../types';

type Link = Parameters<typeof TooltipLinkList>['0']['links'][0];

const emptyBackgroundMap: BackgroundMap = {};

export const BackgroundTool = memo(function BackgroundSelector() {
const config = useParameter<Config>(KEY);
const [globals, updateGlobals, storyGlobals] = useGlobals();
const [isTooltipVisible, setIsTooltipVisible] = useState(false);

const { options = emptyBackgroundMap, disable = true } = config || {};
if (disable) {
return null;
}

const data = globals[KEY] || {};
const backgroundName: string = data.value;
const isGridActive = data.grid || false;

const item = options[backgroundName];
const isLocked = !!storyGlobals?.[KEY];
const length = Object.keys(options).length;

return (
<Pure
{...{
length,
backgroundMap: options,
item,
updateGlobals,
backgroundName,
setIsTooltipVisible,
isLocked,
isGridActive,
isTooltipVisible,
}}
/>
);
});

interface PureProps {
length: number;
backgroundMap: BackgroundMap;
item: Background | undefined;
updateGlobals: ReturnType<typeof useGlobals>['1'];
backgroundName: string | undefined;
setIsTooltipVisible: React.Dispatch<React.SetStateAction<boolean>>;
isLocked: boolean;
isGridActive: boolean;
isTooltipVisible: boolean;
}

const Pure = memo(function PureTool(props: PureProps) {
const {
item,
length,
updateGlobals,
setIsTooltipVisible,
backgroundMap,
backgroundName,
isLocked,
isGridActive: isGrid,
isTooltipVisible,
} = props;

const update = useCallback(
(input: GlobalStateUpdate) => {
updateGlobals({
[KEY]: input,
});
},
[updateGlobals]
);

return (
<Fragment>
<IconButton
key="grid"
active={isGrid}
disabled={isLocked}
title="Apply a grid to the preview"
onClick={() => update({ value: backgroundName, grid: !isGrid })}
>
<GridIcon />
</IconButton>

{length > 0 ? (
<WithTooltip
key="background"
placement="top"
closeOnOutsideClick
tooltip={({ onHide }) => {
return (
<TooltipLinkList
links={[
...(!!item
? [
{
id: 'reset',
title: 'Reset background',
icon: <RefreshIcon />,
onClick: () => {
update({ value: undefined, grid: isGrid });
onHide();
},
},
]
: []),
...Object.entries(backgroundMap).map<Link>(([k, value]) => ({
id: k,
title: value.name,
icon: <CircleIcon color={value?.value || 'grey'} />,
active: k === backgroundName,
onClick: () => {
update({ value: k, grid: isGrid });
onHide();
},
})),
]}
/>
);
}}
onVisibleChange={setIsTooltipVisible}
>
<IconButton
disabled={isLocked}
key="background"
title="Change the background of the preview"
active={!!item || isTooltipVisible}
>
<PhotoIcon />
</IconButton>
</WithTooltip>
) : null}
</Fragment>
);
});
Loading

0 comments on commit 1165845

Please sign in to comment.