import React, { forwardRef, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { CSSProperties } from 'react'; import { IModalProps } from './type'; import { ModalPlacement, ModalSize, ModalVariant } from './constants'; import { Portal } from '@cleartrip/ct-design-portal'; import { FocusLock } from '@cleartrip/ct-design-focus-lock'; import { Transition } from '@cleartrip/ct-design-transition'; import Backdrop from './Backdrop'; import useMergeRefs from '@cleartrip/ct-design-use-merge-refs'; import { Container, ContainerRef } from '@cleartrip/ct-design-container'; import ModalContext from './ModalContext'; import Handler from './Handler'; import { makeStyles, useStyles, useWebMergeStyles } from '@cleartrip/ct-design-style-manager'; const staticModalStyles = makeStyles(() => ({ modalContainer: { position: 'relative', display: 'flex', flexDirection: 'column', margin: 'auto', height: '100%', width: '100%', alignItems: 'center', }, outerContainer: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, }, })); const handler = new Handler(); const DEFAULT_ANIMATION_TIME = 350; const Modal = forwardRef( ( { children, insidePortal = true, open = false, onClose, onKeyDown, hideCrossIcon, footerAlignment, size = ModalSize.MEDIUM, allowBackdropClose = true, blur = true, backDropComponent, animationDuration = DEFAULT_ANIMATION_TIME, placement = 'center', bottomMargin, variant, styleConfig = {}, ...rest }, forwardedRef, ) => { const { backdropStyles = [], modalContentStyles = [], root: rootStyles = [] } = styleConfig; const overflowY: CSSProperties['overflowY'] = 'auto'; const isFullScreen = size === ModalSize.FULL_SCREEN; const dynamicStyles = useStyles( (theme) => ({ modalContainer: { position: 'relative', zIndex: theme.zIndex.modal, overflowY, display: 'flex', flexDirection: 'column', margin: 'auto', height: '100%', width: '100%', justifyContent: placement === ModalPlacement.BOTTOM ? 'flex-end' : ModalPlacement.CENTER, alignItems: ModalPlacement.CENTER, paddingBottom: placement === ModalPlacement.BOTTOM ? bottomMargin || 188 : 0, }, outerContainer: { zIndex: theme.zIndex.modal, /** * Web only properties */ ...(isFullScreen ? { height: '100vh' as unknown as number, width: '100vw' as unknown as number } : {}), }, childContainer: { boxSizing: 'content-box', borderRadius: size === ModalSize.FULL_SCREEN ? 0 : theme.spacing['3'], borderBottomLeftRadius: size === ModalSize.FULL_SCREEN ? 0 : theme.spacing['3'], borderBottomRightRadius: size === ModalSize.FULL_SCREEN ? 0 : theme.spacing['3'], backgroundColor: variant === ModalVariant.DARK ? theme.color.background.defaultDarkest : theme.color.background.neutral, width: (isFullScreen ? '100%' : '90%') as unknown as number, maxWidth: (isFullScreen ? '100%' : '90%') as unknown as number, overflowY, maxHeight: size === ModalSize.FULL_SCREEN ? ('100vh' as unknown as number) : (`calc(100vh - 64px)` as unknown as number), boxShadow: blur ? `${theme.elevation.modalBlurLight}, ${theme.elevation.modalBlurMedium}, ${theme.elevation.modalBlurHeavy}` : 'none', }, }), [size, overflowY, placement, bottomMargin, isFullScreen, blur, variant], ); const modalContainerClass = useWebMergeStyles( [staticModalStyles.modalContainer, dynamicStyles.modalContainer, ...rootStyles], [dynamicStyles.modalContainer, rootStyles], ); const modalRef = useRef(null); const containerRef = useRef<{ modalRef?: typeof modalRef.current }>({}); const [isOpen, setIsOpen] = useState(open); const handleRef = useMergeRefs(forwardedRef, modalRef); useLayoutEffect(() => { let timeoutId: NodeJS.Timeout | null = null; if (!open) { // to add closing animation timeoutId = setTimeout(() => setIsOpen(false), animationDuration); } else { setIsOpen(true); } return () => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions timeoutId && clearTimeout(timeoutId); }; }, [open, animationDuration]); useEffect(() => { if (modalRef.current) { modalRef.current.scrollTop = 0; } if (open) { containerRef.current.modalRef = modalRef.current; handler.mount({ ref: containerRef.current.modalRef }); } return () => { if (open) { handler.remove({ ref: containerRef.current.modalRef || null }); // @ts-ignore // eslint-disable-next-line react-hooks/exhaustive-deps containerRef.current.modalRef = modalRef.current; } }; }, [open]); const handleBackdropClick = (event: React.MouseEvent) => { if (event.target !== event.currentTarget) { return; } if (allowBackdropClose && onClose) { onClose(); } }; const handleKeyDown = (event: React.KeyboardEvent) => { if (onKeyDown) { onKeyDown(event); } if (event.key !== 'Escape') { return; } event.stopPropagation(); if (onClose) { onClose(); } }; if (!isOpen || !React.isValidElement(children)) { return null; } // TODO Focus trap breaks height sequence const modalElement = (
{backDropComponent || ( )} {React.cloneElement(children, { ...children.props, onClose })}
); if (insidePortal) { return {modalElement}; } return modalElement; }, ); Modal.displayName = 'Modal'; export default Modal;