Source uses Storybook for its API documentation as well as for visual regression testing.
The Source are created within the CSNX storybooks, which can be run locally using make storybooks
and built using make build-storybook
.
Source has a standard way of writing stories:
// MyComponent.tsx
export interface MyComponentProps {
/**
* A string to label the value
*/
label: string;
/**
* An optional boolean to hide the label value
*/
hideLabel?: boolean;
/**
* An optional string value to display. Will show `No value set` if not provided
*/
value?: string;
/**
* An optional id to give the component
*/
id?: string;
}
/**
* [Storybook](https://guardian.github.io/csnx/?path=/story/${MY_COMPONENT_PACKAGE}-${MY_COMPONENT}) •
* [Design System](https://theguardian.design/${MY_COMPONENT_PATH}) •
* [GitHub](https://github.com/guardian/csnx/tree/main/libs/@guardian/${MY_COMPONENT_PACKAGE}/${MY_COMPONENT}) •
* [NPM](https://www.npmjs.com/package/@guardian/${MY_COMPONENT_PACKAGE})
*
* My Component is a nifty way of showing key -> value data
*
* The following themes are supported: `default`, `brand`
*/
export MyComponent = ({}: MyComponentProps) => (
<div id={id}>
{!hideLabel ? <div className="my-component-label">{label}</div> : null}
<div className="my-component-value">{value ?? "No value set"}</div>
</div>
)
// MyComponent.stories.tsx
import { MyComponent } from './MyComponent';
import type { MyComponentProps } from './MyComponent';
import { myComponentBrand } from './index';
import { ThemeProvider } from '@emotion/react';
import type { Story } from '@storybook/react';
export default {
title: 'Source/src-my-component/MyComponent',
component: MyComponent,
argTypes: {
id: { control: { disable: true } },
},
args: {
label: 'Label',
hideLabel: false,
value: 'Value',
},
};
const Template: Story = (args: MyComponentProps) => <MyComponent {...args} />;
export const Default = Template.bind({});
// *****************************************************************************
export const DefaultBrandTheme = (args: MyComponentProps) => (
<ThemeProvider theme={myComponentBrand}>
<Template {...args} />
</ThemeProvider>
);
DefaultBrandTheme.parameters = {
backgrounds: {
default: 'brandBackground.primary',
},
};
// *****************************************************************************
export const HideLabel = Template.bind({});
HideLabel.args = {
hideLabel: true,
};
// *****************************************************************************
export const HideLabelBrandTheme = (args: MyComponentProps) => (
<ThemeProvider theme={myComponentBrand}>
<Template {...args} />
</ThemeProvider>
);
HideLabelLightTheme.args = {
hideLabel: true,
};
HideLabelBrandTheme.parameters = {
backgrounds: {
default: 'brandBackground.primary',
},
};
- Name story files following the pattern
${Component}.stories.tsx
e.g.MyComponent.stories.tsx
- Each exported component should have its own
stories.tsx
- Story titles should follow the pattern
${Source | Editorial}/${package}/${component}
e.g.Source/src-radio/RadioGroup
- Use PascalCase names for stories
-
Define a template for each set of stories
const Template: Story = (args: ComponentProps) => <Component {...args} />;
-
Set the default story args and argTypes on the default export
export default { title: 'MyComponent', component: MyComponent, argTypes: { ... }, args: { ... }, };
-
Define a series of stories detailing all possible states of each component for visual regression testing
-
Separate stories with the following delimiter
// *****************************************************************************
-
Disable controls for deprecated props and for args that won't have any effect in storybook
export default { title: 'MyComponent', component: MyComponent, argTypes: { arg2: { control: { disable: true } } }, args: { ... }, };
-
(Optional) If you want a viewport to be reflected in Chromatic, you need to explicitly add it as a
chromatic
parameter
MyStory.parameters = {
viewport: { defaultViewport: 'tablet' },
chromatic: {
viewports: [breakpoints.tablet],
},
layout: 'fullscreen',
};
-
Document component props using doc blocks
export interface MyComponentProps { /** * A label to display alongside the component */ label: string; }
-
Document Components with JSDoc comments including links to online documentation
/** * [Storybook](https://guardian.github.io/csnx/?path=/story/${MY_COMPONENT_PACKAGE}-${MY_COMPONENT}) • * [Design System](https://theguardian.design/${MY_COMPONENT_PATH}) • * [GitHub](https://github.com/guardian/csnx/tree/main/libs/@guardian/${MY_COMPONENT_PACKAGE}/${MY_COMPONENT}) • * [NPM](https://www.npmjs.com/package/@guardian/${MY_COMPONENT_PACKAGE}) * * My Component is a nifty way of showing key -> value data * * The following themes are supported: `default`, `brand` */ export MyComponent = ({}: MyComponentProps) => {...}
-
Mark any props that should not be documented with the
@ignore
annotation and a comment explaining whyinterface MyChildComponentProps { /** * @ignore passed down by the parent <MyComponent /> */ myProp?: boolean; }
-
Set the
subcomponents
value in the default export for components that have sub-componentsexport default { title: 'Source/src-package/Component', component: Component, subcomponents: { SubComponent }, args: { ... }, argTypes: { ... }, };
-
Pass any sub-component's default story args through to the sub-component in the template of the component story
// SubComponent.stories.tsx export default { title: 'Source/src-package/SubComponent', component: SubComponent, argTypes: { ... }, args: { arg0: 'test', arg1: 1, }, };
// Component.stories.tsx ... import SubComponentStories from './SubComponent.stories'; ... const Template: Story = (args: ComponentProps) => ( <Component {...args}> <SubComponent {...SubComponentStories.args} /> </Component> );
For props where the type is either a string or a JSX element, you can provide options for the value that let the user see a possible value for either type.
interface StoryProps {
label: string | JSX.Element;
}
export const Story = ({ label }: StoryProps) => ...
Story.story = {
argTypes: {
label: {
options: ['string', 'JSX element'],
mapping: {
string: 'Option 1',
'JSX element': <em>Option 1</em>,
},
},
}
}
For props whose behaviour depends on whether the value is defined or not, you can configure two options for the user to choose between to see the different states.
export const Story = ({ error }: StoryProps) => ...
Story.argTypes = {
error: {
options: ['undefined', 'string'],
mapping: {
undefined: undefined,
'string': "An error has occurred",
},
control: { type: 'radio' },
}
}
A decorator is available to simplify creating stories with different themes. To make use of it, simply pass the required theme in as a parameter.
...
const MyComponentBrandTheme = Template.bind({})
MyComponentBrandTheme.parameters = {
theme: myComponentBrand
}