🚲

【React】ユーザー操作擬似クラス(hover, focus, active)を強制したコンポーネントをStorybookに登録したい

2024/07/16に公開

はじめに

Chakra UIをラップしたカスタムコンポーネントを使っている案件で、ユーザー操作擬似クラスを強制してStorybookに登録したい事例がありました。hover, focus, activeはユーザー操作でスタイルが変化するので、どうやったら期待値になるのか調査〜実現までの備忘録を残そうと思います。期待値は以下を想定しています。

ユーザー操作擬似クラスとは?

一部の擬似クラスは、ユーザーが何らかの方法で文書を操作したときにのみ適用されます。これらのユーザー操作の擬似クラスは、動的擬似クラスと呼ばれることもあり、ユーザーが要素を操作したときに、要素にクラスが追加されたかのように動作します。

https://developer.mozilla.org/ja/docs/Learn/CSS/Building_blocks/Selectors/Pseudo-classes_and_pseudo-elements#ユーザー操作擬似クラス

以降、疑似クラスと称します。

結論

Chakra UIが定義しているデータ属性を活用することで擬似クラスをユーザー操作なしにStorybookへ登録することができました。Chakra UIで定義されているデータ属性はこちらのドキュメント、またはコードベースだとこちらから確認することができます。

実装をしてみると以下のようになります。

Button/index.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./index";
import { ComponentProps } from "react";
import { HStack } from "@chakra-ui/react";

const meta = {
  title: "Button",
  component: Button,
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

const Render = (props: ComponentProps<typeof Button>) => {
  return (
    <HStack>
      <Button {...props}>default</Button>
      <Button data-hover {...props}>
        hover
      </Button>
      <Button data-active {...props}>
        active
      </Button>
      <Button data-focus-visible {...props}>
        focus-visible
      </Button>
    </HStack>
  );
};

export const Default: Story = {
  args: {
    children: "Button",
    colorScheme: "blue",
  },
  render: Render,
};

Chakra UIを使っていない場合はどうする?

Chakra UIを使っているなら今回の記事は再現性があるのかなと思いますが、全ての開発でChakra UIを使っているとは限りません。どうすればChakra UI以外でも疑似クラスを強制したコンポーネントをStorybookへ登録できるでしょうか?

まず思いつくのは今回紹介したChakra UIで定義しているデータ属性を踏襲する方法です。純粋なデータ属性によってスタイルを切り替えているのでChakra UIでなくても問題なさそうです。例えばhoverは次のように定義されているので、こちらを参考にすればうまくいきそうですね。

https://github.com/chakra-ui/chakra-ui/blob/6c2f3755073644c4b86e85fabc7ee299d1c71653/packages/react/src/preset-base.ts#L17

他には、擬似クラスのために用意されているStorybookのアドオンが提供されているのでこちらを使用するとよさそうに思いました。

https://storybook.js.org/addons/storybook-addon-pseudo-states

上記の記事で紹介されているstorybook-addon-pseudo-statesを用いた実装は以下のようになります。

.storybook/main.ts
import type { StorybookConfig } from "@storybook/nextjs";

const config: StorybookConfig = {
  stories: ["../app/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-onboarding",
    "@storybook/addon-interactions",
    "storybook-addon-pseudo-states", // 追加する
  ],
  framework: {
    name: "@storybook/nextjs",
    options: {},
  },
};
export default config;
Button/index.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./index";
import { ComponentProps } from "react";
import { HStack } from "@chakra-ui/react";

const meta = {
  title: "Button",
  component: Button,
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

const Render = (props: ComponentProps<typeof Button>) => {
  return (
    <HStack>
      <Button {...props}>default</Button>
      <Button id="hover" {...props}>
        hover
      </Button>
      <Button id="active" {...props}>
        active
      </Button>
      <Button id="focus-visible" {...props}>
        focus-visible
      </Button>
    </HStack>
  );
};
export const Default: Story = {
  args: {
    children: "Button",
    colorScheme: "blue",
  },
  parameters: {
    pseudo: {
      hover: "#hover",
      active: "#active",
      focusVisible: "#focus-visible",
    },
  },
  render: Render,
};

storybook-addon-pseudo-statesを導入した後のコードを動かすと期待値になりました。特に問題なさそうです。

念の為、storybook-addon-pseudo-statesを導入する際の注意点や補足事項を箇条書きします。

"storybook": "^8.2.2",
"storybook-addon-pseudo-states": "^3.1.1",
  • chromaui/storybook-addon-pseudo-statesが対応している擬似クラスは以下になります

https://github.com/chromaui/storybook-addon-pseudo-states/blob/main/src/constants.ts#L11-L20

  • storybook-addon-pseudo-statesのv2系ではStorybookのツールバーから疑似クラスの切り替えが可能だったようですがv3系からツールバーが表示されないissueが挙げられています。関連のissueは以下です。

最後に

調査を通して以下の方法で疑似クラスを強制したコンポーネントをStorybookへ登録できることがわかりました。もし別解や「こっちのほうが良さそう」というご意見がありましたらご教示いただけますと幸いです。

  1. Chakra UIであれば定義されているデータ属性を使用する
  2. Chakra UIを使用していない場合は自前で用意する
  3. またはstorybook-addon-pseudo-statesを使う

簡単ですが以上です。

Discussion