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