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 function useDialogDismiss<T extends HTMLElement>(
9 ref: Ref<T>,
10 onDismiss: () => void
11) {
12 const hasPriority = usePriority(ref);
13
14 useLayoutEffect(() => {
15 if (!ref.current || !hasPriority) return;
16
17 function onKey(event: KeyboardEvent) {
18 if (!event.isComposing && event.code === 'Escape') {
19 // The current dialog can be dismissed by pressing escape if it either has focus
20 // or it has priority
21 const active = document.activeElement;
22 if (hasPriority || (active && contains(ref.current, active))) {
23 event.preventDefault();
24 onDismiss();
25 }
26 }
27 }
28
29 function onClick(event: MouseEvent | TouchEvent) {
30 const { target } = event;
31 if (contains(ref.current, target) || event.defaultPrevented) {
32 return;
33 }
34
35 // The current dialog can be dismissed by pressing outside of it if it either has
36 // focus or it has priority
37 const active = document.activeElement;
38 if (hasPriority || (active && contains(ref.current, active))) {
39 event.preventDefault();
40 onDismiss();
41 }
42 }
43
44 document.addEventListener('mousedown', onClick);
45 document.addEventListener('touchstart', onClick);
46 document.addEventListener('keydown', onKey);
47
48 return () => {
49 document.removeEventListener('mousedown', onClick);
50 document.removeEventListener('touchstart', onClick);
51 document.removeEventListener('keydown', onKey);
52 };
53 }, [ref, hasPriority, onDismiss]);
54}