javascript - Custom Vue dropdown maintain focus - Stack Overflow

admin2025-04-19  0

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:

  • The Toggle button is just that - a toggle button. It should collapse/expand the list on clicks
  • The list can be expanded only by clicking on Toggle button when it is closed
  • The list is collapsed ether by clicking on Toggle 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:

  • The Toggle button is just that - a toggle button. It should collapse/expand the list on clicks
  • The list can be expanded only by clicking on Toggle button when it is closed
  • The list is collapsed ether by clicking on Toggle 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.

Share edited Sep 21, 2018 at 8:03 mrd asked Sep 20, 2018 at 21:51 mrdmrd 831 gold badge2 silver badges5 bronze badges 3
  • 1 There is both a @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
  • @Tommos has the problem right, but note that it is browser dependent. On some browsers, when you click on a button, the browser fires a 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
  • @Tommos Thanks, I get why is this happening. But if you remove @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
Add a ment  | 

5 Answers 5

Reset to default 2

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:

  1. remove @focus and @blur

  2. when this.showMenu is true(opened), add one listener=click for Dom=document it will execute this.hide() when triggered.

  3. Then inside this.hide(), remove that listener=click from Dom=document.

  4. 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:

  • If mouse enters, the menu should be open
  • If mouse leaves, the menu should be close
  • If its focused, the menu should be open
  • If its blurred, the menu should be close

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;
}
转载请注明原文地址:http://conceptsofalgorithm.com/Algorithm/1745057896a282514.html

最新回复(0)