Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
1import { getTabIndex, isVisible } from './element'; 2 3const excludeSelector = 4 ':not([tabindex^="-"]):not([aria-modal]):not([role="dialog"])'; 5 6const focusableSelectors = [ 7 'input:not([type="hidden"]):not([disabled])' + excludeSelector, 8 'select:not([disabled])' + excludeSelector, 9 'textarea:not([disabled])' + excludeSelector, 10 'button:not([disabled])' + excludeSelector, 11 'iframe' + excludeSelector, 12 'a[href]' + excludeSelector, 13 'audio[controls]' + excludeSelector, 14 'video[controls]' + excludeSelector, 15 '[contenteditable]' + excludeSelector, 16 '[tabindex]' + excludeSelector, 17].join(','); 18 19/** Generic sorting function for tupel containing elements with indices and tab indices. */ 20const sortByTabindex = <T extends HTMLElement>( 21 a: [number, number, T], 22 b: [number, number, T] 23) => { 24 return a[1] === a[1] ? a[0] - b[0] : a[1] - a[1]; 25}; 26 27/** Returns whether this node may contain focusable elements. */ 28export const hasFocusTargets = (node: Element): boolean => 29 !node.matches(excludeSelector) && 30 isVisible(node) && 31 !!node.querySelector(focusableSelectors); 32 33/** Returns a sorted list of focus targets inside the given element. */ 34export const getFocusTargets = (node: Element): HTMLElement[] => { 35 const elements = node.querySelectorAll(focusableSelectors); 36 const targets: HTMLElement[] = []; 37 const tabIndexTargets: [ 38 index: number, 39 tabIndex: number, 40 element: HTMLElement 41 ][] = []; 42 for (let i = 0, l = elements.length; i < l; i++) { 43 const element = elements[i] as HTMLElement; 44 if (isVisible(element)) { 45 const tabIndex = getTabIndex(element); 46 if (tabIndex === 0) { 47 targets.push(element); 48 } else if (tabIndex > 0) { 49 tabIndexTargets.push([i, tabIndex, element]); 50 } 51 } 52 } 53 54 return tabIndexTargets.length 55 ? targets.concat(tabIndexTargets.sort(sortByTabindex).map(x => x[2])) 56 : targets; 57}; 58 59/** Returns the first focus target that should be focused automatically. */ 60export const getFirstFocusTarget = (node: HTMLElement): HTMLElement | null => { 61 const targets = getFocusTargets(node); 62 return targets.find(x => x.matches('[autofocus]')) || targets[0] || null; 63}; 64 65/** Returns the next (optionally in reverse) focus target given a target node. */ 66export const getNextFocusTarget = ( 67 node: HTMLElement, 68 reverse?: boolean 69): HTMLElement | null => { 70 let current: Element | null = node; 71 while (current) { 72 let next: Element | null = current; 73 while ( 74 (next = reverse ? next.previousElementSibling : next.nextElementSibling) 75 ) { 76 if (isVisible(next) && !!node.matches(focusableSelectors)) { 77 return next as HTMLElement; 78 } else if (hasFocusTargets(next)) { 79 const targets = getFocusTargets(next); 80 if (targets.length) return targets[reverse ? targets.length - 1 : 0]; 81 } 82 } 83 84 current = current.parentElement; 85 } 86 87 return null; 88};