function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } /* eslint-disable react/prop-types */ import activeElement from 'dom-helpers/activeElement'; import contains from 'dom-helpers/query/contains'; import canUseDom from 'dom-helpers/util/inDOM'; import listen from 'dom-helpers/events/listen'; import PropTypes from 'prop-types'; import componentOrElement from 'prop-types-extra/lib/componentOrElement'; import elementType from 'prop-types-extra/lib/elementType'; import React from 'react'; import ReactDOM from 'react-dom'; import ModalManager from './ModalManager'; import Portal from './Portal'; import getContainer from './utils/getContainer'; import ownerDocument from './utils/ownerDocument'; var modalManager = new ModalManager(); function omitProps(props, propTypes) { var keys = Object.keys(props); var newProps = {}; keys.map(function (prop) { if (!Object.prototype.hasOwnProperty.call(propTypes, prop)) { newProps[prop] = props[prop]; } }); return newProps; } /** * Love them or hate them, `` provides a solid foundation for creating dialogs, lightboxes, or whatever else. * The Modal component renders its `children` node in front of a backdrop component. * * The Modal offers a few helpful features over using just a `` component and some styles: * * - Manages dialog stacking when one-at-a-time just isn't enough. * - Creates a backdrop, for disabling interaction below the modal. * - It properly manages focus; moving to the modal content, and keeping it there until the modal is closed. * - It disables scrolling of the page content while open. * - Adds the appropriate ARIA roles are automatically. * - Easily pluggable animations via a `` component. * * Note that, in the same way the backdrop element prevents users from clicking or interacting * with the page content underneath the Modal, Screen readers also need to be signaled to not to * interact with page content while the Modal is open. To do this, we use a common technique of applying * the `aria-hidden='true'` attribute to the non-Modal elements in the Modal `container`. This means that for * a Modal to be truly modal, it should have a `container` that is _outside_ your app's * React hierarchy (such as the default: document.body). */ var Modal = /*#__PURE__*/ function (_React$Component) { _inheritsLoose(Modal, _React$Component); function Modal() { var _this; for (var _len = arguments.length, _args = new Array(_len), _key = 0; _key < _len; _key++) { _args[_key] = arguments[_key]; } _this = _React$Component.call.apply(_React$Component, [this].concat(_args)) || this; _this.state = { exited: !_this.props.show }; _this.onPortalRendered = function () { if (_this.props.onShow) { _this.props.onShow(); } // autofocus after onShow, to not trigger a focus event for previous // modals before this one is shown. _this.autoFocus(); }; _this.onShow = function () { var doc = ownerDocument(_assertThisInitialized(_assertThisInitialized(_this))); var container = getContainer(_this.props.container, doc.body); _this.props.manager.add(_assertThisInitialized(_assertThisInitialized(_this)), container, _this.props.containerClassName); _this.removeKeydownListener = listen(doc, 'keydown', _this.handleDocumentKeyDown); _this.removeFocusListener = listen(doc, '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(_this.enforceFocus); }, true); }; _this.onHide = function () { _this.props.manager.remove(_assertThisInitialized(_assertThisInitialized(_this))); _this.removeKeydownListener(); _this.removeFocusListener(); if (_this.props.restoreFocus) { _this.restoreLastFocus(); } }; _this.setDialogRef = function (ref) { _this.dialog = ref; }; _this.setBackdropRef = function (ref) { _this.backdrop = ref && ReactDOM.findDOMNode(ref); }; _this.handleHidden = function () { _this.setState({ exited: true }); _this.onHide(); if (_this.props.onExited) { var _this$props; (_this$props = _this.props).onExited.apply(_this$props, arguments); } }; _this.handleBackdropClick = function (e) { if (e.target !== e.currentTarget) { return; } if (_this.props.onBackdropClick) { _this.props.onBackdropClick(e); } if (_this.props.backdrop === true) { _this.props.onHide(); } }; _this.handleDocumentKeyDown = function (e) { if (_this.props.keyboard && e.keyCode === 27 && _this.isTopModal()) { if (_this.props.onEscapeKeyDown) { _this.props.onEscapeKeyDown(e); } _this.props.onHide(); } }; _this.enforceFocus = function () { if (!_this.props.enforceFocus || !_this._isMounted || !_this.isTopModal()) { return; } var currentActiveElement = activeElement(ownerDocument(_assertThisInitialized(_assertThisInitialized(_this)))); if (_this.dialog && !contains(_this.dialog, currentActiveElement)) { _this.dialog.focus(); } }; _this.renderBackdrop = function () { var _this$props2 = _this.props, renderBackdrop = _this$props2.renderBackdrop, Transition = _this$props2.backdropTransition; var backdrop = renderBackdrop({ ref: _this.setBackdropRef, onClick: _this.handleBackdropClick }); if (Transition) { backdrop = React.createElement(Transition, { appear: true, in: _this.props.show }, backdrop); } return backdrop; }; return _this; } Modal.getDerivedStateFromProps = function getDerivedStateFromProps(nextProps) { if (nextProps.show) { return { exited: false }; } else if (!nextProps.transition) { // Otherwise let handleHidden take care of marking exited. return { exited: true }; } return null; }; var _proto = Modal.prototype; _proto.getSnapshotBeforeUpdate = function getSnapshotBeforeUpdate(prevProps) { if (canUseDom && !prevProps.show && this.props.show) { this.lastFocus = activeElement(); } return null; }; _proto.componentDidMount = function componentDidMount() { this._isMounted = true; if (this.props.show) { this.onShow(); } }; _proto.componentDidUpdate = function componentDidUpdate(prevProps) { var transition = this.props.transition; if (prevProps.show && !this.props.show && !transition) { // Otherwise handleHidden will call this. this.onHide(); } else if (!prevProps.show && this.props.show) { this.onShow(); } }; _proto.componentWillUnmount = function componentWillUnmount() { var _this$props3 = this.props, show = _this$props3.show, transition = _this$props3.transition; this._isMounted = false; if (show || transition && !this.state.exited) { this.onHide(); } }; _proto.autoFocus = function autoFocus() { if (!this.props.autoFocus) return; var currentActiveElement = activeElement(ownerDocument(this)); if (this.dialog && !contains(this.dialog, currentActiveElement)) { this.lastFocus = currentActiveElement; this.dialog.focus(); } }; _proto.restoreLastFocus = function restoreLastFocus() { // Support: <=IE11 doesn't support `focus()` on svg elements (RB: #917) if (this.lastFocus && this.lastFocus.focus) { this.lastFocus.focus(); this.lastFocus = null; } }; _proto.isTopModal = function isTopModal() { return this.props.manager.isTopModal(this); }; _proto.render = function render() { var _this$props4 = this.props, show = _this$props4.show, container = _this$props4.container, children = _this$props4.children, renderDialog = _this$props4.renderDialog, _this$props4$role = _this$props4.role, role = _this$props4$role === void 0 ? 'dialog' : _this$props4$role, Transition = _this$props4.transition, backdrop = _this$props4.backdrop, className = _this$props4.className, style = _this$props4.style, onExit = _this$props4.onExit, onExiting = _this$props4.onExiting, onEnter = _this$props4.onEnter, onEntering = _this$props4.onEntering, onEntered = _this$props4.onEntered, props = _objectWithoutPropertiesLoose(_this$props4, ["show", "container", "children", "renderDialog", "role", "transition", "backdrop", "className", "style", "onExit", "onExiting", "onEnter", "onEntering", "onEntered"]); if (!(show || Transition && !this.state.exited)) { return null; } var dialogProps = _extends({ role: role, ref: this.setDialogRef, // apparently only works on the dialog role element 'aria-modal': role === 'dialog' ? true : undefined }, omitProps(props, Modal.propTypes), { style: style, className: className, tabIndex: '-1' }); var dialog = renderDialog ? renderDialog(dialogProps) : React.createElement("div", dialogProps, React.cloneElement(children, { role: 'document' })); if (Transition) { dialog = React.createElement(Transition, { appear: true, unmountOnExit: true, in: show, onExit: onExit, onExiting: onExiting, onExited: this.handleHidden, onEnter: onEnter, onEntering: onEntering, onEntered: onEntered }, dialog); } return React.createElement(Portal, { container: container, onRendered: this.onPortalRendered }, React.createElement(React.Fragment, null, backdrop && this.renderBackdrop(), dialog)); }; return Modal; }(React.Component); Modal.propTypes = { /** * Set the visibility of the Modal */ show: PropTypes.bool, /** * A Node, Component instance, 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.oneOfType([componentOrElement, PropTypes.func]), /** * 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 * 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. */ 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: elementType, /** * A `react-transition-group@2.0.0` `` component used * to control animations for the backdrop components. */ backdropTransition: 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, /** * 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.object.isRequired }; Modal.defaultProps = { show: false, role: 'dialog', backdrop: true, keyboard: true, autoFocus: true, enforceFocus: true, restoreFocus: true, onHide: function onHide() {}, manager: modalManager, renderBackdrop: function renderBackdrop(props) { return React.createElement("div", props); } }; Modal.Manager = ModalManager; export default Modal;