Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
1import { useLayoutEffect } from './utils/react'; 2import { contains } from './utils/element'; 3import { makePriorityHook } from './usePriority'; 4import { Ref } from './types'; 5 6const usePriority = makePriorityHook(); 7 8export interface DismissableOptions { 9 focusLoss?: boolean; 10} 11 12export function useDismissable<T extends HTMLElement>( 13 ref: Ref<T>, 14 onDismiss: () => void, 15 options?: DismissableOptions 16) { 17 const focusLoss = !!(options && options.focusLoss); 18 const hasPriority = usePriority(ref); 19 20 useLayoutEffect(() => { 21 if (!ref.current || !hasPriority) return; 22 23 function onFocusOut(event: FocusEvent) { 24 if (event.defaultPrevented) return; 25 26 const { target, relatedTarget } = event; 27 if ( 28 contains(ref.current, target) && 29 !contains(ref.current, relatedTarget) 30 ) { 31 onDismiss(); 32 } 33 } 34 35 function onKey(event: KeyboardEvent) { 36 if (!event.isComposing && event.code === 'Escape') { 37 // The current dialog can be dismissed by pressing escape if it either has focus 38 // or it has priority 39 const active = document.activeElement; 40 if (hasPriority || (active && contains(ref.current, active))) { 41 event.preventDefault(); 42 onDismiss(); 43 } 44 } 45 } 46 47 function onClick(event: MouseEvent | TouchEvent) { 48 const { target } = event; 49 if (contains(ref.current, target) || event.defaultPrevented) { 50 return; 51 } 52 53 // The current dialog can be dismissed by pressing outside of it if it either has 54 // focus or it has priority 55 const active = document.activeElement; 56 if (hasPriority || (active && contains(ref.current, active))) { 57 event.preventDefault(); 58 onDismiss(); 59 } 60 } 61 62 if (focusLoss) document.body.addEventListener('focusout', onFocusOut); 63 64 document.addEventListener('mousedown', onClick); 65 document.addEventListener('touchstart', onClick); 66 document.addEventListener('keydown', onKey); 67 68 return () => { 69 if (focusLoss) document.body.removeEventListener('focusout', onFocusOut); 70 71 document.removeEventListener('mousedown', onClick); 72 document.removeEventListener('touchstart', onClick); 73 document.removeEventListener('keydown', onKey); 74 }; 75 }, [ref, hasPriority, focusLoss, onDismiss]); 76}