私が歌川です

@utgwkk が書いている

イベントハンドラを設定した要素内にreact-modalのモーダルを置くとイベントハンドラが反応する

tl;dr

イベントハンドラを設定した要素内にreact-modalのモーダルを置かないようにするのが手っ取り早そう。

イベントハンドラを設定したコンポーネント

以下の Clickable コンポーネントは、divに click イベントのハンドラを設定しており、かつchildrenを取るコンポーネントである。divをクリックするとコンソールにログを出力する。

import { FC, MouseEventHandler, useCallback } from "react";

export const Clickable: FC = ({ children }) => {
  const handleMouseDown: MouseEventHandler = useCallback((e) => {
    console.log(`clicked!!!! ${new Date()}`);
  }, []);

  return (
    <div onMouseDown={handleMouseDown}>
      {children}
    </div>
  );
};

モーダル、外に置くか? 中に置くか?

react-modalのモーダルを <Clickable> の外に置くか、それとも中に置くか、について考える。

外に置く

モーダルを <Clickable> の外に置くとこういう感じになる。モーダルを開くボタンだけが <Clickable> の中にある。とくに気にかけることもない。

<div>
  <h2>along with modal</h2>
  <Clickable>
    This area is clickable!!!!!
    <p>
      <button onClick={() => setIsOpen(true)}>open modal</button>
    </p>
  </Clickable>
  <Modal isOpen={isOpen} onRequestClose={handleRequestClose}>
    <div>
      <h2>Modal</h2>
      <button onClick={handleRequestClose}>close</button>
    </div>
  </Modal>
</div>

中に置く

一方で、モーダルを <Clickable> の中に置くとどうなるか? この場合もモーダルを開くことはできるし、モーダルが前面に出てくるのだが、モーダル内をクリックすると <Clickable> のイベントハンドラが反応してしまう。これは多くの場合は望ましい挙動ではないと思う。

<div>
  <h2>contains modal</h2>
  <Clickable>
    This area is clickable!!!!!
    <p>
      <button onClick={() => setIsOpen(true)}>open modal</button>
    </p>
    <Modal isOpen={isOpen} onRequestClose={handleRequestClose}>
      <div>
        <h2>Modal</h2>
        <button onClick={handleRequestClose}>close</button>
      </div>
    </Modal>
  </Clickable>
</div>

どうするか

モーダル内で、モーダルを設置した要素の親のイベントハンドラが呼ばれてほしくないなら、モーダルを外に出したほうがよい。

感想

普通はイベントハンドラを設定した要素の子にモーダルを設置することはないかもしれないけど、モーダルを動的に生成する場合に油断すると引っかかると思う。実際にこの現象に遭遇して、DOM treeの上ではreact-modalのモーダルは完全にroot要素の外にあるけど、Reactが管理するtreeの上では必ずしもそうなっていない*1のがデバッグを難しくしていた。CSSの pointer-events プロパティで解決できるかもしれない、と思っていろいろ試してみたけど解決できなかった。

サンプルコード

github.com

2021/8/2 追記

Reactのポータルのドキュメントを読んでいたところ、まさにこの現象に該当することがドキュメントに書いてあった。

ポータルは DOM ツリーのどこにでも存在できますが、他のあらゆる点では通常の React の子要素と変わらずに振る舞います。コンテクスト (context) のような機能は、たとえ子要素がポータルであろうと全く同じように動きます。というのも、DOM ツリー上の位置にかかわらず、ポータルは依然として React のツリー内にいるからです。

これにはイベントのバブリングも含まれます。ポータルの内部で発火したイベントは React のツリー内の祖先へと伝播します。たとえそれが DOM ツリー上では祖先でなくともです。

ja.reactjs.org

ということは、react-modalのオーバーレイに対して event.stopPropagation() するだけのイベントハンドラを設定しても回避できるかもしれない。

*1:React Developer Toolsで確認できる