Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
at main 3.1 kB view raw
1import { getTabIndex, isVisible } from './element'; 2 3export const inputSelectors = 4 'input:not([type="hidden"]):not([disabled])' + 5 ',textarea:not([disabled])' + 6 ',[contenteditable]'; 7 8export const clickableSelectors = 9 inputSelectors + 10 ',select:not([disabled])' + 11 ',button:not([disabled])' + 12 ',a[href]'; 13 14const focusableSelectors = 15 clickableSelectors + 16 ',select:not([disabled])' + 17 ',button:not([disabled])' + 18 ',iframe' + 19 ',a[href]' + 20 ',audio[controls]' + 21 ',video[controls]' + 22 ',[tabindex]'; 23 24/** Generic sorting function for tupel containing elements with indices and tab indices. */ 25const sortByTabindex = (a: HTMLElement, b: HTMLElement) => { 26 const tabIndexA = getTabIndex(a) || 1 << 29; 27 const tabIndexB = getTabIndex(b) || 1 << 29; 28 return tabIndexA - tabIndexB; 29}; 30 31/** Returns whether this node is focusable. */ 32export const isFocusTarget = (node: Element): boolean => 33 !!node.matches(focusableSelectors) && isVisible(node); 34 35/** Returns a sorted list of focus targets inside the given element. */ 36export const getFocusTargets = (node: Element): HTMLElement[] => 37 ([...node.querySelectorAll(focusableSelectors)] as HTMLElement[]) 38 .filter(isVisible) 39 .sort(sortByTabindex); 40 41/** Returns whether this node may contain focusable elements. */ 42export const hasFocusTargets = (node: Element): boolean => 43 isVisible(node) && !!getFocusTargets(node).length; 44 45/** Returns the first focus target that should be focused automatically in a modal/dialog. */ 46export const getAutofocusTarget = (node: HTMLElement): HTMLElement => { 47 const elements = node.querySelectorAll(focusableSelectors); 48 for (const element of elements) 49 if (isVisible(element) && element.autofocus) return element; 50 node.tabIndex = -1; 51 return node; 52}; 53 54/** Returns the next (optionally in reverse) focus target given a target node. */ 55export const getNextFocusTarget = ( 56 node: HTMLElement, 57 reverse?: boolean 58): HTMLElement | null => { 59 let current: Element | null = node; 60 while (current) { 61 let next: Element | null = current; 62 while ( 63 (next = reverse ? next.previousElementSibling : next.nextElementSibling) 64 ) { 65 if (!isVisible(next)) { 66 continue; 67 } else if (!!next.matches(focusableSelectors)) { 68 return next as HTMLElement; 69 } else { 70 const targets = getFocusTargets(next); 71 if (targets.length) return targets[reverse ? targets.length - 1 : 0]; 72 } 73 } 74 75 current = current.parentElement; 76 } 77 78 return null; 79}; 80 81/** Focuses the given node or blurs if null is passed. */ 82export const focus = (node: Element | null) => { 83 if (node) { 84 (node as HTMLElement).focus(); 85 } else { 86 const active = getActive(); 87 if (active) active.blur(); 88 } 89}; 90 91/** Returns the currently active element, even if it’s contained in a shadow root. */ 92export const getActive = (): HTMLElement | null => { 93 let element = document.activeElement; 94 while (element && element.shadowRoot) 95 element = element.shadowRoot.activeElement; 96 return element as HTMLElement | null; 97};