'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'];