I tried to find the solution here for this, but couldn't and decided to write a question.
So I am trying to build a simple drop-down menu ponent in Vue which has toggle button and the user is free to click around and tab around the items but as soon as the focus leaves the ponent, the menu should collapse.
Now, I maintained the focus with @blur
and @focus
events, but I have a problem with the Toggle
button. If i attach the same listeners to it, then clicking on it shows and immediately hides the menu, so you have to click again to expand it.
Here is the fiddle demonstrating the problem.
If, however I remove the listeners, then clicking on button after the focus has been inside the ponent is problematic. I guess my approach is wrong, so here's the expected behavior:
Toggle
button is just that - a toggle button. It should collapse/expand the list on clicksToggle
button when it is closedToggle
button when it is expanded, or by focusing out of it. This includes the focus out on the button (i.e. clicking the toggle button to expand the list and then clicking somewhere outside without focusing any of the items).EDIT: Thanks to @Sphinx I managed to get the dropdown to work as I expect it to. Here's the updated fiddle.
I tried to find the solution here for this, but couldn't and decided to write a question.
So I am trying to build a simple drop-down menu ponent in Vue which has toggle button and the user is free to click around and tab around the items but as soon as the focus leaves the ponent, the menu should collapse.
Now, I maintained the focus with @blur
and @focus
events, but I have a problem with the Toggle
button. If i attach the same listeners to it, then clicking on it shows and immediately hides the menu, so you have to click again to expand it.
Here is the fiddle demonstrating the problem.
If, however I remove the listeners, then clicking on button after the focus has been inside the ponent is problematic. I guess my approach is wrong, so here's the expected behavior:
Toggle
button is just that - a toggle button. It should collapse/expand the list on clicksToggle
button when it is closedToggle
button when it is expanded, or by focusing out of it. This includes the focus out on the button (i.e. clicking the toggle button to expand the list and then clicking somewhere outside without focusing any of the items).EDIT: Thanks to @Sphinx I managed to get the dropdown to work as I expect it to. Here's the updated fiddle.
@focus
and @click
on your button, both get triggered if you click it (if you haven't just clicked it before). You probably just want to remove the @focus
from the button.
– Tommos
Commented
Sep 20, 2018 at 22:12
focus
event before the click
event. That's what's causing the behavior you're seeing. The focus
event opens the menu, and then the click
event toggles it, which closes it. When you click on the button the second time, there's no focus
event because the button already has focus from the previous click.
– Stephen Thomas
Commented
Sep 20, 2018 at 22:21
@focus
handler, then the list cannot be collapsed by clicking on Toggle
button after clicking on any list item.
– mrd
Commented
Sep 20, 2018 at 23:00
For your case, as the ments under the question already pointed out, you have to handle many situations, like @focus
and @click
will be triggered in a row, @blur
of <button>
and @blur
of <ul></li>
will be triggered when click at either of them.
It is not a good idea. But you can check This fiddle, it is one solution with setTimeout
& clearTimeout
. Then you may already seen it delays 100ms by setTimeout(()=>{}, 100)
( I added some logs, you can open the browser console to check the work flow). The reason is we have to wait enough time to make sure next event handler (like focus is triggered first, then click will be triggered later) can clear previous setTimeout
in time, unless the menu may be open first, then is closed again. (PS: for some old machines, 100ms may not be enough, it depends on how fast current render is finished)
One solution:
remove @focus
and @blur
when this.showMenu
is true(opened), add one listener=click
for Dom=document it will execute this.hide()
when triggered.
Then inside this.hide()
, remove that listener=click
from Dom=document.
to prevent the menu is collpased when click at the button and the menu, add the modifier=stop, it will stop the click event's propagation to upper level Dom nodes.
If you wrap <button>
and <ul>
into one <div>
, then only need to add the modifier=stop
like <template><div @click.stop><button></button<ul>...<ul></div></template>
.
Below is one demo:
Vue.config.productionTip = false
new Vue({
el: "#app",
data: {
showMenu: false,
items: ['Option 1', 'Option 2', 'Option 3', 'Option 4']
},
puted: {
listClass: function() {
if (this.showMenu) {
return 'show';
}
return '';
}
},
methods: {
toggle: function() {
this.showMenu = !this.showMenu
this.showMenu && this.$nextTick(() => {
document.addEventListener('click', this.hide)
})
},
hide: function() {
this.showMenu = false
document.removeEventListener('click', this.hide)
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
ul {
list-style: none;
display: none;
}
li {
padding: 5px;
border: 1px solid #000;
}
li:hover {
cursor: pointer;
background: #aaa;
}
.show {
display: block;
}
<script src="https://cdnjs.cloudflare./ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<h3>
This is one demo:
</h3>
<button @click.stop="toggle" tabindex="0">
Toogle menu
</button>
<ul :class="listClass" @click.stop>
<li v-for="(item, key) in items" tabindex="0">{{ item }}</li>
</ul>
</div>
Im not sure if this fixes what you're trying to acplish, but you could try using @mouseenter and @mouseout
<button
@click="toggle"
tabindex="0"
@mouseenter="itemFocus"
@mouseout="itemBlur"
>
Toogle menu
Here's the fiddle: https://jsfiddle/xhmsf9yw/5/
I'm about 40% certain I understood the problem...
based on your description, it seems like the button shouldn't be a toggle, but an open.
template:
<div id="app">
<button
@click="showMenu"
tabindex="0"
@focus="itemFocus"
@blur="itemBlur"
>
Toogle menu
</button>
<ul :class="listClass">
<li
v-for="(item, key) in items"
tabindex="0"
@focus="itemFocus"
@blur="itemBlur"
>{{ item }}</li>
</ul>
</div>
method:
showMenu: function(){
this.showMenu = true;
},
this will open the menu when it's closed, but will not close it
https://jsfiddle/wc1oehx9/
The trick with these types of UI with multiple ways of interaction is to set state of the UI element, and have the UI element reflect that state.
So what I did was this:
So here's my solution: https://jsfiddle/jaiko86/e0vtf2k1/1/
HTML:
<div id="app">
<button
tabindex="0"
@focus="isMenuOpen = true"
@blur="isMenuOpen = false"
@mouseenter="isMenuOpen = true"
@mouseout="isMenuOpen = false"
>
Toogle menu
</button>
<ul :class="listClass">
<li
v-for="(item, key) in items"
tabindex="0"
@focus="isMenuOpen = true"
@blur="isMenuOpen = false"
@mouseenter="isMenuOpen = true"
@mouseout="isMenuOpen = false"
>{{ item }}</li>
</ul>
</div>
JS:
// simplified for clarity
new Vue({
el: "#app",
data: {
showMenu: false,
items: [ 'Option 1', 'Option 2', 'Option 3', 'Option 4' ],
isMenuOpen: false,
},
puted: {
listClass() { // you can omit the `: function` in new ES standard
return this.isMenuOpen ? 'show' : ''; //ternary op saves lines
},
methods: {
// not needed, as it's done in HTML
// toggle: function(){
// this.showMenu = !this.showMenu
//},
/*
we no longer need these methods:
itemFocus: function() {
var self = this;
Vue.nextTick(function() {
if(!self.showMenu) {
self.showMenu = true;
}
});
},
itemBlur: function() {
var self = this;
Vue.nextTick(function() {
if(self.showMenu) {
self.showMenu = false;
}
});
}
*/
}
})
I had faced a similar issue, I referred to the code below to solve it. For an depth explanation on how this works here is the link to the website I referred.
This is the HTML:
<nav class="flex items-center justify-between h-full p-3 m-auto bg-orange-200">
<span>My Logo</span>
<div class="relative">
<button id="user-menu" aria-label="User menu" aria-haspopup="true">
<img
class="w-8 h-8 rounded-full"
src="https://scontent.fcpt4-1.fna.fbcdn/v/t1.0-1/p480x480/82455849_2533242576932502_5629407411459588096_o.jpg?_nc_cat=100&ccb=2&_nc_sid=7206a8&_nc_ohc=rGM_UBdnnA8AX_pGIdM&_nc_ht=scontent.fcpt4-1.fna&tp=6&oh=7de8686cebfc29e104c118fc3f78c7e5&oe=5FD1C3FE"
/>
</button>
<div
id="user-menu-dropdown"
class="absolute right-0 w-48 mt-2 origin-top-right rounded-lg shadow-lg top-10 menu-hidden"
>
<div
class="p-4 bg-white rounded-md shadow-xs"
role="menu"
aria-orientation="vertical"
aria-labelledby="user-menu"
>
<a
href="#"
class="block px-6 py-2 mb-2 font-bold rounded"
role="menuitem"
>My profile</a
>
<a href="#" class="block px-6 py-2 font-bold rounded" role="menuitem"
>Logout</a
>
</div>
</div>
</div>
</nav>
This is the CSS:
#user-menu ~ #user-menu-dropdown {
transform: scaleX(0) scaleY(0);
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
transition-duration: 75ms;
opacity: 0;
top: 3.25rem;
}
#user-menu ~ #user-menu-dropdown:focus-within,
#user-menu:focus ~ #user-menu-dropdown {
transform: scaleX(1) scaleY(1);
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
transition-duration: 100ms;
opacity: 1;
}