はじめに
これまでドロップダウンメニューが必要な時は React Bootstrap を用いていたのですが、自力でドロップダウンメニューを作る必要があったので、そのときの試行錯誤をメモとして残します。 ドロップダウンメニューが満たしてほしい仕様は以下の通りです。
- ボタンクリックでメニューを表示/非表示
- メニュー外をクリックしてもメニューが非表示になる
Step 1
まずは上記の一つ目の条件のみを満たす、基本的なドロップダウンメニューを作ります
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>
);
};
Step 2
この Step 1 のメニューではメニューを表示した状態でメニュー外をクリックしてもメニューが閉じません。
なのでuseRef
とuseEffect
を使って、ボタンをクリックするとドロップダウンメニューにフォーカスが来るようにし、フォーカスが外れた時に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>
);
};
Step 3
Step 2 のドロップダウンメニューにメニューアイテム間のセパレータとサブメニューを同様に追加してみます。
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>
);
};
一見動作するのですが、少しおかしい挙動が見られます。
というのは、セパレータとサブメニュー「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>
);
};
以上
です。
Qiita にも書きました
React+Hook で作るメニュー外クリックで閉じるドロップダウンメニュー