react-modal-construction-kit
Version:
A flexible Modal component kit for React
347 lines (302 loc) • 10.4 kB
JavaScript
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 _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; }; }();
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; }
import React from 'react';
import PropTypes from 'prop-types';
import Portal from '../Portal/Portal';
import { Transition } from 'react-transition-group';
import { getOriginalBodyPadding, conditionallyUpdateScrollbar, setScrollbarWidth, noop } from '../utils';
export var getTransitionStyles = function getTransitionStyles() {
return {
exited: {
display: 'none'
},
entering: {
opacity: 0.01,
transform: 'scale(1.05)'
},
entered: {
opacity: 1,
transform: 'none'
},
exiting: {
opacity: 0.01,
transform: 'scale(0.90)'
}
};
};
var getStyle = function getStyle(_ref) {
var zIndex = _ref.zIndex,
isCentered = _ref.isCentered,
transitionDuration = _ref.transitionDuration;
return {
component: {
overflowX: 'hidden',
overflowY: 'auto',
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0,
zIndex: zIndex,
outline: 0,
transition: 'opacity ' + transitionDuration / 2 + 'ms linear, transform ' + transitionDuration + 'ms ease-out'
},
dialog: {
display: 'flex',
alignItems: 'center',
position: 'relative',
maxWidth: '500px',
margin: isCentered ? '0 auto' : '1.75rem auto',
minHeight: isCentered && '100%',
width: 'auto',
boxSizing: 'border-box',
pointerEvents: 'none'
},
content: {
position: 'relative',
WebkitBoxOrient: 'vertical',
WebkitBoxDirection: 'normal',
MsFlexDirection: 'column',
flexDirection: 'column',
width: '100%',
pointerEvents: 'auto',
backgroundColor: 'white',
backgroundClip: 'padding-box',
outline: 0,
display: 'flex',
boxSizing: 'border-box'
}
};
};
var Modal = function (_React$Component) {
_inherits(Modal, _React$Component);
function Modal(props) {
_classCallCheck(this, Modal);
var _this = _possibleConstructorReturn(this, (Modal.__proto__ || Object.getPrototypeOf(Modal)).call(this, props));
_this.onOpened = function (node, isAppearing) {
var _this$props = _this.props,
onOpened = _this$props.onOpened,
onEntered = _this$props.onEntered;
onOpened();
onEntered(node, isAppearing);
};
_this.onClosed = function (node) {
var _this$props2 = _this.props,
onClosed = _this$props2.onClosed,
onExited = _this$props2.onExited;
onClosed();
onExited(node);
_this.destroy();
if (_this._isMounted) {
_this.setState({ isOpen: false });
}
};
_this.handleEscape = function (e) {
var _this$props3 = _this.props,
isOpen = _this$props3.isOpen,
hasEscapeClose = _this$props3.hasEscapeClose,
onClosed = _this$props3.onClosed;
if (isOpen && hasEscapeClose && e.keyCode === 27 && onClosed) {
onClosed();
}
};
_this.handleClick = function (e) {
var hasClickedOutside = !_this.contentRef.contains(e.target);
if (hasClickedOutside) {
_this.props.onClosed();
}
};
_this.element = null;
_this.originalBodyPadding = null;
_this.state = {
isOpen: props.isOpen
};
if (props.isOpen) {
_this.init();
}
return _this;
}
_createClass(Modal, [{
key: 'componentDidMount',
value: function componentDidMount() {
if (this.props.onEnter) {
this.props.onEnter();
}
if (this.state.isOpen && this.props.autoFocus) {
this.setFocus();
}
window.addEventListener('keydown', this.handleEscape, true);
this._isMounted = true;
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
if (nextProps.isOpen && !this.props.isOpen) {
this.setState({ isOpen: nextProps.isOpen });
}
}
}, {
key: 'componentWillUpdate',
value: function componentWillUpdate(nextProps, nextState) {
if (nextState.isOpen && !this.state.isOpen) {
this.init();
}
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate(prevProps, prevState) {
if (this.props.autoFocus && this.state.isOpen && !prevState.isOpen) {
this.setFocus();
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
if (this.props.onExit) {
this.props.onExit();
}
if (this.state.isOpen) {
this.destroy();
}
window.removeEventListener('keydown', this.handleEscape, true);
this._isMounted = false;
}
}, {
key: 'setFocus',
value: function setFocus() {
if (this.dialogRef && this.dialogRef.parentNode && typeof this.dialogRef.parentNode.focus === 'function') {
this.dialogRef.parentNode.focus();
}
}
}, {
key: 'init',
value: function 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();
conditionallyUpdateScrollbar();
document.body.appendChild(this.element);
if (!this._bodyStyleAdded) {
document.body.style.overflow = 'hidden';
this._bodyStyleAdded = true;
}
}
}, {
key: 'destroy',
value: function destroy() {
if (this.element) {
document.body.removeChild(this.element);
this.element = null;
}
if (this._bodyStyleAdded) {
document.body.style.overflow = null;
this._bodyStyleAdded = false;
}
setScrollbarWidth(this.originalBodyPadding);
}
}, {
key: 'render',
value: function render() {
var _this2 = this;
if (!this.state.isOpen) {
return null;
}
var _props = this.props,
isOpen = _props.isOpen,
role = _props.role,
transitionDuration = _props.transitionDuration,
children = _props.children,
className = _props.className,
dialogClassName = _props.dialogClassName,
contentClassName = _props.contentClassName,
hasOutsideClickClose = _props.hasOutsideClickClose;
var style = getStyle(this.props);
return React.createElement(
Portal,
{ node: this.element },
React.createElement(
Transition,
{
appear: true,
onEntered: this.onOpened,
onExited: this.onClosed,
timeout: transitionDuration,
'in': isOpen },
function (state) {
return React.createElement(
'div',
{
onClick: hasOutsideClickClose ? _this2.handleClick : null,
tabIndex: '-1',
role: role,
className: className,
style: _extends({}, style.component, getTransitionStyles()[state]) },
React.createElement(
'div',
{
className: dialogClassName,
style: style.dialog,
role: 'document',
ref: function ref(c) {
_this2.dialogRef = c;
} },
React.createElement(
'div',
{
className: contentClassName,
ref: function ref(c) {
_this2.contentRef = c;
},
style: style.content },
children
)
)
);
}
)
);
}
}]);
return Modal;
}(React.Component);
Modal.propTypes = {
isOpen: PropTypes.bool,
autoFocus: PropTypes.bool,
hasEscapeClose: PropTypes.bool,
hasOutsideClickClose: PropTypes.bool,
role: PropTypes.string,
onEnter: PropTypes.func,
onExit: PropTypes.func,
onOpened: PropTypes.func,
onClosed: PropTypes.func,
zIndex: PropTypes.number,
children: PropTypes.node.isRequired,
onEntered: PropTypes.func,
onExited: PropTypes.func,
transitionDuration: PropTypes.number,
className: PropTypes.string,
dialogClassName: PropTypes.string,
contentClassName: PropTypes.string,
// eslint-disable-next-line react/no-unused-prop-types
isCentered: PropTypes.bool
};
Modal.defaultProps = {
isOpen: false,
autoFocus: true,
role: 'dialog',
zIndex: 750,
onOpened: noop,
hasEscapeClose: true,
hasOutsideClickClose: true,
onClosed: noop,
transitionDuration: 300,
onEntered: noop,
onExited: noop,
isCentered: true
};
export default Modal;