簡易的なプルダウンを作ってみよう!

みなさんこんにちは!
PlayGround AdventCalendar2024 9日目はフロントエンドコース所属のrikuが担当します。プログラミングを学び始めてから2年目に突入していますが、Reactがまだきちんと使いこなせなかったり、型エラーが起こったりなど、難しい部分が多いなと感じています…。

今回はReact,TypeScript,Tailwind CSSを少しづつ使って、簡易的なプルダウンを作ってみました。力不足な部分が多いので、温かい目で読んでくれると幸いです!

1.はじめに

プルダウンの作成は、HTMLだと<section> を用いたり、

<select>: HTML 選択要素 - HTML: ハイパーテキストマークアップ言語 | MDN
<select> は HTML の要素で、選択式のメニューを提供するコントロールを表します。

最近流行りのshadcn/uiだとSelectを用いてみたり、

Select
Displays a list of options for the user to pick from—triggered by a button.

と比較的簡単に実装することができます。しかし、上記の内容は自分でカスタマイズしにくいという難点…。
上記の内容を使わずに実際に作ってみようとすると、

  • なんかトグルの開閉が上手くいかない
  • 選択肢の部分はどこにいった?
  • 自分が欲しい位置になかなか合わない…

など、すごく頭を悩ませる事態になりました。
今回は、そのつまづきやすいポイントを押さえながら、段階ごとに分けてトグルボタンの作成について紹介していきます!

2.完成図と環境構築

完成図

以下に完成品とコードを置きました! (こちらはstorybook未導入です)

toggleButton - StackBlitz
Next generation frontend tooling. It&#39;s fast!

ディレクトリ構成

環境構築

  • viteを使って、Reactのプロジェクトを作成します(ReactとTypeScript を選択してください)
npm create vite@latest

  • Storybookの導入
    自分はStorybook上でも確認したかったので導入しました。無くても作成できるので飛ばしても大丈夫です。
npx storybook@latest init

  • Tailwind CSSの導入
    (要注意!)ただインストールするだけでは使うことができません!
npm install -D tailwindcss postcss autoprefixer
インストール
npx tailwindcss init
Tailwind CSSのConfigを作成する

次にファイルを操作していきます

  • tailwind.config.jsの「content」の部分に、Tailwind CSSを反映させたいファイルを書く。
/** @type {import('tailwindcss').Config} */
export default {
  content: ["App.tsx", "./src/**/*.{js,jsx,ts,tsx}"],//←この部分
  theme: {
    extend: {},
  },
  plugins: [],
};
tailwind.config.js
  • プロジェクトを作成したときに出てきた、index.css に以下の内容を追加する。
    tailwind.cssなど新たにファイルを作成し、その中に以下の内容を記述しても構いません。)
@tailwind base;
@tailwind components;
@tailwind utilities;
index.css
  • postcss.config.cjs というファイルを作成し、以下の内容を追加する。(自分もここで詰まりました)
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};
postcss.config.cjs
  • Storybookを導入した人はpreview.tsに以下のimport文を追加する。
    (tailwind.cssなど新たにファイルを作成している場合は、import文の参照パスもそのファイルが指定できるように変えてください。)
import "../src/index.css";
preview.ts

これで環境構築が完了です!

3.useStateで選択肢の開閉をしてみる

ボタンを押すと、選択肢の部分が開いたり閉じたりする実装を行います
開閉だけだと「イベントハンドラ(onClick)」と状態管理の「useState」を用いる事で上手くいきそうです

import "./App.css";
import ExampleToggle from "./components/ui/toggle/toggle";

function App() {
  return (
    <>
      <div>
        <p>選択してください</p>
        <ExampleToggle />
      </div>
    </>
  );
}

export default App;
App.tsx
import * as React from "react";
import { useState } from "react";

export const ExampleToggle: React.FC = () => {
  const [isToggle, setIsToggle] = useState(false);
  
  const lists = [
    { value: "option1" },
    { value: "option2" },
    { value: "option3" },
    { value: "option4" },
    { value: "option5" },
    { value: "option6" },
  ];

  const changeToggle = () => {
    // setIsToggle((prev) => !prev);と書く方がコードが見やすい.
    // 慣れるまでは以下のように書くのがいいかも.
    setIsToggle(true);
    if (isToggle) {
      setIsToggle(false);
    }
  };

  const closeToggle = () => {
    setIsToggle(false);
  };

  return (
    <>
      <button onClick={changeToggle}>{isToggle ? "▲" : "▼"}</button>
      {isToggle && (
        <>
          {lists.map((option) => (
            <button key={option.value} onClick={closeToggle}>
              {option.value}
            </button>
          ))}
        </>
      )}
    </>
  );
};
export default ExampleToggle;
toggle.tsx
import { Meta, StoryObj } from "@storybook/react";

import { ExampleToggle } from "./toggle";

const meta: Meta<typeof ExampleToggle> = {
  component: ExampleToggle,
};

export default meta;

type Story = StoryObj<typeof ExampleToggle>;

export const Default: Story = {
  args: {},
};
toggle.stories.tsx (Storybookを導入した人用)

(注意!)単にoptionと指定すると、何を指しているのかが分からないのでエラーが起こります。listという配列のvalueを表示させたいのでoption.valueとしてあげましょう

 {list.map((option) => (
    <button key={option.value} onClick={closeToggle}>
       {option.value}
    </button>
  ))}
toggle.tsxのコードの一部 option.valueがポイント

4.Tailwind CSSで調整してみよう

物足りなさが半端ないので、Tailwind CSSで調整していきましょう。
枠組みや幅の調整がメインです

【補足】Tailwind CSSとは?

公式によると「A utility-first CSS framework」、実用性の高いCSSフレームワークということである。実際に使うと分かるが、通常のCSSよりコード量が短くなるというメリットがある。 ただ、通常のCSSと書き方が大幅に変わってくるので、使い始めはなかなか慣れないというデメリットもある。(慣れれば楽しいです!)
Tailwind CSS - Rapidly build modern websites without ever leaving your HTML.

また、書き方をまとめてくれているサイトが存在しているので、そちらも参考にしてほしいです!
Tailwind CSS CheatSheet for Beginners and Not Only

 <div className="border-2 flex flex-row gap-5">
    <p>選択してください</p>
    <ExampleToggle />
App.tsx (変更部分のみ表示)
return (
    <div className="flex flex-col gap-1">
      <div className="flex flex-col">
        <div
          className="w-[25px] bg-[#C0C0C0] text-center cursor-pointer hover:bg-[#BFC5CA]"
          onClick={changeToggle}
        >
          {isToggle ? "▲" : "▼"}
        </div>
      </div>
      {isToggle && (
        <div>
          <div className="flex flex-col overflow-y-scroll h-auto max-h-[200px]">
            {lists.map((option) => (
              <button key={option.value} onClick={closeToggle}>
                {option.value}
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
toggle.tsx (return文の中を書き換えます)

【補足】
overflow-y-scroll h-auto max-h-[200px]
選択肢の表示する高さを制限し、幅を超えたらスクロールするという実装です。
y方向をスクロール、表示高さは200pxまでという内容ですね。

途中経過はこんな感じになるかと思います!

5.大きさを引っ張ってくる、位置を動かす

今のままだと、選択肢が枠組みより大きくなっている状況です。今回はuseRefを用いて、親のコンポーネント(App.tsx)から幅を取得し、子のコンポーネント(toggle.tsx)に値を渡してあげましょう。

import "./App.css";
import ExampleToggle from "./components/ui/toggle/toggle";
import { useEffect, useState, useRef } from "react";

function App() {
  const [titleWidth, setTitleWidth] = useState<number>(0);
  const titleRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (titleRef.current) {
      setTitleWidth(titleRef.current.offsetWidth);
    }
  }, []);

  return (
    <>
      <div className="border-2 flex flex-row gap-5" ref={titleRef}>
        <p>選択してください</p>
        <ExampleToggle titleWidth={titleWidth} />
      </div>
    </>
  );
}

export default App;
App.tsx

幅はoffsetWidthで取得することができ、その値をtitleWidthというstateにて管理しています。useEffectを用いる事で1回だけ実行するようにしています。(これがないと実行されません)

(要注意!)どこを参照するか分からないので、<div>にref={titleRef}をつけ忘れないようにしましょう!

/*省略*/
interface toggleProps {
  titleWidth: number;
}

export const ExampleToggle: React.FC<toggleProps> = ({ titleWidth }) => {

/*省略*/

}
return(

/*変更部分のみ表示*/
 {isToggle && (
        <div style={{ width: `${titleWidth}px` }}>
toggle.tsx

TypeScriptは型定義が基本!なので、propsにも型をつけてあげましょう。
分割代入であるので、titleWidthを{ }に挟めることを忘れないでください!)

最後に位置調整です。固定するものにrelative、動かすものにabsoluteをつけることによって、選択肢を自由に動かすことができます!

<div className="border-2 flex flex-row gap-5 relative" ref={titleRef}> 
App.tsx
 {isToggle && (
        <div 
	        className="absolute z-1 top-full left-0" //追加
	        style={{ width: `${titleWidth}px` }}>
toggle.tsx
【活用例】relative,absoluteを使う

buttonと選択肢の場所を変え、さらにtop-fullbottom-fullに変えました。
これで配置の心配も無くなりますね!

これでプルダウンボタンが完成です!

6.チェックマークをつけてみよう

ボタンは完成しましたが、何が選択されているかいまいちよく分かりませんよね。現在選択されているものにチェックマークをつけてみましょう!基本はuseStateです!

export const ExampleToggle: React.FC<toggleProps> = ({ titleWidth }) => {

      // 省略

      const [selectedOption, setSelectedOption] = useState<string>("");//追加

      // closeToggleの中身を変える
      const closeToggle = (value: string) => {
            const newValue = selectedOption === value ? "" : value;
            setSelectedOption(newValue);
            setIsToggle(false);
	};
	
	return(
	/*省略*/
	{isToggle && (
        <div
          className="absolute z-1 top-full left-0"
          style={{ width: `${titleWidth}px` }}
        >
          <div className="flex flex-col overflow-y-scroll m-[4px] h-auto max-h-[200px]">
            {lists.map((option) => (
              <button
                className="flex flex-row justify-between items-center cursor-pointer"
                key={option.value}
                onClick={() => closeToggle(option.value)}
              >
                <p>{option.value}</p>
                {selectedOption === option.value && <p>✅</p>}
              </button>
            ))}
          </div>
        </div>
      )}
    /*以下省略*/
}

7.おわりに

ここまで読んでくださってありがとうございました! 簡易的ではありますが、少しだけReact,TypeScript,Tailwind CSSの知識が身についたかと思います。このプルダウンはフィルター機能にも多く使われていたりするので覚えておいて損は無いと感じます!
理解しずらいところだと思うので、少しでも手を動かして慣れていってもらえばと思います!

明日はhanakaさんです!よろしくお願いします!

参考サイト一覧