'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _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; }; /*eslint-disable react/prop-types */ var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _warning = require('warning'); var _warning2 = _interopRequireDefault(_warning); var _componentOrElement = require('react-prop-types/lib/componentOrElement'); var _componentOrElement2 = _interopRequireDefault(_componentOrElement); var _elementType = require('react-prop-types/lib/elementType'); var _elementType2 = _interopRequireDefault(_elementType); var _Portal = require('./Portal'); var _Portal2 = _interopRequireDefault(_Portal); var _ModalManager = require('./ModalManager'); var _ModalManager2 = _interopRequireDefault(_ModalManager); var _ownerDocument = require('./utils/ownerDocument'); var _ownerDocument2 = _interopRequireDefault(_ownerDocument); var _addEventListener = require('./utils/addEventListener'); var _addEventListener2 = _interopRequireDefault(_addEventListener); var _addFocusListener = require('./utils/addFocusListener'); var _addFocusListener2 = _interopRequireDefault(_addFocusListener); var _inDOM = require('dom-helpers/util/inDOM'); var _inDOM2 = _interopRequireDefault(_inDOM); var _activeElement = require('dom-helpers/activeElement'); var _activeElement2 = _interopRequireDefault(_activeElement); var _contains = require('dom-helpers/query/contains'); var _contains2 = _interopRequireDefault(_contains); var _getContainer = require('./utils/getContainer'); var _getContainer2 = _interopRequireDefault(_getContainer); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var modalManager = new _ModalManager2.default(); /** * 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 = _react2.default.createClass({ displayName: 'Modal', propTypes: _extends({}, _Portal2.default.propTypes, { /** * Set the visibility of the Modal */ show: _react2.default.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: _react2.default.PropTypes.oneOfType([_componentOrElement2.default, _react2.default.PropTypes.func]), /** * A callback fired when the Modal is opening. */ onShow: _react2.default.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: _react2.default.PropTypes.func, /** * Include a backdrop component. */ backdrop: _react2.default.PropTypes.oneOfType([_react2.default.PropTypes.bool, _react2.default.PropTypes.oneOf(['static'])]), /** * A function that returns a backdrop component. Useful for custom * backdrop rendering. * * ```js * renderBackdrop={props => } * ``` */ renderBackdrop: _react2.default.PropTypes.func, /** * A callback fired when the escape key, if specified in `keyboard`, is pressed. */ onEscapeKeyUp: _react2.default.PropTypes.func, /** * A callback fired when the backdrop, if specified, is clicked. */ onBackdropClick: _react2.default.PropTypes.func, /** * A style object for the backdrop component. */ backdropStyle: _react2.default.PropTypes.object, /** * A css class or classes for the backdrop component. */ backdropClassName: _react2.default.PropTypes.string, /** * A css class or set of classes applied to the modal container when the modal is open, * and removed when it is closed. */ containerClassName: _react2.default.PropTypes.string, /** * Close the modal when escape key is pressed */ keyboard: _react2.default.PropTypes.bool, /** * A `` component to use for the dialog and backdrop components. */ transition: _elementType2.default, /** * The `timeout` of the dialog transition if specified. This number is used to ensure that * transition callbacks are always fired, even if browser transition events are canceled. * * See the Transition `timeout` prop for more infomation. */ dialogTransitionTimeout: _react2.default.PropTypes.number, /** * The `timeout` of the backdrop transition if specified. This number is used to * ensure that transition callbacks are always fired, even if browser transition events are canceled. * * See the Transition `timeout` prop for more infomation. */ backdropTransitionTimeout: _react2.default.PropTypes.number, /** * 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: _react2.default.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: _react2.default.PropTypes.bool, /** * Callback fired before the Modal transitions in */ onEnter: _react2.default.PropTypes.func, /** * Callback fired as the Modal begins to transition in */ onEntering: _react2.default.PropTypes.func, /** * Callback fired after the Modal finishes transitioning in */ onEntered: _react2.default.PropTypes.func, /** * Callback fired right before the Modal transitions out */ onExit: _react2.default.PropTypes.func, /** * Callback fired as the Modal begins to transition out */ onExiting: _react2.default.PropTypes.func, /** * Callback fired after the Modal finishes transitioning out */ onExited: _react2.default.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: _react2.default.PropTypes.object.isRequired }), getDefaultProps: function getDefaultProps() { var noop = function noop() {}; return { show: false, backdrop: true, keyboard: true, autoFocus: true, enforceFocus: true, onHide: noop, manager: modalManager, renderBackdrop: function renderBackdrop(props) { return _react2.default.createElement('div', props); } }; }, omitProps: 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; }, getInitialState: function getInitialState() { return { exited: !this.props.show }; }, render: function render() { var _props = this.props, show = _props.show, container = _props.container, children = _props.children, Transition = _props.transition, backdrop = _props.backdrop, dialogTransitionTimeout = _props.dialogTransitionTimeout, className = _props.className, style = _props.style, onExit = _props.onExit, onExiting = _props.onExiting, onEnter = _props.onEnter, onEntering = _props.onEntering, onEntered = _props.onEntered; var dialog = _react2.default.Children.only(children); var filteredProps = this.omitProps(this.props, Modal.propTypes); var mountModal = show || Transition && !this.state.exited; if (!mountModal) { return null; } var _dialog$props = dialog.props, role = _dialog$props.role, tabIndex = _dialog$props.tabIndex; if (role === undefined || tabIndex === undefined) { dialog = (0, _react.cloneElement)(dialog, { role: role === undefined ? 'document' : role, tabIndex: tabIndex == null ? '-1' : tabIndex }); } if (Transition) { dialog = _react2.default.createElement( Transition, { transitionAppear: true, unmountOnExit: true, 'in': show, timeout: dialogTransitionTimeout, onExit: onExit, onExiting: onExiting, onExited: this.handleHidden, onEnter: onEnter, onEntering: onEntering, onEntered: onEntered }, dialog ); } return _react2.default.createElement( _Portal2.default, { ref: this.setMountNode, container: container }, _react2.default.createElement( 'div', _extends({ ref: 'modal', role: role || 'dialog' }, filteredProps, { style: style, className: className }), backdrop && this.renderBackdrop(), dialog ) ); }, renderBackdrop: function renderBackdrop() { var _this = this; var _props2 = this.props, backdropStyle = _props2.backdropStyle, backdropClassName = _props2.backdropClassName, renderBackdrop = _props2.renderBackdrop, Transition = _props2.transition, backdropTransitionTimeout = _props2.backdropTransitionTimeout; var backdropRef = function backdropRef(ref) { return _this.backdrop = ref; }; var backdrop = _react2.default.createElement('div', { ref: backdropRef, style: this.props.backdropStyle, className: this.props.backdropClassName, onClick: this.handleBackdropClick }); if (Transition) { backdrop = _react2.default.createElement( Transition, { transitionAppear: true, 'in': this.props.show, timeout: backdropTransitionTimeout }, renderBackdrop({ ref: backdropRef, style: backdropStyle, className: backdropClassName, onClick: this.handleBackdropClick }) ); } return backdrop; }, componentWillReceiveProps: function componentWillReceiveProps(nextProps) { if (nextProps.show) { this.setState({ exited: false }); } else if (!nextProps.transition) { // Otherwise let handleHidden take care of marking exited. this.setState({ exited: true }); } }, componentWillUpdate: function componentWillUpdate(nextProps) { if (!this.props.show && nextProps.show) { this.checkForFocus(); } }, componentDidMount: function componentDidMount() { if (this.props.show) { this.onShow(); } }, 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(); } }, componentWillUnmount: function componentWillUnmount() { var _props3 = this.props, show = _props3.show, transition = _props3.transition; if (show || transition && !this.state.exited) { this.onHide(); } }, onShow: function onShow() { var doc = (0, _ownerDocument2.default)(this); var container = (0, _getContainer2.default)(this.props.container, doc.body); this.props.manager.add(this, container, this.props.containerClassName); this._onDocumentKeyupListener = (0, _addEventListener2.default)(doc, 'keyup', this.handleDocumentKeyUp); this._onFocusinListener = (0, _addFocusListener2.default)(this.enforceFocus); this.focus(); if (this.props.onShow) { this.props.onShow(); } }, onHide: function onHide() { this.props.manager.remove(this); this._onDocumentKeyupListener.remove(); this._onFocusinListener.remove(); this.restoreLastFocus(); }, setMountNode: function setMountNode(ref) { this.mountNode = ref ? ref.getMountNode() : ref; }, handleHidden: function handleHidden() { this.setState({ exited: true }); this.onHide(); if (this.props.onExited) { var _props4; (_props4 = this.props).onExited.apply(_props4, arguments); } }, handleBackdropClick: function handleBackdropClick(e) { if (e.target !== e.currentTarget) { return; } if (this.props.onBackdropClick) { this.props.onBackdropClick(e); } if (this.props.backdrop === true) { this.props.onHide(); } }, handleDocumentKeyUp: function handleDocumentKeyUp(e) { if (this.props.keyboard && e.keyCode === 27 && this.isTopModal()) { if (this.props.onEscapeKeyUp) { this.props.onEscapeKeyUp(e); } this.props.onHide(); } }, checkForFocus: function checkForFocus() { if (_inDOM2.default) { this.lastFocus = (0, _activeElement2.default)(); } }, focus: function focus() { var autoFocus = this.props.autoFocus; var modalContent = this.getDialogElement(); var current = (0, _activeElement2.default)((0, _ownerDocument2.default)(this)); var focusInModal = current && (0, _contains2.default)(modalContent, current); if (modalContent && autoFocus && !focusInModal) { this.lastFocus = current; if (!modalContent.hasAttribute('tabIndex')) { modalContent.setAttribute('tabIndex', -1); (0, _warning2.default)(false, 'The modal content node does not accept focus. ' + 'For the benefit of assistive technologies, the tabIndex of the node is being set to "-1".'); } modalContent.focus(); } }, 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; } }, enforceFocus: function enforceFocus() { var enforceFocus = this.props.enforceFocus; if (!enforceFocus || !this.isMounted() || !this.isTopModal()) { return; } var active = (0, _activeElement2.default)((0, _ownerDocument2.default)(this)); var modal = this.getDialogElement(); if (modal && modal !== active && !(0, _contains2.default)(modal, active)) { modal.focus(); } }, //instead of a ref, which might conflict with one the parent applied. getDialogElement: function getDialogElement() { var node = this.refs.modal; return node && node.lastChild; }, isTopModal: function isTopModal() { return this.props.manager.isTopModal(this); } }); Modal.Manager = _ModalManager2.default; exports.default = Modal; module.exports = exports['default'];