【JS,React】枠外をクリックしてメニューを閉じたい!

3 min read読了の目安(約3100字

枠外をクリックしてメニューを閉じたい!

スマホサイトでよくあるメニューが横から出てくるやつです。グリーンのメニュー内部をクリックしても閉じませんがグレーのメニュー外部をクリックするとメニューが閉じます。(以下ではグリーン背景の部分を「メニュー」と呼んでいます。)

まずは完成形を載せます。Reactで書いていますがプレーンなJSでも考え方は同じなので実装は簡単にできます。要望があれば書くのでお気軽にどうぞ!。アドバイスがあればそちらもよろしくお願いします!。特に用語の使い方がまだしっかりわかっていないのでその辺のミスも指摘していただけるとありがたいです!

こういう挙動をするものをよく見かけますが実装の仕方がよくわかりませんでした。ググって検索してみるとなぜかjQueryで実装しているものが多く、bodyにイベントリスナーを付けている方法や応用が利かないものもあり個人的にあんまりしっくりきませんでした。それやったら自分で実装してみたらええやんと思い実装してみました!

今回の実装の特長

JSのイベントのtarget(e.target.valuetarget)とcurrentTargetの性質の違いを使って実装しました。

  • target : イベントが発生した要素を取得する(実際にクリックされたDOMを取得する)
  • currentTarget : イベントハンドラがアタッチされた要素を取得する
    これらを使ってメニューの中、外のどちらをクリックしたか判定します。

実装の詳細

以下の3つのパートがあるのでそれぞれ説明します。

  1. メニューの開閉の状態を持つstate
  2. メニューの中、外のどちらをクリックしたか判定し外をクリックした場合はメニューを閉じるメソッド(最重要)
  3. HTML(JSX)の部分

メニューの開閉の状態を持つstate

useStateで状態を持たせます。

const [isShow, setIsShow] = useState(false);

メニューの中、外のどちらをクリックしたか判定し外をクリックした場合はメニューを閉じるメソッド

3つあるパートのうちここが1番重要です。
(メソッドは1回しか作る必要がないからコンポーネント(const App = () => {})の外側で定義する方がいいのでしょうか。。?)

名前はそのままです(笑)。解説のためにログを吐いてます。

const closeWithClickOutSideMethod = (e, setter) => {
  console.log("e.target", e.target);
  console.log("e.currentTarget", e.currentTarget);
  if (e.target === e.currentTarget) {
    //メニューの外側をクリックしたときだけメニューを閉じる
    console.log("メニューの外側をクリックした");
    setter(false);
  } else {
    console.log("メニューの内側をクリックした");
  }
};

menuWrapperonClickイベントを置いているのでe.currentTargetは常にmenuWrapperのDOMを取得しています。吐かれるログは常に変わりません。一方e.targetは実際にクリックされたDOMを取得しています。よってクリックする場所を変えるたびにe.targetのログは変わります。ログは以下の画像の通りです。

これを利用してクリックしてたまたまe.currentTargete.targetが同じ場合にのみ(つまりメニューの外側をクリックしたとき)にsetIsShow(false)を実行してメニューを閉じます。

HTML(JSX)の部分

最後にHTML部分ですがトリッキーなことはしていません。

return (
   <div>      
     <button
       onClick={() => {
         setIsShow(!isShow);
       }}
     >
       toggle menu
     </button>
     <div
       className={`menuWrapper ${isShow ? "menuWrapper__active" : ""}`}
       onClick={(e) => {
         closeWithClickOutSideMethod(e, setIsShow);
       }}
     >
       <div className="menu">
         <ul className="menuList">
           <li>ここを押しても閉じない</li>
           <li>でも枠外のグレーを押すと</li>
           <li>閉じるよ</li>
         </ul>
       </div>
     </div>
   </div>
);

構造はこんな感じです。

  1. button

    • メニューを表示(トグル)させるボタン
  2. menuWrapper

    • メニューの枠外がクリックされたときのみ閉じる挙動をするためのメソッドを書く
    • 画面いっぱいにグレーの背景を表示する
    • メニューを表示させている時だけクラス(menuWrapper__active)を追加で当てる
  3. menu,menuList

    • メニューの中身(本筋とは関係ない)

最後に

currentTargettargetの性質を使ってうまいことメニューの外側をクリックしてメニューを閉じる機能を実装できました。例として横からメニューが出てくるものを例にして実装しましたがPCサイトのグロナビにも今回紹介した方法が使えます。ググっても出てこなかった(調べ方が下手説も..)実装方法なのでこういう記事が書けて嬉しいです!

e.target===e.currentTargetのようにDOMを直接比較して条件式を作るよりもそのDOMのidやclassなどを使い条件式を作った方が良かったかなとも思ってます(アドバイスください)。

タイポ、コードの修正点、他のいい実装法があればお気軽にコメントよろしくお願いいたします!!