JavaScript | 1 日 120 分 × 7 日アプリ学習:モーダルウィンドウ管理

Web APP JavaScript
スポンサーリンク

モーダル管理を“より実務的に強くする”3日目のテーマ

3日目は、1〜2日目で作った Modal クラスModalManager をさらに発展させ、
「アプリで本当に使えるモーダル管理」を完成形に近づける回です。

今日の学習ポイントは次の3つです。

  • クラス設計を“拡張しやすい形”に進化させる
  • UI 状態制御を「状態遷移」として扱う
  • イベント伝播を応用し、複雑なクリック制御を安全にする

そして実装としては、
開く / 閉じる、背景クリック、ESCキー対応を
より堅牢で、複数モーダルでも破綻しない形 に仕上げます。


モーダルの「状態遷移」を導入して UI を安定させる

状態を boolean だけで持つ限界

2日目までは isOpen を true/false で管理していました。

しかし、実際の UI はもっと複雑です。

  • 開き始めた(アニメーション中)
  • 開き終わった
  • 閉じ始めた
  • 閉じ終わった

このように、UI には「途中の状態」が存在します。

状態を文字列で管理する

this.state = "closed"; 
// "opening" | "opened" | "closing" | "closed"
JavaScript

状態遷移の例

open() {
  if (this.state !== "closed") return;

  this.state = "opening";
  this.root.classList.add("is-open");

  setTimeout(() => {
    this.state = "opened";
  }, 200); // アニメーション時間
}
JavaScript

深掘りポイント:状態遷移は UI の“真実の源泉”

状態を文字列で管理すると、

  • 「開いている途中に閉じる」などの事故を防げる
  • アニメーションと状態がズレない
  • 複数モーダルでも状態が衝突しない

UI の安定性が一気に上がります。


背景クリック制御を“誤作動ゼロ”にする応用テクニック

2日目までの背景クリック

背景要素 .modal__backdrop にクリックイベントを付ける方法でした。

これは基本として正しいですが、
実務では次のようなケースが出てきます。

  • モーダルの中にスクロール領域がある
  • モーダル内でドラッグ操作がある
  • モーダル内でクリックイベントを多用する

こうなると、
「背景クリックと中身クリックの判定」が難しくなります。

より安全な判定方法:クリック位置で判断する

handleRootClick(event) {
  if (!this.content.contains(event.target)) {
    this.close();
  }
}
JavaScript

仕組み

  • モーダル全体(root)にクリックイベントを付ける
  • クリックされた要素が content の内側かどうかを判定
  • 内側なら無視、外側なら閉じる

深掘りポイント:contains() は“クリック判定の最強ツール”

element.contains(target)
「target が element の内側にあるか?」を判定します。

これにより、

  • stopPropagation を使わなくてもよい
  • モーダル構造が変わっても壊れない
  • ネストした要素でも正しく判定できる

というメリットがあります。


ESC キー対応を“複数モーダルの優先順位”まで考える

複数モーダルが重なるケース

実務では、
「モーダルの上に別のモーダルが開く」
ということが普通にあります。

例:
設定モーダル → 中の「削除確認」モーダル

このとき、ESC を押したら
一番上のモーダルだけ閉じる
必要があります。

ModalManager にスタック(stack)を導入する

class ModalManager {
  constructor() {
    this.stack = [];
    document.addEventListener("keydown", (event) => {
      if (event.key === "Escape") {
        const top = this.stack[this.stack.length - 1];
        if (top) top.close();
      }
    });
  }

  push(modal) {
    this.stack.push(modal);
  }

  pop(modal) {
    this.stack = this.stack.filter(m => m !== modal);
  }
}
JavaScript

モーダル側で登録する

open() {
  if (this.state !== "closed") return;

  this.manager.push(this);
  this.state = "opened";
}
close() {
  this.manager.pop(this);
  this.state = "closed";
}
JavaScript

深掘りポイント:スタック構造は“UI の重なり”を自然に表現する

スタック(LIFO)は
「最後に開いたものが最初に閉じる」
というモーダルの性質と完全に一致します。


クラス設計を“拡張しやすい形”にする

コールバックを導入する

モーダルが開いたとき、閉じたときに
外部で処理をしたいことがあります。

例:

  • 開いたらフォームを初期化したい
  • 閉じたらスクロール位置を戻したい

コールバックを受け取れるようにする

class Modal {
  constructor(root, options = {}) {
    this.root = root;
    this.onOpen = options.onOpen || (() => {});
    this.onClose = options.onClose || (() => {});
  }

  open() {
    this.state = "opened";
    this.onOpen();
  }

  close() {
    this.state = "closed";
    this.onClose();
  }
}
JavaScript

深掘りポイント:クラスは“拡張ポイント”を持つと強くなる

コールバックを用意しておくと、

  • クラスを改造しなくても機能追加できる
  • 外部から自由に振る舞いを変えられる
  • 再利用性が高くなる

というメリットがあります。


3日目のまとめ

今日あなたがやったことを整理するとこうなります。

  • モーダルの状態を「状態遷移」として扱い、UI を安定させた
  • 背景クリックを contains() 判定でより安全にした
  • ModalManager にスタックを導入し、複数モーダルの優先順位を管理した
  • コールバックを導入して、クラスを拡張しやすくした

どれも、実務でモーダルを扱うときに必ず必要になるテクニックです。

タイトルとURLをコピーしました