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