@@ -11,7 +11,68 @@ import EventListener from './utils/EventListener';
11
11
// - Add `modal-body` div if only one child passed in that doesn't already have it
12
12
// - Tests
13
13
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
+
14
74
const Modal = React . createClass ( {
75
+
15
76
mixins : [ BootstrapMixin , FadeMixin ] ,
16
77
17
78
propTypes : {
@@ -21,7 +82,8 @@ const Modal = React.createClass({
21
82
closeButton : React . PropTypes . bool ,
22
83
animation : React . PropTypes . bool ,
23
84
onRequestHide : React . PropTypes . func . isRequired ,
24
- dialogClassName : React . PropTypes . string
85
+ dialogClassName : React . PropTypes . string ,
86
+ enforceFocus : React . PropTypes . bool
25
87
} ,
26
88
27
89
getDefaultProps ( ) {
@@ -30,13 +92,20 @@ const Modal = React.createClass({
30
92
backdrop : true ,
31
93
keyboard : true ,
32
94
animation : true ,
33
- closeButton : true
95
+ closeButton : true ,
96
+ enforceFocus : true
34
97
} ;
35
98
} ,
36
99
100
+ getInitialState ( ) {
101
+ return { } ;
102
+ } ,
103
+
37
104
render ( ) {
38
- let modalStyle = { display : 'block' } ;
105
+ let state = this . state ;
106
+ let modalStyle = { ...state . dialogStyles , display : 'block' } ;
39
107
let dialogClasses = this . getBsClassSet ( ) ;
108
+
40
109
delete dialogClasses . modal ;
41
110
dialogClasses [ 'modal-dialog' ] = true ;
42
111
@@ -66,7 +135,7 @@ const Modal = React.createClass({
66
135
) ;
67
136
68
137
return this . props . backdrop ?
69
- this . renderBackdrop ( modal ) : modal ;
138
+ this . renderBackdrop ( modal , state . backdropStyles ) : modal ;
70
139
} ,
71
140
72
141
renderBackdrop ( modal ) {
@@ -91,8 +160,8 @@ const Modal = React.createClass({
91
160
let closeButton ;
92
161
if ( this . props . closeButton ) {
93
162
closeButton = (
94
- < button type = "button" className = "close" aria-hidden = "true" onClick = { this . props . onRequestHide } > ×</ button >
95
- ) ;
163
+ < button type = "button" className = "close" aria-hidden = "true" onClick = { this . props . onRequestHide } > ×</ button >
164
+ ) ;
96
165
}
97
166
98
167
return (
@@ -119,30 +188,63 @@ const Modal = React.createClass({
119
188
} ,
120
189
121
190
componentDidMount ( ) {
191
+ const doc = domUtils . ownerDocument ( this ) ;
192
+ const win = domUtils . ownerWindow ( this ) ;
193
+
122
194
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 ) ;
124
205
125
- let container = ( this . props . container && React . findDOMNode ( this . props . container ) ) ||
126
- domUtils . ownerDocument ( this ) . body ;
127
206
container . className += container . className . length ? ' modal-open' : 'modal-open' ;
128
207
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
+ }
130
215
131
216
if ( this . props . backdrop ) {
132
217
this . iosClickHack ( ) ;
133
218
}
219
+
220
+ this . setState ( this . _getStyles ( ) //eslint-disable-line react/no-did-mount-set-state
221
+ , ( ) => this . focusModalContent ( ) ) ;
134
222
} ,
135
223
136
224
componentDidUpdate ( prevProps ) {
137
225
if ( this . props . backdrop && this . props . backdrop !== prevProps . backdrop ) {
138
226
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 ) ;
139
233
}
140
234
} ,
141
235
142
236
componentWillUnmount ( ) {
143
237
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
+
146
248
container . className = container . className . replace ( / ? m o d a l - o p e n / , '' ) ;
147
249
148
250
this . restoreLastFocus ( ) ;
@@ -162,8 +264,12 @@ const Modal = React.createClass({
162
264
}
163
265
} ,
164
266
267
+ handleWindowResize ( ) {
268
+ this . setState ( this . _getStyles ( ) ) ;
269
+ } ,
270
+
165
271
focusModalContent ( ) {
166
- this . lastFocus = domUtils . ownerDocument ( this ) . activeElement ;
272
+ this . lastFocus = domUtils . activeElement ( this ) ;
167
273
let modalContent = React . findDOMNode ( this . refs . modal ) ;
168
274
modalContent . focus ( ) ;
169
275
} ,
@@ -173,6 +279,36 @@ const Modal = React.createClass({
173
279
this . lastFocus . focus ( ) ;
174
280
this . lastFocus = null ;
175
281
}
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
+ } ;
176
312
}
177
313
} ) ;
178
314
0 commit comments