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 };