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