💡

dialogタグを知っていますか? Reactでdialogタグを使ったDialog・Modal Componentの作成方法

2023/09/23に公開

こんにちは、AIQ株式会社のフロントエンドエンジニアのまさぴょんです!

今回は、Reactでdialogタグを使ったDialog・Modal Componentの作成方法について解説していきます。

dialogタグを知っていますか?

あなたは、dialogタグを知っていますか?

今まで、Dialog・Modalなどを実装するときは、MUIなどのUIライブラリを使用することが多かったのですが。。。

今回、新規プロジェクトで、MUIを使わない状態で、ゼロイチでDialog・Modal Componentを作成する際に何かいいものはないかと調べていたら、dialogタグを発見しました。

https://developer.mozilla.org/ja/docs/Web/HTML/Element/dialog

dialogタグの使い方

それでは、React で、dialogタグを使った Dialog Componentを作成する前に、dialogタグの使い方について解説していきます。

dialogタグの使い方について、すでにご存知の方は、ここのSectionは、スキップしてください。

dialogタグの使い方は、すでに個人Blogの方にまとめていますが、改めて、まとめたものをこちらにも掲載しておきます。
https://masanyon.com/html-javascript-dialog-tag-open-show-modal/

dialogタグのブラウザ対応状況

まず、一番重要な各ブラウザのdialogタグの対応状況は次のとおりです。

各ブラウザで、対応済みなので、問題なく使用できることで一安心です。

引用: MDN dialog要素

dialogタグの使い方と特徴

dialogタグを使用する上で押さえておきたい使い方と特徴は、次のとおりです。

dialogタグの使い方と特徴
  1. dialog要素にはopen属性をつけない場合は何も表示されないです。
    • 実態は、open属性の有無によるdisplay属性のnoneblockかのSwitchになります。
    • つまり、dialog要素にopen属性を追加することで、Dialogが表示されるので、動的なopen属性の追加が必要になります。
  2. HTMLDialogElement.show()またはHTMLDialogElement.showModal()を使用することで、open属性を追加して、Dialogを表示することができます。
    • HTMLDialogElement.show()は、open属性を追加するだけのメソッドです。
    • HTMLDialogElement.showModal()は、open属性の他にtop-layerbackdropの属性も追加するメソッドです。
      • top-layerは、z-indexを設定しなくても一番上の Layer として表示される属性です。
      • backdropは、Dialogの背景色を設定するための属性です。
  3. HTMLDialogElement.close()を使用することで、open属性を削除して、Dialogを非表示にすることができます。

それでは、上記の特徴を理解した上で、SampleCodeとその実行結果を見ていきます。

dialogタグで作るDialogのSampleCodeと実行結果

今回のDialogのSampleCodeを実行すると、初期画面は、次のような Openボタンが表示されているだけです。

そして、これをClickすると、次のような Dialogが中央に表示されます。

上記のSampleCodeは、次のとおりです。
要点には、Numberと解説を掲載しています。

dialog.html
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Dialog-Test</title>
  </head>
  <style>
    /* 
      1. 背景のスクロールの停止
      => ページ内のコンテンツが多くスクロールが可能なページで、Dialog を表示すると、 
      => Dialog が中央に表示されていても背景のスクロールを行うことができます。
      => Dialog が表示された状態で背景のスクロールを停止するために 次のようなStyleを追加します。
    */
    html:has(dialogアコーディオンにtitleを入力してください) {
      overflow: hidden;
    }
    /* 2. dialog の背景は backdrop で設定されているので backdrop に背景色を設定することで指定した色に変わります。 */
    dialog::backdrop {
      background-color: skyblue;
      /* background-color: rgb(250, 250, 250, 0.5); */
    }
    /* 3. 「padding: 0」を追加で、Dialog内部の paddingでのClick判定を無効化する */
    .custom-dialog {
      padding: 0;
      width: 500px;
      border: 1 solid black;
      border-radius: 8px;
    }
  </style>
  <body>
    <div>
      <button class="openBtn">Open</button>

      <!-- 4. dialogタグ -->
      <dialog class="custom-dialog">
        <!-- 
          5. divタグ 
          => style を設定することで dialog の領域一杯にクリック領域を調整しています 
          => dialog 要素の中に div 要素を追加することで、追加した div 要素内であればクリックしたとしても Dialog を非表示にさせないように設定を行います。
        -->
        <div style="padding: 1em">
          <h2>Dialog</h2>
          <button class="closeBtn">Close</button>
        </div>
      </dialog>

      <script>
        // 6. Dialog & openBtn & closeBtn Element
        const dialog = document.querySelector("dialog");
        const openBtn = document.querySelector(".openBtn");
        const closeBtn = document.querySelector(".closeBtn");

        openBtn.addEventListener("click", () => {
          // 7. HTMLDialogElement.showModal()
          // => open属性追加 & top-layer & backdrop の追加
          // => top-layer は z-index を設定しなくても一番上の Layer として表示されるので、
          // => Dialog が表示された場合に下の Layer にアクセスすることができせん。
          // => backdrop は背景色を設定するために利用することができます。デフォルトでは薄いグレーが設定されています。
          dialog.showModal();
        });

        closeBtn.addEventListener("click", () => {
          // 8. HTMLDialogElement.close() を使用することで、open属性を削除して、Dialogを非表示にすることができます。
          dialog.close();
        });

        dialog.addEventListener("close", (e) => {
          // 9. close イベント
          // => Dialog を非表示にする場合に close イベントを設定することで Dialog が非表示になったことを検知することができます。
          console.log("Dialog が Close されました");
        });

        /**
         * 10. Dialog の外側のクリック
         * => Dialog の外側にある背景をクリックすることで Dialog を非表示にする場合の設定を確認していきます。
         * => モーダルウィンドウでは背景をオーバーレイと呼ぶことがありますが、オーバーレイをクリックした場合に表示されている Dialog を非表示にします。
         */
        dialog.addEventListener("click", (event) => {
          console.log("Dialog_Click");
          console.log(event.target);
          // ポイント1: 外側の領域も含めた DialogElementを Clickしていたら、Closeする
          // ポイント2: dialogタグ内部の divタグより内側は、対象外にする
          if (event.target === dialog) {
            dialog.close();
          }
        });
      </script>
    </div>
  </body>
</html>

Dialogの領域展開・Dialogの外側のクリックと、内側のクリックの判定制御について

Dialog の外側のクリックと、内側のクリックの判定制御について、Logicは、SrcCodeに記載してある解説のとおりになります。

ポイントをまとめると、次のとおりです。

Dialogの領域展開・Dialogの外側のクリックと、内側のクリックの判定制御Logicまとめ
  1. Dialogは、HTMLDialogElement.showModal()で表示しているので、top-layerに領域展開している。
    • 周りの背景部分も、Dialogの領域。
  2. dialogタグにpadding: 0を追加で、Dialog内部のpaddingでのClick判定を無効化する。
  3. dialogタグ直下のdivタグをstyle設定で、Dialog内部すべてを満たすようにする。
  4. dialogタグのclickイベントの際に、dialogタグかdialog以外かで、判定する。

Reactでdialogタグを使ったDialog・Modal Componentの作成方法

dialogタグの理解ができたところで、本題の Reactでdialogタグを使ったDialog・Modal Componentの作成方法に移ります。

作成するDialog Componentのイメージ

まずは、作成するDialog Componentのイメージを共有します。

作成するのは、次のような DialogをOpenするためのボタンと、

次のような Dialogになります。

dialogタグを使ったDialog ComponentのSampleCode

Dialog ComponentのSampleCodeは、次のとおりです。

Dialog Componentでの処理の要点をまとめると、次のとおりです。

Dialog Componentを呼ぶための処理の要点
  1. useRef()で、dialogタグを参照する。
  2. useEffect()を使って、isOpenの変更を検知して、開閉処理を制御する。
  3. showModal()で、Dialogを開くので、top-layerでDialogを領域展開する。
  4. dialogと、Dialog内部のdivのStyleで、展開した領域の範囲を制御する。
  5. Dialogの内側は、CloseBtn以外では、閉じられないようにevent.stopPropagation();で、EventをCancelする。
CustomDialog.tsx
import { CSSProperties, useCallback, useEffect, useRef } from "react";

/** Propsの型定義 */
interface PropsType {
  /** 開閉を管理する Flag */
  isOpen: boolean;
  /** Dialogを閉じるためのFunction */
  onClose?: VoidFunction;
  dialogStyle?: CSSProperties;
  dialogInnerStyle?: CSSProperties;
}

/**
 * NOTE: CustomDialog
 * => dialogタグを使って、作成した Custom 可能な Dialog Component
 */
const CustomDialog = (props: PropsType) => {
  const { onClose, isOpen, dialogStyle, dialogInnerStyle } = props;

  /**
   * Dialog_Element
   * => useRef() で、dialogタグを参照する
   */
  const dialogRef = useRef<HTMLDialogElement>(null);

  /** isOpen の変更を検知して動作する */
  useEffect((): void => {
    const dialogElement = dialogRef.current;
    if (!dialogElement) {
      return;
    }
    // 1. Dialog Open Flag === true
    if (isOpen) {
      // 1-2. 属性値: open が Set されていたら、処理終了
      if (dialogElement.hasAttribute("open")) {
        return;
      }
      // 1-3. showModal()で、Dialogを表示する
      // => top-layer と backdrop の設定も自動で追加される
      dialogElement.showModal();
    } else {
      // 2. Dialog Open Flag === false
      // 2-1. 属性値: open が Set されていなかったら、処理終了
      if (!dialogElement.hasAttribute("open")) {
        return;
      }
      // 2-2. close()で、Dialogを閉じる
      dialogElement.close();
    }
  }, [isOpen]);

  /**
   * Dialog を Close する Func
   * => Closeボタン or Dialog の外部領域を Click すると Call
   */
  const onCloseDialog = useCallback((): void => {
    onClose?.();
  }, [onClose]);

  /** Dialog の 内部領域の Close Event を Cancel する */
  const handleClickContent = useCallback(
    (event: React.MouseEvent<HTMLDivElement>): void => {
      // clickイベントの伝搬を止める。
      event.stopPropagation();
      console.log("Dialog Click");
    },
    []
  );

  /** Dialog の DefaultStyle */
  const defaultDialogStyle = {
    padding: "0" /** PaddingもDialog外部領域と判定されるので、0にする */,
    position: "absolute",
    top: "50%",
    left: "50%",
    transform: "translate(-50%, -50%)",
    border: "1px solid black",
    borderRadius: "8px",
  } as CSSProperties;

  /** Dialogの Inner Div の Style */
  const defaultDialogInnerStyle = {
    width: "500px",
    height: "500px",
    padding: "1em",
    textAlign: "center",
    display: "flex",
    flexDirection: "column",
    justifyContent: "space-evenly",
    alignItems: "center",
  } as CSSProperties;

  return (
    <dialog
      ref={dialogRef}
      onClick={() => {
        onCloseDialog();
      }}
      style={dialogStyle ? dialogStyle : defaultDialogStyle}
    >
      {/* Dialog Inner 領域 => 内側は、CloseBtn 以外では、閉じられないように EventCancel する */}
      <div
        onClick={(e) => {
          handleClickContent(e);
        }}
        style={dialogInnerStyle ? dialogInnerStyle : defaultDialogInnerStyle}
      >
        <h2>Dialog_Test</h2>
        <button
          onClick={() => onCloseDialog()}
          style={{ width: "60px", height: "30px" }}
        >
          Close
        </button>
      </div>
    </dialog>
  );
};
export default CustomDialog;

上記のDialog Componentを親Componentでは、次のように使います。

親Componentでの処理の要点をまとめると、次のとおりです。

親Componentで、Dialog Componentを呼ぶための処理の要点
  1. Dialog の開閉を管理する Flagを用意する
  2. Openのボタンと、Dialogを開くOpenFuncを用意する。
  3. Dialogを閉じるCloseFuncを用意して、Dialog Component に CallBackFuncとして渡す。
Home.tsx
import { useState } from "react";
import CustomDialog from "./atoms/dialog/CustomDialog";

const Home = () => {
  /** Dialog の開閉を管理する Flag */
  const [isOpen, setIsOpen] = useState(false);
  /** Dialog を開く Func */
  const openDialog = () => {
    setIsOpen(true);
  };
  /** Dialog を閉じる Func */
  const closeDialog = () => {
    setIsOpen(false);
  };

  return (
    <div>
      {/* Dialogを Openする Btn */}
      <div style={{ textAlign: "center" }}>
        <button onClick={() => openDialog()}>Dialog Open</button>
      </div>

      {/* Dialog_Component */}
      <CustomDialog isOpen={isOpen} onClose={closeDialog} />
    </div>
  );
};

export default Home;

まとめ

dialogタグを活用することで、シンプルにDialogを作成できることがわかりました。

また、Reactでの実装でも簡単に、Customize可能なDialogを作成することが可能だとわかったので、活用していきたいと思います。

[参考・引用]

https://masanyon.com/html-javascript-dialog-tag-open-show-modal/

注意事項

この記事は、AIQ 株式会社の社員による個人の見解であり、所属する組織の公式見解ではありません。

求む、冒険者!

AIQ株式会社では、一緒に働いてくれるエンジニアを絶賛、募集しております🐱🐹✨

エンジニア視点での我が社のおすすめポイント

  1. フルリモート・フルフレックスの働きやすい環境!
    • 前の会社でアサインしてた現場は、フル出社だったので、ありがたすぎる。。。
    • もうフル出社には、戻れなくなります!
  2. 経験豊富なエンジニアの先輩方
    • 私は、3年目の駆け出しエンジニアなので、これが、かなりありがたいです!
  3. 自社開発とR&D(受託開発)を両方している会社なので、経験できる技術が多い。
    • 自社のProduct開発と、他社からの受託案件で、いろいろな技術を学ぶことができます。
  4. AI関連の最新の技術に触れられるチャンスが多い。
    • 自社で特許を持つほど、AI技術に強い会社で、プロファイリングを得意とした技術体系があります。
    • ChatGPTを自社アプリに搭載など、AIトレンドも、もちろん追っており、最新の技術に触れられるチャンスが多いです。
  5. たまに、札幌ラボ(東京から札幌) or 東京オフィス(札幌から東京)に出張で行ける!
    • 東京と、札幌に2拠点ある会社なので、会合などで集まる際に、出張で行けます。

採用技術 (一部抜粋)

  • FrontEnd: TypeScript, JavaScript, React.js, Vue.js, Next.js, Nuxt.js など
  • BackEnd: Node.js, Express,Python など
  • その他技術: Docker, AWS, Git, GitHub など

エントリー方法

  1. 私達と東京か札幌で一緒に働ける仲間を募集しています。
    詳しくは、Wantedly (https://www.wantedly.com/companies/aiqlab)を見てみてください。

Webエンジニア向け説明

https://www.wantedly.com/projects/1089410

データサイエンティスト向け説明

https://www.wantedly.com/projects/1089406

人事に直通(?)・ご紹介Plan(リファラル採用)

私経由で、ご紹介もできますので、興味のある方や気軽にどんな会社なのか知りたい方は、X(旧:Twitter)にて、DMを送ってくれても大丈夫です。
https://twitter.com/masanyon1212

AIQ Tech Blog (有志)

Discussion