From 3869ca2d3a020916963d552fae6006129b5f5a27 Mon Sep 17 00:00:00 2001 From: jquense Date: Wed, 10 Jun 2015 14:12:13 -0400 Subject: [PATCH] [fixed] Modal doesn't "jump" when container is overflowing Correctly pads the modal to account for the container having a scroll bar --- src/Modal.js | 95 +++++++++++++++++++++++++++++++++++++++---- src/utils/domUtils.js | 17 ++++++++ 2 files changed, 104 insertions(+), 8 deletions(-) diff --git a/src/Modal.js b/src/Modal.js index a91cbe962f..13e426ef74 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -11,7 +11,46 @@ import EventListener from './utils/EventListener'; // - Add `modal-body` div if only one child passed in that doesn't already have it // - Tests +/** + * 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) { + let doc = domUtils.ownerDocument(context); + + return (container === doc.body || container === doc.documentElement) + ? doc.documentElement.clientHeight + : container.clientHeight; +} + +function getContainer(context){ + return (context.props.container && React.findDOMNode(context.props.container)) || + domUtils.ownerDocument(context).body; +} + + +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: { @@ -35,8 +74,10 @@ const Modal = React.createClass({ }, 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; @@ -119,30 +160,47 @@ 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); + + 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); 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(); + + let container = getContainer(this); + container.className = container.className.replace(/ ?modal-open/, ''); this.restoreLastFocus(); @@ -162,8 +220,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 +235,23 @@ const Modal = React.createClass({ this.lastFocus.focus(); this.lastFocus = null; } + }, + + _getStyles() { + if ( !domUtils.canUseDom ) { return {}; } + + let node = React.findDOMNode(this.refs.modal) + , scrollHt = node.scrollHeight + , container = getContainer(this) + , containerIsOverflowing = this._containerIsOverflowing + , 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..ae56eb210d 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,13 @@ function ownerDocument(componentOrElement) { return (elem && elem.ownerDocument) || document; } +function ownerWindow(componentOrElement) { + let doc = ownerDocument(componentOrElement); + return doc.defaultView + ? doc.defaultView + : doc.parentWindow; +} + /** * Shortcut to compute element style * @@ -138,7 +153,9 @@ function contains(elem, inner){ } export default { + canUseDom, contains, + ownerWindow, ownerDocument, getComputedStyles, getOffset,