import _extends from "@babel/runtime/helpers/esm/extends"; import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose"; /* eslint-disable @typescript-eslint/no-use-before-define, react/prop-types */ import activeElement from 'dom-helpers/activeElement'; import contains from 'dom-helpers/contains'; import canUseDOM from 'dom-helpers/canUseDOM'; import listen from 'dom-helpers/listen'; import PropTypes from 'prop-types'; import React, { useState, useRef, useCallback, useImperativeHandle, forwardRef, useEffect } from 'react'; import ReactDOM from 'react-dom'; import useMounted from '@restart/hooks/useMounted'; import useWillUnmount from '@restart/hooks/useWillUnmount'; import usePrevious from '@restart/hooks/usePrevious'; import useEventCallback from '@restart/hooks/useEventCallback'; import ModalManager from './ModalManager'; import useWaitForDOMRef from './useWaitForDOMRef'; var manager; function getManager() { if (!manager) manager = new ModalManager(); return manager; } function useModalManager(provided) { var modalManager = provided || getManager(); var modal = useRef({ dialog: null, backdrop: null }); return Object.assign(modal.current, { add: function add(container, className) { return modalManager.add(modal.current, container, className); }, remove: function remove() { return modalManager.remove(modal.current); }, isTopModal: function isTopModal() { return modalManager.isTopModal(modal.current); }, setDialogRef: useCallback(function (ref) { modal.current.dialog = ref; }, []), setBackdropRef: useCallback(function (ref) { modal.current.backdrop = ref; }, []) }); } var Modal = /*#__PURE__*/forwardRef(function (_ref, ref) { var _ref$show = _ref.show, show = _ref$show === void 0 ? false : _ref$show, _ref$role = _ref.role, role = _ref$role === void 0 ? 'dialog' : _ref$role, className = _ref.className, style = _ref.style, children = _ref.children, _ref$backdrop = _ref.backdrop, backdrop = _ref$backdrop === void 0 ? true : _ref$backdrop, _ref$keyboard = _ref.keyboard, keyboard = _ref$keyboard === void 0 ? true : _ref$keyboard, onBackdropClick = _ref.onBackdropClick, onEscapeKeyDown = _ref.onEscapeKeyDown, transition = _ref.transition, backdropTransition = _ref.backdropTransition, _ref$autoFocus = _ref.autoFocus, autoFocus = _ref$autoFocus === void 0 ? true : _ref$autoFocus, _ref$enforceFocus = _ref.enforceFocus, enforceFocus = _ref$enforceFocus === void 0 ? true : _ref$enforceFocus, _ref$restoreFocus = _ref.restoreFocus, restoreFocus = _ref$restoreFocus === void 0 ? true : _ref$restoreFocus, restoreFocusOptions = _ref.restoreFocusOptions, renderDialog = _ref.renderDialog, _ref$renderBackdrop = _ref.renderBackdrop, renderBackdrop = _ref$renderBackdrop === void 0 ? function (props) { return /*#__PURE__*/React.createElement("div", props); } : _ref$renderBackdrop, providedManager = _ref.manager, containerRef = _ref.container, containerClassName = _ref.containerClassName, onShow = _ref.onShow, _ref$onHide = _ref.onHide, onHide = _ref$onHide === void 0 ? function () {} : _ref$onHide, onExit = _ref.onExit, onExited = _ref.onExited, onExiting = _ref.onExiting, onEnter = _ref.onEnter, onEntering = _ref.onEntering, onEntered = _ref.onEntered, rest = _objectWithoutPropertiesLoose(_ref, ["show", "role", "className", "style", "children", "backdrop", "keyboard", "onBackdropClick", "onEscapeKeyDown", "transition", "backdropTransition", "autoFocus", "enforceFocus", "restoreFocus", "restoreFocusOptions", "renderDialog", "renderBackdrop", "manager", "container", "containerClassName", "onShow", "onHide", "onExit", "onExited", "onExiting", "onEnter", "onEntering", "onEntered"]); var container = useWaitForDOMRef(containerRef); var modal = useModalManager(providedManager); var isMounted = useMounted(); var prevShow = usePrevious(show); var _useState = useState(!show), exited = _useState[0], setExited = _useState[1]; var lastFocusRef = useRef(null); useImperativeHandle(ref, function () { return modal; }, [modal]); if (canUseDOM && !prevShow && show) { lastFocusRef.current = activeElement(); } if (!transition && !show && !exited) { setExited(true); } else if (show && exited) { setExited(false); } var handleShow = useEventCallback(function () { modal.add(container, containerClassName); removeKeydownListenerRef.current = listen(document, 'keydown', handleDocumentKeyDown); removeFocusListenerRef.current = listen(document, 'focus', // the timeout is necessary b/c this will run before the new modal is mounted // and so steals focus from it function () { return setTimeout(handleEnforceFocus); }, true); if (onShow) { onShow(); } // autofocus after onShow to not trigger a focus event for previous // modals before this one is shown. if (autoFocus) { var currentActiveElement = activeElement(document); if (modal.dialog && currentActiveElement && !contains(modal.dialog, currentActiveElement)) { lastFocusRef.current = currentActiveElement; modal.dialog.focus(); } } }); var handleHide = useEventCallback(function () { modal.remove(); removeKeydownListenerRef.current == null ? void 0 : removeKeydownListenerRef.current(); removeFocusListenerRef.current == null ? void 0 : removeFocusListenerRef.current(); if (restoreFocus) { var _lastFocusRef$current; // Support: <=IE11 doesn't support `focus()` on svg elements (RB: #917) (_lastFocusRef$current = lastFocusRef.current) == null ? void 0 : _lastFocusRef$current.focus == null ? void 0 : _lastFocusRef$current.focus(restoreFocusOptions); lastFocusRef.current = null; } }); // TODO: try and combine these effects: https://github.com/react-bootstrap/react-overlays/pull/794#discussion_r409954120 // Show logic when: // - show is `true` _and_ `container` has resolved useEffect(function () { if (!show || !container) return; handleShow(); }, [show, container, /* should never change: */ handleShow]); // Hide cleanup logic when: // - `exited` switches to true // - component unmounts; useEffect(function () { if (!exited) return; handleHide(); }, [exited, handleHide]); useWillUnmount(function () { handleHide(); }); // -------------------------------- var handleEnforceFocus = useEventCallback(function () { if (!enforceFocus || !isMounted() || !modal.isTopModal()) { return; } var currentActiveElement = activeElement(); if (modal.dialog && currentActiveElement && !contains(modal.dialog, currentActiveElement)) { modal.dialog.focus(); } }); var handleBackdropClick = useEventCallback(function (e) { if (e.target !== e.currentTarget) { return; } onBackdropClick == null ? void 0 : onBackdropClick(e); if (backdrop === true) { onHide(); } }); var handleDocumentKeyDown = useEventCallback(function (e) { if (keyboard && e.keyCode === 27 && modal.isTopModal()) { onEscapeKeyDown == null ? void 0 : onEscapeKeyDown(e); if (!e.defaultPrevented) { onHide(); } } }); var removeFocusListenerRef = useRef(); var removeKeydownListenerRef = useRef(); var handleHidden = function handleHidden() { setExited(true); for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } onExited == null ? void 0 : onExited.apply(void 0, args); }; var Transition = transition; if (!container || !(show || Transition && !exited)) { return null; } var dialogProps = _extends({ role: role, ref: modal.setDialogRef, // apparently only works on the dialog role element 'aria-modal': role === 'dialog' ? true : undefined }, rest, { style: style, className: className, tabIndex: -1 }); var dialog = renderDialog ? renderDialog(dialogProps) : /*#__PURE__*/React.createElement("div", dialogProps, /*#__PURE__*/React.cloneElement(children, { role: 'document' })); if (Transition) { dialog = /*#__PURE__*/React.createElement(Transition, { appear: true, unmountOnExit: true, "in": !!show, onExit: onExit, onExiting: onExiting, onExited: handleHidden, onEnter: onEnter, onEntering: onEntering, onEntered: onEntered }, dialog); } var backdropElement = null; if (backdrop) { var BackdropTransition = backdropTransition; backdropElement = renderBackdrop({ ref: modal.setBackdropRef, onClick: handleBackdropClick }); if (BackdropTransition) { backdropElement = /*#__PURE__*/React.createElement(BackdropTransition, { appear: true, "in": !!show }, backdropElement); } } return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/ReactDOM.createPortal( /*#__PURE__*/React.createElement(React.Fragment, null, backdropElement, dialog), container)); }); var propTypes = { /** * Set the visibility of the Modal */ show: PropTypes.bool, /** * A DOM element, a `ref` to an element, or function that returns either. The Modal is appended to it's `container` element. * * For the sake of assistive technologies, the container should usually be the document body, so that the rest of the * page content can be placed behind a virtual backdrop as well as a visual one. */ container: PropTypes.any, /** * A callback fired when the Modal is opening. */ onShow: PropTypes.func, /** * A callback fired when either the backdrop is clicked, or the escape key is pressed. * * The `onHide` callback only signals intent from the Modal, * you must actually set the `show` prop to `false` for the Modal to close. */ onHide: PropTypes.func, /** * Include a backdrop component. */ backdrop: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['static'])]), /** * A function that returns the dialog component. Useful for custom * rendering. **Note:** the component should make sure to apply the provided ref. * * ```js static * renderDialog={props => } * ``` */ renderDialog: PropTypes.func, /** * A function that returns a backdrop component. Useful for custom * backdrop rendering. * * ```js * renderBackdrop={props => } * ``` */ renderBackdrop: PropTypes.func, /** * A callback fired when the escape key, if specified in `keyboard`, is pressed. * * If preventDefault() is called on the keyboard event, closing the modal will be cancelled. */ onEscapeKeyDown: PropTypes.func, /** * A callback fired when the backdrop, if specified, is clicked. */ onBackdropClick: PropTypes.func, /** * A css class or set of classes applied to the modal container when the modal is open, * and removed when it is closed. */ containerClassName: PropTypes.string, /** * Close the modal when escape key is pressed */ keyboard: PropTypes.bool, /** * A `react-transition-group@2.0.0` `` component used * to control animations for the dialog component. */ transition: PropTypes.elementType, /** * A `react-transition-group@2.0.0` `` component used * to control animations for the backdrop components. */ backdropTransition: PropTypes.elementType, /** * When `true` The modal will automatically shift focus to itself when it opens, and * replace it to the last focused element when it closes. This also * works correctly with any Modal children that have the `autoFocus` prop. * * Generally this should never be set to `false` as it makes the Modal less * accessible to assistive technologies, like screen readers. */ autoFocus: PropTypes.bool, /** * When `true` The modal will prevent focus from leaving the Modal while open. * * Generally this should never be set to `false` as it makes the Modal less * accessible to assistive technologies, like screen readers. */ enforceFocus: PropTypes.bool, /** * When `true` The modal will restore focus to previously focused element once * modal is hidden */ restoreFocus: PropTypes.bool, /** * Options passed to focus function when `restoreFocus` is set to `true` * * @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#Parameters */ restoreFocusOptions: PropTypes.shape({ preventScroll: PropTypes.bool }), /** * Callback fired before the Modal transitions in */ onEnter: PropTypes.func, /** * Callback fired as the Modal begins to transition in */ onEntering: PropTypes.func, /** * Callback fired after the Modal finishes transitioning in */ onEntered: PropTypes.func, /** * Callback fired right before the Modal transitions out */ onExit: PropTypes.func, /** * Callback fired as the Modal begins to transition out */ onExiting: PropTypes.func, /** * Callback fired after the Modal finishes transitioning out */ onExited: PropTypes.func, /** * A ModalManager instance used to track and manage the state of open * Modals. Useful when customizing how modals interact within a container */ manager: PropTypes.instanceOf(ModalManager) }; Modal.displayName = 'Modal'; Modal.propTypes = propTypes; export default Object.assign(Modal, { Manager: ModalManager });