UNPKG

react-modal

Version:

Accessible modal dialog component for React.JS

445 lines (361 loc) 16.4 kB
"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; }; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _react = require("react"); var _propTypes = require("prop-types"); var _propTypes2 = _interopRequireDefault(_propTypes); var _focusManager = require("../helpers/focusManager"); var focusManager = _interopRequireWildcard(_focusManager); var _scopeTab = require("../helpers/scopeTab"); var _scopeTab2 = _interopRequireDefault(_scopeTab); var _ariaAppHider = require("../helpers/ariaAppHider"); var ariaAppHider = _interopRequireWildcard(_ariaAppHider); var _classList = require("../helpers/classList"); var classList = _interopRequireWildcard(_classList); var _safeHTMLElement = require("../helpers/safeHTMLElement"); var _safeHTMLElement2 = _interopRequireDefault(_safeHTMLElement); var _portalOpenInstances = require("../helpers/portalOpenInstances"); var _portalOpenInstances2 = _interopRequireDefault(_portalOpenInstances); require("../helpers/bodyTrap"); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 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; } // so that our CSS is statically analyzable var CLASS_NAMES = { overlay: "ReactModal__Overlay", content: "ReactModal__Content" }; /** * We need to support the deprecated `KeyboardEvent.keyCode` in addition to * `KeyboardEvent.code` for apps that still support IE11. Can be removed when * `react-modal` only supports React >18 (which dropped IE support). */ var isTabKey = function isTabKey(event) { return event.code === "Tab" || event.keyCode === 9; }; var isEscKey = function isEscKey(event) { return event.code === "Escape" || event.keyCode === 27; }; var ariaHiddenInstances = 0; var ModalPortal = function (_Component) { _inherits(ModalPortal, _Component); function ModalPortal(props) { _classCallCheck(this, ModalPortal); var _this = _possibleConstructorReturn(this, (ModalPortal.__proto__ || Object.getPrototypeOf(ModalPortal)).call(this, props)); _this.setOverlayRef = function (overlay) { _this.overlay = overlay; _this.props.overlayRef && _this.props.overlayRef(overlay); }; _this.setContentRef = function (content) { _this.content = content; _this.props.contentRef && _this.props.contentRef(content); }; _this.afterClose = function () { var _this$props = _this.props, appElement = _this$props.appElement, ariaHideApp = _this$props.ariaHideApp, htmlOpenClassName = _this$props.htmlOpenClassName, bodyOpenClassName = _this$props.bodyOpenClassName, parentSelector = _this$props.parentSelector; var parentDocument = parentSelector && parentSelector().ownerDocument || document; // Remove classes. bodyOpenClassName && classList.remove(parentDocument.body, bodyOpenClassName); htmlOpenClassName && classList.remove(parentDocument.getElementsByTagName("html")[0], htmlOpenClassName); // Reset aria-hidden attribute if all modals have been removed if (ariaHideApp && ariaHiddenInstances > 0) { ariaHiddenInstances -= 1; if (ariaHiddenInstances === 0) { ariaAppHider.show(appElement); } } if (_this.props.shouldFocusAfterRender) { if (_this.props.shouldReturnFocusAfterClose) { focusManager.returnFocus(_this.props.preventScroll); focusManager.teardownScopedFocus(); } else { focusManager.popWithoutFocus(); } } if (_this.props.onAfterClose) { _this.props.onAfterClose(); } _portalOpenInstances2.default.deregister(_this); }; _this.open = function () { _this.beforeOpen(); if (_this.state.afterOpen && _this.state.beforeClose) { clearTimeout(_this.closeTimer); _this.setState({ beforeClose: false }); } else { if (_this.props.shouldFocusAfterRender) { focusManager.setupScopedFocus(_this.node); focusManager.markForFocusLater(); } _this.setState({ isOpen: true }, function () { _this.openAnimationFrame = requestAnimationFrame(function () { _this.setState({ afterOpen: true }); if (_this.props.isOpen && _this.props.onAfterOpen) { _this.props.onAfterOpen({ overlayEl: _this.overlay, contentEl: _this.content }); } }); }); } }; _this.close = function () { if (_this.props.closeTimeoutMS > 0) { _this.closeWithTimeout(); } else { _this.closeWithoutTimeout(); } }; _this.focusContent = function () { return _this.content && !_this.contentHasFocus() && _this.content.focus({ preventScroll: true }); }; _this.closeWithTimeout = function () { var closesAt = Date.now() + _this.props.closeTimeoutMS; _this.setState({ beforeClose: true, closesAt: closesAt }, function () { _this.closeTimer = setTimeout(_this.closeWithoutTimeout, _this.state.closesAt - Date.now()); }); }; _this.closeWithoutTimeout = function () { _this.setState({ beforeClose: false, isOpen: false, afterOpen: false, closesAt: null }, _this.afterClose); }; _this.handleKeyDown = function (event) { if (isTabKey(event)) { (0, _scopeTab2.default)(_this.content, event); } if (_this.props.shouldCloseOnEsc && isEscKey(event)) { event.stopPropagation(); _this.requestClose(event); } }; _this.handleOverlayOnClick = function (event) { if (_this.shouldClose === null) { _this.shouldClose = true; } if (_this.shouldClose && _this.props.shouldCloseOnOverlayClick) { if (_this.ownerHandlesClose()) { _this.requestClose(event); } else { _this.focusContent(); } } _this.shouldClose = null; }; _this.handleContentOnMouseUp = function () { _this.shouldClose = false; }; _this.handleOverlayOnMouseDown = function (event) { if (!_this.props.shouldCloseOnOverlayClick && event.target == _this.overlay) { event.preventDefault(); } }; _this.handleContentOnClick = function () { _this.shouldClose = false; }; _this.handleContentOnMouseDown = function () { _this.shouldClose = false; }; _this.requestClose = function (event) { return _this.ownerHandlesClose() && _this.props.onRequestClose(event); }; _this.ownerHandlesClose = function () { return _this.props.onRequestClose; }; _this.shouldBeClosed = function () { return !_this.state.isOpen && !_this.state.beforeClose; }; _this.contentHasFocus = function () { return document.activeElement === _this.content || _this.content.contains(document.activeElement); }; _this.buildClassName = function (which, additional) { var classNames = (typeof additional === "undefined" ? "undefined" : _typeof(additional)) === "object" ? additional : { base: CLASS_NAMES[which], afterOpen: CLASS_NAMES[which] + "--after-open", beforeClose: CLASS_NAMES[which] + "--before-close" }; var className = classNames.base; if (_this.state.afterOpen) { className = className + " " + classNames.afterOpen; } if (_this.state.beforeClose) { className = className + " " + classNames.beforeClose; } return typeof additional === "string" && additional ? className + " " + additional : className; }; _this.attributesFromObject = function (prefix, items) { return Object.keys(items).reduce(function (acc, name) { acc[prefix + "-" + name] = items[name]; return acc; }, {}); }; _this.state = { afterOpen: false, beforeClose: false }; _this.shouldClose = null; _this.moveFromContentToOverlay = null; return _this; } _createClass(ModalPortal, [{ key: "componentDidMount", value: function componentDidMount() { if (this.props.isOpen) { this.open(); } } }, { key: "componentDidUpdate", value: function componentDidUpdate(prevProps, prevState) { if (process.env.NODE_ENV !== "production") { if (prevProps.bodyOpenClassName !== this.props.bodyOpenClassName) { // eslint-disable-next-line no-console console.warn('React-Modal: "bodyOpenClassName" prop has been modified. ' + "This may cause unexpected behavior when multiple modals are open."); } if (prevProps.htmlOpenClassName !== this.props.htmlOpenClassName) { // eslint-disable-next-line no-console console.warn('React-Modal: "htmlOpenClassName" prop has been modified. ' + "This may cause unexpected behavior when multiple modals are open."); } } if (this.props.isOpen && !prevProps.isOpen) { this.open(); } else if (!this.props.isOpen && prevProps.isOpen) { this.close(); } // Focus only needs to be set once when the modal is being opened if (this.props.shouldFocusAfterRender && this.state.isOpen && !prevState.isOpen) { this.focusContent(); } } }, { key: "componentWillUnmount", value: function componentWillUnmount() { if (this.state.isOpen) { this.afterClose(); } clearTimeout(this.closeTimer); cancelAnimationFrame(this.openAnimationFrame); } }, { key: "beforeOpen", value: function beforeOpen() { var _props = this.props, appElement = _props.appElement, ariaHideApp = _props.ariaHideApp, htmlOpenClassName = _props.htmlOpenClassName, bodyOpenClassName = _props.bodyOpenClassName, parentSelector = _props.parentSelector; var parentDocument = parentSelector && parentSelector().ownerDocument || document; // Add classes. bodyOpenClassName && classList.add(parentDocument.body, bodyOpenClassName); htmlOpenClassName && classList.add(parentDocument.getElementsByTagName("html")[0], htmlOpenClassName); if (ariaHideApp) { ariaHiddenInstances += 1; ariaAppHider.hide(appElement); } _portalOpenInstances2.default.register(this); } // Don't steal focus from inner elements }, { key: "render", value: function render() { var _props2 = this.props, id = _props2.id, className = _props2.className, overlayClassName = _props2.overlayClassName, defaultStyles = _props2.defaultStyles, children = _props2.children; var contentStyles = className ? {} : defaultStyles.content; var overlayStyles = overlayClassName ? {} : defaultStyles.overlay; if (this.shouldBeClosed()) { return null; } var overlayProps = { ref: this.setOverlayRef, className: this.buildClassName("overlay", overlayClassName), style: _extends({}, overlayStyles, this.props.style.overlay), onClick: this.handleOverlayOnClick, onMouseDown: this.handleOverlayOnMouseDown }; var contentProps = _extends({ id: id, ref: this.setContentRef, style: _extends({}, contentStyles, this.props.style.content), className: this.buildClassName("content", className), tabIndex: "-1", onKeyDown: this.handleKeyDown, onMouseDown: this.handleContentOnMouseDown, onMouseUp: this.handleContentOnMouseUp, onClick: this.handleContentOnClick, role: this.props.role, "aria-label": this.props.contentLabel }, this.attributesFromObject("aria", _extends({ modal: true }, this.props.aria)), this.attributesFromObject("data", this.props.data || {}), { "data-testid": this.props.testId }); var contentElement = this.props.contentElement(contentProps, children); return this.props.overlayElement(overlayProps, contentElement); } }]); return ModalPortal; }(_react.Component); ModalPortal.defaultProps = { style: { overlay: {}, content: {} }, defaultStyles: {} }; ModalPortal.propTypes = { isOpen: _propTypes2.default.bool.isRequired, defaultStyles: _propTypes2.default.shape({ content: _propTypes2.default.object, overlay: _propTypes2.default.object }), style: _propTypes2.default.shape({ content: _propTypes2.default.object, overlay: _propTypes2.default.object }), className: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.object]), overlayClassName: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.object]), parentSelector: _propTypes2.default.func, bodyOpenClassName: _propTypes2.default.string, htmlOpenClassName: _propTypes2.default.string, ariaHideApp: _propTypes2.default.bool, appElement: _propTypes2.default.oneOfType([_propTypes2.default.instanceOf(_safeHTMLElement2.default), _propTypes2.default.instanceOf(_safeHTMLElement.SafeHTMLCollection), _propTypes2.default.instanceOf(_safeHTMLElement.SafeNodeList), _propTypes2.default.arrayOf(_propTypes2.default.instanceOf(_safeHTMLElement2.default))]), onAfterOpen: _propTypes2.default.func, onAfterClose: _propTypes2.default.func, onRequestClose: _propTypes2.default.func, closeTimeoutMS: _propTypes2.default.number, shouldFocusAfterRender: _propTypes2.default.bool, shouldCloseOnOverlayClick: _propTypes2.default.bool, shouldReturnFocusAfterClose: _propTypes2.default.bool, preventScroll: _propTypes2.default.bool, role: _propTypes2.default.string, contentLabel: _propTypes2.default.string, aria: _propTypes2.default.object, data: _propTypes2.default.object, children: _propTypes2.default.node, shouldCloseOnEsc: _propTypes2.default.bool, overlayRef: _propTypes2.default.func, contentRef: _propTypes2.default.func, id: _propTypes2.default.string, overlayElement: _propTypes2.default.func, contentElement: _propTypes2.default.func, testId: _propTypes2.default.string }; exports.default = ModalPortal; module.exports = exports["default"];