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;