Skip to content

Commit e159628

Browse files
authored
fix(Modal): Account for body padding when scrollbar is present (#165)
1 parent 980ca76 commit e159628

3 files changed

Lines changed: 93 additions & 0 deletions

File tree

src/Modal.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import ReactDOM from 'react-dom';
33
import classNames from 'classnames';
44
import TransitionGroup from 'react-addons-transition-group';
55
import Fade from './Fade';
6+
import {
7+
getOriginalBodyPadding,
8+
conditionallyUpdateScrollbar,
9+
setScrollbarWidth
10+
} from './utils';
611

712
const propTypes = {
813
isOpen: PropTypes.bool,
@@ -29,6 +34,8 @@ class Modal extends React.Component {
2934
constructor(props) {
3035
super(props);
3136

37+
this.originalBodyPadding = null;
38+
this.isBodyOverflowing = false;
3239
this.togglePortal = this.togglePortal.bind(this);
3340
this.handleBackdropClick = this.handleBackdropClick.bind(this);
3441
this.handleEscape = this.handleEscape.bind(this);
@@ -105,6 +112,7 @@ class Modal extends React.Component {
105112
}
106113

107114
document.body.className = classNames(classes).trim();
115+
setScrollbarWidth(this.originalBodyPadding);
108116
}
109117

110118
hide() {
@@ -115,6 +123,9 @@ class Modal extends React.Component {
115123
const classes = document.body.className;
116124
this._element = document.createElement('div');
117125
this._element.setAttribute('tabindex', '-1');
126+
this.originalBodyPadding = getOriginalBodyPadding();
127+
128+
conditionallyUpdateScrollbar();
118129

119130
document.body.appendChild(this._element);
120131

src/__tests__/utils.spec.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,42 @@ describe('Utils', () => {
113113
expect(Utils.getTetherAttachments('left bottom')).toEqual(expectedConfig);
114114
});
115115
});
116+
117+
// TODO
118+
// describe('getScrollbarWidth', () => {
119+
// // jsdom workaround https://github.com/tmpvar/jsdom/issues/135#issuecomment-68191941
120+
// Object.defineProperties(window.HTMLElement.prototype, {
121+
// offsetLeft: {
122+
// get: function () { return parseFloat(window.getComputedStyle(this).marginLeft) || 0; }
123+
// },
124+
// offsetTop: {
125+
// get: function () { return parseFloat(window.getComputedStyle(this).marginTop) || 0; }
126+
// },
127+
// offsetHeight: {
128+
// get: function () { return parseFloat(window.getComputedStyle(this).height) || 0; }
129+
// },
130+
// offsetWidth: {
131+
// get: function () { return parseFloat(window.getComputedStyle(this).width) || 0; }
132+
// }
133+
// });
134+
//
135+
// it('should return scrollbarWidth', () => {
136+
// expect(Utils.getScrollbarWidth()).toBe();
137+
// });
138+
// });
139+
140+
// TODO verify setScrollbarWidth is called with values when body overflows
141+
// it('should conditionallyUpdateScrollbar when isBodyOverflowing is true', () => {
142+
// const stubbedSetScrollbarWidth = jasmine.createSpy('Utils', setScrollbarWidth).and.callThrough();
143+
// const prevClientWidth = document.body.clientWidth;
144+
// const prevWindowInnerWidth = window.innerWidth;
145+
// document.body.clientWidth = 100;
146+
// window.innerWidth = 500;
147+
//
148+
// conditionallyUpdateScrollbar();
149+
// expect(stubbedSetScrollbarWidth).toHaveBeenCalled();
150+
//
151+
// document.body.clientWidth = prevClientWidth;
152+
// window.innerWidth = prevWindowInnerWidth;
153+
// });
116154
});

src/utils.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,47 @@ export const tetherAttachements = [
105105
'left middle',
106106
'left bottom'
107107
];
108+
109+
// https://github.com/twbs/bootstrap/blob/v4.0.0-alpha.4/js/src/modal.js#L436-L443
110+
export function getScrollbarWidth() {
111+
let scrollDiv = document.createElement('div');
112+
// .modal-scrollbar-measure styles // https://github.com/twbs/bootstrap/blob/v4.0.0-alpha.4/scss/_modal.scss#L106-L113
113+
scrollDiv.style.position = 'absolute';
114+
scrollDiv.style.top = '-9999px';
115+
scrollDiv.style.width = '50px';
116+
scrollDiv.style.height = '50px';
117+
scrollDiv.style.overflow = 'scroll';
118+
document.body.appendChild(scrollDiv);
119+
const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
120+
document.body.removeChild(scrollDiv);
121+
return scrollbarWidth;
122+
}
123+
124+
export function setScrollbarWidth(padding) {
125+
document.body.style.paddingRight = padding > 0 ? `${padding}px` : null;
126+
}
127+
128+
export function isBodyOverflowing() {
129+
return document.body.clientWidth < window.innerWidth;
130+
}
131+
132+
export function getOriginalBodyPadding() {
133+
return parseInt(
134+
window.getComputedStyle(document.body, null).getPropertyValue('padding-right') || 0,
135+
10
136+
);
137+
}
138+
139+
export function conditionallyUpdateScrollbar() {
140+
const scrollbarWidth = getScrollbarWidth();
141+
// https://github.com/twbs/bootstrap/blob/v4.0.0-alpha.4/js/src/modal.js#L420
142+
const fixedContent = document.querySelectorAll('.navbar-fixed-top, .navbar-fixed-bottom, .is-fixed')[0];
143+
const bodyPadding = fixedContent ? parseInt(
144+
fixedContent.style.paddingRight || 0,
145+
10
146+
) : 0;
147+
148+
if (isBodyOverflowing()) {
149+
setScrollbarWidth(bodyPadding + scrollbarWidth);
150+
}
151+
}

0 commit comments

Comments
 (0)