Help us understand the problem. What is going on with this article?

【js】DropdownのUIを実装

はじめに

JavaScriptのフレームワークやライブラリを使わずに JavaScript、HTML、そしてCSSでドロップダウンUIを実装 したので、そのメモです。

ちなみにスタイル用途として、CSSフレームワークbulmaとWebアイコンのfont-awesomeを利用しています。

DropwdownのUI動作確認

実際のDropdownのUIは以下のCodepensより確認できます。

See the Pen OJyqPmJ by shinji uyama (@ushinji_0612) on CodePen.

解説

HTML

まず初めにDropwDownのHTMLの解説です。今回のDropwdownのHTMLコードは、bulmaのコード例を参考にしています。
https://versions.bulma.io/0.7.1/documentation/components/dropdown/

今回のDropdownのHTMLを簡略化すると以下の要素になります。

<!-- Dropwdown全体 -->
<div class="dropdown is-active">
  <div class="dropdown-trigger">
    <button>
      <!-- ボタンの開閉ボタン -->
    </button>
  </div>
  <div class="dropdown-menu" id="dropdown-menu" role="menu">
    <!-- ドロップダウンのMenu -->
  </div>
</div>

DropdownのMenuの表示/非表示は <div class="dropdown">に当てられているis-activeの有無で管理します。

また、Dropdownの開閉UIは<div class="dropdown-trigger">配下のbuttonによって行います。Buttonクリックが行われた際はis-activeを追加 or is-activeを削除することで、開閉を実現しています。

CSS

次にbulmaのCSSクラスの中で、Dropdownの開閉を行うis-activeの挙動を解説します。

まず初めに開閉対象であるMenuのCSSクラス.dropdown-menuを見るとdisplay: none;が当てられています。そのため、デフォルトではブラウザ上では表示させないようにできます。

一方で、is-activeが追加することで.dropdown-menuに対してdisplay: blockを当たるため、.dropdown-menu要素が表示されます。

<!--  コードを一部省略しています -->

.dropdown
  &.is-active
    .dropdown-menu
      display: block

.dropdown-menu
  display: none

CSS詳細を知りたい方は、以下のリンクよりbulmaの該当コードを確認ください。
https://github.com/jgthms/bulma/blob/9a28ea17876715d00d0a8a59b9fdabfee967e56b/sass/components/dropdown.sass#L20

JavaScript

Dropdownを開く場合

次にDropwdownの開閉を制御するJavaScriptについての解説です。

以下のコードがDropdownを開くコードになります。

document.addEventListener('DOMContentLoaded', function() {
  // 1. DOMが読み込まれた際に`.dropdown-trigger`のClassを持つHTMLElementを検索
  var nodelist = document.querySelectorAll('.dropdown-trigger');
  var elements = Array.prototype.slice.call(nodelist, 0);

  elements.forEach(function(element) {
    // 2. Dropdownの開閉ボタンを取得と、開閉を管理するDropdownのElementを取得
    var button = element.querySelector('button');
    var dropdown = element.parentNode;

    // 3. Dropdownの開閉ボタンがクリックされた際に、`is-active`クラスを追加するイベント追加
    button.addEventListener('click', function() {
      dropdown.classList.add('is-active');
    });
  });
});

処理の流れとしては、DOMがマウントされた際にDropdownに関連するHTMLに対してクリックイベントを登録することで、DropdownUIを実現させています。

具体的にはDropdownのTriggerとなるButtonがクリックされた際に<div class="dropdown">.is-activeを当てることで、DropwdownのMenuを表示させています。

Dropdownを閉じる場合

次に Dropdownを閉じるUIの処理 について説明します。

そもそもDropdownを閉じたいケースを考えると、以下の2つが考えられます。
1. DropdownのMenu項目がクリックされた場合
2. Dropdown以外の範囲がクリックされた場合

1については、Menuのそれぞれの要素がクリックされた場合に、個別に閉じる処理を行う必要があります。また、Menuの要素が<a>タグの場合、クリック時にページ遷移が行われるのでDropdownが閉じる処理は考えなくても良いです。

そのため今回は「2. Dropdown以外の範囲がクリックされた場合」にフォーカスして話をします。

onBlurを利用する

Dropdown以外の範囲がクリックされた場合を検知する一番簡単な方法は、TriggerであるButtonのfocusが外れた場合、つまりblurを検知 すれば良いです。

具体的には以下のコードを追記すれば大丈夫です。

document.addEventListener('DOMContentLoaded', function() {
  var nodelist = document.querySelectorAll('.dropdown-trigger');
  var elements = Array.prototype.slice.call(nodelist, 0);

  elements.forEach(function(element) {
    var button = element.querySelector('button');
    var dropdown = element.parentNode;

    button.addEventListener('click', function() {
      dropdown.classList.add('is-active');
    });

// 【追記】 Dropdownを閉じるコード追記
+    button.addEventListener('blur', function() {
+      dropdown.classList.remove('is-active');
+    });
  });
});

この処理のメリットは一番簡単に実装できることです。一方で、focusが外れた際にDropdownを閉じるため、キーボード操作ではMenuを選択する前にDropdownMenuが消えてしまいます。そのため、より良いアクセシビリティを考えると、別の方法考える必要があります。

クリック位置を検知する

次は クリック位置を検知して、Dropdown Menu範囲外の場合にDropDownを閉じる方法 です。

document.addEventListener('DOMContentLoaded', function() {
  var nodelist = document.querySelectorAll('.dropdown-trigger');
  var elements = Array.prototype.slice.call(nodelist, 0);

  // ※ Dropwdonwを開く処理は省略

  // Dropdownを閉じる処理
  // 1. window全体に対して、クリックイベントを登録
  window.onclick = function(event) {
    elements.forEach(function(element) {
      var button = element.querySelector('button');
      var dropdown = element.parentNode;

      // 2. aria-controlsより対象のDropdownのMenuを取得
      var menu = document.querySelector('#' + button.getAttribute('aria-controls'));

      // 3. 自身のTriggerButtonクリック時はMenuを閉じない
      if(event.target && element.contains(event.target)) {
        return;
      }

      // 4. クリックがDropdownのMenuの範囲外の場合は、Dropdownを閉じる
      if(event.target && !menu.contains(event.target)) {
        dropdown.classList.remove('is-active');
      }
    });
  };
});

処理の流れとして、window全体に対してクリックイベントを設定します。設定するクリックイベントは、クリック位置がDropdownのMenuの範囲内であるか調べ、範囲外の場合はドロップダウンを閉じるという処理となっています。

クリック範囲を調べる方法はincludesメソッドを利用します。これは指定したHTML要素の子要素の中に、目的の要素が含まれるか調べるメソッドになります。

今回は event.target(クリック位置にあるHTML要素)が、DropdownのMenuのHTML要素に含まれるか調べること、すなわりクリック位置がMenu範囲に含まれるかを調べることができます。

また、クリック位置を調べる前に、そのクリックイベントがMenuを開くTrigger Buttonのイベントであるかチェックします。理由として、Menuを開くクリックイベント自体がMenu要素の範囲外であるため、以降の処理でMenuを閉じてしまうからです。そのため、Trigger Buttonのクリックイベントかどうか調べ、その場合は以降の処理を行わないようにしています。

最後に

今回自前でDropdownの開閉UIを実装することで、すごく勉強になりました。

簡単に実装する場合はblurを使えば良いですし、よりよいUIを目指すのであればクリック位置を判定する処理にすれば良いですね。

今後もより良いUIを実装できるよう、日々努力していきたいです。

Ushinji
Software engineer.
rymansat
普段は宇宙開発に関わっていないサラリーマンが身近で誰でもできる宇宙開発を実現させることがリーマンサット・プロジェクト(Ryman Sat Project=rsp.)の目的です。キューブサットの開発をはじめ、宇宙を軸として様々なコミュニティやクリエイターとコラボレーションし、民間宇宙開発に関するネットワークを強化、拡張することを目指して活動しています。
https://www.rymansat.com/
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
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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
ユーザーは見つかりませんでした