import { styled } from '@linaria/react'; import { AnimatePresence, motion } from 'framer-motion'; import React, { useContext, useRef } from 'react'; import { createPortal } from 'react-dom'; import { isDefined } from 'twenty-shared/utils'; import { themeCssVariables } from '@ui/theme-constants'; import { ThemeContext } from '@ui/theme'; import { type ModalOverlay } from '../types/ModalOverlay'; import { type ModalPadding } from '../types/ModalPadding'; import { type ModalProps } from '../types/ModalProps'; import { type ModalSize } from '../types/ModalSize'; import { ModalBackdrop } from './ModalBackdrop'; const DEFAULT_MODAL_Z_INDEX = 40; const DEFAULT_BACKDROP_Z_INDEX = 39; const StyledModalDiv = styled.div<{ size?: ModalSize; padding?: ModalPadding; isMobile: boolean; overlay: ModalOverlay; gap?: number; smallBorderRadius?: boolean; narrowWidth?: boolean; autoHeight?: boolean; modalZIndex: number; }>` display: flex; flex-direction: column; box-shadow: ${({ overlay }) => overlay === 'dark' ? themeCssVariables.boxShadow.superHeavy : overlay === 'transparent' ? 'none' : themeCssVariables.boxShadow.strong}; background: ${({ overlay }) => overlay === 'transparent' ? 'transparent' : themeCssVariables.background.primary}; color: ${themeCssVariables.font.color.primary}; border-radius: ${({ isMobile, overlay, smallBorderRadius }) => { if (isMobile === true || overlay === 'transparent') return '0'; if (smallBorderRadius === true) return themeCssVariables.spacing[1]; return themeCssVariables.border.radius.md; }}; overflow-x: hidden; overflow-y: auto; z-index: ${({ modalZIndex }) => modalZIndex}; gap: ${({ gap }) => gap !== undefined ? `var(--t-spacing-${gap})` : 'unset'}; width: ${({ isMobile, size, narrowWidth }) => { if (narrowWidth === true) return `calc(400px - ${themeCssVariables.spacing[32]})`; if (isMobile) return themeCssVariables.modal.size.fullscreen.width ?? 'auto'; switch (size) { case 'small': return themeCssVariables.modal.size.sm.width ?? 'auto'; case 'medium': return themeCssVariables.modal.size.md.width ?? 'auto'; case 'large': return themeCssVariables.modal.size.lg.width ?? 'auto'; case 'extraLarge': return themeCssVariables.modal.size.xl.width ?? 'auto'; default: return 'auto'; } }}; padding: ${({ padding }) => { switch (padding) { case 'none': return themeCssVariables.spacing[0]; case 'small': return themeCssVariables.spacing[2]; case 'medium': return themeCssVariables.spacing[4]; case 'large': return themeCssVariables.spacing[6]; default: return 'auto'; } }}; height: ${({ isMobile, size, autoHeight }) => { if (autoHeight === true) return 'auto'; if (isMobile) return themeCssVariables.modal.size.fullscreen.height ?? 'auto'; switch (size) { case 'extraLarge': return themeCssVariables.modal.size.xl.height ?? 'auto'; default: return 'auto'; } }}; max-height: ${({ isMobile }) => (isMobile ? 'none' : '90dvh')}; `; const AnimatedModalDiv = motion.create(StyledModalDiv); const AnimatedBackdrop = motion.create(ModalBackdrop); const modalAnimation = { hidden: { opacity: 0 }, visible: { opacity: 1 }, exit: { opacity: 0 }, }; export const Modal = ({ isOpen, children, size = 'medium', padding = 'medium', overlay = 'dark', isMobile = false, isInContainer = false, container, gap, smallBorderRadius, narrowWidth, autoHeight, modalZIndex = DEFAULT_MODAL_Z_INDEX, backdropZIndex = DEFAULT_BACKDROP_Z_INDEX, backdropTestId = 'modal-backdrop', backdropClickOutsideId, preventClickOutside, onBackdropMouseDown, modalRef: externalRef, }: ModalProps) => { const internalRef = useRef(null); const resolvedRef = externalRef ?? internalRef; const { theme } = useContext(ThemeContext); const handleBackdropMouseDown = (e: React.MouseEvent) => { e.stopPropagation(); onBackdropMouseDown?.(e); }; const content = ( {isOpen && ( {children} )} ); if (isDefined(container)) { return createPortal(content, container); } return content; };