-
Notifications
You must be signed in to change notification settings - Fork 20
Description
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.