Ensuring Vue dropdown retains focus with custom configuration

After an unsuccessful search for a solution, I decided to write a question myself.

My goal is to create a basic drop-down menu component in Vue with a toggle button that collapses the menu when focus leaves the component. Despite using @blur and @focus events to maintain focus, I encountered an issue with the toggle button. If I apply the same listeners to it, clicking on the button results in the menu appearing and disappearing instantly, requiring another click to expand it.

You can see the problem demonstrated in this fiddle.

If I remove the listeners from the button, accessing the button after focusing inside the component becomes problematic. It seems like my approach needs adjusting, so here's what I anticipate:

  • The toggle button should simply toggle the list's visibility upon clicks
  • The list can only be expanded by clicking the toggle button when it's closed
  • The list collapses when either clicking the toggle button while it's open or when focus shifts elsewhere, including focusing out of the button itself (i.e., expanding the list by clicking the toggle button and then clicking outside without selecting any items).

EDIT: Thanks to @Sphinx, I successfully achieved the desired dropdown functionality. Here's the updated fiddle.

Answer №1

Given your situation, it has been noted in the comments below the question that you will encounter various scenarios, such as consecutive triggers of @focus and @click, as well as simultaneous triggering of @blur on <button> and <ul></li> when clicking on either of them.

This approach is considered unfavorable. However, you can refer to this fiddle for one solution involving the use of setTimeout & clearTimeout. As evidenced by a delay of 100ms through setTimeout(()=>{}, 100) (additional logs have been provided for verification by opening the browser console), waiting an adequate amount of time ensures that subsequent event handlers (e.g., focus triggered before click) can successfully clear previous instances of setTimeout prior to the specified time to prevent inadvertent menu openings and closings. (Note: Depending on the speed of current rendering, 100ms may be insufficient for older machines).

A potential solution:

  1. Eliminate @focus and @blur

  2. If this.showMenu is true (open), attach an event listener=click to Dom=document that executes this.hide() upon activation.

  3. Within this.hide(), remove the aforementioned event listener=click from Dom=document.

  4. To avoid unintended menu collapse when clicking on both the button and the menu, utilize the modifier=stop to halt propagation of the click event to higher level Dom nodes.

By enclosing both <button> and <ul> within a single <div>, simply include the modifier=stop like so:

<template><div @click.stop><button></button<ul>...<ul></div></template>
.

Below is a demonstration:

Vue.config.productionTip = false

new Vue({
  el: "#app",
  data: {
    showMenu: false,
    items: ['Option 1', 'Option 2', 'Option 3', 'Option 4']
  },
  computed: {
    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.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
  <h3>
    This is one demo:
  </h3>
  <button @click.stop="toggle" tabindex="0">
    Toggle menu
  </button>
  <ul :class="listClass" @click.stop>
    <li v-for="(item, key) in items" tabindex="0">{{ item }}</li>
  </ul>
</div>

Answer №2

Not entirely certain if this solution will address your needs, but you might want to consider utilizing the @mouseenter and @mouseout events

<button 
@click="toggle" 
tabindex="0"
@mouseenter="itemFocus"
@mouseout="itemBlur"
>
Toggle menu

Feel free to check out the fiddle for more information: https://jsfiddle.net/xhmsf9yw/6/

Answer №3

I believe I have a good grasp of the issue, about 40% certainty...

From what you've explained, it appears that the button should not toggle but rather open.

Here is a suggested template:

<div id="app">
  <button 
    @click="showMenu" 
    tabindex="0"
    @focus="itemFocus"
    @blur="itemBlur"
    >
    Open 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 if closed, without closing it

Here is an example on jsFiddle

Answer №4

Ensuring smooth user interaction in UI elements with multiple functionalities involves effectively managing the state of the element and ensuring it reflects the desired behavior.

My approach to handling this was as follows:

  • When the mouse enters, the menu should open
  • When the mouse leaves, the menu should close
  • If the element is focused, the menu should remain open
  • If the element is blurred, the menu should be closed

To see the implementation, you can check out my solution at: https://jsfiddle.net/jaiko86/e0vtf2k1/1/

HTML:

<div id="app">
  <button 
    tabindex="0"
    @focus="isMenuOpen = true"
    @blur="isMenuOpen = false"
    @mouseenter="isMenuOpen = true"
    @mouseout="isMenuOpen = false"
    >
    Toggle 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,
  },
  computed: {
    listClass() {
      return this.isMenuOpen ? 'show' : '';
    },
  methods: {
    
  }
})

Answer №5

After encountering a similar issue, I found a helpful solution in the code snippet below. If you're looking for a detailed explanation on how this functionality works, check out the link to the website I used as a reference.

Here is the HTML structure:

<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.net/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>

And here's the accompanying CSS styling:

#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;
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is it permissible to assign the same element as a child to multiple parent elements in jQuery?

Imagine you have the following HTML structure: <div id="first"></div> <div id="second"></div> Now, if you use JavaScript and JQuery to perform the following actions: var $child = $("<span id='child'>Hello</span ...

Issue: The function "generateActiveToken" is not recognized as a function

I encountered an issue in my Node.js project and I'm unsure about the root cause of this error. Within the config folder, there is a file named generateToken.js which contains the following code snippet: const jwt = require('jsonwebtoken'); ...

What is the solution for the red underline issue with the @json Blade directive in VSC?

Everything seems to be functioning correctly with these variables, but I'm curious as to why they are underlined in red. Does anyone have a solution for this issue? https://i.sstatic.net/mzlkY.png Error message https://i.sstatic.net/4LajF.png ...

Leverage the power of dynamic PHP variables within your JavaScript code

I have an image database and a search form. I want to display the images on the next page using JavaScript with the OpenLayers library. Here is the PHP code I wrote: <?php mysql_connect('localhost','root',""); mysql_select_ ...

Encountering difficulties with integrating map controls into the Nuxt3/Vue3 mapbox feature for zooming

Recently, I started exploring Mapbox within my new Nuxt3 application. I managed to successfully render the map in my custom Map.vue component. However, I am facing trouble when trying to add controls and other options. Despite my efforts, I can't see ...

Navigating through asynchronous transactions in Node.js: A Guide

My application utilizes Nodejs, Mongodb, and amqp. The main module receives messages from amqp, processes them by saving message information into mongodb within a message transaction, and executes additional logic. function messageReceiverCallback(msg) { ...

Click to Resize Window with the Same Dimensions

I have a link on my website that opens a floating window containing more links when clicked. <a href='javascript:void(0);' onclick='window.open("http://mylink.html","ZenPad","width=150, height=900");' target='ZenPad'>&l ...

Confirming the data in every cell across each row of SlickGrid

How can I validate all cells in a SlickGrid? Is there a way to use JavaScript to trigger validation for each cell, ensuring that the user has provided something other than the default value? We have a use case where every cell must be edited by the user w ...

Utilize Laravel in conjunction with AngularJs by implementing a base path in place of the current one when using ng-src

Let me try to explain my issue clearly. I am delving into using angularJs in my Laravel project for the first time. The controller is responsible for fetching the uploaded photos from the database. public function index() { JavaScript::put([ ...

Struggling to achieve success in redirecting with passport for Facebook

For a "todolist" web application that utilizes passport-facebook for third party authentication, the following code is implemented: passport.use(new FacebookStrategy({ clientID: '566950043453498', clientSecret: '555022a61da40afc8ead59 ...

Directly insert an image into your webpage by uploading it from the input field without the need to go through the server

Can an image be uploaded directly from <input type="file" /> into an HTML page, for example through the use of Javascript, without needing to first save the image to the server? I am aware that AJAX can accomplish this, but if there is a way to ...

What methods can I use to integrate a Google HeatMap into the GoogleMap object in the Angular AGM library?

I am trying to fetch the googleMap object in agm and utilize it to create a HeatMapLayer in my project. However, the following code is not functioning as expected: declare var google: any; @Directive({ selector: 'my-comp', }) export class MyC ...

When utilizing Rx.Observable with the pausable feature, the subscribe function is not executed

Note: In my current project, I am utilizing TypeScript along with RxJS version 2.5.3. My objective is to track idle click times on a screen for a duration of 5 seconds. var noClickStream = Rx.Observable.fromEvent<MouseEvent>($window.document, &apos ...

The Jest mock for dates is completely ineffective and always ends up returning the constructor

beforeAll(() => { ... const mockedData = '2020-11-26T00:00:00.000Z' jest.spyOn(global, 'Date').mockImplementation(() => mockedData) Date.now = () => 1606348800 }) describe('getIventory', () => { ...

Quickly view products in Opencart will automatically close after adding them to the cart and redirecting the

I've integrated the product quickview feature into my OpenCart theme, which opens a product in a popup. However, when I add a product to the cart from the popup, it doesn't update on the main page until I refresh. I'm looking for a way to re ...

Optimize Material-UI input fields to occupy the entire toolbar

I'm having trouble getting the material-ui app bar example to work as I want. I've created a CodeSandbox example based on the Material-UI website. My Goal: My goal is to make the search field expand fully to the right side of the app bar, regar ...

"Unlocking the power of Webpack in Electron: Incorporating and executing a bundled function within the

I am facing an issue where I am trying to access a function from my webpack bundle, but the function is turning out to be undefined. The entire object seems to be empty. In electron-main.js, you can see that I am calling appBundle.test(). I have searched ...

unable to retrieve access-token and uid from the response headers

I am attempting to extract access-token and uid from the response headers of a post request, as shown in the screenshot at this https://i.sstatic.net/8w8pV.png Here is how I am approaching this task from the service side: signup(postObj: any){ let url = e ...

Changing a string array into an array with JavaScript

After making a request to my django-rest-framework API, I receive the following item: services = "['service1', 'service2', 'service3']" I am aiming for services = ['service1', 'service2', 'service3&a ...

Obtaining localStream for muting microphone during SIP.js call reception

My approach to muting the microphone involves using a mediastream obtained through the sessionDescriptionHandler's userMedia event. session.sessionDescriptionHandler.on('userMedia', onUserMediaObtained.bind(this)) function onUserMediaObta ...