doc: add documentation how to write stories in shared components (#31831)
This commit is contained in:
@@ -121,6 +121,140 @@ Most components should be written as [MVVM pattern](../../docs/MVVM.md) view
|
|||||||
components. See existing components for examples. The exceptions are low level
|
components. See existing components for examples. The exceptions are low level
|
||||||
components that don't need a view model.
|
components that don't need a view model.
|
||||||
|
|
||||||
|
### Write Storybook Stories
|
||||||
|
|
||||||
|
All components should have accompanying Storybook stories for documentation and visual testing. Stories are written in TypeScript using the [Component Story Format (CSF)](https://storybook.js.org/docs/api/csf).
|
||||||
|
|
||||||
|
#### Story File Structure
|
||||||
|
|
||||||
|
Place the story file next to the component with the `.stories.tsx` extension:
|
||||||
|
|
||||||
|
```
|
||||||
|
MyComponent/
|
||||||
|
├── MyComponent.tsx
|
||||||
|
├── MyComponent.module.css
|
||||||
|
└── MyComponent.stories.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Regular Component Stories
|
||||||
|
|
||||||
|
For regular React components (non-MVVM), create stories by defining a meta object and story variations:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
|
import { fn } from "storybook/test";
|
||||||
|
import { MyComponent } from "./MyComponent";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Category/MyComponent",
|
||||||
|
component: MyComponent,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
args: {
|
||||||
|
// Default args for all stories
|
||||||
|
label: "Default Label",
|
||||||
|
onClick: fn(), // Mock function for tracking interactions
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof MyComponent>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
// Default story uses the default args
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
// Override specific args for variations
|
||||||
|
export const WithCustomLabel: Story = {
|
||||||
|
args: {
|
||||||
|
label: "Custom Label",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MVVM Component Stories
|
||||||
|
|
||||||
|
For MVVM components, create a wrapper component that uses `useMockedViewModel`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React, { type JSX } from "react";
|
||||||
|
import { fn } from "storybook/test";
|
||||||
|
import type { Meta, StoryFn } from "@storybook/react-vite";
|
||||||
|
import { MyComponentView, type MyComponentViewSnapshot, type MyComponentViewActions } from "./MyComponentView";
|
||||||
|
import { useMockedViewModel } from "../../useMockedViewModel";
|
||||||
|
|
||||||
|
// Combine snapshot and actions for easier typing
|
||||||
|
type MyComponentProps = MyComponentViewSnapshot & MyComponentViewActions;
|
||||||
|
|
||||||
|
// Wrapper component that creates a mocked ViewModel
|
||||||
|
const MyComponentViewWrapper = ({ onAction, ...rest }: MyComponentProps): JSX.Element => {
|
||||||
|
const vm = useMockedViewModel(rest, {
|
||||||
|
onAction,
|
||||||
|
});
|
||||||
|
return <MyComponentView vm={vm} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Category/MyComponentView",
|
||||||
|
component: MyComponentViewWrapper,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
args: {
|
||||||
|
// Snapshot properties (state)
|
||||||
|
title: "Default Title",
|
||||||
|
isLoading: false,
|
||||||
|
// Action properties (callbacks)
|
||||||
|
onAction: fn(),
|
||||||
|
},
|
||||||
|
} as Meta<typeof MyComponentViewWrapper>;
|
||||||
|
|
||||||
|
const Template: StoryFn<typeof MyComponentViewWrapper> = (args) => <MyComponentViewWrapper {...args} />;
|
||||||
|
|
||||||
|
export const Default = Template.bind({});
|
||||||
|
|
||||||
|
export const Loading = Template.bind({});
|
||||||
|
Loading.args = {
|
||||||
|
isLoading: true,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Thanks to this approach, we can directly use primitives in the story arguments instead of a view model object.
|
||||||
|
|
||||||
|
#### Linking Figma Designs
|
||||||
|
|
||||||
|
This package uses [@storybook/addon-designs](https://github.com/storybookjs/addon-designs) to embed Figma designs directly in Storybook. This helps developers compare their implementation with the design specs.
|
||||||
|
|
||||||
|
1. **Get the Figma URL**: Open your design in Figma, click "Share" → "Copy link"
|
||||||
|
2. **Add to story parameters**: Include the `design` object in the meta's `parameters`
|
||||||
|
3. **Supported URL formats**:
|
||||||
|
- File links: `https://www.figma.com/file/...`
|
||||||
|
- Design links: `https://www.figma.com/design/...`
|
||||||
|
- Specific node: `https://www.figma.com/design/...?node-id=123-456`
|
||||||
|
|
||||||
|
Example with Figma integration:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export default {
|
||||||
|
title: "Room List/RoomListSearchView",
|
||||||
|
component: RoomListSearchViewWrapper,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
args: {
|
||||||
|
// ... your args
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
design: {
|
||||||
|
type: "figma",
|
||||||
|
url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=98-1979",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Meta<typeof RoomListSearchViewWrapper>;
|
||||||
|
```
|
||||||
|
|
||||||
|
The Figma design will appear in the "Design" tab in Storybook.
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
Two types of tests are available: unit tests and visual regression tests.
|
Two types of tests are available: unit tests and visual regression tests.
|
||||||
|
|||||||
Reference in New Issue
Block a user