/
Dialog.tsx
120 lines (111 loc) · 4.88 KB
/
Dialog.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
'use client';
import { useState } from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { twJoin, twMerge } from 'tailwind-merge';
import { ChildrenProps } from '@/features/shared/ui/ChildrenProps';
import { XIcon } from '@/features/shared/ui/icon/XIcon';
import { RouterAction } from '@/features/shared/routing/RouterActions';
import { useRouterAction } from '@/features/shared/routing/useRouterAction';
/*
* Scrollable overlay
* "Move the content inside the overlay to render a dialog with overflow."
* Ref: https://www.radix-ui.com/primitives/docs/components/dialog#scrollable-overlay
*
* We want to have a fade-in / fade-out only in the overlay on mobile, but since we have to
* move content inside the overlay to have it scrollable, if we animate Dialog.Overlay we also
* animate Dialog.Content. So we make Dialog.Overlay invisible, and inside it we have
* a visible one animated, that doesn't wrap Dialog.Content.
* But we need some animation on Dialog.Overlay in order for Radix to hold its DOM element while
* the Dialog exit animation plays.
* So we have this "fake" animation from opacity: 1 to opacity: 1 on Dialog.Overlay.
*/
export const invisibleOverlayFakeAnimation =
'[state=open]:animate-[fake-fade_200ms_ease-out] data-[state=closed]:animate-[fake-fade_200ms_ease-out]';
export const invisibleOverlayClassNames = `group/overlay fixed inset-0 w-full h-full top-0 left-0 right-0 bottom-0 overflow-y-auto ${invisibleOverlayFakeAnimation}`;
/**/
export const visibleOverlayAnimation =
'group-data-[state=open]/overlay:animate-[fade-in_200ms_ease-out] group-data-[state=closed]/overlay:animate-[fade-out_200ms_ease-out]';
export const visibleOverlayClassNames = `fixed inset-0 bg-black/50 ${visibleOverlayAnimation}`;
export const dialogContentAnimationSm = `data-[state=open]:animation-dialog-content-show-sm data-[state=closed]:animation-dialog-content-hide-sm`;
export const dialogContentAnimationMd = `md:data-[state=open]:animate-[dialog-content-show-md_300ms_ease-out] md:data-[state=closed]:animate-[dialog-content-hide-md_200ms_ease-in]`;
export const dialogContentClassNames = `flex flex-col mx-auto w-full rounded-lg bg-white p-4 md:w-[40rem] translate-y-[10%] md:translate-y-0 ${dialogContentAnimationSm} ${dialogContentAnimationMd}`;
interface DialogProps extends ChildrenProps {
readonly defaultOpen?: boolean;
readonly headerButtons?: React.ReactNode;
readonly noCloseButton?: boolean;
readonly onOpenAutoFocus?: (event: Event) => void;
readonly onOpenChange?: (open: boolean) => void;
readonly open?: boolean;
readonly routerActionOnClose?: RouterAction;
readonly title?: React.ReactNode;
readonly trigger?: React.ReactNode;
}
export const Dialog = ({
children,
defaultOpen,
headerButtons,
noCloseButton,
onOpenAutoFocus,
onOpenChange,
open,
routerActionOnClose,
title,
trigger,
}: DialogProps) => {
const [isOpen, setIsOpen] = useState(open || defaultOpen);
const routerAction = useRouterAction(routerActionOnClose);
const _onOpenChange = (_open: boolean) => {
setIsOpen(_open);
if (_open) {
if (onOpenChange) onOpenChange(_open);
} else {
setTimeout(() => {
routerAction();
if (onOpenChange) onOpenChange(_open);
}, 300);
}
};
return (
<DialogPrimitive.Root
defaultOpen={defaultOpen}
{...(open === undefined
? { open: isOpen, onOpenChange: _onOpenChange }
: { open, onOpenChange })}
>
{trigger && <DialogPrimitive.Trigger asChild>{trigger}</DialogPrimitive.Trigger>}
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className={twMerge(invisibleOverlayClassNames, 'z-40')}>
<div className={visibleOverlayClassNames} aria-hidden="true" />
<div className="flex fixed inset-0 md:items-center">
<DialogPrimitive.Content
className={dialogContentClassNames}
onOpenAutoFocus={onOpenAutoFocus}
>
<div
className={twJoin(
'flex justify-between',
title ? 'items-center' : 'items-start h-[90%] md:h-full gap-x-3',
)}
>
{title ? (
<DialogPrimitive.Title className="text-xl">{title}</DialogPrimitive.Title>
) : (
children
)}
<div className="flex flex-row gap-x-3">
{headerButtons}
{!noCloseButton && (
<DialogPrimitive.Close className="-m-2.5 rounded-md p-1.5 text-gray-700 hover:bg-gray-200">
<XIcon aria-hidden="true" />
</DialogPrimitive.Close>
)}
</div>
</div>
{title && children}
</DialogPrimitive.Content>
</div>
</DialogPrimitive.Overlay>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
);
};