javascriptweb-componentshadow-domtabindexfocusable

Simulating a Tab Key Press using Plain JavaScript Supporting Shadow DOM


Note: Existing questions exists here and elsewhere but they are all jQuery specific and there is no canonical answer covering plain JavaScript, including support for web components.

I'd like to simulate a tab key press so that the focus is shifted to the next element in the tab order. The next element could be any HTML focusable HTML element or an element with tabindex="0". This also needs to work where some descendant HTML elements could be custom web components with shadow DOM's.

The potential solution could be to fire a key down event or iterating over descendant nodes looking for one that is focusable.


Solution

  • I've tried to make the keyboard events work but sadly I can't get it to work nicely. Based on the docs I believe it the correct way to do it would be:

    document.activeElement.dispatchEvent(new KeyboardEvent("keypress", { 
        key: "Tab" 
    }));
    
    // -----------------------------------------------------
    // Find all the elements that have a tabindex set, 
    // I would keep this file scoped for performance sake
    // ------------------------------------------------------
    const tabElements = Array.from(document
        // Get all elements that can be focusable
      .querySelectorAll('a, button, input, textarea, select, details, [tabindex]'))
      
      // remove any that have a tabIndex of -1
      .filter(element => element.tabIndex > -1)
      
      // reverse, then sort by tabIndex descending to put 0s last but maintain original order
      .reverse().sort((a, b) => a.tabIndex > b.tabIndex ? -1 : 1)
    

    then selecting the next element:

    // ------------------------------------------------------------------------------
    // Method to find the next element to focus and change the focus to that element
    // ------------------------------------------------------------------------------
    const DispatchTab = () => {
    
        // If the current focused element is a -1 then we wont find the index in the elements list, got to the start
        if (document.activeElement.tabIndex === -1) {
        tabElements[0].focus();
        return;
      }
    
        // find the current index in the tab list of the currently focused element
        const currentIndex = tabElements.findIndex(e => e === document.activeElement);
    
      // get the next element in the list ("%" will loop the index around to 0)
      const nextIndex = (currentIndex + 1) % tabElements.length;
      tabElements[nextIndex].focus();
    }
    

    I have created a JS fiddle with an approach that doesn't use the KeyboardEvent at all. It might need tweaking to your liking but should do the job. I feel Tab Example