import React, { useRef, useEffect, useState } from 'react'; import propTypes from 'prop-types'; import ReactDOM from 'react-dom'; import classnames from 'classnames'; import styles from './styles.module.scss'; const Modal: React.FC = ({ visible, width, height, children, onBlur, onEscape, }: { visible: boolean, children: React.ReactNode, width?: string | number, height?: string | number, onBlur?: () => void, onEscape?: () => void, }) => { const [modalState, setModalState] = useState< | 'norender' | 'invisible' | 'visible' >('norender'); const dialog = useRef(null); const container = useRef(null); // Manage internal visibility/render state // This is needed to make animations behave properly useEffect(() => { let unmounted = false; if (visible) { setModalState('visible'); } else if (modalState !== 'norender') { setModalState('invisible'); setTimeout(() => !unmounted && setModalState('norender'), 200); } return () => unmounted = true; }, [visible]); // Focus the dialog whenever the modal state changes useEffect(() => { if (dialog.current) { dialog.current.focus(); } if (container.current) { container.current.scrollTop = 0; } }, [modalState]); const inlineStyle = {} as any; if (width) { inlineStyle.width = '100%'; inlineStyle.maxWidth = width; } if (height) { inlineStyle.height = height; } return modalState === 'norender' ? null : ReactDOM.createPortal(
e.keyCode === 27 && onEscape && onEscape()} onMouseDown={onBlur} ref={container} className={classnames( styles.dashboardModalBackdrop, {[styles.visible]: modalState === 'visible'} )} >
e.stopPropagation()} > {children}
, document.body, ); } Modal.propTypes = { visible: propTypes.bool.isRequired, children: propTypes.node.isRequired, width: propTypes.oneOfType([propTypes.string, propTypes.number]), height: propTypes.oneOfType([propTypes.string, propTypes.number]), onBlur: propTypes.func, onEscape: propTypes.func, }; Modal.displayName = 'Modal'; export default Modal;