import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices'; import { ModalHotkeysAndClickOutsideEffect } from '@/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect'; import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope'; import { ModalComponentInstanceContext } from '@/ui/layout/modal/contexts/ModalComponentInstanceContext'; import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState'; import { MODAL_BACKDROP_CLICK_OUTSIDE_ID } from '@/ui/layout/modal/constants/ModalBackdropClickOutsideId'; import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID } from '@/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { AnimatePresence, motion } from 'framer-motion'; import React, { useRef } from 'react'; const StyledModalDiv = styled(motion.div)<{ size?: ModalSize; padding?: ModalPadding; isMobile: boolean; modalVariant: ModalVariants; }>` display: flex; flex-direction: column; box-shadow: ${({ theme, modalVariant }) => modalVariant === 'primary' ? theme.boxShadow.superHeavy : theme.boxShadow.strong}; background: ${({ theme }) => theme.background.primary}; color: ${({ theme }) => theme.font.color.primary}; border-radius: ${({ theme, isMobile }) => { if (isMobile) return `0`; return theme.border.radius.md; }}; overflow-x: hidden; overflow-y: auto; z-index: ${RootStackingContextZIndices.RootModal}; // should be higher than Backdrop's z-index width: ${({ isMobile, size, theme }) => { if (isMobile) return theme.modal.size.fullscreen.width; switch (size) { case 'small': return theme.modal.size.sm.width; case 'medium': return theme.modal.size.md.width; case 'large': return theme.modal.size.lg.width; case 'extraLarge': return theme.modal.size.xl.width; default: return 'auto'; } }}; padding: ${({ padding, theme }) => { switch (padding) { case 'none': return theme.spacing(0); case 'small': return theme.spacing(2); case 'medium': return theme.spacing(4); case 'large': return theme.spacing(6); default: return 'auto'; } }}; height: ${({ isMobile, theme, size }) => { if (isMobile) return theme.modal.size.fullscreen.height; switch (size) { case 'extraLarge': return theme.modal.size.xl.height; default: return 'auto'; } }}; max-height: ${({ isMobile }) => (isMobile ? 'none' : '90dvh')}; `; const StyledHeader = styled.div` align-items: center; display: flex; flex-direction: row; height: 60px; overflow: hidden; padding: ${({ theme }) => theme.spacing(5)}; `; const StyledContent = styled.div<{ isVerticalCentered?: boolean; isHorizontalCentered?: boolean; }>` display: flex; flex: 1; flex: 1 1 0%; flex-direction: column; padding: ${({ theme }) => theme.spacing(10)}; ${({ isVerticalCentered }) => isVerticalCentered && css` align-items: center; `} ${({ isHorizontalCentered }) => isHorizontalCentered && css` justify-content: center; `} `; const StyledFooter = styled.div` align-items: center; display: flex; flex-direction: row; height: 60px; overflow: hidden; padding: ${({ theme }) => theme.spacing(5)}; `; const StyledBackDrop = styled(motion.div)<{ modalVariant: ModalVariants; }>` align-items: center; background: ${({ theme, modalVariant }) => modalVariant === 'primary' ? theme.background.overlayPrimary : modalVariant === 'secondary' ? theme.background.overlaySecondary : theme.background.overlayTertiary}; display: flex; height: 100%; justify-content: center; left: 0; position: fixed; top: 0; width: 100%; z-index: ${RootStackingContextZIndices.RootModalBackDrop}; user-select: none; `; type ModalHeaderProps = React.PropsWithChildren & { className?: string; }; const ModalHeader = ({ children, className }: ModalHeaderProps) => ( {children} ); type ModalContentProps = React.PropsWithChildren & { className?: string; isVerticalCentered?: boolean; isHorizontalCentered?: boolean; }; const ModalContent = ({ children, className, isVerticalCentered, isHorizontalCentered, }: ModalContentProps) => ( {children} ); type ModalFooterProps = React.PropsWithChildren & { className?: string; }; const ModalFooter = ({ children, className }: ModalFooterProps) => ( {children} ); export type ModalSize = 'small' | 'medium' | 'large' | 'extraLarge'; export type ModalPadding = 'none' | 'small' | 'medium' | 'large'; export type ModalVariants = 'primary' | 'secondary' | 'tertiary'; export type ModalProps = React.PropsWithChildren & { modalId: string; size?: ModalSize; padding?: ModalPadding; className?: string; hotkeyScope?: ModalHotkeyScope; onEnter?: () => void; modalVariant?: ModalVariants; dataGloballyPreventClickOutside?: boolean; } & ( | { isClosable: true; onClose?: () => void } | { isClosable?: false; onClose?: never } ); const modalAnimation = { hidden: { opacity: 0 }, visible: { opacity: 1 }, exit: { opacity: 0 }, }; export const Modal = ({ modalId, children, size = 'medium', padding = 'medium', className, onEnter, isClosable = false, onClose, modalVariant = 'primary', dataGloballyPreventClickOutside = false, }: ModalProps) => { const isMobile = useIsMobile(); const modalRef = useRef(null); const theme = useTheme(); const stopEventPropagation = (e: React.MouseEvent) => { e.stopPropagation(); }; const isModalOpened = useRecoilComponentValueV2( isModalOpenedComponentState, modalId, ); const { closeModal } = useModal(); const handleClose = () => { onClose?.(); closeModal(modalId); }; return ( {isModalOpened && ( {children} )} ); }; Modal.Header = ModalHeader; Modal.Content = ModalContent; Modal.Footer = ModalFooter; Modal.Backdrop = StyledBackDrop;