react-bootstrap
Version:
Bootstrap 3 components build with React
530 lines (410 loc) • 16.3 kB
JavaScript
/*eslint-disable react/prop-types */
'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; };
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
var _utilsCreateChainedFunction = require('./utils/createChainedFunction');
var _utilsCreateChainedFunction2 = _interopRequireDefault(_utilsCreateChainedFunction);
var _BootstrapMixin = require('./BootstrapMixin');
var _BootstrapMixin2 = _interopRequireDefault(_BootstrapMixin);
var _FadeMixin = require('./FadeMixin');
var _FadeMixin2 = _interopRequireDefault(_FadeMixin);
var _utilsDomUtils = require('./utils/domUtils');
var _utilsDomUtils2 = _interopRequireDefault(_utilsDomUtils);
var _utilsEventListener = require('./utils/EventListener');
var _utilsEventListener2 = _interopRequireDefault(_utilsEventListener);
var _utilsDeprecationWarning = require('./utils/deprecationWarning');
var _utilsDeprecationWarning2 = _interopRequireDefault(_utilsDeprecationWarning);
var _Portal = require('./Portal');
var _Portal2 = _interopRequireDefault(_Portal);
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');
var _ModalFooter2 = _interopRequireDefault(_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}
*/
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;
}
function requiredIfNot(key, type) {
return function (props, propName, componentName) {
var propType = type;
if (props[key] === undefined) {
propType = propType.isRequired;
}
return propType(props, propName, componentName);
};
}
function toChildArray(children) {
var result = [];
_react2['default'].Children.forEach(children, function (c) {
return result.push(c);
});
return result;
}
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;
}
var ModalMarkup = _react2['default'].createClass({
displayName: 'ModalMarkup',
mixins: [_BootstrapMixin2['default'], _FadeMixin2['default']],
propTypes: {
/**
* The Modal title text
* @deprecated Use the "Modal.Header" component instead
*/
title: _react2['default'].PropTypes.node,
/**
* 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]),
/**
* Include a backdrop component. Specify 'static' for a backdrop that doesn't trigger an "onHide" when clicked.
*/
keyboard: _react2['default'].PropTypes.bool,
/**
* Specify whether the Modal heading should contain a close button
* @deprecated Use the "Modal.Header" Component instead
*/
closeButton: _react2['default'].PropTypes.bool,
/**
* Open and close the Modal with a slide and fade animation.
*/
animation: _react2['default'].PropTypes.bool,
/**
* A Callback fired when the header closeButton or non-static backdrop is clicked.
* @type {function}
* @required
*/
onHide: requiredIfNot('onRequestHide', _react2['default'].PropTypes.func),
/**
* A Callback fired when the header closeButton or non-static backdrop is clicked.
* @deprecated Replaced by `onHide`.
*/
onRequestHide: _react2['default'].PropTypes.func,
/**
* A css class to apply to the Modal dialog DOM node.
*/
dialogClassName: _react2['default'].PropTypes.string,
/**
* 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',
backdrop: true,
keyboard: true,
animation: true,
closeButton: true,
autoFocus: true,
enforceFocus: true
};
},
getInitialState: function getInitialState() {
return {};
},
render: function render() {
var state = this.state;
var modalStyle = _extends({}, state.dialogStyles, { display: 'block' });
var dialogClasses = this.getBsClassSet();
delete dialogClasses.modal;
dialogClasses['modal-dialog'] = true;
var classes = {
modal: true,
fade: this.props.animation,
'in': !this.props.animation
};
var modal = _react2['default'].createElement(
'div',
_extends({}, this.props, {
title: null,
tabIndex: '-1',
role: 'dialog',
style: modalStyle,
className: (0, _classnames2['default'])(this.props.className, classes),
onClick: this.props.backdrop === true ? this.handleBackdropClick : null,
ref: 'modal' }),
_react2['default'].createElement(
'div',
{ className: (0, _classnames2['default'])(this.props.dialogClassName, dialogClasses) },
_react2['default'].createElement(
'div',
{ className: 'modal-content', role: 'document' },
this.renderContent()
)
)
);
return this.props.backdrop ? this.renderBackdrop(modal, state.backdropStyles) : modal;
},
renderContent: function renderContent() {
var _this = this;
var children = toChildArray(this.props.children); // b/c createFragment is in addons and children can be a key'd object
var hasNewHeader = children.some(function (c) {
return c.type.__isModalHeader;
});
if (!hasNewHeader && this.props.title != null) {
(0, _utilsDeprecationWarning2['default'])('Specifying `closeButton` or `title` Modal props', 'the new Modal.Header, and Modal.Title components');
children.unshift(_react2['default'].createElement(
_ModalHeader2['default'],
{ closeButton: this.props.closeButton, onHide: this._getHide() },
this.props.title && _react2['default'].createElement(
_ModalTitle2['default'],
null,
this.props.title
)
));
}
return _react2['default'].Children.map(children, function (child) {
// TODO: use context in 0.14
if (child.type.__isModalHeader) {
return (0, _react.cloneElement)(child, {
onHide: (0, _utilsCreateChainedFunction2['default'])(_this._getHide(), child.props.onHide)
});
}
return child;
});
},
renderBackdrop: function renderBackdrop(modal) {
var classes = {
'modal-backdrop': true,
fade: this.props.animation,
'in': !this.props.animation
};
var onClick = this.props.backdrop === true ? this.handleBackdropClick : null;
return _react2['default'].createElement(
'div',
null,
_react2['default'].createElement('div', { className: (0, _classnames2['default'])(classes), ref: 'backdrop', onClick: onClick }),
modal
);
},
_getHide: function _getHide() {
if (!this.props.onHide && this.props.onRequestHide) {
(0, _utilsDeprecationWarning2['default'])('The Modal prop `onRequestHide`', 'the `onHide` prop');
}
return this.props.onHide || this.props.onRequestHide;
},
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 () {};
},
componentWillMount: function componentWillMount() {
this.checkForFocus();
},
componentDidMount: function componentDidMount() {
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();
});
},
componentDidUpdate: function componentDidUpdate(prevProps) {
if (this.props.backdrop && this.props.backdrop !== prevProps.backdrop) {
this.iosClickHack();
this.setState(this._getStyles()); //eslint-disable-line react/no-did-update-set-state
}
if (this.props.container !== prevProps.container) {
var container = getContainer(this);
this._containerIsOverflowing = container.scrollHeight > containerClientHeight(container, this);
}
},
componentWillUnmount: function componentWillUnmount() {
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();
},
handleBackdropClick: function handleBackdropClick(e) {
if (e.target !== e.currentTarget) {
return;
}
this._getHide()();
},
handleDocumentKeyUp: function handleDocumentKeyUp(e) {
if (this.props.keyboard && e.keyCode === 27) {
this._getHide()();
}
},
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.modal);
var current = _utilsDomUtils2['default'].activeElement(this);
var focusInModal = current && _utilsDomUtils2['default'].contains(modalContent, current);
if (this.props.autoFocus && !focusInModal) {
this.lastFocus = current;
modalContent.focus();
}
},
restoreLastFocus: function restoreLastFocus() {
if (this.lastFocus) {
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.modal);
if (modal !== active && !_utilsDomUtils2['default'].contains(modal, active)) {
modal.focus();
}
},
_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
}
};
}
});
var Modal = _react2['default'].createClass({
displayName: 'Modal',
propTypes: _extends({}, _Portal2['default'].propTypes, ModalMarkup.propTypes),
defaultProps: {
show: null
},
render: function render() {
var _props = this.props;
var show = _props.show;
var props = _objectWithoutProperties(_props, ['show']);
var modal = _react2['default'].createElement(
ModalMarkup,
props,
this.props.children
);
// I can't think of another way to not break back compat while defaulting container
if (!this.props.__isUsedInModalTrigger && show != null) {
return _react2['default'].createElement(
_Portal2['default'],
{ container: props.container },
show && modal
);
} else {
return modal;
}
}
});
Modal.Body = _ModalBody2['default'];
Modal.Header = _ModalHeader2['default'];
Modal.Title = _ModalTitle2['default'];
Modal.Footer = _ModalFooter2['default'];
exports['default'] = Modal;
module.exports = exports['default'];