import React, { forwardRef, MouseEvent, PropsWithChildren, ReactNode, Ref, StatelessComponent, useCallback, useEffect, useRef, useState, } from 'react' import { SimpleInterpolation } from 'styled-components' import { AppName, BorderRadius, borderRadius, Colors, ElevationRange, flexFlow, FontSizes, getColor, getElevationShadow, gothamFontFamily, sizes, typographyFont, } from '@monorail/helpers/exports' import { useEventListener, useTimeout } from '@monorail/helpers/hooks' import styled, { css, CSSProp, keyframes, ThemeProvider, } from '@monorail/helpers/styled-components' import { Mode } from '@monorail/helpers/theme' import { Div } from '@monorail/StyleHelpers' import { CommonComponentType } from '@monorail/types' import { AppIcon } from '@monorail/visualComponents/appIcon/AppIcon' import { ButtonDisplay, IconButtonShape, } from '@monorail/visualComponents/buttons/buttonTypes' import { IconButton } from '@monorail/visualComponents/buttons/IconButton' import { Icon } from '@monorail/visualComponents/icon/Icon' import { IconType } from '@monorail/visualComponents/icon/IconType' import { ModalSize } from '@monorail/visualComponents/modals/modalTypes' import { SearchContainer } from '../inputs/Search' /** * Modal Hooks */ type UseModalAnimationParams = { closingAnimationCompleted: () => void isOpen: boolean } export function useModalAnimation( params: UseModalAnimationParams, ) { const { closingAnimationCompleted, isOpen } = params const modalBackgroundRef = useRef(null) const [isRendered, setIsRendered] = useState(false) useEffect(() => setIsRendered(true), []) const eventListener = useCallback( event => { if (modalBackgroundRef.current === event.target && !isOpen) { closingAnimationCompleted() } }, [closingAnimationCompleted, isOpen], ) // HACK: Something is wrong with our close animations and it causes modals not to be destroyed. This is a workaround // until 1) it is figured out or 2) we migrate to material-ui modals. useEffect(() => { if (!isOpen) { closingAnimationCompleted() } }, [closingAnimationCompleted, isOpen]) useEventListener({ eventName: 'animationend', eventListener, element: modalBackgroundRef.current, }) return { modalBackgroundRef, isRendered, } } /* * * Modal Animation * */ export const modalAnimationDuration = 100 export const mediumModalOpenAnimation = keyframes` from { opacity: 0; transform: rotateX(15deg) translateY(16px) scale(.95) } to { opacity: 0.9999; transform: rotateX(0) translateY(0) scale(1); } ` export const mediumModalCloseAnimation = keyframes` from { opacity: 0.9999; transform: rotateX(0) translateY(0) scale(1); } to { opacity: 0; transform: rotateX(15deg) translateY(16px) scale(.95) } ` export const largeModalOpenAnimation = keyframes` from { opacity: 0; transform: rotateX(3deg) translateY(16px) scale(.98); } to { opacity: 0.9999; transform: rotateX(0) translateY(0) scale(1); } ` export const largeModalCloseAnimation = keyframes` from { opacity: 0.9999; transform: rotateX(0) translateY(0) scale(1); } to { opacity: 0; transform: rotateX(3deg) translateY(16px) scale(.98); } ` export const fullScreenModalOpenAnimation = keyframes` from { opacity: 0; } to { opacity: 0.9999; } ` export const fullScreenModalCloseAnimation = keyframes` from { opacity: 0.9999; } to { opacity: 0; } ` export const overlayOpenAnimation = keyframes` from { opacity: 0; } to { opacity: 0.9999; } ` export const overlayCloseAnimation = keyframes` from { opacity: 0.9999; } to { opacity: 0; } ` /* * * Modal Background * */ /* * Types */ export type BBModalSize = { size: ModalSize } export type BBModalBackgroundProps = BBModalSize & PropsWithChildren /* * Component */ const modalWidth = { [ModalSize.Mini]: `${sizes.modals.mini.width}px`, [ModalSize.Small]: `${sizes.modals.small.width}px`, [ModalSize.Medium]: `${sizes.modals.medium.width}px`, [ModalSize.MediumLarge]: `${sizes.modals.mediumLarge.width}px`, [ModalSize.Large]: 'calc(100vw - 32px)', [ModalSize.FullScreen]: '100vw', } /* className set for customizing the modal through global styling */ export const BBModalBackground = styled( forwardRef( ({ size, cssOverrides, className, ...otherProps }, ref) => (
), ), )( ({ size, cssOverrides }) => css` ${size === ModalSize.Mini ? css` height: ${sizes.modals.mini.height}px; ` : css` margin: 16px; `}; ${size === ModalSize.Large && css` height: calc(100vh - 32px); `}; ${borderRadius(BorderRadius.XLarge)}; ${flexFlow()}; ${getElevationShadow(ElevationRange.Elevation24)}; background: ${getColor(Colors.White)}; overflow: hidden; position: relative; /* position: relative; so that the shadow works when on the BBModalOverlay */ width: ${modalWidth[size]}; will-change: transform, opacity; transform-origin: bottom center; ${cssOverrides}; `, ) /* * * Modal Header * */ /* * Styles */ export const BBModalHeaderContainer = styled.div< BBModalSize & { cssOverrides: SimpleInterpolation | CSSProp } >( ({ size, cssOverrides }) => css` ${flexFlow(size === ModalSize.Mini ? 'column' : 'row')}; ${getElevationShadow(ElevationRange.Elevation2)}; background: ${getColor(Colors.BrandDarkBlue)}; flex-shrink: 0; user-select: none; z-index: 1; ${SearchContainer} { ${size === ModalSize.Mini ? css` margin: 8px 16px 16px; ` : css` margin: auto 16px auto auto; `}; } ${cssOverrides}; `, ) export const BBModalHeaderRow = styled.div( ({ size }) => css` ${flexFlow('row')}; align-items: center; height: ${size === ModalSize.Mini || size === ModalSize.Small ? 48 : 56}px; padding: 0 16px; width: 100%; overflow: hidden; `, ) const BBModalHeaderTitle = styled.h1( ({ size }) => css` ${size === ModalSize.Mini || size === ModalSize.Small ? typographyFont(700, FontSizes.Title4) : typographyFont(700, FontSizes.Title3)}; color: ${getColor(Colors.White)}; white-space: nowrap; margin: 0; `, ) const baseIconStyles = css` color: ${getColor(Colors.White)}; ` const StyledAppIconLeft = styled(AppIcon)` ${baseIconStyles}; margin-right: 16px; ` const StyledIconLeft = styled(Icon)` ${baseIconStyles}; margin-right: 16px; ` const StyledIconRight = styled(Icon)` ${baseIconStyles}; margin-left: 16px; ` /* * Types */ type BBModalHeaderProps = BBModalSize & { appIcon?: AppName customCloseButton?: ReactNode headerRowChildren?: ReactNode iconLeft?: IconType iconRight?: IconType onClose?: (event: MouseEvent) => void title: string titleId?: string cssOverrides?: SimpleInterpolation | CSSProp } type DefaultCloseButtonProps = Pick< BBModalHeaderProps, 'headerRowChildren' | 'onClose' > /* * Component */ export const DefaultCloseButton = ({ headerRowChildren, onClose, }: DefaultCloseButtonProps) => ( ) export const BBModalHeader: StatelessComponent = ({ appIcon, children, customCloseButton, headerRowChildren, iconLeft, iconRight, size, onClose, title, titleId, cssOverrides, }) => ( ({ ...theme, mode: Mode.Dark })}> {appIcon && } {iconLeft && } {title} {headerRowChildren} {iconRight && } {size !== ModalSize.Mini && onClose && customCloseButton !== undefined ? ( customCloseButton ) : ( )} {children} ) /* * * Modal Footer * */ /* * Styles */ export const BBModalFooter = styled.div` ${flexFlow('row')}; ${getElevationShadow(ElevationRange.Elevation6)}; align-items: center; background: ${getColor(Colors.Grey98)}; height: 48px; justify-content: flex-end; margin: auto 0 0; padding: 0 16px; flex-shrink: 0; ` /* * * Modal Overlay * */ /* * Styles */ export const BBModalOverlayContainer = styled.div( ({ isOpen, chromeless, cssOverrides }) => css` ${!chromeless && css` background: ${getColor(Colors.Black, 0.36)}; `}; bottom: 0; cursor: pointer; left: 0; position: fixed; right: 0; top: 0; ${cssOverrides}; `, ) /* * Types */ export type BBModalOverlayProps = CommonComponentType & { isOpen: boolean onClick?: (event: MouseEvent) => void chromeless?: boolean } /* * Component */ export const BBModalOverlay: StatelessComponent = ({ children, chromeless, isOpen, onClick, cssOverrides, ...otherProps }) => ( ) => { if (event.target === event.currentTarget) { onClick(event) } } : undefined } {...otherProps} > {children} ) /* * * Modal Container * */ /* * Styles */ export const BBModalContainer = styled.div< CommonComponentType & { isOpen: boolean usesScaleAnimation: boolean zIndex?: number } >( ({ isOpen, cssOverrides, zIndex }) => css` ${isOpen ? css` pointer-events: all; ${flexFlow()} ` : css` pointer-events: none; display: none; `}; ${gothamFontFamily}; align-items: center; bottom: 0; justify-content: center; left: 0; perspective: 1500px; position: fixed; right: 0; top: 0; z-index: ${zIndex}; ${cssOverrides}; `, ) /* * * Modal Content * */ /* * Styles */ export const BBModalContent = styled.div( ({ cssOverrides }) => css` ${flexFlow()}; height: 100%; max-height: 100%; overflow: auto; ${cssOverrides}; `, )