Skip to content

Instantly share code, notes, and snippets.

@donaldpipowitch
Created September 27, 2023 06:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save donaldpipowitch/0a911f5e9222cf40637ef2792126ef2a to your computer and use it in GitHub Desktop.
Save donaldpipowitch/0a911f5e9222cf40637ef2792126ef2a to your computer and use it in GitHub Desktop.
Storybook: Conditionally show story/canvas

In our Storybook we document components, but also whole pages. We also use the Docs addon. Sadly our Docs pages became pretty slow when a lot of documented pages were included. With the following solution we only render the first story/canvas by default and the remaining stories will only be rendered, when you click on them.

We do this by introducing a custom <Stories/> and <DocsStory/> component. Afterwards we can wrap every story/canvas in a <ConditionalCanvas/>.

// here you go: our custom logic for conditionally rendering the story/canvas
import { useState, FC, ReactNode } from 'react';
import styled from 'styled-components';
export const ConditionalCanvas: FC<{ children: ReactNode; index: number }> = ({
children,
index,
}) => {
const [clicked, setClicked] = useState(index === 0);
return (
<div onClick={() => setClicked(true)}>
{clicked ? children : <Preview>Click to show 👁️</Preview>}
</div>
);
};
// sadly the preview primitives from storybook are not exported
const Preview = styled.button`
/* copied from storybook: */
margin: 25px 0 40px;
border-radius: 4px;
background: #ffffff;
box-shadow: rgba(0, 0, 0, 0.1) 0 1px 3px 0;
border: 1px solid hsla(203, 50%, 30%, 0.15);
/* custom: */
width: 100%;
padding: 40px;
background: #f8f8f8;
text-align: center;
color: #777777;
cursor: pointer;
:hover,
:focus {
background: #efefef;
}
`;
// we already use different Docs pages for our components and for our documented pages.
// but now we import a custom `<Stories/>` component and don't use the one from @storybook/addon-docs.
import {
Title,
Subtitle,
Description,
ArgTypes,
PRIMARY_STORY,
} from '@storybook/addon-docs';
import { ConditionalCanvas } from '.storybook/utils/conditional-canvas';
import { Stories } from '.storybook/utils/stories';
export const ComponentDocs = () => (
<>
<Title />
<Subtitle />
<Description />
<ArgTypes of={PRIMARY_STORY} />
<Stories includePrimary />
</>
);
export const PageDocs = () => (
<>
<Title />
<Subtitle />
<Description />
<div className="custom-page-docs sb-unstyled">
<Stories
includePrimary
renderCanvas={(canvas, { index }) => (
<ConditionalCanvas index={index}>{canvas}</ConditionalCanvas>
)}
/>
</div>
</>
);
// again copy'n'pasting https://github.com/storybookjs/storybook/blob/next/code/ui/blocks/src/blocks/DocsStory.tsx
// and adding modifications
import {
Anchor,
Canvas,
Description,
Subheading,
DocsStoryProps,
useOf,
} from '@storybook/addon-docs';
import { PreparedStory } from '@storybook/types';
import type { FC } from 'react';
type CanvasMeta = {
story: PreparedStory;
};
type Props = DocsStoryProps & {
renderCanvas?: (canvas: JSX.Element, meta: CanvasMeta) => JSX.Element; // MODIFICATION
};
export const DocsStory: FC<Props> = ({
of,
expanded = true,
withToolbar: withToolbarProp = false,
__forceInitialArgs = false,
__primary = false,
renderCanvas = (canvas) => canvas, // MODIFICATION
}) => {
const { story } = useOf(of || 'story', ['story']);
// use withToolbar from parameters or default to true in autodocs
const withToolbar =
story.parameters.docs?.canvas?.withToolbar ?? withToolbarProp;
return (
<Anchor storyId={story.id}>
{expanded && (
<>
<Subheading>{story.name}</Subheading>
<Description of={of} />
</>
)}
{renderCanvas(
<Canvas
of={of}
withToolbar={withToolbar}
story={{ __forceInitialArgs, __primary }}
source={{ __forceInitialArgs }}
/>,
{ story }
)}
</Anchor>
);
};
// sadly we have to copy'n'past https://github.com/storybookjs/storybook/blob/next/code/ui/blocks/src/blocks/Stories.tsx
// and add some modification on top.
// we will have to do the same for `<DocsStory/>
import { DocsContext, Heading } from '@storybook/addon-docs';
import { styled } from '@storybook/theming';
import { PreparedStory } from '@storybook/types';
import { FC, useContext } from 'react';
import { DocsStory } from '.storybook/utils/docs-story';
type CanvasMeta = {
story: PreparedStory;
index: number;
};
interface StoriesProps {
title?: JSX.Element | string;
includePrimary?: boolean;
renderCanvas?: (canvas: JSX.Element, meta: CanvasMeta) => JSX.Element; // MODIFICATION
}
const StyledHeading: typeof Heading = styled(Heading)(({ theme }) => ({
fontSize: `${theme.typography.size.s2 - 1}px`,
fontWeight: theme.typography.weight.bold,
lineHeight: '16px',
letterSpacing: '0.35em',
textTransform: 'uppercase',
color: theme.textMutedColor,
border: 0,
marginBottom: '12px',
'&:first-of-type': {
// specificity issue
marginTop: '56px',
},
}));
export const Stories: FC<StoriesProps> = ({
title,
includePrimary = true,
renderCanvas = (canvas) => canvas, // MODIFICATION
}) => {
const { componentStories } = useContext(DocsContext);
let stories = componentStories().filter(
(story) => !story.parameters?.docs?.disable
);
if (!includePrimary) stories = stories.slice(1);
if (!stories || stories.length === 0) {
return null;
}
return (
<>
<StyledHeading>{title}</StyledHeading>
{stories.map(
(story, index) =>
story && (
<DocsStory
key={story.id}
of={story.moduleExport}
expanded
__forceInitialArgs
renderCanvas={(canvas, meta) =>
renderCanvas(canvas, { ...meta, index })
}
/>
)
)}
</>
);
};
Stories.defaultProps = {
title: 'Stories',
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment