UNPKG

devongovett-react-overlays

Version:

Utilities for creating robust overlay components

604 lines (464 loc) 18.2 kB
'use strict'; exports.__esModule = 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; }; var _activeElement = require('dom-helpers/activeElement'); var _activeElement2 = _interopRequireDefault(_activeElement); var _contains = require('dom-helpers/query/contains'); var _contains2 = _interopRequireDefault(_contains); var _inDOM = require('dom-helpers/util/inDOM'); var _inDOM2 = _interopRequireDefault(_inDOM); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); var _componentOrElement = require('prop-types-extra/lib/componentOrElement'); var _componentOrElement2 = _interopRequireDefault(_componentOrElement); var _deprecated = require('prop-types-extra/lib/deprecated'); var _deprecated2 = _interopRequireDefault(_deprecated); var _elementType = require('prop-types-extra/lib/elementType'); var _elementType2 = _interopRequireDefault(_elementType); var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _warning = require('warning'); var _warning2 = _interopRequireDefault(_warning); var _Portal = require('./Portal'); var _Portal2 = _interopRequireDefault(_Portal); var _ModalManager = require('./ModalManager'); var _ModalManager2 = _interopRequireDefault(_ModalManager); var _addEventListener = require('./utils/addEventListener'); var _addEventListener2 = _interopRequireDefault(_addEventListener); var _addFocusListener = require('./utils/addFocusListener'); var _addFocusListener2 = _interopRequireDefault(_addFocusListener); var _getContainer = require('./utils/getContainer'); var _getContainer2 = _interopRequireDefault(_getContainer); var _ownerDocument = require('./utils/ownerDocument'); var _ownerDocument2 = _interopRequireDefault(_ownerDocument); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /*eslint-disable react/prop-types */ var modalManager = new _ModalManager2.default(); /** * Love them or hate them, `<Modal/>` 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 `<Portal/>` 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 `<Transition/>` 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 = function (_React$Component) { _inherits(Modal, _React$Component); function Modal() { var _temp, _this, _ret; _classCallCheck(this, Modal); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this = _possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _initialiseProps.call(_this), _temp), _possibleConstructorReturn(_this, _ret); } Modal.prototype.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; }; Modal.prototype.render = function render() { var _props = this.props, show = _props.show, container = _props.container, children = _props.children, Transition = _props.transition, backdrop = _props.backdrop, 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, { appear: true, unmountOnExit: true, 'in': show, 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: this.setModalNode, role: role || 'dialog' }, filteredProps, { style: style, className: className }), backdrop && this.renderBackdrop(), dialog ) ); }; Modal.prototype.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 }); } }; Modal.prototype.componentWillUpdate = function componentWillUpdate(nextProps) { if (!this.props.show && nextProps.show) { this.checkForFocus(); } }; Modal.prototype.componentDidMount = function componentDidMount() { this._isMounted = true; if (this.props.show) { this.onShow(); } }; Modal.prototype.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(); } }; Modal.prototype.componentWillUnmount = function componentWillUnmount() { var _props2 = this.props, show = _props2.show, transition = _props2.transition; this._isMounted = false; if (show || transition && !this.state.exited) { this.onHide(); } }; //instead of a ref, which might conflict with one the parent applied. return Modal; }(_react2.default.Component); Modal.propTypes = _extends({}, _Portal2.default.propTypes, { /** * Set the visibility of the Modal */ show: _propTypes2.default.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: _propTypes2.default.oneOfType([_componentOrElement2.default, _propTypes2.default.func]), /** * A callback fired when the Modal is opening. */ onShow: _propTypes2.default.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: _propTypes2.default.func, /** * Include a backdrop component. */ backdrop: _propTypes2.default.oneOfType([_propTypes2.default.bool, _propTypes2.default.oneOf(['static'])]), /** * A function that returns a backdrop component. Useful for custom * backdrop rendering. * * ```js * renderBackdrop={props => <MyBackdrop {...props} />} * ``` */ renderBackdrop: _propTypes2.default.func, /** * A callback fired when the escape key, if specified in `keyboard`, is pressed. */ onEscapeKeyDown: _propTypes2.default.func, /** * Support for this function will be deprecated. Please use `onEscapeKeyDown` instead * A callback fired when the escape key, if specified in `keyboard`, is pressed. * @deprecated */ onEscapeKeyUp: (0, _deprecated2.default)(_propTypes2.default.func, 'Please use onEscapeKeyDown instead for consistency'), /** * A callback fired when the backdrop, if specified, is clicked. */ onBackdropClick: _propTypes2.default.func, /** * A style object for the backdrop component. */ backdropStyle: _propTypes2.default.object, /** * A css class or classes for the backdrop component. */ backdropClassName: _propTypes2.default.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: _propTypes2.default.string, /** * Close the modal when escape key is pressed */ keyboard: _propTypes2.default.bool, /** * A `react-transition-group@2.0.0` `<Transition/>` component used * to control animations for the dialog component. */ transition: _elementType2.default, /** * A `react-transition-group@2.0.0` `<Transition/>` component used * to control animations for the backdrop components. */ backdropTransition: _elementType2.default, /** * 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: _propTypes2.default.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: _propTypes2.default.bool, /** * When `true` The modal will restore focus to previously focused element once * modal is hidden */ restoreFocus: _propTypes2.default.bool, /** * Callback fired before the Modal transitions in */ onEnter: _propTypes2.default.func, /** * Callback fired as the Modal begins to transition in */ onEntering: _propTypes2.default.func, /** * Callback fired after the Modal finishes transitioning in */ onEntered: _propTypes2.default.func, /** * Callback fired right before the Modal transitions out */ onExit: _propTypes2.default.func, /** * Callback fired as the Modal begins to transition out */ onExiting: _propTypes2.default.func, /** * Callback fired after the Modal finishes transitioning out */ onExited: _propTypes2.default.func, /** * A ModalManager instance used to track and manage the state of open * Modals. Useful when customizing how modals interact within a container */ manager: _propTypes2.default.object.isRequired }); Modal.defaultProps = { show: false, backdrop: true, keyboard: true, autoFocus: true, enforceFocus: true, restoreFocus: true, onHide: function onHide() {}, manager: modalManager, renderBackdrop: function renderBackdrop(props) { return _react2.default.createElement('div', props); } }; var _initialiseProps = function _initialiseProps() { var _this2 = this; this.state = { exited: !this.props.show }; this.renderBackdrop = function () { var _props3 = _this2.props, backdropStyle = _props3.backdropStyle, backdropClassName = _props3.backdropClassName, renderBackdrop = _props3.renderBackdrop, Transition = _props3.backdropTransition; var backdropRef = function backdropRef(ref) { return _this2.backdrop = ref; }; var backdrop = renderBackdrop({ ref: backdropRef, style: backdropStyle, className: backdropClassName, onClick: _this2.handleBackdropClick }); if (Transition) { backdrop = _react2.default.createElement( Transition, { appear: true, 'in': _this2.props.show }, backdrop ); } return backdrop; }; this.onShow = function () { var doc = (0, _ownerDocument2.default)(_this2); var container = (0, _getContainer2.default)(_this2.props.container, doc.body); _this2.props.manager.add(_this2, container, _this2.props.containerClassName); _this2._onDocumentKeydownListener = (0, _addEventListener2.default)(doc, 'keydown', _this2.handleDocumentKeyDown); _this2._onDocumentKeyupListener = (0, _addEventListener2.default)(doc, 'keyup', _this2.handleDocumentKeyUp); _this2._onFocusinListener = (0, _addFocusListener2.default)(_this2.enforceFocus); _this2.focus(); if (_this2.props.onShow) { _this2.props.onShow(); } }; this.onHide = function () { _this2.props.manager.remove(_this2); _this2._onDocumentKeydownListener.remove(); _this2._onDocumentKeyupListener.remove(); _this2._onFocusinListener.remove(); if (_this2.props.restoreFocus) { _this2.restoreLastFocus(); } }; this.setMountNode = function (ref) { _this2.mountNode = ref ? ref.getMountNode() : ref; }; this.setModalNode = function (ref) { _this2.modalNode = ref; }; this.handleHidden = function () { _this2.setState({ exited: true }); _this2.onHide(); if (_this2.props.onExited) { var _props4; (_props4 = _this2.props).onExited.apply(_props4, arguments); } }; this.handleBackdropClick = function (e) { if (e.target !== e.currentTarget) { return; } if (_this2.props.onBackdropClick) { _this2.props.onBackdropClick(e); } if (_this2.props.backdrop === true) { _this2.props.onHide(); } }; this.handleDocumentKeyDown = function (e) { if (_this2.props.keyboard && e.key === 'Escape' && _this2.isTopModal()) { if (_this2.props.onEscapeKeyDown) { _this2.props.onEscapeKeyDown(e); } _this2.props.onHide(); } }; this.handleDocumentKeyUp = function (e) { if (_this2.props.keyboard && e.key === 'Escape' && _this2.isTopModal()) { if (_this2.props.onEscapeKeyUp) { _this2.props.onEscapeKeyUp(e); } } }; this.checkForFocus = function () { if (_inDOM2.default) { _this2.lastFocus = (0, _activeElement2.default)(); } }; this.focus = function () { var autoFocus = _this2.props.autoFocus; var modalContent = _this2.getDialogElement(); var current = (0, _activeElement2.default)((0, _ownerDocument2.default)(_this2)); var focusInModal = current && (0, _contains2.default)(modalContent, current); if (modalContent && autoFocus && !focusInModal) { _this2.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(); } }; this.restoreLastFocus = function () { // Support: <=IE11 doesn't support `focus()` on svg elements (RB: #917) if (_this2.lastFocus && _this2.lastFocus.focus) { _this2.lastFocus.focus(); _this2.lastFocus = null; } }; this.enforceFocus = function () { var enforceFocus = _this2.props.enforceFocus; if (!enforceFocus || !_this2._isMounted || !_this2.isTopModal()) { return; } var active = (0, _activeElement2.default)((0, _ownerDocument2.default)(_this2)); var modal = _this2.getDialogElement(); if (modal && modal !== active && !(0, _contains2.default)(modal, active)) { modal.focus(); } }; this.getDialogElement = function () { var node = _this2.modalNode; return node && node.lastChild; }; this.isTopModal = function () { return _this2.props.manager.isTopModal(_this2); }; }; Modal.Manager = _ModalManager2.default; exports.default = Modal; module.exports = exports['default'];