import { CloseIcon } from '@bigcommerce/big-design-icons' ; import focusTrap , { FocusTrap } from 'focus-trap' ; import React , { useEffect , useMemo , useRef , useState } from 'react' ; import { createPortal } from 'react-dom' ; import { useUniqueId } from '../../hooks' ; import { typedMemo } from '../../utils' ; import { Button , ButtonProps } from '../Button' ; import { MessagingButton } from '../Button/private' ; import { H2 } from '../Typography' ; import { StyledModal , StyledModalActions , StyledModalBody , StyledModalClose , StyledModalContent , StyledModalHeader , } from './styled' ; export interface ModalProps { actions ? : ModalAction [ ] ; backdrop ? : boolean ; closeOnClickOutside ? : boolean ; closeOnEscKey ? : boolean ; header ? : string ; isOpen ? : boolean ; variant ? : 'modal' | 'dialog' ; onClose ? ( ) : void ; } export interface ModalAction extends Omit < ButtonProps , 'children' > { text ? : string ; } export const Modal = typedMemo ( ( props ) => { const [ modalContainer , setModalContainer ] = useState < HTMLDivElement | null > ( null ) ; useEffect ( ( ) => { const container = document . createElement ( 'div' ) ; document . body . appendChild ( container ) ; setModalContainer ( container ) ; } , [ ] ) ; useEffect ( ( ) => { return ( ) => { if ( modalContainer ) { document . body . removeChild ( modalContainer ) ; } } ; } , [ modalContainer ] ) ; return props . isOpen && modalContainer ? createPortal ( < InternalModal { ... props } / > , modalContainer ) : null ; } ) ; const InternalModal = ( { actions , backdrop = true , children , closeOnClickOutside = false , closeOnEscKey = true , header , onClose = ( ) => null , variant = 'modal' , } ) => { const initialBodyOverflowYRef = useRef ( '' ) ; const internalTrap = useRef < FocusTrap | null > ( null ) ; const headerUniqueId = useUniqueId ( 'modal_header' ) ; const [ modalRef , setModalRef ] = useState < HTMLDivElement | null > ( null ) ; const onClickAway = ( event ) => { if ( closeOnClickOutside && modalRef === event . target ) { onClose ( ) ; } } ; const onKeyDown = ( event ) => { if ( closeOnEscKey && event . key === 'Escape' ) { onClose ( ) ; } } ; useEffect ( ( ) => { initialBodyOverflowYRef . current = document . body . style . overflowY || '' ; document . body . style . overflowY = 'hidden' ; return ( ) => { document . body . style . overflowY = initialBodyOverflowYRef . current ; } ; } , [ ] ) ; useEffect ( ( ) => { if ( modalRef && internalTrap . current === null ) { internalTrap . current = focusTrap ( modalRef as HTMLElement , { fallbackFocus : modalRef } ) ; internalTrap . current . activate ( ) ; } return ( ) => { internalTrap . current ?. deactivate ( ) ; } ; } , [ internalTrap , modalRef ] ) ; const renderedClose = useMemo ( ( ) => variant === 'modal' && ( < StyledModalClose > < MessagingButton onClick = { onClose } iconOnly = { < CloseIcon title = "Close" / > } /> < /StyledModalClose>), [onClose, variant]); const renderedHeader = useMemo ( ( ) => header && typeof header === 'string' && ( < StyledModalHeader id = { headerUniqueId } > < H2 margin = "none" > { header } < /H2> < /StyledModalHeader>), [header, headerUniqueId]); const renderedActions = useMemo ( ( ) => Array . isArray ( actions ) && ( < StyledModalActions justifyContent = "flex-end" > { actions . map ( ( { text , onClick , ... props } , index ) => ( < Button key = { index } { ... props } onClick = { ( event ) => { internalTrap . current ?. deactivate ( ) ; if ( typeof onClick === 'function' ) { onClick ( event ) ; } } } > { text } < /Button>))} < /StyledModalActions>), [actions]); return ( < StyledModal backdrop = { backdrop } onClick = { onClickAway } onKeyDown = { onKeyDown } ref = { setModalRef } variant = { variant } tabIndex = { - 1 } > < StyledModalContent variant = { variant } aria - labelledby = { headerUniqueId } flexDirection = "column" > { renderedClose } { renderedHeader } < StyledModalBody > { children } < /StyledModalBody> { renderedActions } < /StyledModalContent> < /StyledModal>); } ;