Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
at main 2.4 kB view raw
1import { 2 RestoreSelection, 3 snapshotSelection, 4 restoreSelection, 5} from './utils/selection'; 6 7import { getActive, getAutofocusTarget, getFocusTargets } from './utils/focus'; 8import { useLayoutEffect } from './utils/react'; 9import { contains, getRoot } from './utils/element'; 10import { makePriorityHook } from './usePriority'; 11import { Ref } from './types'; 12 13const usePriority = makePriorityHook(); 14 15export interface ModalFocusOptions { 16 disabled?: boolean; 17} 18 19export function useModalFocus<T extends HTMLElement>( 20 ref: Ref<T>, 21 options?: ModalFocusOptions 22) { 23 const disabled = !!(options && options.disabled); 24 const hasPriority = usePriority(ref, disabled); 25 26 useLayoutEffect(() => { 27 const { current: element } = ref; 28 if (!element || disabled) return; 29 30 const root = getRoot(element); 31 const active = getActive(); 32 let selection: RestoreSelection | null = null; 33 if (!active || !contains(element, active)) { 34 const newTarget = ref.current ? getAutofocusTarget(ref.current) : null; 35 selection = snapshotSelection(); 36 if (newTarget) newTarget.focus(); 37 } 38 39 function onBlur(event: FocusEvent) { 40 const { current: element } = ref; 41 if (!hasPriority.current || !element || event.defaultPrevented) return; 42 43 if ( 44 contains(element, event.target) && 45 !contains(element, event.relatedTarget) 46 ) { 47 const target = getFocusTargets(element)[0]; 48 if (target) target.focus(); 49 } 50 } 51 52 function onKeyDown(event: KeyboardEvent) { 53 const { current: element } = ref; 54 if (!hasPriority.current || !element || event.defaultPrevented) return; 55 56 if (event.code === 'Tab') { 57 const activeElement = getActive()!; 58 const targets = getFocusTargets(element); 59 const index = targets.indexOf(activeElement); 60 if (event.shiftKey && index === 0) { 61 event.preventDefault(); 62 targets[targets.length - 1].focus(); 63 } else if (!event.shiftKey && index === targets.length - 1) { 64 event.preventDefault(); 65 targets[0].focus(); 66 } 67 } 68 } 69 70 root.addEventListener('focusout', onBlur); 71 root.addEventListener('keydown', onKeyDown); 72 73 return () => { 74 root.removeEventListener('focusout', onBlur); 75 root.removeEventListener('keydown', onKeyDown); 76 restoreSelection(selection); 77 }; 78 }, [ref.current, hasPriority, disabled]); 79}