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}