Skip to content

Commit 3869ca2

Browse files
committed
[fixed] Modal doesn't "jump" when container is overflowing
Correctly pads the modal to account for the container having a scroll bar
1 parent fd31cbc commit 3869ca2

File tree

2 files changed

+104
-8
lines changed

2 files changed

+104
-8
lines changed

src/Modal.js

+87-8
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,46 @@ import EventListener from './utils/EventListener';
1111
// - Add `modal-body` div if only one child passed in that doesn't already have it
1212
// - Tests
1313

14+
/**
15+
* Gets the correct clientHeight of the modal container
16+
* when the body/window/document you need to use the docElement clientHeight
17+
* @param {HTMLElement} container
18+
* @param {ReactElement|HTMLElement} context
19+
* @return {Number}
20+
*/
21+
function containerClientHeight(container, context) {
22+
let doc = domUtils.ownerDocument(context);
23+
24+
return (container === doc.body || container === doc.documentElement)
25+
? doc.documentElement.clientHeight
26+
: container.clientHeight;
27+
}
28+
29+
function getContainer(context){
30+
return (context.props.container && React.findDOMNode(context.props.container)) ||
31+
domUtils.ownerDocument(context).body;
32+
}
33+
34+
35+
if ( domUtils.canUseDom) {
36+
let scrollDiv = document.createElement('div');
37+
38+
scrollDiv.style.position = 'absolute';
39+
scrollDiv.style.top = '-9999px';
40+
scrollDiv.style.width = '50px';
41+
scrollDiv.style.height = '50px';
42+
scrollDiv.style.overflow = 'scroll';
43+
44+
document.body.appendChild(scrollDiv);
45+
46+
scrollbarSize = scrollDiv.offsetWidth - scrollDiv.clientWidth;
47+
48+
document.body.removeChild(scrollDiv);
49+
scrollDiv = null;
50+
}
51+
1452
const Modal = React.createClass({
53+
1554
mixins: [BootstrapMixin, FadeMixin],
1655

1756
propTypes: {
@@ -35,8 +74,10 @@ const Modal = React.createClass({
3574
},
3675

3776
render() {
38-
let modalStyle = {display: 'block'};
77+
let state = this.state;
78+
let modalStyle = { ...state.dialogStyles, display: 'block'};
3979
let dialogClasses = this.getBsClassSet();
80+
4081
delete dialogClasses.modal;
4182
dialogClasses['modal-dialog'] = true;
4283

@@ -119,30 +160,47 @@ const Modal = React.createClass({
119160
},
120161

121162
componentDidMount() {
163+
const doc = domUtils.ownerDocument(this);
164+
const win = domUtils.ownerWindow(this);
165+
122166
this._onDocumentKeyupListener =
123-
EventListener.listen(domUtils.ownerDocument(this), 'keyup', this.handleDocumentKeyUp);
167+
EventListener.listen(doc, 'keyup', this.handleDocumentKeyUp);
168+
169+
this._onWindowResizeListener =
170+
EventListener.listen(win, 'resize', this.handleWindowResize);
171+
172+
let container = getContainer(this);
124173

125-
let container = (this.props.container && React.findDOMNode(this.props.container)) ||
126-
domUtils.ownerDocument(this).body;
127174
container.className += container.className.length ? ' modal-open' : 'modal-open';
128175

129-
this.focusModalContent();
176+
this._containerIsOverflowing = container.scrollHeight > containerClientHeight(container, this);
130177

131178
if (this.props.backdrop) {
132179
this.iosClickHack();
133180
}
181+
182+
this.setState(this._getStyles() //eslint-disable-line react/no-did-mount-set-state
183+
, () => this.focusModalContent());
134184
},
135185

136186
componentDidUpdate(prevProps) {
137187
if (this.props.backdrop && this.props.backdrop !== prevProps.backdrop) {
138188
this.iosClickHack();
189+
this.setState(this._getStyles()); //eslint-disable-line react/no-did-update-set-state
190+
}
191+
192+
if (this.props.container !== prevProps.container) {
193+
let container = getContainer(this);
194+
this._containerIsOverflowing = container.scrollHeight > containerClientHeight(container, this);
139195
}
140196
},
141197

142198
componentWillUnmount() {
143199
this._onDocumentKeyupListener.remove();
144-
let container = (this.props.container && React.findDOMNode(this.props.container)) ||
145-
domUtils.ownerDocument(this).body;
200+
this._onWindowResizeListener.remove();
201+
202+
let container = getContainer(this);
203+
146204
container.className = container.className.replace(/ ?modal-open/, '');
147205

148206
this.restoreLastFocus();
@@ -162,8 +220,12 @@ const Modal = React.createClass({
162220
}
163221
},
164222

223+
handleWindowResize() {
224+
this.setState(this._getStyles());
225+
},
226+
165227
focusModalContent () {
166-
this.lastFocus = domUtils.ownerDocument(this).activeElement;
228+
this.lastFocus = domUtils.activeElement(this);
167229
let modalContent = React.findDOMNode(this.refs.modal);
168230
modalContent.focus();
169231
},
@@ -173,6 +235,23 @@ const Modal = React.createClass({
173235
this.lastFocus.focus();
174236
this.lastFocus = null;
175237
}
238+
},
239+
240+
_getStyles() {
241+
if ( !domUtils.canUseDom ) { return {}; }
242+
243+
let node = React.findDOMNode(this.refs.modal)
244+
, scrollHt = node.scrollHeight
245+
, container = getContainer(this)
246+
, containerIsOverflowing = this._containerIsOverflowing
247+
, modalIsOverflowing = scrollHt > containerClientHeight(container, this);
248+
249+
return {
250+
dialogStyles: {
251+
paddingRight: containerIsOverflowing && !modalIsOverflowing ? scrollbarSize : void 0,
252+
paddingLeft: !containerIsOverflowing && modalIsOverflowing ? scrollbarSize : void 0
253+
}
254+
};
176255
}
177256
});
178257

src/utils/domUtils.js

+17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import React from 'react';
22

3+
4+
let canUseDom = !!(
5+
typeof window !== 'undefined' &&
6+
window.document &&
7+
window.document.createElement
8+
);
9+
10+
311
/**
412
* Get elements owner document
513
*
@@ -11,6 +19,13 @@ function ownerDocument(componentOrElement) {
1119
return (elem && elem.ownerDocument) || document;
1220
}
1321

22+
function ownerWindow(componentOrElement) {
23+
let doc = ownerDocument(componentOrElement);
24+
return doc.defaultView
25+
? doc.defaultView
26+
: doc.parentWindow;
27+
}
28+
1429
/**
1530
* Shortcut to compute element style
1631
*
@@ -138,7 +153,9 @@ function contains(elem, inner){
138153
}
139154

140155
export default {
156+
canUseDom,
141157
contains,
158+
ownerWindow,
142159
ownerDocument,
143160
getComputedStyles,
144161
getOffset,

0 commit comments

Comments
 (0)