From 3869ca2d3a020916963d552fae6006129b5f5a27 Mon Sep 17 00:00:00 2001 From: jquense Date: Wed, 10 Jun 2015 14:12:13 -0400 Subject: [PATCH 1/4] [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, From 66f0f921ddcbc943f72b97187df28a76fb4b738d Mon Sep 17 00:00:00 2001 From: jquense Date: Wed, 10 Jun 2015 14:18:55 -0400 Subject: [PATCH 2/4] [added] enforceFocus prop to Modal Allows you to configure whether the modal should enforce focus when open. Ideally this doesn't need to be a public API, but in the case of static models (like in the docs) you need to turn it off, because you can't assume its the only one open on the page. --- docs/examples/ModalStatic.js | 1 + package.json | 2 +- src/Modal.js | 59 +++++++++++++++++++++++++++++++++--- src/utils/domUtils.js | 15 +++++++++ 4 files changed, 71 insertions(+), 6 deletions(-) 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'); @@ -60,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() { @@ -69,10 +92,15 @@ const Modal = React.createClass({ backdrop: true, keyboard: true, animation: true, - closeButton: true + closeButton: true, + enforceFocus: true }; }, + getInitialState(){ + return { }; + }, + render() { let state = this.state; let modalStyle = { ...state.dialogStyles, display: 'block'}; @@ -107,7 +135,7 @@ const Modal = React.createClass({ ); return this.props.backdrop ? - this.renderBackdrop(modal) : modal; + this.renderBackdrop(modal, state.backdropStyles) : modal; }, renderBackdrop(modal) { @@ -132,8 +160,8 @@ const Modal = React.createClass({ let closeButton; if (this.props.closeButton) { closeButton = ( - - ); + + ); } return ( @@ -169,6 +197,10 @@ const Modal = React.createClass({ this._onWindowResizeListener = EventListener.listen(win, 'resize', this.handleWindowResize); + if (this.props.enforceFocus) { + this._onFocusinListener = onFocus(this, this.enforceFocus); + } + let container = getContainer(this); container.className += container.className.length ? ' modal-open' : 'modal-open'; @@ -199,6 +231,10 @@ const Modal = React.createClass({ this._onDocumentKeyupListener.remove(); this._onWindowResizeListener.remove(); + if (this._onFocusinListener) { + this._onFocusinListener.remove(); + } + let container = getContainer(this); container.className = container.className.replace(/ ?modal-open/, ''); @@ -237,6 +273,19 @@ const Modal = React.createClass({ } }, + enforceFocus() { + if ( !this.isMounted() ) { + return; + } + + let active = domUtils.activeElement(this) + , modal = React.findDOMNode(this.refs.modal); + + if (modal !== active && !domUtils.contains(modal, active)){ + modal.focus(); + } + }, + _getStyles() { if ( !domUtils.canUseDom ) { return {}; } diff --git a/src/utils/domUtils.js b/src/utils/domUtils.js index ae56eb210d..31fa7c0340 100644 --- a/src/utils/domUtils.js +++ b/src/utils/domUtils.js @@ -26,6 +26,20 @@ function ownerWindow(componentOrElement) { : 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 * @@ -160,5 +174,6 @@ export default { getComputedStyles, getOffset, getPosition, + activeElement: getActiveElement, offsetParent: offsetParentFunc }; From d18dadbf9e37063655e1ec3f957bb4cb97a2b02e Mon Sep 17 00:00:00 2001 From: jquense Date: Wed, 10 Jun 2015 15:01:11 -0400 Subject: [PATCH 3/4] [fixed] container content no longer shifts when overflowing Incorporates upstream tbs logic to pad the container before hiding the scrollbar with the `.modal-open` class fixes #354 --- src/Modal.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Modal.js b/src/Modal.js index 2f5ade2f84..dcc2d0fb97 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -207,6 +207,12 @@ const Modal = React.createClass({ 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(); } @@ -237,6 +243,8 @@ const Modal = React.createClass({ let container = getContainer(this); + container.style.paddingRight = this._originalPadding; + container.className = container.className.replace(/ ?modal-open/, ''); this.restoreLastFocus(); From a0034e1cb7aac0b9904ab89a7d0cf1d060136e4f Mon Sep 17 00:00:00 2001 From: jquense Date: Sat, 13 Jun 2015 19:02:21 -0400 Subject: [PATCH 4/4] Remove single variable style declarations --- src/Modal.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Modal.js b/src/Modal.js index dcc2d0fb97..a6a5b20df1 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -33,14 +33,14 @@ function getContainer(context){ /** * Firefox doesn't have a focusin event so using capture is easiest way to get bubbling - * IE8 can't do addEventListener, but does have onfocus in, so we use that in ie8 + * IE8 can't do addEventListener, but does have onfocusin, so we use that in ie8 * @param {ReactElement|HTMLElement} context * @param {Function} handler */ function onFocus(context, handler) { let doc = domUtils.ownerDocument(context); - let useFocusin = !doc.addEventListener - , remove; + let useFocusin = !doc.addEventListener; + let remove; if (useFocusin) { document.attachEvent('onfocusin', handler); @@ -54,7 +54,7 @@ function onFocus(context, handler) { let scrollbarSize; -if ( domUtils.canUseDom) { +if (domUtils.canUseDom) { let scrollDiv = document.createElement('div'); scrollDiv.style.position = 'absolute'; @@ -286,8 +286,8 @@ const Modal = React.createClass({ return; } - let active = domUtils.activeElement(this) - , modal = React.findDOMNode(this.refs.modal); + let active = domUtils.activeElement(this); + let modal = React.findDOMNode(this.refs.modal); if (modal !== active && !domUtils.contains(modal, active)){ modal.focus(); @@ -297,11 +297,11 @@ const Modal = React.createClass({ _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); + 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: {