hacca8

ドロップダウンメニューなどの作成に便利なclosestについて

closestメソッドの使用法についてメモします。closestメソッドは、指定したセレクタと合致する親要素を探索します。(ルートまで遡ります)
例としてドロップダウンメニューを作成して確認します。対象以外をクリックしたら閉じる動作に使用します。

サンプルコード

  • HTML
<nav>
  <div>
    <h2>menu1</h2>
    <ul>
      <li>menu1-1</li>
      <li>menu1-2</li>
      <li>menu1-3</li>
    </ul>
  </div>

  <div>
    <h2>menu2</h2>
    <ul>
      <li>menu2-1</li>
      <li>menu2-2</li>
      <li>menu2-3</li>
    </ul>
  </div>

  <div>
    <h2>menu3</h2>
    <ul>
      <li>menu3-1</li>
      <li>menu3-2</li>
      <li>menu3-3</li>
    </ul>
  </div>
</nav>
  • CSS
nav {
  display: flex;
}
nav h2 {
  padding: 0 1em;
  cursor: pointer;
}
nav ul {
  display: none;
}
nav div.open ul {
  display: block;
}
  • JavaScript
(function () {
  const menus = document.querySelectorAll('nav div');
  const menuTitles = document.querySelectorAll('nav h2');

  function closeSubMenu() {
    menus.forEach(value => value.classList.remove('open'));
  }
  function toggleSubMenu() {
    const target = this.parentNode;
    if (target.classList.contains('open')) {
      target.classList.remove('open');
    } else {
      closeSubMenu();
      target.classList.add('open');
    }
  }

  menuTitles.forEach(value => {
    value.addEventListener('click', toggleSubMenu);
  });
  document.addEventListener('click', e => {
    !e.target.closest('.open') && closeSubMenu();
  });
})();

今回は以下のような動作を実装します。所謂トグルメニューのような仕組みで、ドロップダウンメニューではよくある動作かと思います。

  • メニューをクリックするとサブメニューが表示・非表示される
  • 他の要素をクリックするとサブメニューが非表示される

メニューをクリックするとサブメニューが表示・非表示される

サンプルコードから一部抜粋します。

function closeSubMenu() {
  menus.forEach(value => value.classList.remove('open'));
}
function toggleSubMenu() {
  const target = this.parentNode;
  if (target.classList.contains('open')) {
    target.classList.remove('open');
  } else {
    closeSubMenu();
    target.classList.add('open');
  }
}

menuTitles.forEach(value => {
  value.addEventListener('click', toggleSubMenu);
});
  • closeSubMenu()

メニューのclass属性からopenを削除します。

  • toggleSubMenu()

メニュータイトルをクリックした際に動作する関数です。
class属性にopenがある場合は、openを削除します。
openがない場合はcloseSubMenu()を実行した後に、openを追加します。
メニュータイトルの親(各メニューのdiv)を操作したいのでconst target = this.parentNode;と指定しています。

  • クリックイベントの登録

以下の記述で登録しています。メニューをクリックするとtoggleSubMenu()を実行します。

menuTitles.forEach(value => {
  value.addEventListener('click', toggleSubMenu);
});

他の要素をクリックするとサブメニューが非表示される

closestを利用して実装します。

document.addEventListener('click', e => {
  !e.target.closest('.open') && closeSubMenu();
});

closestは指定したセレクタと合致する親要素を探索します。ない場合はnullを返します。
上記の!e.target.closest('.open')は、クリックした要素の親にopenがなかったら、を意味しています。その後の&& closeSubMenu()は、条件と一致したらcloseSubMenu()を実行します。
つまり、クリックした要素の親にopenがなかったら、サブメニューを閉じる動作をします。
これによりメニュー以外をクリックしてもサブメニューが閉じるよう動作するようになります。

今回は親要素にopenがあるかを見ればわかりますが、closestはルートまでさかのぼって探索するので、ネストの深い要素の処理でも動作します。

PS

closestを利用するとそれ以外をクリックした場合を簡単に検出できて便利です。IEの対応が不要であれば利用可能です。
フローティング系のメニューも同様に対応できます。ホバーが外れたら閉じるだと都合が悪いようなケースで便利です。
ちなみにですが、開閉するメニューのみであればHTMLのdetailsタグでも作成できますが、アニメーションをつけたりする場合に不便なことが多いので、まだ現在(2022.12)はあまり利用するケースが少ない状況です。