ConfirmationModal.tsx162 lines · main
1'use client'
2
3// Required to avoid issue:
4// The inferred type of ConfirmationModal cannot be named without a reference to DialogProps
5import { Dialog as _RadixDialog } from 'radix-ui'
6import { forwardRef, MouseEventHandler, useEffect, useState } from 'react'
7import {
8 Alert,
9 Button,
10 cn,
11 Dialog,
12 DialogContent,
13 DialogSection,
14 DialogSectionSeparator,
15 DialogTitle,
16} from 'ui'
17import { DialogDescription, DialogHeader } from 'ui/src/components/shadcn/ui/dialog'
18
19import { Admonition } from './../admonition'
20
21export interface ConfirmationModalProps {
22 loading?: boolean
23 visible: boolean
24 title: string | React.ReactNode
25 description?: string | React.ReactNode
26 size?: React.ComponentProps<typeof DialogContent>['size']
27 confirmLabel?: string
28 confirmLabelLoading?: string
29 cancelLabel?: string
30 onConfirm: () => void
31 onCancel: () => void
32 disabled?: boolean
33 variant?: React.ComponentProps<typeof Alert>['variant']
34 alert?: {
35 base?: React.ComponentProps<typeof Alert>
36 title?: string
37 description?: string | React.ReactNode
38 }
39 className?: string
40}
41
42export const ConfirmationModal = forwardRef<
43 React.ElementRef<typeof DialogContent>,
44 React.ComponentPropsWithoutRef<typeof Dialog> & ConfirmationModalProps
45>(
46 (
47 {
48 title,
49 description,
50 size = 'small',
51 visible,
52 onCancel,
53 onConfirm,
54 loading: loading_,
55 cancelLabel = 'Cancel',
56 confirmLabel = 'Submit',
57 confirmLabelLoading,
58 alert = undefined,
59 children,
60 variant = 'default',
61 disabled,
62 className,
63 ...props
64 },
65 ref
66 ) => {
67 // [Joshen] If `loading_` is provided, let loading state be entirely controlled by the param
68 // Otherwise, if the action onConfirm errors out, the UI is stuck in a loading state
69 const [loading, setLoading] = useState(loading_ !== undefined ? loading_ : false)
70
71 const onSubmit: MouseEventHandler<HTMLButtonElement> = (e) => {
72 e.preventDefault()
73 e.stopPropagation()
74 onConfirm()
75 if (loading_ === undefined) setLoading(true)
76 }
77
78 useEffect(() => {
79 if (visible && loading_ === undefined) {
80 setLoading(false)
81 }
82 }, [visible])
83
84 useEffect(() => {
85 if (loading_ !== undefined) setLoading(loading_)
86 }, [loading_])
87
88 const { title: _alertBaseTitle, children: _alertBaseChildren, ...alertBase } = alert?.base ?? {}
89 const alertTitleProps = alert?.title ? { label: alert.title } : {}
90
91 return (
92 <Dialog
93 open={visible}
94 {...props}
95 onOpenChange={() => {
96 if (visible) {
97 onCancel()
98 }
99 }}
100 >
101 <DialogContent
102 aria-describedby={undefined}
103 ref={ref}
104 className="p-0 gap-0 pb-5 block!"
105 size={size}
106 >
107 <DialogHeader className={cn('border-b')} padding={'small'}>
108 <DialogTitle>{title}</DialogTitle>
109 {description && <DialogDescription>{description}</DialogDescription>}
110 </DialogHeader>
111 {alert && (
112 <Admonition
113 type={variant as 'default' | 'destructive' | 'warning'}
114 description={alert.description}
115 {...alertTitleProps}
116 className="border-x-0 rounded-none -mt-px"
117 {...alertBase}
118 />
119 )}
120 {children && (
121 <>
122 <DialogSection padding="small" className={className}>
123 {children}
124 </DialogSection>
125 <DialogSectionSeparator />
126 </>
127 )}
128 <div className="flex gap-2 px-5 pt-5">
129 <Button
130 size="medium"
131 block
132 type="default"
133 disabled={loading}
134 onClick={() => onCancel()}
135 >
136 {cancelLabel}
137 </Button>
138
139 <Button
140 block
141 size="medium"
142 type={
143 variant === 'destructive' ? 'danger' : variant === 'warning' ? 'warning' : 'primary'
144 }
145 htmlType="submit"
146 loading={loading}
147 disabled={loading || disabled}
148 onClick={onSubmit}
149 className="truncate"
150 >
151 {loading && confirmLabelLoading ? confirmLabelLoading : confirmLabel}
152 </Button>
153 </div>
154 </DialogContent>
155 </Dialog>
156 )
157 }
158)
159
160ConfirmationModal.displayName = 'ConfirmationModal'
161
162export default ConfirmationModal