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