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