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