UNPKG

mdc-react

Version:

Material Components for the web implemented in React

233 lines (196 loc) 7.53 kB
import { forwardRef, useRef, useCallback, useImperativeHandle } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { clone, Clone } from '../component'; import { useUpdated, useUnmounted } from '../hooks'; import IconButton from '../icon-button'; import Layer from '../layer'; import { numbers, cssClasses } from './constants'; import DialogHeader from './DialogHeader'; import DialogContent from './DialogContent'; import DialogActions from './DialogActions'; const Dialog = forwardRef(({ title, header, content, actions, closeIcon, open = false, appear = false, fullscreen = false, persistent = false, sheet = false, stacked = false, autoStackButtons = false, noContentPadding, onClose = Function.prototype, element: Element = 'div', className, children = content, ...props }, ref) => { const rootRef = useRef(); const contentRef = useRef(); const actionsRef = useRef(); useImperativeHandle(ref, () => rootRef.current); useUpdated(() => { if (persistent) return; function handleDocumentKeyDown(event) { if (event.key === 'Escape' || event.keyCode === 27) { event.stopPropagation(); onClose(); } } if (open) { document.addEventListener('keydown', handleDocumentKeyDown); } else { document.removeEventListener('keydown', handleDocumentKeyDown); } return () => document.removeEventListener('keydown', handleDocumentKeyDown); }, [open, persistent, onClose]); useUpdated(() => { if (!contentRef.current || !open) return; const contentElement = contentRef.current; const shouldScroll = contentElement.scrollHeight > contentElement.offsetHeight; if (shouldScroll) { rootRef.current.classList.add(cssClasses.SCROLLABLE); } }, [open]); useUpdated(() => { if (!actionsRef.current || !open || !autoStackButtons) return; const actionsElement = actionsRef.current; if (actionsElement.clientHeight > numbers.DEFAULT_ACTIONS_HEIGHT) { rootRef.current.classList.add(cssClasses.STACKED); } }, [open, autoStackButtons]); useUpdated(() => { if (!contentRef.current || !fullscreen) return; const contentElement = contentRef.current; function handleScroll() { const isScrollAtTop = contentElement.scrollTop === 0; const isScrollAtBottom = Math.ceil(contentElement.scrollHeight - contentElement.scrollTop) === contentElement.clientHeight; rootRef.current.classList.toggle(cssClasses.SCROLL_DIVIDER_HEADER, !isScrollAtTop); rootRef.current.classList.toggle(cssClasses.SCROLL_DIVIDER_FOOTER, !isScrollAtBottom); } contentElement.addEventListener('scroll', handleScroll); if (open) { handleScroll(); } return () => contentElement.removeEventListener('scroll', handleScroll); }, [open, fullscreen]); useUnmounted(() => { document.body.classList.remove(cssClasses.SCROLL_LOCK); }); const handleEnter = useCallback(() => { document.body.classList.add(cssClasses.SCROLL_LOCK); }, []); const handleExited = useCallback(() => { document.body.classList.remove(cssClasses.SCROLL_LOCK); }, []); const handleScrimClick = useCallback(() => { if (persistent) return; onClose(); }, [persistent, onClose]); const classNames = classnames(cssClasses.ROOT, { [cssClasses.FULLSCREEN]: fullscreen, [cssClasses.SHEET]: sheet, [cssClasses.STACKED]: stacked, [cssClasses.NO_CONTENT_PADDING]: noContentPadding }, className); return ( <Layer modal in={open} appear={appear} timeout={{ enter: numbers.DIALOG_ANIMATION_OPEN_TIME_MS, exit: numbers.DIALOG_ANIMATION_CLOSE_TIME_MS }} classNames={{ appear: cssClasses.OPENING, appearActive: cssClasses.OPEN, enter: cssClasses.OPENING, enterActive: cssClasses.OPEN, enterDone: cssClasses.OPEN, exit: cssClasses.CLOSING }} mountOnEnter unmountOnExit onEnter={handleEnter} onExited={handleExited} > <Element ref={rootRef} className={classNames} {...props} > <div className={cssClasses.CONTAINER}> <div className={cssClasses.SURFACE} role="alertdialog" aria-modal="true" > {sheet && <Clone component={closeIcon} fallback={<IconButton icon="close" />} className={cssClasses.CLOSE} onClick={onClose} /> } {title && <DialogHeader title={title} closeIcon={closeIcon} fullscreen={fullscreen} onClose={onClose} /> } {header?.type === DialogHeader && clone(header, { fullscreen, onClose }) } {children && (children?.type === DialogContent ? clone(children, { ref: contentRef }) : <DialogContent ref={contentRef}>{children}</DialogContent> )} {actions && (actions.type === DialogActions ? clone(actions, { ref: actionsRef }) : <DialogActions ref={actionsRef}>{actions}</DialogActions> )} </div> </div> <div className={cssClasses.SCRIM} onClick={handleScrimClick} /> </Element> </Layer> ); }); Dialog.displayName = 'MDCDialog'; Dialog.propTypes = { title: PropTypes.node, header: PropTypes.node, content: PropTypes.node, actions: PropTypes.oneOfType([ PropTypes.node, PropTypes.arrayOf(PropTypes.node) ]), closeIcon: PropTypes.node, open: PropTypes.bool, appear: PropTypes.bool, fullscreen: PropTypes.bool, persistent: PropTypes.bool, sheet: PropTypes.bool, stacked: PropTypes.bool, autoStackButtons: PropTypes.bool, noContentPadding: PropTypes.bool, onClose: PropTypes.func }; export default Dialog;