diff --git a/docs/examples/ModalStatic.js b/docs/examples/ModalStatic.js
index 545efbee73..f01a4d058e 100644
--- a/docs/examples/ModalStatic.js
+++ b/docs/examples/ModalStatic.js
@@ -1,6 +1,7 @@
const modalInstance = (
document.detachEvent('onfocusin', handler);
+ } else {
+ document.addEventListener('focus', handler, true);
+ remove = () => document.removeEventListener('focus', handler, true);
+ }
+ return { remove };
+}
+
+let scrollbarSize;
+
+if (domUtils.canUseDom) {
+ let 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;
+}
+
const Modal = React.createClass({
+
mixins: [BootstrapMixin, FadeMixin],
propTypes: {
@@ -21,7 +82,8 @@ const Modal = React.createClass({
closeButton: React.PropTypes.bool,
animation: React.PropTypes.bool,
onRequestHide: React.PropTypes.func.isRequired,
- dialogClassName: React.PropTypes.string
+ dialogClassName: React.PropTypes.string,
+ enforceFocus: React.PropTypes.bool
},
getDefaultProps() {
@@ -30,13 +92,20 @@ const Modal = React.createClass({
backdrop: true,
keyboard: true,
animation: true,
- closeButton: true
+ closeButton: true,
+ enforceFocus: true
};
},
+ getInitialState(){
+ return { };
+ },
+
render() {
- let modalStyle = {display: 'block'};
+ let state = this.state;
+ let modalStyle = { ...state.dialogStyles, display: 'block'};
let dialogClasses = this.getBsClassSet();
+
delete dialogClasses.modal;
dialogClasses['modal-dialog'] = true;
@@ -66,7 +135,7 @@ const Modal = React.createClass({
);
return this.props.backdrop ?
- this.renderBackdrop(modal) : modal;
+ this.renderBackdrop(modal, state.backdropStyles) : modal;
},
renderBackdrop(modal) {
@@ -91,8 +160,8 @@ const Modal = React.createClass({
let closeButton;
if (this.props.closeButton) {
closeButton = (
-
- );
+
+ );
}
return (
@@ -119,30 +188,63 @@ const Modal = React.createClass({
},
componentDidMount() {
+ const doc = domUtils.ownerDocument(this);
+ const win = domUtils.ownerWindow(this);
+
this._onDocumentKeyupListener =
- EventListener.listen(domUtils.ownerDocument(this), 'keyup', this.handleDocumentKeyUp);
+ EventListener.listen(doc, 'keyup', this.handleDocumentKeyUp);
+
+ this._onWindowResizeListener =
+ EventListener.listen(win, 'resize', this.handleWindowResize);
+
+ if (this.props.enforceFocus) {
+ this._onFocusinListener = onFocus(this, this.enforceFocus);
+ }
+
+ let container = getContainer(this);
- let container = (this.props.container && React.findDOMNode(this.props.container)) ||
- domUtils.ownerDocument(this).body;
container.className += container.className.length ? ' modal-open' : 'modal-open';
- this.focusModalContent();
+ this._containerIsOverflowing = container.scrollHeight > containerClientHeight(container, this);
+
+ this._originalPadding = container.style.paddingRight;
+
+ if (this._containerIsOverflowing) {
+ container.style.paddingRight = parseInt(this._originalPadding || 0, 10) + scrollbarSize + 'px';
+ }
if (this.props.backdrop) {
this.iosClickHack();
}
+
+ this.setState(this._getStyles() //eslint-disable-line react/no-did-mount-set-state
+ , () => this.focusModalContent());
},
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) {
+ let container = getContainer(this);
+ this._containerIsOverflowing = container.scrollHeight > containerClientHeight(container, this);
}
},
componentWillUnmount() {
this._onDocumentKeyupListener.remove();
- let container = (this.props.container && React.findDOMNode(this.props.container)) ||
- domUtils.ownerDocument(this).body;
+ this._onWindowResizeListener.remove();
+
+ if (this._onFocusinListener) {
+ this._onFocusinListener.remove();
+ }
+
+ let container = getContainer(this);
+
+ container.style.paddingRight = this._originalPadding;
+
container.className = container.className.replace(/ ?modal-open/, '');
this.restoreLastFocus();
@@ -162,8 +264,12 @@ const Modal = React.createClass({
}
},
+ handleWindowResize() {
+ this.setState(this._getStyles());
+ },
+
focusModalContent () {
- this.lastFocus = domUtils.ownerDocument(this).activeElement;
+ this.lastFocus = domUtils.activeElement(this);
let modalContent = React.findDOMNode(this.refs.modal);
modalContent.focus();
},
@@ -173,6 +279,36 @@ const Modal = React.createClass({
this.lastFocus.focus();
this.lastFocus = null;
}
+ },
+
+ enforceFocus() {
+ if ( !this.isMounted() ) {
+ return;
+ }
+
+ let active = domUtils.activeElement(this);
+ let modal = React.findDOMNode(this.refs.modal);
+
+ if (modal !== active && !domUtils.contains(modal, active)){
+ modal.focus();
+ }
+ },
+
+ _getStyles() {
+ if ( !domUtils.canUseDom ) { return {}; }
+
+ let node = React.findDOMNode(this.refs.modal);
+ let scrollHt = node.scrollHeight;
+ let container = getContainer(this);
+ let containerIsOverflowing = this._containerIsOverflowing;
+ let modalIsOverflowing = scrollHt > containerClientHeight(container, this);
+
+ return {
+ dialogStyles: {
+ paddingRight: containerIsOverflowing && !modalIsOverflowing ? scrollbarSize : void 0,
+ paddingLeft: !containerIsOverflowing && modalIsOverflowing ? scrollbarSize : void 0
+ }
+ };
}
});
diff --git a/src/utils/domUtils.js b/src/utils/domUtils.js
index cbac69af5d..31fa7c0340 100644
--- a/src/utils/domUtils.js
+++ b/src/utils/domUtils.js
@@ -1,5 +1,13 @@
import React from 'react';
+
+let canUseDom = !!(
+ typeof window !== 'undefined' &&
+ window.document &&
+ window.document.createElement
+);
+
+
/**
* Get elements owner document
*
@@ -11,6 +19,27 @@ function ownerDocument(componentOrElement) {
return (elem && elem.ownerDocument) || document;
}
+function ownerWindow(componentOrElement) {
+ let doc = ownerDocument(componentOrElement);
+ return doc.defaultView
+ ? doc.defaultView
+ : doc.parentWindow;
+}
+
+/**
+ * get the active element, safe in IE
+ * @return {HTMLElement}
+ */
+function getActiveElement(componentOrElement){
+ let doc = ownerDocument(componentOrElement);
+
+ try {
+ return doc.activeElement || doc.body;
+ } catch (e) {
+ return doc.body;
+ }
+}
+
/**
* Shortcut to compute element style
*
@@ -138,10 +167,13 @@ function contains(elem, inner){
}
export default {
+ canUseDom,
contains,
+ ownerWindow,
ownerDocument,
getComputedStyles,
getOffset,
getPosition,
+ activeElement: getActiveElement,
offsetParent: offsetParentFunc
};