はじめに
- メニューボタン押すと開く
- メニューボタンもう一度押すと閉じる
- メニューの外側押しても閉じる
この操作はこちら↓の記事を読んでもらえればできます。
[Vue.js]外側をクリックすると閉じるドロップダウンメニュー
ですが、複数ボタンがある場合、この処理だけだと操作性の問題が生じたため、いろいろ改善しました。それを紹介します。
注意 : 本記事は上で紹介した記事の技術がわかった上でお話をします。
[問題 1] メニューが閉じない場合がある
複数のボタンがある時、メニューを一度開き、別のボタンを押すと先に開いていたメニューが閉じない。
デモ (JSFiddle)
以下の画像をご覧ください。
Info 1
から順番にボタンを押していくと、Info 2
を押した時にInfo 1
のメニューが閉じない。
構造
親コンポーネント↓ : <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をやめて自前で実装。 - クリックした要素が開けるボタンだった時に、閉じる処理を発火させないようにする。
まず、<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が閉じる! 解決できました。
[問題 2] Tab移動でInfo内部から抜けた時に勝手に閉じて欲しい
これは問題というより、そういう設計にしたかったという願望です。
Infoのサイズが大きい場合にはUX的にもそうしたいですよね。
問題2 解決策
これはめちゃ簡単。新しくイベントリスナー追加するだけ。
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編)
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)
}
})
}
}
})
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));
},
//~~~
}