/** * Copied from chakra-ui, license MIT * Accessed 2021-12-26, Commit July 25th, 2021 * See also: https://github.com/chakra-ui/chakra-ui/blob/69b7ef1/packages/utils/src/dom-query.ts */ import { isHTMLElement } from "./dom"; import { isFocusable, isTabbable } from "./tabbable"; const focusableElList = [ "input:not([disabled])", "select:not([disabled])", "textarea:not([disabled])", "embed", "iframe", "object", "a[href]", "area[href]", "button:not([disabled])", "[tabindex]", "audio[controls]", "video[controls]", "*[tabindex]:not([aria-disabled])", "*[contenteditable]", ]; const focusableElSelector = focusableElList.join(); export function getAllFocusable(container: T) { const focusableEls = Array.from( container.querySelectorAll(focusableElSelector), ); focusableEls.unshift(container); return focusableEls .filter(isFocusable) .filter((el) => window.getComputedStyle(el).display !== "none"); } export function getFirstFocusable(container: T) { const allFocusable = getAllFocusable(container); return allFocusable.length ? allFocusable[0] : null; } export function getAllTabbable( container: T, fallbackToFocusable?: boolean, ) { const allFocusable = Array.from( container.querySelectorAll(focusableElSelector), ); const allTabbable = allFocusable.filter(isTabbable); if (isTabbable(container)) { allTabbable.unshift(container); } if (!allTabbable.length && fallbackToFocusable) { return allFocusable; } return allTabbable; } export function getFirstTabbableIn( container: T, fallbackToFocusable?: boolean, ): T | null { const [first] = getAllTabbable(container, fallbackToFocusable); return first || null; } export function getLastTabbableIn( container: T, fallbackToFocusable?: boolean, ): T | null { const allTabbable = getAllTabbable(container, fallbackToFocusable); return allTabbable[allTabbable.length - 1] || null; } export function getNextTabbable( container: T, fallbackToFocusable?: boolean, ): T | null { const allFocusable = getAllFocusable(container); const index = allFocusable.indexOf(document.activeElement as T); const slice = allFocusable.slice(index + 1); return ( slice.find(isTabbable) || allFocusable.find(isTabbable) || (fallbackToFocusable ? slice[0] : null) ); } export function getPreviousTabbable( container: T, fallbackToFocusable?: boolean, ): T | null { const allFocusable = getAllFocusable(container).reverse(); const index = allFocusable.indexOf(document.activeElement as T); const slice = allFocusable.slice(index + 1); return ( slice.find(isTabbable) || allFocusable.find(isTabbable) || (fallbackToFocusable ? slice[0] : null) ); } export function focusNextTabbable( container: T, fallbackToFocusable?: boolean, ) { const nextTabbable = getNextTabbable(container, fallbackToFocusable); if (nextTabbable && isHTMLElement(nextTabbable)) { nextTabbable.focus(); } } export function focusPreviousTabbable( container: T, fallbackToFocusable?: boolean, ) { const previousTabbable = getPreviousTabbable(container, fallbackToFocusable); if (previousTabbable && isHTMLElement(previousTabbable)) { previousTabbable.focus(); } } function matches(element: Element, selectors: string): boolean { if ("matches" in element) return element.matches(selectors); if ("msMatchesSelector" in element) return (element as any).msMatchesSelector(selectors); return (element as any).webkitMatchesSelector(selectors); } export function closest(element: T, selectors: string) { if ("closest" in element) return element.closest(selectors); do { if (matches(element, selectors)) return element; element = (element.parentElement || element.parentNode) as any; } while (element !== null && element.nodeType === 1); return null; }