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 (disabled) return; 33 34 let selection: RestoreSelection | null = null; 35 if ( 36 !document.activeElement || 37 !contains(ref.current, document.activeElement) 38 ) { 39 const newTarget = ref.current ? getAutofocusTarget(ref.current) : null; 40 selection = snapshotSelection(); 41 if (newTarget) newTarget.focus(); 42 } 43 44 function onBlur(event: FocusEvent) { 45 const { current: element } = ref; 46 if (!hasPriority.current || !element || event.defaultPrevented) return; 47 48 if ( 49 contains(element, event.target) && 50 !contains(element, event.relatedTarget) 51 ) { 52 const target = getFirstFocusTarget(element); 53 if (target) target.focus(); 54 } 55 } 56 57 function onKeyDown(event: KeyboardEvent) { 58 const { current: element } = ref; 59 if (!hasPriority.current || !element || event.defaultPrevented) return; 60 61 if (event.code === 'Tab') { 62 const activeElement = document.activeElement as HTMLElement; 63 const targets = getFocusTargets(element); 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 document.body.removeEventListener('focusout', onBlur); 80 document.removeEventListener('keydown', onKeyDown); 81 restoreSelection(selection); 82 }; 83 }, [ref, hasPriority, disabled]); 84}