UNPKG

@react-ui-org/react-ui

Version:

React UI is a themeable UI library for React apps.

263 lines (248 loc) 7.25 kB
import PropTypes from 'prop-types'; import React, { useCallback, useEffect, useImperativeHandle, useRef, } from 'react'; import { createPortal } from 'react-dom'; import { classNames } from '../../helpers/classNames'; import { transferProps } from '../../helpers/transferProps'; import { withGlobalProps } from '../../providers/globalProps'; import { getRootColorClassName } from '../_helpers/getRootColorClassName'; import { dialogOnCancelHandler } from './_helpers/dialogOnCancelHandler'; import { dialogOnClickHandler } from './_helpers/dialogOnClickHandler'; import { dialogOnCloseHandler } from './_helpers/dialogOnCloseHandler'; import { dialogOnKeyDownHandler } from './_helpers/dialogOnKeyDownHandler'; import { getPositionClassName } from './_helpers/getPositionClassName'; import { getSizeClassName } from './_helpers/getSizeClassName'; import { useModalFocus } from './_hooks/useModalFocus'; import { useModalScrollPrevention } from './_hooks/useModalScrollPrevention'; import styles from './Modal.module.scss'; const preRender = ( children, color, dialogRef, position, size, events, restProps, ) => ( <dialog {...transferProps(restProps)} {...transferProps(events)} className={classNames( styles.root, color && getRootColorClassName(color, styles), getSizeClassName(size, styles), getPositionClassName(position, styles), )} ref={dialogRef} > {children} </dialog> ); export const Modal = ({ allowCloseOnBackdropClick, allowCloseOnEscapeKey, allowPrimaryActionOnEnterKey, autoFocus, children, closeButtonRef, color, dialogRef, portalId, position, preventScrollUnderneath, primaryButtonRef, size, ...restProps }) => { const internalDialogRef = useRef(); const mouseDownTarget = useRef(null); useEffect(() => { internalDialogRef.current.showModal(); }, []); // We need to have a reference to the dialog element to be able to call its methods, // but at the same time we want to expose this reference to the parent component for // case someone wants to call dialog methods from outside the component. useImperativeHandle(dialogRef, () => internalDialogRef.current); useModalFocus(autoFocus, internalDialogRef, primaryButtonRef); useModalScrollPrevention(preventScrollUnderneath); const onCancel = useCallback( (e) => { if (e.target !== internalDialogRef.current) { return; } dialogOnCancelHandler(e, closeButtonRef, restProps.onCancel); }, [closeButtonRef, restProps.onCancel], ); const onClick = useCallback( (e) => dialogOnClickHandler( e, closeButtonRef, internalDialogRef, allowCloseOnBackdropClick, mouseDownTarget.current, ), [allowCloseOnBackdropClick, closeButtonRef, internalDialogRef], ); const onClose = useCallback( (e) => dialogOnCloseHandler(e, closeButtonRef, restProps.onClose), [closeButtonRef, restProps.onClose], ); const onKeyDown = useCallback( (e) => dialogOnKeyDownHandler( e, closeButtonRef, primaryButtonRef, allowCloseOnEscapeKey, allowPrimaryActionOnEnterKey, ), [ allowCloseOnEscapeKey, allowPrimaryActionOnEnterKey, closeButtonRef, primaryButtonRef, ], ); const onMouseDown = useCallback((e) => { mouseDownTarget.current = e.target; }, []); const events = { onCancel, onClick, onClose, onKeyDown, onMouseDown, }; if (portalId === undefined) { return preRender( children, color, internalDialogRef, position, size, events, restProps, ); } return createPortal( preRender( children, color, internalDialogRef, position, size, events, restProps, ), document.getElementById(portalId), ); }; Modal.defaultProps = { allowCloseOnBackdropClick: true, allowCloseOnEscapeKey: true, allowPrimaryActionOnEnterKey: true, autoFocus: true, children: undefined, closeButtonRef: undefined, color: undefined, dialogRef: undefined, portalId: undefined, position: 'center', preventScrollUnderneath: window.document.body, primaryButtonRef: undefined, size: 'medium', }; Modal.propTypes = { /** * If `true`, the `Modal` can be closed by clicking on the backdrop. */ allowCloseOnBackdropClick: PropTypes.bool, /** * If `true`, the `Modal` can be closed by pressing the Escape key. */ allowCloseOnEscapeKey: PropTypes.bool, /** * If `true`, the `Modal` can be submitted by pressing the Enter key. */ allowPrimaryActionOnEnterKey: PropTypes.bool, /** * If `true`, focus the first input element in the `Modal`, or primary button (referenced by the `primaryButtonRef` * prop), or other focusable element when the `Modal` is opened. If there are none or `autoFocus` is set to `false`, * focus the Modal itself. */ autoFocus: PropTypes.bool, /** * Nested elements. Supported types are: * * * `ModalHeader` * * `ModalBody` * * `ModalFooter` * * At least `ModalBody` is required. */ children: PropTypes.node, /** * Reference to close button element. It is used to close modal when Escape key is pressed * or the backdrop is clicked. */ closeButtonRef: PropTypes.shape({ // eslint-disable-next-line react/forbid-prop-types current: PropTypes.any, }), /** * Color to clarify importance and meaning of the modal. Implements * [Feedback color collection](/docs/foundation/collections#colors). */ color: PropTypes.oneOf(['success', 'warning', 'danger', 'help', 'info', 'note']), /** * Reference to dialog element */ dialogRef: PropTypes.shape({ // eslint-disable-next-line react/forbid-prop-types current: PropTypes.any, }), /** * If set, modal is rendered in the React Portal with that ID. */ portalId: PropTypes.string, /** * Vertical position of the modal inside browser window. */ position: PropTypes.oneOf(['top', 'center']), /** * Mode in which Modal prevents scroll of elements bellow: * * `off` - Modal does not prevent any scroll * * [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) - Modal prevents scroll on this HTML element * * object * * `reset` - method called on Modal's unmount to reset scroll prevention * * `start` - method called on Modal's mount to custom scroll prevention */ preventScrollUnderneath: PropTypes.oneOfType([ PropTypes.oneOf([ HTMLElement, 'off', ]), PropTypes.shape({ reset: PropTypes.func, start: PropTypes.func, }), ]), /** * Reference to primary button element. It is used to submit modal when Enter key is pressed and as fallback * when `autoFocus` functionality does not find any input element to be focused. */ primaryButtonRef: PropTypes.shape({ // eslint-disable-next-line react/forbid-prop-types current: PropTypes.any, }), /** * Size of the modal. */ size: PropTypes.oneOf(['small', 'medium', 'large', 'fullscreen', 'auto']), }; export const ModalWithGlobalProps = withGlobalProps(Modal, 'Modal'); export default ModalWithGlobalProps;