返回整合
@mui/material

整合Material UI與 Storybook

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

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

# Add Storybook:
npx storybook@latest init

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 綁定可以獲得更好的效能。

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

首先,將字體安裝為依賴項。

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

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

// .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 裝飾器將它們應用於您的故事,方法是將其新增到 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 中將會出現一個工具列選單,用於為您的故事選擇所需的主題。

4. 使用 Material UI prop 類型來獲得更好的控制項和文件

Storybook 控制項為您提供圖形控制項來操作元件的屬性。它們對於尋找元件的邊緣案例和在瀏覽器中建立原型非常方便。

通常,您必須手動設定控制項。但是,如果您使用 Typescript,您可以重複使用 Material UI 的元件屬性類型來自動產生故事控制項。作為額外的好處,這也會自動填寫文件標籤中的屬性表。

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 屬性作為 MuiButton 的子元素,並傳遞所有其他屬性。但是,當我們將其渲染到 Storybook 中時,我們的控制項面板只允許我們更改自己宣告的 label 屬性。

The button story with only a label prop control

這是因為 Storybook 只會將在元件的屬性類型或故事參數中明確宣告的屬性新增到控制項表中。讓我們更新 Storybook 的 Docgen 設定,以便將 Material UI 的 Button 屬性也加入控制項表中。

// .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 中的參數,以顯示控制項表的描述和預設欄位。

// .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 屬性,將所有這些屬性新增到控制項中。

// 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 的屬性的控制項。

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

選擇要顯示的控制項

我們的按鈕現在有 27 個屬性,這可能對您的使用案例來說有點太多了。為了控制哪些屬性是可見的,我們可以使用 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 屬性。

The button story with only the controls specified

📣 特別感謝 Eric Mudrak 精彩的 Storybook with React & TypeScript 文章,該文章啟發了此技巧。

標籤
貢獻者
  • shaunlloyd
    shaunlloyd