-
Notifications
You must be signed in to change notification settings - Fork 40
/
Copy pathModalDialog.tsx
123 lines (103 loc) · 3.29 KB
/
ModalDialog.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import { useMergeRefs } from '@floating-ui/react';
import { Slot } from '@radix-ui/react-slot';
import cl from 'clsx/lite';
import type { DialogHTMLAttributes } from 'react';
import { forwardRef, useContext, useEffect, useRef } from 'react';
import { ModalContext } from './ModalRoot';
import { useModalState } from './useModalState';
import { useScrollLock } from './useScrollLock';
export type ModalDialogProps = {
/**
* Close modal when clicking on backdrop.
* @default undefined
*/
onInteractOutside?: () => void;
/**
* Callback that is called when the modal is closed.
* @default undefined
*/
onClose?: () => void;
/**
* Called before the modal is closed when using the close button, `closeOnBackdropClick` or `ESCAPE`.
* If the function returns `false` the modal will not close.
*/
onBeforeClose?: () => boolean | undefined;
asChild?: boolean;
} & DialogHTMLAttributes<HTMLDialogElement>;
export const ModalDialog = forwardRef<HTMLDialogElement, ModalDialogProps>(
(
{
onInteractOutside,
onClose,
onBeforeClose,
asChild,
className,
children,
...rest
},
ref,
) => {
const Component = asChild ? Slot : 'dialog';
// This local ref is used to make sure the modal works without a ModalRoot
const modalDialogRef = useRef<HTMLDialogElement>(null);
const { modalRef, setOpen, setCloseModal } = useContext(ModalContext);
const open = useModalState(modalDialogRef);
useEffect(() => {
setCloseModal?.(() => {
if (onBeforeClose && onBeforeClose() === false) return;
modalDialogRef.current?.close();
});
}, [onBeforeClose, setCloseModal]);
const mergedRefs = useMergeRefs([modalRef, ref, modalDialogRef]);
useScrollLock(modalDialogRef, 'ds-modal--lock-scroll');
useEffect(() => {
setOpen(open);
}, [open, setOpen]);
useEffect(() => {
const modalEl = modalRef.current;
const handleBackdropClick = (e: MouseEvent) => {
if (e.target === modalEl && onInteractOutside) {
// Fix bug where if you select text spanning two divs it thinks you clicked outside
if (window.getSelection()?.toString()) return;
onInteractOutside?.();
}
};
if (modalEl) modalEl.addEventListener('click', handleBackdropClick);
return () => {
if (modalEl) {
modalEl.removeEventListener('click', handleBackdropClick);
}
};
}, [onInteractOutside, modalRef, onBeforeClose, ref]);
useEffect(() => {
const modalEl = modalRef.current;
const handleModalClose = () => {
onClose?.();
};
if (modalEl) modalEl.addEventListener('close', handleModalClose);
return () => {
if (modalEl) {
modalEl.removeEventListener('close', handleModalClose);
}
};
}, [modalRef, onClose]);
const onCancel: ModalDialogProps['onCancel'] = (e) => {
if (onBeforeClose && onBeforeClose() === false) {
e.preventDefault();
return;
}
modalRef.current?.close();
};
return (
<Component
ref={mergedRefs}
className={cl('ds-modal', className)}
onCancel={onCancel}
{...rest}
>
{children}
</Component>
);
},
);
ModalDialog.displayName = 'ModalDialog';