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