All Articles

React Hooksで作るメニュー外クリックで閉じるドロップダウンメニュー

はじめに

これまでドロップダウンメニューが必要な時は React Bootstrap を用いていたのですが、自力でドロップダウンメニューを作る必要があったので、そのときの試行錯誤をメモとして残します。 ドロップダウンメニューが満たしてほしい仕様は以下の通りです。

  1. ボタンクリックでメニューを表示/非表示
  2. メニュー外をクリックしてもメニューが非表示になる

Step 1

まずは上記の一つ目の条件のみを満たす、基本的なドロップダウンメニューを作ります image.png

const Dropdown1 = () => {
  const [isOpenMenu, setIsOpenMenu] = React.useState(false);
  const handleClick = (text) => () => {
    alert(text);
    setIsOpenMenu(false);
  };
  return (
    <div className="menu-container" onClick={() => setIsOpenMenu(!isOpenMenu)}>
      <div className="menuButton">Menu 1</div>
      <ul className="menu" hidden={!isOpenMenu}>
        <li className="item" onClick={handleClick("a")}>
          {" "}
          a{" "}
        </li>
        <li className="item" onClick={handleClick("b")}>
          {" "}
          b{" "}
        </li>
        <li className="item" onClick={handleClick("c")}>
          {" "}
          c{" "}
        </li>
      </ul>
    </div>
  );
};

CodePen でソースを見る

Step 2

この Step 1 のメニューではメニューを表示した状態でメニュー外をクリックしてもメニューが閉じません。 なのでuseRefuseEffectを使って、ボタンをクリックするとドロップダウンメニューにフォーカスが来るようにし、フォーカスが外れた時にonBlurイベントにメニューを閉じる関数を登録します。

const Dropdown2 = () => {
  const [isOpenMenu, setIsOpenMenu] = React.useState(false);
  const menuRef = React.useRef(null);
  React.useEffect(() => {
    isOpenMenu && menuRef.current.focus();
  }, [isOpenMenu]);
  const handleClick = (text) => () => {
    alert(text);
  };
  return (
    <div
      className="menu-container"
      onClick={() => setIsOpenMenu(!isOpenMenu)}
      ref={menuRef}
      onBlur={() => setIsOpenMenu(false)}
      tabIndex={0}
    >
      <div className="menuButton">Menu 2</div>
      <ul className="menu" hidden={!isOpenMenu}>
        <li className="item" onClick={handleClick("a")}>
          {" "}
          a{" "}
        </li>
        <li className="item" onClick={handleClick("b")}>
          {" "}
          b{" "}
        </li>
        <li className="item" onClick={handleClick("c")}>
          {" "}
          c{" "}
        </li>
      </ul>
    </div>
  );
};

CodePen でソースを見る

Step 3

Step 2 のドロップダウンメニューにメニューアイテム間のセパレータとサブメニューを同様に追加してみます。

image.png

const Dropdown3 = () => {
  const [isOpenMenu, setIsOpenMenu] = React.useState(false);
  const menuRef = React.useRef(null);
  React.useEffect(() => {
    isOpenMenu && menuRef.current.focus();
  }, [isOpenMenu]);
  const handleClick = (text) => () => {
    alert(text);
  };
  return (
    <div
      className="menu-container"
      onClick={() => setIsOpenMenu(!isOpenMenu)}
      ref={menuRef}
      onBlur={() => setIsOpenMenu(false)}
      tabIndex={0}
    >
      <div className="menuButton">Menu 3</div>
      <ul className="menu" hidden={!isOpenMenu}>
        <li className="item" onClick={handleClick("a")}>
          {" "}
          a{" "}
        </li>
        <li className="separator"></li>
        <li className="item" onClick={handleClick("b")}>
          {" "}
          b{" "}
        </li>
        <li className="item">
          {" "}
          c<span></span>
          <ul className="submenu">
            <li className="item" onClick={handleClick("c-1")}>
              {" "}
              c-1{" "}
            </li>
            <li className="item" onClick={handleClick("c-2")}>
              {" "}
              c-2{" "}
            </li>
          </ul>
        </li>
      </ul>
    </div>
  );
};

CodePen でソースを見る

一見動作するのですが、少しおかしい挙動が見られます。 というのは、セパレータとサブメニュー「c」をクリックしてもメニューが閉じてしまうのです。 これは親要素へクリックイベントが伝播してしまい、div要素のonClick関数が実行されてしまうからです。(恐らく)

Step 4

そこでドロップダウンメニュー全体の親であるulとサブメニューの親であるli(e) => e.stopPropagation()を追加したものが以下です。 これにより先述のおかしい動作は見られなくなります。

const Dropdown4 = () => {
  const [isOpenMenu, setIsOpenMenu] = React.useState(false);
  const menuRef = React.useRef(null);
  React.useEffect(() => {
    isOpenMenu && menuRef.current.focus();
  }, [isOpenMenu]);
  const handleClick = (text) => () => {
    alert(text);
  };
  return (
    <div
      className="menu-container"
      onClick={() => setIsOpenMenu(!isOpenMenu)}
      ref={menuRef}
      onBlur={() => setIsOpenMenu(false)}
      tabIndex={0}
    >
      <div className="menuButton">Menu 4</div>
      <ul
        className="menu"
        hidden={!isOpenMenu}
        onClick={(e) => e.stopPropagation()}
      >
        <li className="item" onClick={handleClick("a")}>
          {" "}
          a{" "}
        </li>
        <li className="separator"></li>
        <li className="item" onClick={handleClick("b")}>
          {" "}
          b{" "}
        </li>
        <li className="item" onClick={(e) => e.stopPropagation()}>
          {" "}
          c<span></span>
          <ul className="submenu">
            <li className="item" onClick={handleClick("c-1")}>
              {" "}
              c-1{" "}
            </li>
            <li className="item" onClick={handleClick("c-2")}>
              {" "}
              c-2{" "}
            </li>
          </ul>
        </li>
      </ul>
    </div>
  );
};

CodePen でソースを見る

以上

です。

Qiita にも書きました
React+Hook で作るメニュー外クリックで閉じるドロップダウンメニュー