Skip to content

Commit d8daf3a

Browse files
committed
fix(modal): prevent body content shifting when vertical scrollbar
- If body has vertical scrollbar and appendTo element is 'body', add right padding to body equivalent to scrollbar width. Also, conditionally add right or left padding to modal window Closes angular-ui#3714
1 parent d859f42 commit d8daf3a

File tree

4 files changed

+135
-4
lines changed

4 files changed

+135
-4
lines changed

karma.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module.exports = function(config) {
1919
'node_modules/angular/angular.js',
2020
'node_modules/angular-mocks/angular-mocks.js',
2121
'node_modules/angular-sanitize/angular-sanitize.js',
22+
'node_modules/bootstrap/dist/css/bootstrap.css',
2223
'misc/test-lib/helpers.js',
2324
'src/**/*.js',
2425
'template/**/*.js'

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"version": "1.0.0-SNAPSHOT",
55
"homepage": "http://angular-ui.github.io/bootstrap/",
66
"dependencies": {},
7-
"scripts":{
7+
"scripts": {
88
"test": "grunt"
99
},
1010
"repository": {
@@ -15,6 +15,7 @@
1515
"angular": "^1.4.4",
1616
"angular-mocks": "^1.4.4",
1717
"angular-sanitize": "^1.4.4",
18+
"bootstrap": "^3.3.5",
1819
"grunt": "^0.4.5",
1920
"grunt-contrib-concat": "^0.5.1",
2021
"grunt-contrib-copy": "^0.8.0",

src/modal/modal.js

+80-3
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,31 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
106106
}
107107
}])
108108

109+
/**
110+
* A helper, for measuring and maintaining scrollbar properties - used by modalWindow and $modalStack
111+
*/
112+
.factory('$$scrollbarHelper', function () {
113+
var scrollbarWidth;
114+
115+
return {
116+
measureScrollbar: measureScrollbar,
117+
scrollbarWidth: scrollbarWidth
118+
};
119+
120+
// from modal.js of bootstrap
121+
function measureScrollbar () { // thx walsh
122+
var scrollDiv = document.createElement('div');
123+
scrollDiv.className = 'modal-scrollbar-measure';
124+
document.body.appendChild(scrollDiv);
125+
var width = scrollDiv.offsetWidth - scrollDiv.clientWidth;
126+
document.body.removeChild(scrollDiv);
127+
this.scrollbarWidth = width;
128+
}
129+
})
130+
109131
.directive('uibModalWindow', [
110-
'$uibModalStack', '$q', '$animate', '$injector',
111-
function($modalStack , $q , $animate, $injector) {
132+
'$uibModalStack', '$q', '$animate', '$injector', '$$scrollbarHelper',
133+
function($modalStack , $q , $animate, $injector, $$scrollbarHelper) {
112134
var $animateCss = null;
113135

114136
if ($injector.has('$animateCss')) {
@@ -129,6 +151,33 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
129151
element.addClass(attrs.windowTopClass || '');
130152
scope.size = attrs.size;
131153

154+
// get topmost modal object
155+
var bodyIsOverflowing;
156+
var modal = $modalStack.getTop();
157+
158+
// only when modal is attached to body
159+
if(modal && modal.value && modal.value.appendTo === 'body'){
160+
161+
// check bodyOverflowing property that was set when opening modal
162+
if(modal.value.modalDomEl && modal.value.modalDomEl.bodyOverflowing){
163+
bodyIsOverflowing = true;
164+
}
165+
166+
// start - from adjustDialog method of modal.js of bootstrap
167+
// check if modal is overflowing
168+
var modalIsOverflowing = element[0].scrollHeight > document.documentElement.clientHeight;
169+
170+
if(!$$scrollbarHelper.scrollbarWidth){
171+
$$scrollbarHelper.measureScrollbar();
172+
}
173+
174+
element.css({
175+
'padding-left': (!bodyIsOverflowing && modalIsOverflowing ? $$scrollbarHelper.scrollbarWidth : '') + 'px',
176+
'padding-right': (bodyIsOverflowing && !modalIsOverflowing ? $$scrollbarHelper.scrollbarWidth : '') + 'px'
177+
});
178+
// end - from adjustDialog method of modal.js of bootstrap
179+
}
180+
132181
scope.close = function(evt) {
133182
var modal = $modalStack.getTop();
134183
if (modal && modal.value.backdrop && modal.value.backdrop !== 'static' && (evt.target === evt.currentTarget)) {
@@ -235,11 +284,13 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
235284
'$injector',
236285
'$$multiMap',
237286
'$$stackedMap',
287+
'$$scrollbarHelper',
238288
function($animate , $timeout , $document , $compile , $rootScope ,
239289
$q,
240290
$injector,
241291
$$multiMap,
242-
$$stackedMap) {
292+
$$stackedMap,
293+
$$scrollbarHelper) {
243294
var $animateCss = null;
244295

245296
if ($injector.has('$animateCss')) {
@@ -291,6 +342,13 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
291342
openedClasses.remove(modalBodyClass, modalInstance);
292343
appendToElement.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass));
293344
toggleTopWindowClass(true);
345+
346+
// reset any right padding set to body when opening the modal
347+
// make sure no other modal is open before resetting
348+
if(modalWindow.appendTo === 'body' && openedWindows.length() === 0){
349+
var body = $document.find('body').eq(0);
350+
body.css('padding-right', modalWindow.modalDomEl.originalBodyRightPadding);
351+
}
294352
});
295353
checkRemoveBackdrop();
296354

@@ -463,6 +521,25 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
463521
appendToElement.append(modalDomEl);
464522
appendToElement.addClass(modalBodyClass);
465523

524+
// if modal is attached to body, then check if body is overflowing and set bodyOverflowing property on modal object and set right padding equal to scrollbarWidth
525+
if (modal.appendTo === 'body' && document.body.scrollHeight > document.documentElement.clientHeight){
526+
527+
// set bodyOverflowing property
528+
modalDomEl.bodyOverflowing = true;
529+
530+
// set right padding to body
531+
if(openedWindows.length() === 1){
532+
if(!$$scrollbarHelper.scrollbarWidth){
533+
$$scrollbarHelper.measureScrollbar();
534+
}
535+
536+
var body = $document.find('body').eq(0);
537+
// set originalBodyRightPadding property to reset it when the last modal closes
538+
modalDomEl.originalBodyRightPadding = body.css('padding-right');
539+
body.css('padding-right', ($$scrollbarHelper.scrollbarWidth + parseInt(modalDomEl.originalBodyRightPadding || '0'))+'px');
540+
}
541+
}
542+
466543
$modalStack.clearFocusListCache();
467544
};
468545

src/modal/test/modal.spec.js

+52
Original file line numberDiff line numberDiff line change
@@ -1225,4 +1225,56 @@ describe('$uibModal', function () {
12251225
expect(called).toBeTruthy();
12261226
});
12271227
});
1228+
1229+
describe('body content when vertical scrollbar present', function() {
1230+
it('should not shift document elements - no body right padding specified', function() {
1231+
1232+
var largeBlockWithButton = '<div class="container"><button class="btn btn-default" id="clickMeButton" ng-click="open()">Open me!</button><div style="height: 3000px;"><p>&nbsp;</p><p>Block with large height to force vertical scroll</p></div></div></div>';
1233+
var element = angular.element(largeBlockWithButton);
1234+
angular.element(document.body).append(element);
1235+
1236+
var clickMeButton = $document.find('div #clickMeButton');
1237+
var buttonLeftBeforeModalOpen = clickMeButton.offset().left;
1238+
1239+
var modal = open({template: '<div>Content</div>'});
1240+
expect($document).toHaveModalsOpen(1);
1241+
var buttonLeftAfterModalOpen = clickMeButton.offset().left;
1242+
expect(buttonLeftAfterModalOpen).toBe(buttonLeftBeforeModalOpen);
1243+
1244+
dismiss(modal, 'closing in test');
1245+
1246+
expect($document).toHaveModalsOpen(0);
1247+
var buttonLeftAfterModalClose = clickMeButton.offset().left;
1248+
expect(buttonLeftAfterModalClose).toBe(buttonLeftBeforeModalOpen);
1249+
1250+
element.remove();
1251+
});
1252+
1253+
it('should not shift document elements - body has right padding specified', function() {
1254+
1255+
// add right body padding
1256+
var body = $document.find('body').eq(0);
1257+
body.css('padding-right', '50px');
1258+
1259+
var largeBlockWithButton = '<div class="container"><button class="btn btn-default" id="clickMeButton" ng-click="open()">Open me!</button><div style="height: 3000px;"><p>&nbsp;</p><p>Block with large height to force vertical scroll</p></div></div></div>';
1260+
var element = angular.element(largeBlockWithButton);
1261+
angular.element(document.body).append(element);
1262+
1263+
var clickMeButton = $document.find('div #clickMeButton');
1264+
var buttonLeftBeforeModalOpen = clickMeButton.offset().left;
1265+
1266+
var modal = open({template: '<div>Content</div>'});
1267+
expect($document).toHaveModalsOpen(1);
1268+
var buttonLeftAfterModalOpen = clickMeButton.offset().left;
1269+
expect(buttonLeftAfterModalOpen).toBe(buttonLeftBeforeModalOpen);
1270+
1271+
dismiss(modal, 'closing in test');
1272+
1273+
expect($document).toHaveModalsOpen(0);
1274+
var buttonLeftAfterModalClose = clickMeButton.offset().left;
1275+
expect(buttonLeftAfterModalClose).toBe(buttonLeftBeforeModalOpen);
1276+
1277+
element.remove();
1278+
});
1279+
});
12281280
});

0 commit comments

Comments
 (0)