diff --git a/package.json b/package.json index 57b30570e..b7c4c19c0 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,8 @@ "lodash.tonumber": "^4.0.3", "prop-types": "^15.5.8", "react-popper": "^0.7.2", - "react-transition-group": "^2.2.0" + "react-portal": "^4.1.2", + "react-transition-group": "^2.2.1" }, "peerDependencies": { "react": "^0.14.9 || ^15.3.0 || ^16.0.0", @@ -117,7 +118,7 @@ "css-loader": "^0.25.0", "ejs": "^2.5.1", "enzyme": "^3.0.0", - "enzyme-adapter-react-16": "^1.0.0", + "enzyme-adapter-react-16": "^1.1.1", "eslint": "^4.1.1", "eslint-config-airbnb": "^15.1.0", "eslint-plugin-import": "^2.6.0", diff --git a/src/Modal.js b/src/Modal.js index 086106203..d23c2247a 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; +import { Portal } from 'react-portal'; import classNames from 'classnames'; import Fade from './Fade'; import { @@ -73,40 +73,62 @@ class Modal extends React.Component { constructor(props) { super(props); - this.originalBodyPadding = null; - this.isBodyOverflowing = false; - this.togglePortal = this.togglePortal.bind(this); + this._element = null; + this._originalBodyPadding = null; this.handleBackdropClick = this.handleBackdropClick.bind(this); this.handleEscape = this.handleEscape.bind(this); - this.destroy = this.destroy.bind(this); this.onOpened = this.onOpened.bind(this); this.onClosed = this.onClosed.bind(this); + + this.state = { + isOpen: props.isOpen, + }; + + if (props.isOpen) { + this.init(); + } } componentDidMount() { - if (this.props.isOpen) { - this.togglePortal(); - } if (this.props.onEnter) { this.props.onEnter(); } + + if (this.state.isOpen && this.props.autoFocus) { + this.setFocus(); + } + + this._isMounted = true; } - componentDidUpdate(prevProps) { - if (this.props.isOpen !== prevProps.isOpen) { - // handle portal events/dom updates - this.togglePortal(); - } else if (this._element) { - // rerender portal - this.renderIntoSubtree(); + componentWillReceiveProps(nextProps) { + if (nextProps.isOpen && !this.props.isOpen) { + this.setState({ isOpen: nextProps.isOpen }); + } + } + + componentWillUpdate(nextProps, nextState) { + if (nextState.isOpen && !this.state.isOpen) { + this.init(); + } + } + + componentDidUpdate(prevProps, prevState) { + if (this.props.autoFocus && this.state.isOpen && !prevState.isOpen) { + this.setFocus(); } } componentWillUnmount() { - this.destroy(); if (this.props.onExit) { this.props.onExit(); } + + if (this.state.isOpen) { + this.destroy(); + } + + this._isMounted = false; } onOpened(node, isAppearing) { @@ -116,19 +138,24 @@ class Modal extends React.Component { onClosed(node) { // so all methods get called before it is unmounted - setTimeout(() => this.destroy(), 0); this.props.onClosed(); (this.props.modalTransition.onExited || noop)(node); + this.destroy(); + + if (this._isMounted) { + this.setState({ isOpen: false }); + } } - handleEscape(e) { - if (this.props.keyboard && e.keyCode === 27 && this.props.toggle) { - this.props.toggle(); + setFocus() { + if (this._dialog && this._dialog.parentNode && typeof this._dialog.parentNode.focus === 'function') { + this._dialog.parentNode.focus(); } } handleBackdropClick(e) { - if (this.props.backdrop !== true) return; + e.stopPropagation(); + if (!this.props.isOpen || this.props.backdrop !== true) return; const container = this._dialog; @@ -137,65 +164,39 @@ class Modal extends React.Component { } } - togglePortal() { - if (this.props.isOpen) { - if (this.props.autoFocus) { - this._focus = true; - } - this.show(); - } else { - this.hide(); - } - } - - destroy() { - if (this._element) { - ReactDOM.unmountComponentAtNode(this._element); - document.body.removeChild(this._element); - this._element = null; - } - - if (this.bodyClassAdded) { - const modalOpenClassName = mapToCssModules('modal-open', this.props.cssModule); - // Use regex to prevent matching `modal-open` as part of a different class, e.g. `my-modal-opened` - const modalOpenClassNameRegex = new RegExp(`(^| )${modalOpenClassName}( |$)`); - document.body.className = document.body.className.replace(modalOpenClassNameRegex, ' ').trim(); - this.bodyClassAdded = false; + handleEscape(e) { + if (this.props.isOpen && this.props.keyboard && e.keyCode === 27 && this.props.toggle) { + this.props.toggle(); } - setScrollbarWidth(this.originalBodyPadding); - } - - hide() { - this.renderIntoSubtree(); } - show() { - if (this._dialog) { - if (this.props.toggle) { - this.props.toggle(true); - } - return; - } - const classes = document.body.className; + init() { this._element = document.createElement('div'); this._element.setAttribute('tabindex', '-1'); this._element.style.position = 'relative'; this._element.style.zIndex = this.props.zIndex; - this.originalBodyPadding = getOriginalBodyPadding(); + this._originalBodyPadding = getOriginalBodyPadding(); conditionallyUpdateScrollbar(); document.body.appendChild(this._element); - if (!this.bodyClassAdded) { - document.body.className = classNames( - classes, - mapToCssModules('modal-open', this.props.cssModule) - ); - this.bodyClassAdded = true; - } + document.body.className = classNames( + document.body.className, + mapToCssModules('modal-open', this.props.cssModule) + ); + } - this.renderIntoSubtree(); + destroy() { + document.body.removeChild(this._element); + this._element = null; + + const modalOpenClassName = mapToCssModules('modal-open', this.props.cssModule); + // Use regex to prevent matching `modal-open` as part of a different class, e.g. `my-modal-opened` + const modalOpenClassNameRegex = new RegExp(`(^| )${modalOpenClassName}( |$)`); + document.body.className = document.body.className.replace(modalOpenClassNameRegex, ' ').trim(); + + setScrollbarWidth(this._originalBodyPadding); } renderModalDialog() { @@ -203,6 +204,7 @@ class Modal extends React.Component { return (