react-bootstrap
Version:
Bootstrap 3 components build with React
524 lines (400 loc) • 15.5 kB
JavaScript
/*eslint-disable react/prop-types */
'use strict';
var _extends = require('babel-runtime/helpers/extends')['default'];
var _objectWithoutProperties = require('babel-runtime/helpers/object-without-properties')['default'];
var _Object$isFrozen = require('babel-runtime/core-js/object/is-frozen')['default'];
var _Object$keys = require('babel-runtime/core-js/object/keys')['default'];
var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default'];
exports.__esModule = true;
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
var _utilsDomUtils = require('./utils/domUtils');
var _utilsDomUtils2 = _interopRequireDefault(_utilsDomUtils);
var _utilsEventListener = require('./utils/EventListener');
var _utilsEventListener2 = _interopRequireDefault(_utilsEventListener);
var _utilsCreateChainedFunction = require('./utils/createChainedFunction');
var _utilsCreateChainedFunction2 = _interopRequireDefault(_utilsCreateChainedFunction);
var _utilsCustomPropTypes = require('./utils/CustomPropTypes');
var _utilsCustomPropTypes2 = _interopRequireDefault(_utilsCustomPropTypes);
var _Portal = require('./Portal');
var _Portal2 = _interopRequireDefault(_Portal);
var _Fade = require('./Fade');
var _Fade2 = _interopRequireDefault(_Fade);
var _ModalDialog = require('./ModalDialog');
var _ModalDialog2 = _interopRequireDefault(_ModalDialog);
var _ModalBody = require('./ModalBody');
var _ModalBody2 = _interopRequireDefault(_ModalBody);
var _ModalHeader = require('./ModalHeader');
var _ModalHeader2 = _interopRequireDefault(_ModalHeader);
var _ModalTitle = require('./ModalTitle');
var _ModalTitle2 = _interopRequireDefault(_ModalTitle);
var _ModalFooter = require('./ModalFooter');
/**
* Gets the correct clientHeight of the modal container
* when the body/window/document you need to use the docElement clientHeight
* @param {HTMLElement} container
* @param {ReactElement|HTMLElement} context
* @return {Number}
*/
var _ModalFooter2 = _interopRequireDefault(_ModalFooter);
function containerClientHeight(container, context) {
var doc = _utilsDomUtils2['default'].ownerDocument(context);
return container === doc.body || container === doc.documentElement ? doc.documentElement.clientHeight : container.clientHeight;
}
function getContainer(context) {
return context.props.container && _react2['default'].findDOMNode(context.props.container) || _utilsDomUtils2['default'].ownerDocument(context).body;
}
var currentFocusListener = undefined;
/**
* Firefox doesn't have a focusin event so using capture is easiest way to get bubbling
* IE8 can't do addEventListener, but does have onfocusin, so we use that in ie8
*
* We only allow one Listener at a time to avoid stack overflows
*
* @param {ReactElement|HTMLElement} context
* @param {Function} handler
*/
function onFocus(context, handler) {
var doc = _utilsDomUtils2['default'].ownerDocument(context);
var useFocusin = !doc.addEventListener;
var remove = undefined;
if (currentFocusListener) {
currentFocusListener.remove();
}
if (useFocusin) {
document.attachEvent('onfocusin', handler);
remove = function () {
return document.detachEvent('onfocusin', handler);
};
} else {
document.addEventListener('focus', handler, true);
remove = function () {
return document.removeEventListener('focus', handler, true);
};
}
currentFocusListener = { remove: remove };
return currentFocusListener;
}
var scrollbarSize = undefined;
function getScrollbarSize() {
if (scrollbarSize !== undefined) {
return scrollbarSize;
}
var scrollDiv = document.createElement('div');
scrollDiv.style.position = 'absolute';
scrollDiv.style.top = '-9999px';
scrollDiv.style.width = '50px';
scrollDiv.style.height = '50px';
scrollDiv.style.overflow = 'scroll';
document.body.appendChild(scrollDiv);
scrollbarSize = scrollDiv.offsetWidth - scrollDiv.clientWidth;
document.body.removeChild(scrollDiv);
scrollDiv = null;
return scrollbarSize;
}
var Modal = _react2['default'].createClass({
displayName: 'Modal',
propTypes: _extends({}, _Portal2['default'].propTypes, _ModalDialog2['default'].propTypes, {
/**
* Include a backdrop component. Specify 'static' for a backdrop that doesn't trigger an "onHide" when clicked.
*/
backdrop: _react2['default'].PropTypes.oneOf(['static', true, false]),
/**
* Close the modal when escape key is pressed
*/
keyboard: _react2['default'].PropTypes.bool,
/**
* Open and close the Modal with a slide and fade animation.
*/
animation: _react2['default'].PropTypes.bool,
/**
* A Component type that provides the modal content Markup. This is a useful prop when you want to use your own
* styles and markup to create a custom modal component.
*/
dialogComponent: _utilsCustomPropTypes2['default'].elementType,
/**
* When `true` The modal will automatically shift focus to itself when it opens, and replace it to the last focused element when it closes.
* 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.
* Consider leaving the default value here, as it is necessary to make the Modal work well with assistive technologies,
* such as screen readers.
*/
enforceFocus: _react2['default'].PropTypes.bool
}),
getDefaultProps: function getDefaultProps() {
return {
bsClass: 'modal',
dialogComponent: _ModalDialog2['default'],
show: false,
animation: true,
backdrop: true,
keyboard: true,
autoFocus: true,
enforceFocus: true
};
},
getInitialState: function getInitialState() {
return { exited: !this.props.show };
},
render: function render() {
var _props = this.props;
var children = _props.children;
var animation = _props.animation;
var backdrop = _props.backdrop;
var props = _objectWithoutProperties(_props, ['children', 'animation', 'backdrop']);
var onExit = props.onExit;
var onExiting = props.onExiting;
var onEnter = props.onEnter;
var onEntering = props.onEntering;
var onEntered = props.onEntered;
var show = !!props.show;
var Dialog = props.dialogComponent;
var mountModal = show || animation && !this.state.exited;
if (!mountModal) {
return null;
}
var modal = _react2['default'].createElement(
Dialog,
_extends({}, props, {
ref: this._setDialogRef,
className: _classnames2['default']({ 'in': show && !animation }),
onClick: backdrop === true ? this.handleBackdropClick : null
}),
this.renderContent()
);
if (animation) {
modal = _react2['default'].createElement(
_Fade2['default'],
{
transitionAppear: true,
unmountOnExit: true,
'in': show,
duration: Modal.TRANSITION_DURATION,
onExit: onExit,
onExiting: onExiting,
onExited: this.handleHidden,
onEnter: onEnter,
onEntering: onEntering,
onEntered: onEntered
},
modal
);
}
if (backdrop) {
modal = this.renderBackdrop(modal);
}
return _react2['default'].createElement(
_Portal2['default'],
{ container: props.container },
modal
);
},
renderContent: function renderContent() {
var _this = this;
return _react2['default'].Children.map(this.props.children, function (child) {
// TODO: use context in 0.14
if (child && child.type && child.type.__isModalHeader) {
return _react.cloneElement(child, {
onHide: _utilsCreateChainedFunction2['default'](_this.props.onHide, child.props.onHide)
});
}
return child;
});
},
renderBackdrop: function renderBackdrop(modal) {
var _props2 = this.props;
var animation = _props2.animation;
var bsClass = _props2.bsClass;
var duration = Modal.BACKDROP_TRANSITION_DURATION;
// Don't handle clicks for "static" backdrops
var onClick = this.props.backdrop === true ? this.handleBackdropClick : null;
var backdrop = _react2['default'].createElement('div', { ref: "backdrop",
className: _classnames2['default'](bsClass + '-backdrop', { 'in': this.props.show && !animation }),
onClick: onClick
});
return _react2['default'].createElement(
'div',
{ ref: 'modal' },
animation ? _react2['default'].createElement(
_Fade2['default'],
{ transitionAppear: true, 'in': this.props.show, duration: duration },
backdrop
) : backdrop,
modal
);
},
_setDialogRef: function _setDialogRef(ref) {
// issue #1074
// due to: https://github.com/facebook/react/blob/v0.13.3/src/core/ReactCompositeComponent.js#L842
//
// when backdrop is `false` react hasn't had a chance to reassign the refs to a usable object, b/c there are no other
// "classic" refs on the component (or they haven't been processed yet)
// TODO: Remove the need for this in next breaking release
if (_Object$isFrozen(this.refs) && !_Object$keys(this.refs).length) {
this.refs = {};
}
this.refs.dialog = ref;
//maintains backwards compat with older component breakdown
if (!this.props.backdrop) {
this.refs.modal = ref;
}
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
if (nextProps.show) {
this.setState({ exited: false });
} else if (!nextProps.animation) {
// Otherwise let handleHidden take care of marking exited.
this.setState({ exited: true });
}
},
componentWillUpdate: function componentWillUpdate(nextProps) {
if (nextProps.show) {
this.checkForFocus();
}
},
componentDidMount: function componentDidMount() {
if (this.props.show) {
this.onShow();
}
},
componentDidUpdate: function componentDidUpdate(prevProps) {
var animation = this.props.animation;
if (prevProps.show && !this.props.show && !animation) {
//otherwise handleHidden will call this.
this.onHide();
} else if (!prevProps.show && this.props.show) {
this.onShow();
}
},
componentWillUnmount: function componentWillUnmount() {
if (this.props.show) {
this.onHide();
}
},
onShow: function onShow() {
var _this2 = this;
var doc = _utilsDomUtils2['default'].ownerDocument(this);
var win = _utilsDomUtils2['default'].ownerWindow(this);
this._onDocumentKeyupListener = _utilsEventListener2['default'].listen(doc, 'keyup', this.handleDocumentKeyUp);
this._onWindowResizeListener = _utilsEventListener2['default'].listen(win, 'resize', this.handleWindowResize);
if (this.props.enforceFocus) {
this._onFocusinListener = onFocus(this, this.enforceFocus);
}
var container = getContainer(this);
container.className += container.className.length ? ' modal-open' : 'modal-open';
this._containerIsOverflowing = container.scrollHeight > containerClientHeight(container, this);
this._originalPadding = container.style.paddingRight;
if (this._containerIsOverflowing) {
container.style.paddingRight = parseInt(this._originalPadding || 0, 10) + getScrollbarSize() + 'px';
}
if (this.props.backdrop) {
this.iosClickHack();
}
this.setState(this._getStyles(), //eslint-disable-line react/no-did-mount-set-state
function () {
return _this2.focusModalContent();
});
},
onHide: function onHide() {
this._onDocumentKeyupListener.remove();
this._onWindowResizeListener.remove();
if (this._onFocusinListener) {
this._onFocusinListener.remove();
}
var container = getContainer(this);
container.style.paddingRight = this._originalPadding;
container.className = container.className.replace(/ ?modal-open/, '');
this.restoreLastFocus();
},
handleHidden: function handleHidden() {
this.setState({ exited: true });
this.onHide();
if (this.props.onExited) {
var _props3;
(_props3 = this.props).onExited.apply(_props3, arguments);
}
},
handleBackdropClick: function handleBackdropClick(e) {
if (e.target !== e.currentTarget) {
return;
}
this.props.onHide();
},
handleDocumentKeyUp: function handleDocumentKeyUp(e) {
if (this.props.keyboard && e.keyCode === 27) {
this.props.onHide();
}
},
handleWindowResize: function handleWindowResize() {
this.setState(this._getStyles());
},
checkForFocus: function checkForFocus() {
if (_utilsDomUtils2['default'].canUseDom) {
try {
this.lastFocus = document.activeElement;
} catch (e) {} // eslint-disable-line no-empty
}
},
focusModalContent: function focusModalContent() {
var modalContent = _react2['default'].findDOMNode(this.refs.dialog);
var current = _utilsDomUtils2['default'].activeElement(this);
var focusInModal = current && _utilsDomUtils2['default'].contains(modalContent, current);
if (modalContent && this.props.autoFocus && !focusInModal) {
this.lastFocus = current;
modalContent.focus();
}
},
restoreLastFocus: function restoreLastFocus() {
if (this.lastFocus && this.lastFocus.focus) {
this.lastFocus.focus();
this.lastFocus = null;
}
},
enforceFocus: function enforceFocus() {
if (!this.isMounted()) {
return;
}
var active = _utilsDomUtils2['default'].activeElement(this);
var modal = _react2['default'].findDOMNode(this.refs.dialog);
if (modal && modal !== active && !_utilsDomUtils2['default'].contains(modal, active)) {
modal.focus();
}
},
iosClickHack: function iosClickHack() {
// IOS only allows click events to be delegated to the document on elements
// it considers 'clickable' - anchors, buttons, etc. We fake a click handler on the
// DOM nodes themselves. Remove if handled by React: https://github.com/facebook/react/issues/1169
_react2['default'].findDOMNode(this.refs.modal).onclick = function () {};
_react2['default'].findDOMNode(this.refs.backdrop).onclick = function () {};
},
_getStyles: function _getStyles() {
if (!_utilsDomUtils2['default'].canUseDom) {
return {};
}
var node = _react2['default'].findDOMNode(this.refs.modal);
var scrollHt = node.scrollHeight;
var container = getContainer(this);
var containerIsOverflowing = this._containerIsOverflowing;
var modalIsOverflowing = scrollHt > containerClientHeight(container, this);
return {
dialogStyles: {
paddingRight: containerIsOverflowing && !modalIsOverflowing ? getScrollbarSize() : void 0,
paddingLeft: !containerIsOverflowing && modalIsOverflowing ? getScrollbarSize() : void 0
}
};
}
});
Modal.Body = _ModalBody2['default'];
Modal.Header = _ModalHeader2['default'];
Modal.Title = _ModalTitle2['default'];
Modal.Footer = _ModalFooter2['default'];
Modal.Dialog = _ModalDialog2['default'];
Modal.TRANSITION_DURATION = 300;
Modal.BACKDROP_TRANSITION_DURATION = 150;
exports['default'] = Modal;
module.exports = exports['default'];