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 const { current: element } = ref; 33 if (!element || !hasPriority || disabled) return; 34 35 let selection: RestoreSelection | null = null; 36 if (!document.activeElement || !contains(element, document.activeElement)) { 37 const newTarget = getAutofocusTarget(element); 38 selection = snapshotSelection(element); 39 newTarget.focus(); 40 } 41 42 function onBlur(event: FocusEvent) { 43 if (!element || event.defaultPrevented) return; 44 45 if ( 46 contains(element, event.target) && 47 !contains(element, event.relatedTarget) 48 ) { 49 const target = getFirstFocusTarget(element); 50 if (target) target.focus(); 51 } 52 } 53 54 function onKeyDown(event: KeyboardEvent) { 55 if (!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 restoreSelection(selection); 76 document.body.removeEventListener('focusout', onBlur); 77 document.removeEventListener('keydown', onKeyDown); 78 }; 79 }, [ref.current, hasPriority, disabled]); 80}