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

Web APP JavaScript
スポンサーリンク

モーダル管理を“アプリ全体で破綻しないレベル”へ仕上げる 6日目のテーマ

6日目は、1〜5日目で作ってきた Modal クラスModalManager をさらに強化し、
「アプリのどこに置いても壊れないモーダル」を完成形に近づける回です。

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

  • クラス設計を“責務の分離”という観点で整理し直す
  • UI 状態制御を「外部環境(スクロール・フォーカス)」まで含めて扱う
  • イベント伝播を応用し、複雑な UI でも誤作動しない仕組みを作る

実装としては、
開く / 閉じる、背景クリック、ESCキー対応を
アプリ全体の UX と整合する形 に仕上げます。


モーダルが開いている間の「外部スクロール」を制御する

モーダルが開いているのに背景がスクロールする問題

初心者が最初にぶつかるのがこれです。

  • モーダルを開く
  • 背景がスクロールしてしまう
  • モーダルがズレて UX が崩れる

これは、モーダルを開いたときに
body のスクロールを止めていない ことが原因です。

スクロールを止める実装

lockScroll() {
  document.body.style.overflow = "hidden";
}

unlockScroll() {
  document.body.style.overflow = "";
}
JavaScript

open() と close() に組み込む

open() {
  if (this.state !== "closed") return;
  this.state = "opening";
  this.root.classList.add("is-open");
  this.lockScroll();
}
close() {
  if (this.state !== "opened") return;
  this.state = "closing";
  this.root.classList.remove("is-open");
  this.unlockScroll();
}
JavaScript

深掘りポイント:スクロール制御は「モーダルの責務」

モーダルが開いている間は
ユーザーの視線をモーダルに固定する 必要があります。

そのためには、

  • 背景スクロール禁止
  • 背景クリック制御
  • ESC キー対応

これらがセットで必要になります。


フォーカス制御で「キーボード操作に強いモーダル」にする

モーダルが開いたときの問題

  • Tab キーで背景のボタンにフォーカスが移動してしまう
  • キーボード操作がモーダルの外に飛んでしまう

これはアクセシビリティの観点でも NG です。

解決策:モーダル内にフォーカスを閉じ込める

trapFocus(event) {
  const focusable = this.root.querySelectorAll(
    'button, a, input, textarea, select, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  if (event.key === "Tab") {
    if (event.shiftKey && document.activeElement === first) {
      event.preventDefault();
      last.focus();
    } else if (!event.shiftKey && document.activeElement === last) {
      event.preventDefault();
      first.focus();
    }
  }
}
JavaScript

open() でフォーカスをセット

open() {
  this.state = "opening";
  this.root.classList.add("is-open");
  this.lockScroll();
  this.root.addEventListener("keydown", this.trapFocus.bind(this));
  this.content.querySelector("button, input")?.focus();
}
JavaScript

深掘りポイント:フォーカス制御は“プロ品質のモーダル”の必須要素

実務では、
フォーカスが外に逃げるモーダルは不具合扱い です。

キーボード操作だけで使える UI を作るためにも、
フォーカス制御は欠かせません。


背景クリック制御を「複数レイヤー対応」にする

モーダルの上に別のモーダルが開くケース

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

このとき、背景クリックの判定が難しくなります。

解決策:ModalManager に「最前面モーダル」を問い合わせる

class ModalManager {
  getTopModal() {
    return this.stack[this.stack.length - 1];
  }
}
JavaScript

モーダル側で判定する

handleRootClick(event) {
  const top = this.manager.getTopModal();
  if (top !== this) return; // 最前面でなければ無視

  const isInside = this.safeAreas.some(el => el.contains(event.target));
  if (!isInside) this.close();
}
JavaScript

深掘りポイント:複数モーダルがあるときは“最前面だけが操作対象”

ユーザーが見ているのは最前面のモーダルだけです。
背景クリックも ESC も、
最前面のモーダルだけが反応する のが正しい挙動です。


ESC キー対応を「状態遷移」と連動させる

4〜5日目の ESC 対応

  • ESC で閉じる
  • disableEsc で無効化できる

ここに 6日目では
状態遷移との連動 を追加します。

実装

handleKeydown(event) {
  if (event.key !== "Escape") return;
  if (this.disableEsc) return;
  if (this.state !== "opened") return;

  this.close();
}
JavaScript

深掘りポイント:アニメーション中の ESC は絶対に無効

アニメーション中に ESC を受け付けると、

  • 開き途中で閉じる
  • 閉じ途中でさらに閉じる
  • 状態がズレる

という UI 崩壊が起きます。

状態遷移と ESC を連動させることで
モーダルの動きが常に一貫する ようになります。


クラス設計を「責務の分離」で整理し直す

Modal の責務

  • UI の状態管理
  • 開く / 閉じる
  • フォーカス制御
  • 背景クリック制御
  • アニメーション同期

ModalManager の責務

  • 複数モーダルのスタック管理
  • ESC キーの一元管理
  • 最前面モーダルの判定

深掘りポイント:責務を分けると“壊れにくくなる”

UI コンポーネントは、
1つのクラスに責務を詰め込みすぎると壊れやすい
という性質があります。

Modal と ModalManager を分けることで、

  • どこを直せばいいか分かりやすい
  • 機能追加がしやすい
  • バグが起きにくい

というメリットが生まれます。


6日目のまとめ

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

  • モーダル開閉時のスクロール制御を導入し、背景の動きを止めた
  • フォーカス制御を追加し、キーボード操作に強いモーダルにした
  • 背景クリックを「最前面モーダルだけが反応する」形にした
  • ESC キー対応を状態遷移と連動させ、アニメーション中の誤作動を防いだ
  • Modal と ModalManager の責務を整理し、拡張しやすい設計にした

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


7日目では、
「モーダルをアプリ全体の UI コンポーネントとして完成させる」
という最終仕上げに入ります。

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