返回整合
@mui/material

整合Material UI與 Storybook 整合

Material UI 是一個基於 Google Material Design 規範的元件庫。
先決條件

本食譜假設您已經有一個使用 @mui/material 的 React 應用程式,並且剛剛使用入門指南設定了 Storybook >= 7.0。還沒有嗎?請按照 MUI 的設定指示操作,然後執行

# Add Storybook:
npm create storybook@latest

1. 新增 @storybook/addon-themes

首先,您需要安裝 @storybook/addon-themes

執行以下腳本以安裝並註冊附加元件

npx storybook@latest add @storybook/addon-themes
組態腳本失敗了嗎?

在底層,這會執行 npx @storybook/auto-config themes,它應該讀取您的專案並嘗試使用正確的裝飾器來設定您的 Storybook。如果直接執行該命令無法解決您的問題,請在 @storybook/auto-config 儲存庫上提交錯誤報告,以便我們進一步改進它。若要手動新增此附加元件,請先安裝它,然後將其新增至 .storybook/main.ts 中的 addons 陣列。

2. 捆綁字型和圖示以獲得更好的效能

Material UI 依賴兩種字型才能按預期呈現,即 Google 的 RobotoMaterial Icons。雖然您可以直接從 Google Fonts CDN 加載這些字型,但將字型與 Storybook 捆綁在一起對於效能來說更好。

  • 🏎️ 字型載入更快,因為它們來自與您的應用程式相同的位置
  • ✈️ 字型將離線載入,因此您可以隨時隨地繼續開發您的 stories
  • 📸 不再有不一致的快照測試,因為字型會立即載入

首先,安裝字型作為依賴項。

yarn add @fontsource/roboto @fontsource/material-icons

然後將 CSS 檔案匯入到 .storybook/preview.js,您的 Storybook 的進入點。

// .storybook/preview.js
 
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import '@fontsource/material-icons';

3. 載入您的主題和全域 CSS

.storybook/preview.js 內,匯入 <CssBaseline /><ThemeProvider /> 和您的主題,然後使用 withThemeFromJSXProvider 裝飾器將它們應用於您的 stories,方法是將其新增至 decorators 陣列。

// .storybook/preview.js
 
import { CssBaseline, ThemeProvider } from '@mui/material';
import { withThemeFromJSXProvider } from '@storybook/addon-themes';
import { lightTheme, darkTheme } from '../src/themes.js';
 
/* snipped for brevity */
 
export const decorators = [
  withThemeFromJSXProvider({
    themes: {
      light: lightTheme,
      dark: darkTheme,
    },
    defaultTheme: 'light',
    Provider: ThemeProvider,
    GlobalStyles: CssBaseline,
  }),
];

當您提供多個主題時,Storybook UI 中將會出現一個工具列選單,以選擇您想要的 stories 主題。

4. 使用 Material UI 屬性類型以獲得更好的控制和文件

Storybook controls 為您提供圖形化控件來操作元件的 props。它們對於尋找元件的邊緣案例和在瀏覽器中進行原型設計非常方便。

通常,您必須手動設定 controls。但是,如果您使用 Typescript,則可以重複使用 Material UI 的元件屬性類型來自動產生 story controls。作為額外的好處,這也將自動在您的文件標籤中填入屬性表。

Changing the button components props using Storybook controls

讓我們以以下 Button 元件為例。

// button.component.tsx
 
import React from 'react';
import { Button as MuiButton } from '@mui/material';
 
export interface ButtonProps {
  label: string;
}
 
export const Button = ({ label, ...rest }: ButtonProps) => <MuiButton {...rest}>{label}</MuiButton>;

在這裡,我使用 label prop 作為 MuiButton 的 child,並傳遞所有其他 props。但是,當我們將其渲染到 Storybook 中時,我們的 controls 面板僅允許我們變更我們自己宣告的 label prop。

The button story with only a label prop control

這是因為 Storybook 僅將元件的屬性類型或 Story Args 中明確宣告的 props 新增到 controls 表中。讓我們更新 Storybook 的 Docgen 組態,以將 Material UI 的 Button props 也帶入 controls 表中。

// .storybook/main.ts
 
module.exports = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@storybook/addon-essentials', '@storybook/addon-styling'],
  framework: '@storybook/your-framework',
  typescript: {
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      // Speeds up Storybook build time
      compilerOptions: {
        allowSyntheticDefaultImports: false,
        esModuleInterop: false,
      },
      // Makes union prop types like variant and size appear as select controls
      shouldExtractLiteralValuesFromEnum: true,
      // Makes string and boolean types that can be undefined appear as inputs and switches
      shouldRemoveUndefinedFromOptional: true,
      // Filter out third-party props from node_modules except @mui packages
      propFilter: (prop) =>
        prop.parent
          ? !/node_modules\/(?!@mui)/.test(prop.parent.fileName)
          : true,
    },
  },
};

我們也想要更新 .storybook/preview.js 中的 parameters,以顯示 controls 表的描述和預設欄位。

// .storybook/preview.js
 
export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    expanded: true, // Adds the description and default columns
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

最後,更新 ButtonProps 類型以從 Material UI 的 Button props 擴展,以將所有這些 props 新增到 controls 中。

// button.component.tsx
 
import React from 'react';
import {
  Button as MuiButton,
  ButtonProps as MuiButtonProps,
} from '@mui/material';
 
export interface ButtonProps extends MuiButtonProps {
  label: string;
}
 
export const Button = ({ label, ...rest }: ButtonProps) => (
  <MuiButton {...rest}>{label}</MuiButton>
);

重新啟動您的 Storybook 伺服器,以便這些組態變更生效。您現在應該看到 Button 也具有所有 MuiButton 的 props 的 controls。

The button story with all 27 prop controls from the MUI button props

選擇要顯示的 controls

我們的按鈕現在有 27 個 props,這對於您的用例來說可能有點太多了。為了控制哪些 props 是可見的,我們可以使用 TypeScript 的 Pick<type, keys>Omit<type, keys> 工具類型。

// button.component.tsx
 
import React from 'react';
import {
  Button as MuiButton,
  ButtonProps as MuiButtonProps,
} from '@mui/material';
 
// Only include variant, size, and color
type ButtonBaseProps = Pick<MuiButtonProps, 'variant' | 'size' | 'color'>;
 
// Use all except disableRipple
// type ButtonBaseProps = Omit<MuiButtonProps, "disableRipple">;
 
export interface ButtonProps extends ButtonBaseProps {
  label: string;
}
 
export const Button = ({ label, ...rest }: ButtonProps) => (
  <MuiButton {...rest}>{label}</MuiButton>
);

現在我們的 Button 將僅從 MuiButton 取得 variant、size 和 color props。

The button story with only the controls specified

📣 感謝 Eric Mudrak 精彩的 Storybook with React & TypeScript 文章,它啟發了這個技巧。

標籤
貢獻者
  • shaunlloyd
    shaunlloyd