0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Vue.js]外側をクリックすると閉じるドロップダウンメニュー(複数の場合)

Last updated at Posted at 2020-02-16

はじめに

  • メニューボタン押すと開く
  • メニューボタンもう一度押すと閉じる
  • メニューの外側押しても閉じる

この操作はこちら↓の記事を読んでもらえればできます。
[Vue.js]外側をクリックすると閉じるドロップダウンメニュー

ですが、複数ボタンがある場合、この処理だけだと操作性の問題が生じたため、いろいろ改善しました。それを紹介します。

注意 : 本記事は上で紹介した記事の技術がわかった上でお話をします。

[問題 1] メニューが閉じない場合がある

複数のボタンがある時、メニューを一度開き、別のボタンを押すと先に開いていたメニューが閉じない。
デモ (JSFiddle)

以下の画像をご覧ください。
Info 1 から順番にボタンを押していくと、Info 2を押した時にInfo 1のメニューが閉じない。
image.png

構造

親コンポーネント↓ : <msg-comp> を複数呼び出して並べる。

<div id="app">
  <div class="containter">
    <msg-comp
      v-for="msg in msgList"
      :msg="msg"
    ></msg-comp>
  </div>
</div>

<msg-comp>コンポーネント↓ : ボタンを押すと <info-menu> が開く。

<div class="msg">
  <button v-on:click.stop="showInfo = !showInfo">Info {{msg}}</button>
  <info-menu 
      v-if="showInfo"
      v-on:close="showInfo = false">
  </info-menu>
</div>

<info-menu>コンポーネント↓ : クリックイベントが発生した際、クリックした要素がこのコンポーネント内の要素ではない場合に閉じる。

<div class="info">
    <div>AAA</div>
    <div>BBB</div>
    <div>CCC</div>
</div>

問題1 の原因

<!--  <msg-comp> コンポーネント内  -->
<button v-on:click.stop="showInfo = !showInfo">Info {{msg}}</button>

.stop modifierでバブリングの停止を行っているのが原因。
Info 2 を押してメニューを開ける時に、自分自身のメニューを閉じるイベントが発火しないようになっているのはいいんだが、 Info 1のメニューを閉じるイベントも発火しない。

問題1 解決策

  • .stop modifierをやめて自前で実装。
  • クリックした要素が開けるボタンだった時に、閉じる処理を発火させないようにする。

問題1 の解決策 デモ (JSFiddle)

まず、<msg-comp>コンポーネント内のbuttonから.stopを削除。
ref="msgBtn"でボタンのDOMを取得。取得したDOM要素を<info-menu>コンポーネントへpropsで投げる。

<!--  <msg-comp> コンポーネント内  -->
  <div class="msg">
    <button 
      ref="msgBtn" 
      v-on:click="showInfo = !showInfo"
    >
      Info {{msg}}
    </button>
    <info-menu 
      v-if="showInfo"
      v-on:close="showInfo = false"
      :msgBtn="$refs.msgBtn"
    >
    </info-menu>
  </div>

<info-menu>コンポーネントで、ボタンのDOM(msgBtn)を受け取る。
イベントリスナーの発火条件を追加。
 これで判定できる→ !this.msgBtn.contains(e.target)
 → 意味:クリックしたDOMが、ボタンのDOM内にあればfalseになる

Vue.component('info-menu', {
  mixins:[listener],
  template: '#info',
  props: {
    msgBtn: {
      type: HTMLButtonElement
    }
  },
  created:function(){
    this.listen(window, 'click', function(e){
      if (!this.$el.contains(e.target) && !this.msgBtn.contains(e.target)){
        this.$emit('close');
      }
    }.bind(this));
  }
}

注意点:propsにtype指定する際、msgBtnのDOMの型(type)をHTMLButtonElementとしている。a要素とかを使うなら変わる。

以上です。
Info 1を押して開いた状態でInfo 2を押すとInfo 1のmenuが閉じる! 解決できました。

image.png

[問題 2] Tab移動でInfo内部から抜けた時に勝手に閉じて欲しい

これは問題というより、そういう設計にしたかったという願望です。
Infoのサイズが大きい場合にはUX的にもそうしたいですよね。

問題2 解決策

問題2 の解決策 デモ (JSFiddle)

これはめちゃ簡単。新しくイベントリスナー追加するだけ。

created:function(){
  // ~~~
  this.listen(window, 'keyup', function(e){
    if (!this.$el.contains(e.target) && e.key === 'Tab'){
      this.$emit('close');
    }
  }.bind(this));
}

注意点としてはkeydownではなくkeyupにするところ。
keydownにしてしまうと、まだbuttonにいて、Infoに移ろうとする時に発火してしまい、すぐ閉じてしまう。
keyupにしておくと、Tabのkeydown時にInfoに移るので、keyupが発火する時にはInfoにいる。よって閉じない。

[おまけ] Nuxt.jsでやりたい!

Nuxt.jsでやる際に参考になりそうな話をします。

mixinをNuxtでやる

Nuxt.jsの場合、mixinは pluginsフォルダにmixin-common-methods.js 的なものを作り、
nuxt-config.js 内に plugins: ['@/plugins/mixin-common-methods'] とすれば良い。
各コンポーネントからわざわざ呼び出しとかもなし、これだけで使える。

参考 : Nuxt.jsで異なるコンポーネントから共通で利用できる関数を定義する(mixin編)

plugins/mixin-common-methods.js
import Vue from 'vue'

Vue.mixin({
  destroyed() {
    if (this._eventRemovers) {
      this._eventRemovers.forEach(function(eventRemover) {
        eventRemover.remove()
      })
    }
  },
  methods: {
    listen(target, eventType, callback) {
      if (!this._eventRemovers) {
        this._eventRemovers = []
      }
      target.addEventListener(eventType, callback)
      this._eventRemovers.push({
        remove() {
          target.removeEventListener(eventType, callback)
        }
      })
    }
  }
})
~/components/info-menu.vue
export default {
  props: {
    msgBtn: {
      type: HTMLButtonElement
    }
  },
  //~~~
  created:function(){
    this.listen(window, 'click', function(e){
      if (!this.$el.contains(e.target) && !this.msgBtn.contains(e.target)){
        this.$emit('close');
      }
    }.bind(this));
  },
  //~~~
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?