@@ -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+
1474const 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 } > ×</ button >
95- ) ;
163+ < button type = "button" className = "close" aria-hidden = "true" onClick = { this . props . onRequestHide } > ×</ 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 ( / ? m o d a l - o p e n / , '' ) ;
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
0 commit comments