MUIでフォーカスした時に Popover を表示する TextField を実装する

こんにちは、Gaji-Labo アシスタントエンジニアの石垣です。

今回は、MUI (Material-UI) を使って、フォーカスした時に Popover を表示する TextField を実装する方法についてまとめたいと思います。

概要

テキストフィールドにフォーカスした時に要素を覆う形でポップオーバーが表示され、ポップオーバー外をクリックすると消える様子のキャプチャ動画

今回は上のような、TextField にフォーカスした時に要素を覆う形でPopover を表示し、入力するかPopover の外をクリックすることでPopover を非表示するコンポーネントを実装します。

MUI v5 には Autocomplete というコンポーネントがあり、ほぼ同等の機能を一つのコンポーネントで再現することが出来ます。(参考:MUI v5 の AutoComplete コンポーネントを触ってみた

しかし今回のようにTextField を覆う形でPopover を表示したいなど込み入ったカスタマイズがしたい場合には、より自由に実装・カスタマイズできるという点で複数コンポーネントを組み合わせて作るのも選択肢としてありかと思います。

実装

TextField コンポーネントと Popover コンポーネントを組み合わせて実装します。

import { useCallback, useState } from "react";
import { TextField, Popover } from "@mui/material";

export function TextFieldWithPopover() {
  // TextField の value
  const [value, setValue] = useState("");

  // Popover の開閉状態
  const [opened, setOpened] = useState(false);

  // Popover の width
  const [popoverWidth, setPopoverWidth] = useState(0);

  // Popover を開いた時に、Popover の幅を TextField と同じ幅にする。
  // リサイズしたとしても Popover と TextField が自動的に同じ幅になる。
  const [popoverAnchorEl, setPopoverAnchorEl] = useState<HTMLDivElement | null>(
    null
  );
  const divRef = useCallback(
    (node) => {
      if (node !== null && opened) {
        setPopoverAnchorEl(node);
        setPopoverWidth(node.clientWidth);
      }
    },
    [opened]
  );

  return (
    <div ref={divRef}>
      {/* Popover を開くための TextField */}
      <TextField
        variant="outlined"
        value={value}
        fullWidth
        onClick={() => {
          setOpened(true);
        }}
        inputProps={{ readonly: true }}
      />
      {/* Popover 本体 */}
      <Popover
        anchorEl={popoverAnchorEl}
        open={opened}
        // スタイル用
        PaperProps={{
          style: {
            width: popoverWidth,
            boxSizing: "content-box",
            margin: "-8px 4px",
            padding: "8px",
          },
        }}
        onClose={() => {
          setOpened(false);
        }}
      >
        {/* Popover 内の入力用 TextField */}
        <TextField
          variant="outlined"
          value={value}
          fullWidth
          onChange={(e) => setValue(e.target.value)}
          autoFocus
        />
        Content
      </Popover>
    </div>
  );
}

実装としては、

  1. TextField (表示用) にフォーカスする
  2. TextField (表示用) を覆うように Popover が表示され、TextField (入力用) が表示用と同じ位置に来る
  3. 2つの TextField は value が同期しており、片方の value が変更されるともう片方も変更される
  4. Popover の外側をクリックすると Popover が閉じる

というものになっています。

ポイントは Popover の anchorEl に useCallback を使用した divRef を渡している点です。

ここで useCallback を介して Popover を開いた時に divRef の width を取得することで、 Popover と TextField の幅がどの画面幅でも常に同じになるようにしています。

リサイズしても TextField と Popover が常に同じ幅になるようになっている

また、 TextField に input 要素が持つ属性を渡したい時は inputProps を使う点や、Popover のスタイルを変更したい時は PaperProps を使って Paper コンポーネントにスタイルを渡す点は少々注意が必要なところです。

これでフォーカスした時にPopover を表示するTextField を実装することができました。

まとめ

今回は、MUI (Material-UI) を使ってフォーカスした時にPopover を表示するTextField を実装する方法についてまとめました。MUI を使用している方の参考にしていただければと思います。

Gaji-Laboでは、React経験が豊富なフロントエンドエンジニアを募集しています

弊社ではReactの知見で事業作りに貢献したいフロントエンドエンジニアを募集しています。大きな制作会社や事業会社とはひと味もふた味も違うGaji-Laboを味わいに来ませんか?

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください。お仕事お問い合わせや採用への応募、共に大歓迎です!

求人応募してみる!

投稿者 Ishigaki Shotaro

アシスタントエンジニアとしてHTML/CSS/JavaScriptの実装やRailsの組み込み、スタイルガイドの構築などを担当しています。 業務の中でさまざまな学びを吸収しながら、文書構造やアクセシビリティに目を向けたマークアップの学習やJavaScriptの学習などを行っています。チームに貢献できるエンジニアとなるために日々奮闘中です。