Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
at v0.1.0 3.7 kB view raw
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 is focusable. */ 28export const isFocusTarget = (node: Element): boolean => 29 !!node.matches(focusableSelectors) && isVisible(node); 30 31/** Returns whether this node may contain focusable elements. */ 32export const hasFocusTargets = (node: Element): boolean => 33 !node.matches(excludeSelector) && 34 isVisible(node) && 35 !!node.querySelector(focusableSelectors); 36 37/** Returns a sorted list of focus targets inside the given element. */ 38export const getFocusTargets = (node: Element): HTMLElement[] => { 39 const elements = node.querySelectorAll(focusableSelectors); 40 const targets: HTMLElement[] = []; 41 const tabIndexTargets: [ 42 index: number, 43 tabIndex: number, 44 element: HTMLElement 45 ][] = []; 46 for (let i = 0, l = elements.length; i < l; i++) { 47 const element = elements[i] as HTMLElement; 48 if (isVisible(element)) { 49 const tabIndex = getTabIndex(element); 50 if (tabIndex === 0) { 51 targets.push(element); 52 } else if (tabIndex > 0) { 53 tabIndexTargets.push([i, tabIndex, element]); 54 } 55 } 56 } 57 58 return tabIndexTargets.length 59 ? tabIndexTargets 60 .sort(sortByTabindex) 61 .map(x => x[2]) 62 .concat(targets) 63 : targets; 64}; 65 66/** Returns the first focus target that should be focused automatically. */ 67export const getFirstFocusTarget = (node: HTMLElement): HTMLElement | null => 68 getFocusTargets(node)[0] || null; 69 70/** Returns the first focus target that should be focused automatically in a modal/dialog. */ 71export const getAutofocusTarget = (node: HTMLElement): HTMLElement => { 72 const elements = node.querySelectorAll(focusableSelectors); 73 for (let i = 0, l = elements.length; i < l; i++) { 74 const element = elements[i] as HTMLElement; 75 if (isVisible(element) && element.matches('[autofocus]')) return element; 76 } 77 78 node.setAttribute('tabindex', '-1'); 79 return node; 80}; 81 82/** Returns the next (optionally in reverse) focus target given a target node. */ 83export const getNextFocusTarget = ( 84 node: HTMLElement, 85 reverse?: boolean 86): HTMLElement | null => { 87 let current: Element | null = node; 88 while (current) { 89 let next: Element | null = current; 90 while ( 91 (next = reverse ? next.previousElementSibling : next.nextElementSibling) 92 ) { 93 if (isVisible(next) && !!next.matches(focusableSelectors)) { 94 return next as HTMLElement; 95 } else if (hasFocusTargets(next)) { 96 const targets = getFocusTargets(next); 97 if (targets.length) return targets[reverse ? targets.length - 1 : 0]; 98 } 99 } 100 101 current = current.parentElement; 102 } 103 104 return null; 105}; 106 107/** Focuses the given node or blurs if null is passed. */ 108export const focus = (node: Element | null) => { 109 if (node) { 110 (node as HTMLElement).focus(); 111 } else if (document.activeElement) { 112 (document.activeElement as HTMLElement).blur(); 113 } 114};