import { DialogTrigger } from '@radix-ui/react-dialog'; import clsx from 'clsx'; import { createContext, useCallback, useContext, useMemo, useState, } from 'react'; import type { ButtonProps } from '../button'; import { Button } from '../button'; import { desktopStyles, mobileStyles } from './confirm-modal.css'; import type { ModalProps } from './modal'; import { Modal } from './modal'; const styles = BUILD_CONFIG.isMobileEdition ? mobileStyles : desktopStyles; export interface ConfirmModalProps extends ModalProps { customConfirmButton?: () => React.ReactNode; confirmButtonOptions?: Omit; childrenContentClassName?: string; onConfirm?: (() => void) | (() => Promise); onCancel?: (() => void) | false; confirmText?: React.ReactNode; cancelText?: React.ReactNode; cancelButtonOptions?: Omit; reverseFooter?: boolean; /** * Whether to use row layout for mobile footer * @default false */ rowFooter?: boolean; /** * Auto focus on confirm button when modal opened * @default true */ autoFocusConfirm?: boolean; } export const ConfirmModal = ({ children, confirmButtonOptions, customConfirmButton: CustomConfirmButton, // FIXME: we need i18n confirmText, cancelText = 'Cancel', cancelButtonOptions, reverseFooter, onConfirm, onCancel, width = 480, autoFocusConfirm = true, headerClassName, descriptionClassName, childrenContentClassName, contentOptions, rowFooter = false, ...props }: ConfirmModalProps) => { const onConfirmClick = useCallback(() => { Promise.resolve(onConfirm?.()).catch(err => { console.error(err); }); }, [onConfirm]); const handleCancel = useCallback(() => { if (onCancel === false) { return; } onCancel?.(); }, [onCancel]); return ( { e.stopPropagation(); handleCancel(); }, }} width={width} closeButtonOptions={{ onClick: handleCancel, }} headerClassName={clsx(styles.header, headerClassName)} descriptionClassName={clsx(styles.description, descriptionClassName)} {...props} > {children ? (
{children}
) : null}
{onCancel !== false ? ( ) : null} {CustomConfirmButton ? ( ) : ( )}
); }; interface OpenConfirmModalOptions { autoClose?: boolean; onSuccess?: () => void; } interface ConfirmModalContextProps { modalProps: ConfirmModalProps; openConfirmModal: ( props?: ConfirmModalProps, options?: OpenConfirmModalOptions ) => void; closeConfirmModal: () => void; } const ConfirmModalContext = createContext({ modalProps: { open: false }, openConfirmModal: () => {}, closeConfirmModal: () => {}, }); export const ConfirmModalProvider = ({ children }: React.PropsWithChildren) => { const [modalProps, setModalProps] = useState({ open: false, }); const setLoading = useCallback((value: boolean) => { setModalProps(prev => ({ ...prev, confirmButtonOptions: { ...prev.confirmButtonOptions, loading: value, }, })); }, []); const closeConfirmModal = useCallback(() => { setModalProps({ open: false }); }, []); const openConfirmModal = useCallback( (props?: ConfirmModalProps, options?: OpenConfirmModalOptions) => { const { autoClose = true, onSuccess } = options ?? {}; if (!props) { setModalProps({ open: true }); return; } const { onConfirm: _onConfirm, ...otherProps } = props; const onConfirm = () => { setLoading(true); return Promise.resolve(_onConfirm?.()) .then(() => onSuccess?.()) .catch(console.error) .finally(() => setLoading(false)) .finally(() => autoClose && closeConfirmModal()); }; setModalProps({ ...otherProps, onConfirm, open: true }); }, [closeConfirmModal, setLoading] ); const onOpenChange = useCallback( (open: boolean) => { modalProps.onOpenChange?.(open); setModalProps(props => ({ ...props, open })); }, [modalProps] ); const confirmModalContextValue = useMemo( () => ({ openConfirmModal, closeConfirmModal, modalProps }), [closeConfirmModal, modalProps, openConfirmModal] ); return ( {children} {/* TODO(@catsjuice): multi-instance support(unnecessary for now) */} ); }; export const useConfirmModal = () => { const context = useContext(ConfirmModalContext); if (!context) { throw new Error( 'useConfirmModal must be used within a ConfirmModalProvider' ); } return { openConfirmModal: context.openConfirmModal, closeConfirmModal: context.closeConfirmModal, }; };