Skip to content

Commit 6c7a5f0

Browse files
committed
Merge pull request react-bootstrap#810 from react-bootstrap/modal-improvements
Modal improvements
2 parents 9352546 + a0034e1 commit 6c7a5f0

File tree

3 files changed

+182
-13
lines changed

3 files changed

+182
-13
lines changed

docs/examples/ModalStatic.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const modalInstance = (
22
<div className='static-modal'>
33
<Modal title='Modal title'
4+
enforceFocus={false}
45
backdrop={false}
56
animation={false}
67
container={mountNode}

src/Modal.js

+149-13
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,68 @@ 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+
* Firefox doesn't have a focusin event so using capture is easiest way to get bubbling
36+
* IE8 can't do addEventListener, but does have onfocusin, so we use that in ie8
37+
* @param {ReactElement|HTMLElement} context
38+
* @param {Function} handler
39+
*/
40+
function onFocus(context, handler) {
41+
let doc = domUtils.ownerDocument(context);
42+
let useFocusin = !doc.addEventListener;
43+
let remove;
44+
45+
if (useFocusin) {
46+
document.attachEvent('onfocusin', handler);
47+
remove = () => document.detachEvent('onfocusin', handler);
48+
} else {
49+
document.addEventListener('focus', handler, true);
50+
remove = () => document.removeEventListener('focus', handler, true);
51+
}
52+
return { remove };
53+
}
54+
55+
let scrollbarSize;
56+
57+
if (domUtils.canUseDom) {
58+
let scrollDiv = document.createElement('div');
59+
60+
scrollDiv.style.position = 'absolute';
61+
scrollDiv.style.top = '-9999px';
62+
scrollDiv.style.width = '50px';
63+
scrollDiv.style.height = '50px';
64+
scrollDiv.style.overflow = 'scroll';
65+
66+
document.body.appendChild(scrollDiv);
67+
68+
scrollbarSize = scrollDiv.offsetWidth - scrollDiv.clientWidth;
69+
70+
document.body.removeChild(scrollDiv);
71+
scrollDiv = null;
72+
}
73+
1474
const Modal = React.createClass({
75+
1576
mixins: [BootstrapMixin, FadeMixin],
1677

1778
propTypes: {
@@ -21,7 +82,8 @@ const Modal = React.createClass({
2182
closeButton: React.PropTypes.bool,
2283
animation: React.PropTypes.bool,
2384
onRequestHide: React.PropTypes.func.isRequired,
24-
dialogClassName: React.PropTypes.string
85+
dialogClassName: React.PropTypes.string,
86+
enforceFocus: React.PropTypes.bool
2587
},
2688

2789
getDefaultProps() {
@@ -30,13 +92,20 @@ const Modal = React.createClass({
3092
backdrop: true,
3193
keyboard: true,
3294
animation: true,
33-
closeButton: true
95+
closeButton: true,
96+
enforceFocus: true
3497
};
3598
},
3699

100+
getInitialState(){
101+
return { };
102+
},
103+
37104
render() {
38-
let modalStyle = {display: 'block'};
105+
let state = this.state;
106+
let modalStyle = { ...state.dialogStyles, display: 'block'};
39107
let dialogClasses = this.getBsClassSet();
108+
40109
delete dialogClasses.modal;
41110
dialogClasses['modal-dialog'] = true;
42111

@@ -66,7 +135,7 @@ const Modal = React.createClass({
66135
);
67136

68137
return this.props.backdrop ?
69-
this.renderBackdrop(modal) : modal;
138+
this.renderBackdrop(modal, state.backdropStyles) : modal;
70139
},
71140

72141
renderBackdrop(modal) {
@@ -91,8 +160,8 @@ const Modal = React.createClass({
91160
let closeButton;
92161
if (this.props.closeButton) {
93162
closeButton = (
94-
<button type="button" className="close" aria-hidden="true" onClick={this.props.onRequestHide}>&times;</button>
95-
);
163+
<button type="button" className="close" aria-hidden="true" onClick={this.props.onRequestHide}>&times;</button>
164+
);
96165
}
97166

98167
return (
@@ -119,30 +188,63 @@ const Modal = React.createClass({
119188
},
120189

121190
componentDidMount() {
191+
const doc = domUtils.ownerDocument(this);
192+
const win = domUtils.ownerWindow(this);
193+
122194
this._onDocumentKeyupListener =
123-
EventListener.listen(domUtils.ownerDocument(this), 'keyup', this.handleDocumentKeyUp);
195+
EventListener.listen(doc, 'keyup', this.handleDocumentKeyUp);
196+
197+
this._onWindowResizeListener =
198+
EventListener.listen(win, 'resize', this.handleWindowResize);
199+
200+
if (this.props.enforceFocus) {
201+
this._onFocusinListener = onFocus(this, this.enforceFocus);
202+
}
203+
204+
let container = getContainer(this);
124205

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

129-
this.focusModalContent();
208+
this._containerIsOverflowing = container.scrollHeight > containerClientHeight(container, this);
209+
210+
this._originalPadding = container.style.paddingRight;
211+
212+
if (this._containerIsOverflowing) {
213+
container.style.paddingRight = parseInt(this._originalPadding || 0, 10) + scrollbarSize + 'px';
214+
}
130215

131216
if (this.props.backdrop) {
132217
this.iosClickHack();
133218
}
219+
220+
this.setState(this._getStyles() //eslint-disable-line react/no-did-mount-set-state
221+
, () => this.focusModalContent());
134222
},
135223

136224
componentDidUpdate(prevProps) {
137225
if (this.props.backdrop && this.props.backdrop !== prevProps.backdrop) {
138226
this.iosClickHack();
227+
this.setState(this._getStyles()); //eslint-disable-line react/no-did-update-set-state
228+
}
229+
230+
if (this.props.container !== prevProps.container) {
231+
let container = getContainer(this);
232+
this._containerIsOverflowing = container.scrollHeight > containerClientHeight(container, this);
139233
}
140234
},
141235

142236
componentWillUnmount() {
143237
this._onDocumentKeyupListener.remove();
144-
let container = (this.props.container && React.findDOMNode(this.props.container)) ||
145-
domUtils.ownerDocument(this).body;
238+
this._onWindowResizeListener.remove();
239+
240+
if (this._onFocusinListener) {
241+
this._onFocusinListener.remove();
242+
}
243+
244+
let container = getContainer(this);
245+
246+
container.style.paddingRight = this._originalPadding;
247+
146248
container.className = container.className.replace(/ ?modal-open/, '');
147249

148250
this.restoreLastFocus();
@@ -162,8 +264,12 @@ const Modal = React.createClass({
162264
}
163265
},
164266

267+
handleWindowResize() {
268+
this.setState(this._getStyles());
269+
},
270+
165271
focusModalContent () {
166-
this.lastFocus = domUtils.ownerDocument(this).activeElement;
272+
this.lastFocus = domUtils.activeElement(this);
167273
let modalContent = React.findDOMNode(this.refs.modal);
168274
modalContent.focus();
169275
},
@@ -173,6 +279,36 @@ const Modal = React.createClass({
173279
this.lastFocus.focus();
174280
this.lastFocus = null;
175281
}
282+
},
283+
284+
enforceFocus() {
285+
if ( !this.isMounted() ) {
286+
return;
287+
}
288+
289+
let active = domUtils.activeElement(this);
290+
let modal = React.findDOMNode(this.refs.modal);
291+
292+
if (modal !== active && !domUtils.contains(modal, active)){
293+
modal.focus();
294+
}
295+
},
296+
297+
_getStyles() {
298+
if ( !domUtils.canUseDom ) { return {}; }
299+
300+
let node = React.findDOMNode(this.refs.modal);
301+
let scrollHt = node.scrollHeight;
302+
let container = getContainer(this);
303+
let containerIsOverflowing = this._containerIsOverflowing;
304+
let modalIsOverflowing = scrollHt > containerClientHeight(container, this);
305+
306+
return {
307+
dialogStyles: {
308+
paddingRight: containerIsOverflowing && !modalIsOverflowing ? scrollbarSize : void 0,
309+
paddingLeft: !containerIsOverflowing && modalIsOverflowing ? scrollbarSize : void 0
310+
}
311+
};
176312
}
177313
});
178314

src/utils/domUtils.js

+32
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,27 @@ 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+
29+
/**
30+
* get the active element, safe in IE
31+
* @return {HTMLElement}
32+
*/
33+
function getActiveElement(componentOrElement){
34+
let doc = ownerDocument(componentOrElement);
35+
36+
try {
37+
return doc.activeElement || doc.body;
38+
} catch (e) {
39+
return doc.body;
40+
}
41+
}
42+
1443
/**
1544
* Shortcut to compute element style
1645
*
@@ -138,10 +167,13 @@ function contains(elem, inner){
138167
}
139168

140169
export default {
170+
canUseDom,
141171
contains,
172+
ownerWindow,
142173
ownerDocument,
143174
getComputedStyles,
144175
getOffset,
145176
getPosition,
177+
activeElement: getActiveElement,
146178
offsetParent: offsetParentFunc
147179
};

0 commit comments

Comments
 (0)