Skip to content

Focus gets trapped within input when navigating using a keyboard #59

@paulrobertlloyd

Description

@paulrobertlloyd

Out of the box, a token field will capture focus when navigating a page using a keyboard, where it gets trapped.

I’d be curious to hear if @KaneCohen you think this is something this library should prevent happening by default, or maybe provide hooks or options to prevent this from happening. Or maybe provide an example in the documentation?

For anyone else who may come across this issue, and perhaps to spark some ideas as how to address this as part of the library or within any documentation, this is the solution I’ve currently arrived at.


For the token field, I’m using the onInput method, and listening for the Tab key as an indication the user wants to advance to the next focusable element, and Shift + Tab that they want to go to the previous focusable element:

import { focusableElements } from "./focusable-elements.js";

tokenField.onInput = (value, event) => {
  if (event.shiftKey && event.key === "Tab") {
    focusableElements.previous.focus();
  } else if (event.key === "Tab") {
    focusableElements.next.focus();
  }

  return value;
};

The focusableElements object provides previous and next values, which return the previous and next focusable (visible, and keyboard navigable) elements on the page:

const focusableSelector = `a[href], input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), audio[controls], video[controls], iframe, embed, object, summary, [contenteditable], [tabindex]`;

export const focusableElements = {
  /**
   * @returns {Array} All focusable, keyboard navigable and visible elements
   */
  get all() {
    // Get all focusable elements
    const focusable = [...document.querySelectorAll(focusableSelector)];

    // Remove elements that cannot be navigated via the tab key
    const navigable = focusable.filter((element) => element.tabIndex !== -1);

    // Remove elements that are not visible on the page
    const visible = navigable.filter(
      (element) => window.getComputedStyle(element).display !== "none"
    );

    return visible;
  },

  /**
   * @returns {number} Index of currently focused element
   */
  get currentIndex() {
    return this.all.indexOf(document.activeElement);
  },

  /**
   * @returns {number} Index of next focusable element
   */
  get nextIndex() {
    return (this.currentIndex + 1) % this.all.length;
  },

  /**
   * @returns {number} Index of previous focusable element
   */
  get previousIndex() {
    return (this.currentIndex - 1 + this.all.length) % this.all.length;
  },

  /**
   * @returns {HTMLElement} Next focusable element
   */
  get next() {
    return this.all[this.nextIndex];
  },

  /**
   * @returns {HTMLElement} Previous focusable element
   */
  get previous() {
    return this.all[this.previousIndex];
  },
};

This is my first pass at addressing this issue, I’m sure there are edge cases where this might break down, and further refinements that could be made.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions