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

はじめに

これまでドロップダウンメニューが必要な時は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でソースを見る

以上

です。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account