简体   繁体   中英

How can you wrap a story with a react-router layout route?

I have a very simple and plain ComponentX that renders some styled HTML, no data fetching or even routing needed. It has a single, simple story. ComponentX is meant to be used in a dark-themed website, so it assumes that it will inherit color: white; and other such styles. This is crucial to rendering ComponentX correctly. I won't bore you with the code for ComponentX.

Those contextual styles, such as background-color: black; and color: white; , are applied to the <body> by the GlobalStyles component. GlobalStyles uses the css-in-js library Emotion to apply styles to the document.

import { Global } from '@emotion/react';

export const GlobalStyles = () => (
  <>
    <Global styles={{ body: { backgroundColor: 'black' } }} />
    <Outlet />
  </>
);

As you can see, this component does not accept children, but rather is meant to be used as a layout route, so it renders an <Outlet /> . I expect the application to render a Route tree like the below, using a layout route indicated by the (1)

  <Router>
    <Routes>
      <Route element={<GlobalStyles/>} >      <== (1)
        <Route path="login">
          <Route index element={<Login />} />
          <Route path="multifactor" element={<Mfa />} />
        </Route>

Not pictured: the <Login> and <Mfa> pages call ComponentX.

And this works!

The problem is with the Stories. If I render a plain story with ComponentX, it will be hard to see because it expects all of those styles on <body> to be present. The obvious solution is to create a decorator that wraps each story with this <Route element={<GlobalStyles/>} > . How can this be accomplished? Here's my working-but-not-as-intended component-x.stories.tsx:

import React from 'react';

import ComponentX from './ComponentX';

export default {
  component: ComponentX,
  title: 'Component X',
};

const Template = args => <ComponentX {...args} />;

export const Default = Template.bind({});
Default.args = {};
Default.decorators = [
  (story) => <div style={{ padding: '3rem' }}>{story()}</div>
];

(I realize that I can make <GlobalStyles> a simple wrapper component around the entire <Router> , but I want to use this pattern to create stories for other components that assume other, intermediate layout routes.)

What I've usually done is to create custom decorator components to handle wrapping the stories that need specific "contexts" provided to them.

Example usage:

Create story decorator functions

import React from 'react';
import { Story } from '@storybook/react';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
import theme from '../src/constants/theme';
import { AppLayout } from '../src/components/layout';

// Provides global theme and resets/normalizes browser CSS
export const ThemeDecorator = (Story: Story) => (
  <ThemeProvider theme={theme}>
    <CssBaseline />
    <Story />
  </ThemeProvider>
);

// Render a story into a routing context inside a UI layout
export const AppScreen = (Story: Story) => (
  <Router>
    <Routes>
      <Route element={<AppLayout />}>
        <Route path="/*" element={<Story />} />
      </Route>
    </Routes>
  </Router>
);

.storybook/preview.js

import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
import { ThemeDecorator } from './decorators';

export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
  options: {
    storySort: {
      includeName: true,
      method: 'alphabetical',
      order: ['Example', 'Theme', 'Components', 'Pages', '*'],
    },
  },
  viewport: {
    viewports: {
      ...INITIAL_VIEWPORTS,
    },
  },
};

export const decorators = [ThemeDecorator]; // <-- provide theme/CSS always

Any story that needs the app layout and routing context:

import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { AppScreen, MarginPageLayout } from '../../.storybook/decorators';

import BaseComponentX from './ComponentX';

export default {
  title: 'Components/Component X',
  component: BaseComponentX,
  decorators: [AppScreen], // <-- apply additional decorators
  parameters: {
    layout: 'fullscreen',
  },
} as ComponentMeta<typeof BaseComponentX>;

const BaseComponentXTemplate: ComponentStory<typeof BaseComponentX> = () => (
  <BaseComponentX />
);

export const ComponentX = BaseComponentXTemplate.bind({});

In my example you could conceivably place all your providers and that Global component (w/ props) in what I've implemented as ThemeDecorator and set as a default decorator for all stories.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM