はじめに
これまでドロップダウンメニューが必要な時は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>
);
};
以上
です。

