JavaScript | DOM 操作:イベント発展 – stopPropagation

JavaScript JavaScript
スポンサーリンク

stopPropagation とは何か

stopPropagation は「イベントが親要素へ伝わっていく流れ(伝播)を止める」ためのメソッドです。クリックなどのイベントは、最初に操作された要素から外側へ向かって段階的に届きます(バブリング)。ここが重要です:親でイベント委譲(親に1つだけリスナーを付けて子の操作に反応)している設計では、安易な stopPropagation が“親の正しい処理”を遮ってバグになります。必要な場面だけ、理由を明確にして使いましょう。


伝播の仕組み(キャプチャ → ターゲット → バブリング)

段階のイメージと出力の違い

<div id="outer" style="padding:10px;border:1px solid #ccc">
  <button id="inner">押す</button>
</div>
<script>
  outer.addEventListener("click", () => console.log("キャプチャ:外"), { capture: true });
  inner.addEventListener("click", () => console.log("ターゲット:内"));
  outer.addEventListener("click", () => console.log("バブリング:外"));
</script>
HTML

イベントは「キャプチャ(外側から内側へ探す段階)→ターゲット(実際に押された要素)→バブリング(内側から外側へ戻る段階)」の順に処理されます。ここが重要です:stopPropagation は“今の段階より外へ行かせない”。バブリング中に止めれば親へ届きません。キャプチャ中に止めれば、内側に降りずに遮断できます。


基本の使い方(親の処理を止める)

内側だけで完結させ、親へ伝えない

<div id="card" style="padding:10px;border:1px solid #ccc">
  <button id="delete">削除</button>
</div>
<script>
  card.addEventListener("click", () => console.log("カードクリック"));
  delete.addEventListener("click", (e) => {
    e.stopPropagation();               // 親へ伝えない
    console.log("削除ボタンの専用動作");
  });
</script>
HTML

ここが重要です:delete のクリックで card のクリック処理が走ると困る設計なら、内側で stopPropagation。対策前提が“親の処理を走らせないこと”にある場合に限定します。


preventDefault との違い(役割を絶対に混同しない)

既定動作と伝播は別もの

<a id="link" href="/next">次へ</a>
<script>
  link.addEventListener("click", (e) => {
    // e.preventDefault();    // ページ遷移を止める(既定動作)
    // e.stopPropagation();   // 親へイベントを伝えない(伝播)
  });
</script>
HTML

preventDefault は「ブラウザの既定動作(遷移・送信・スクロール等)」を止めます。stopPropagation は「親へイベントが届くのを止めます」。ここが重要です:遷移を止めたいのに stopPropagation を使うのは誤用。役割を明確に分けましょう。


stopImmediatePropagation(同じ要素内の他ハンドラも止める)

同一要素に複数のリスナーがある場合の完全停止

<button id="btn">押す</button>
<script>
  btn.addEventListener("click", () => console.log("A"));
  btn.addEventListener("click", (e) => { e.stopImmediatePropagation(); console.log("B(ここで完全停止)"); });
  btn.addEventListener("click", () => console.log("C"));
</script>
HTML

ここが重要です:stopPropagation は“親への伝播”だけ止めます。stopImmediatePropagation は“同じ要素の後続ハンドラ”も含めて完全停止。強力なので乱用は避け、明確な理由があるときだけ使います。


イベント委譲との付き合い方(極力、条件分岐で解決する)

親一箇所で子の操作を選別する

<ul id="list"></ul>
<script>
  list.addEventListener("click", (e) => {
    const del = e.target.closest(".delete");
    if (del) { del.closest("li")?.remove(); return; }

    const edit = e.target.closest(".edit");
    if (edit) { console.log("編集"); return; }

    // 他のクリックは無視
  });
</script>
HTML

ここが重要です:委譲設計では“親で選別”すれば stopPropagation が不要です。closest と条件分岐で意図した対象だけ処理し、他は何もしない。これが規模に強く、保守性も高い基本パターンです。


よくある使用場面(外クリック検知・ネストしたUI・モーダル)

メニューの外クリックで閉じる(stopPropagationでメニュー内クリックを除外)

<div id="menu" class="open">
  <button id="toggle">開閉</button>
</div>
<script>
  toggle.addEventListener("click", (e) => { e.stopPropagation(); /* メニュー内クリックは外へ伝えない */ });

  document.addEventListener("click", () => {
    menu.classList.remove("open");   // 外側クリックで閉じる
  });
</script>
HTML

ここが重要です:内側で stopPropagation を使い、外側(document)で“外クリック”を受ける。外クリック検知の定番パターンです。

ネストしたカードの二重反応を防ぐ

<div id="card" style="padding:10px;border:1px solid #ccc">
  <div id="chip" style="display:inline-block;padding:4px;border:1px solid #aaa">タグ</div>
</div>
<script>
  card.addEventListener("click", () => console.log("カード選択"));
  chip.addEventListener("click", (e) => { e.stopPropagation(); console.log("タグ編集"); });
</script>
HTML

ここが重要です:同じクリックで“カード選択”と“タグ編集”が重なるのを避ける。UIの責務が衝突する箇所でのみ使います。

モーダルの中は操作可、背景クリックで閉じる

<div id="backdrop" style="position:fixed;inset:0;background:rgba(0,0,0,.4)">
  <div id="modal" style="width:280px;margin:80px auto;background:white;padding:12px">内容</div>
</div>
<script>
  modal.addEventListener("click", (e) => e.stopPropagation()); // モーダル内のクリックは外へ伝えない
  backdrop.addEventListener("click", () => backdrop.remove()); // 背景クリックで閉じる
</script>
HTML

ここが重要です:背景と内容で“役割が違う”ため、内側は遮断、外側で閉じる。直感に沿った設計になります。


落とし穴と回避策(乱用しない・デバッグしやすく)

委譲の前提を壊す乱用

stopPropagation をあちこちに入れると、親の処理が予期せず止まり、拡張や修正が難しくなります。ここが重要です:まず委譲で“親が選別”する設計にし、それでも衝突する箇所だけに限定して使う。

preventDefault と併用の混乱

既定動作を止めたいのに stopPropagation を使ってしまうミスに注意。ここが重要です:遷移・送信・スクロールを止めるのは preventDefault。伝播を止めるのは stopPropagation。両者は別の目的。

同一要素の複数ハンドラが残る

部分的に止めても別のハンドラが動いてしまう場合は、stopImmediatePropagation を検討。ただし強すぎるため、構造的な整理(ハンドラを一つに集約)を優先します。

シャドウDOMや複雑なツリー

想定外の target になることがあります。ここが重要です:e.composedPath() で実際の通過経路を確認し、closest と contains で“意図した塊”を基準に判定するのが堅牢です。


実践例(外クリック検知の完全版、キーボード混在)

外クリック検知(contains で範囲判定)

<div id="menu" class="open">…</div>
<script>
  document.addEventListener("click", (e) => {
    if (!menu.classList.contains("open")) return;
    if (!menu.contains(e.target)) menu.classList.remove("open");
  });

  // メニュー内クリックは外へ伝えない
  menu.addEventListener("click", (e) => e.stopPropagation());
</script>
HTML

ここが重要です:stopPropagation と contains 判定を組み合わせると誤判定が減ります。先に“範囲チェック”できるなら stopPropagation なしでも設計可能です。

キーボードとクリックの両対応(アクセシビリティ)

<button id="open">開く</button>
<div id="dialog" hidden role="dialog" aria-modal="true">…</div>
<script>
  open.addEventListener("click", () => dialog.hidden = false);
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape" && !dialog.hidden) dialog.hidden = true;
  });
  dialog.addEventListener("click", (e) => e.stopPropagation()); // 内側操作は外へ伝えない
  document.addEventListener("click", () => { if (!dialog.hidden) dialog.hidden = true; });
</script>
HTML

ここが重要です:クリックの伝播制御に加えて、Esc の既定動作を設計に取り込み、誰でも直感的に閉じられる UI にします。


まとめ

stopPropagation は「イベントの伝播」を止めるためのメソッドです。preventDefault(既定動作停止)とは目的が違うことを徹底理解し、委譲で“親が選別”する設計を基本に、衝突する箇所だけ最小範囲で使う。強力版の stopImmediatePropagation は乱用せず、必要時のみ。外クリック検知やモーダル・ネストUIで“内側は遮断、外側で反応”を実現するのが典型。伝播の段階(キャプチャ/ターゲット/バブリング)を意識し、closest・contains・composedPath を活用すれば、初心者でも読みやすく壊れにくいイベント制御が書けます。

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