-
Notifications
You must be signed in to change notification settings - Fork 6.7k
fix(modal): prevent body content shifting when vertical scrollbar #4782
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -106,9 +106,31 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap']) | |
} | ||
}]) | ||
|
||
/** | ||
* A helper, for measuring and maintaining scrollbar properties - used by modalWindow and $modalStack | ||
*/ | ||
.factory('$$scrollbarHelper', function () { | ||
var scrollbarWidth; | ||
|
||
return { | ||
measureScrollbar: measureScrollbar, | ||
scrollbarWidth: scrollbarWidth | ||
}; | ||
|
||
// from modal.js of bootstrap | ||
function measureScrollbar () { // thx walsh | ||
var scrollDiv = document.createElement('div'); | ||
scrollDiv.className = 'modal-scrollbar-measure'; | ||
document.body.appendChild(scrollDiv); | ||
var width = scrollDiv.offsetWidth - scrollDiv.clientWidth; | ||
document.body.removeChild(scrollDiv); | ||
this.scrollbarWidth = width; | ||
} | ||
}) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to see this in the $position service as scrollbarWidth(), we might need to use it for auto/smart positioning. Would be nice to cache the result to avoid manipulating the DOM every time we need this value as it should not change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree - we have a related issue in #4317. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. $position service now has a scrollbarWidth function. |
||
.directive('uibModalWindow', [ | ||
'$uibModalStack', '$q', '$animate', '$injector', | ||
function($modalStack , $q , $animate, $injector) { | ||
'$uibModalStack', '$q', '$animate', '$injector', '$$scrollbarHelper', | ||
function($modalStack , $q , $animate, $injector, $$scrollbarHelper) { | ||
var $animateCss = null; | ||
|
||
if ($injector.has('$animateCss')) { | ||
|
@@ -202,6 +224,34 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap']) | |
var modal = $modalStack.getTop(); | ||
if (modal) { | ||
$modalStack.modalRendered(modal.key); | ||
|
||
// get topmost modal object | ||
var bodyIsOverflowing; | ||
var modalIsOverflowing; | ||
|
||
// only when modal is attached to body | ||
if(modal.value.appendTo === 'body'){ | ||
|
||
// check bodyOverflowing property that was set when opening modal | ||
if(modal.value.modalDomEl.bodyOverflowing){ | ||
bodyIsOverflowing = true; | ||
} | ||
|
||
// start - from adjustDialog method of modal.js of bootstrap | ||
// check if modal is overflowing | ||
modalIsOverflowing = element[0].scrollHeight > document.documentElement.clientHeight; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should use |
||
|
||
if(!$$scrollbarHelper.scrollbarWidth){ | ||
$$scrollbarHelper.measureScrollbar(); | ||
} | ||
|
||
if(modalIsOverflowing && !bodyIsOverflowing){ | ||
element.css('padding-left', $$scrollbarHelper.scrollbarWidth + 'px'); | ||
}else if(bodyIsOverflowing && !modalIsOverflowing){ | ||
element.css('padding-right', $$scrollbarHelper.scrollbarWidth + 'px'); | ||
} | ||
// end - from adjustDialog method of modal.js of bootstrap | ||
} | ||
} | ||
}); | ||
} | ||
|
@@ -235,11 +285,13 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap']) | |
'$injector', | ||
'$$multiMap', | ||
'$$stackedMap', | ||
'$$scrollbarHelper', | ||
function($animate , $timeout , $document , $compile , $rootScope , | ||
$q, | ||
$injector, | ||
$$multiMap, | ||
$$stackedMap) { | ||
$$stackedMap, | ||
$$scrollbarHelper) { | ||
var $animateCss = null; | ||
|
||
if ($injector.has('$animateCss')) { | ||
|
@@ -291,6 +343,13 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap']) | |
openedClasses.remove(modalBodyClass, modalInstance); | ||
appendToElement.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass)); | ||
toggleTopWindowClass(true); | ||
|
||
// reset any right padding set to body when opening the modal | ||
// make sure no other modal is open before resetting | ||
if(modalWindow.appendTo === 'body' && openedWindows.length() === 0){ | ||
var body = $document.find('body'); | ||
body.css('padding-right', modalWindow.modalDomEl.originalBodyRightPadding); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems way too opinionated - this will make it near impossible for a user to override this, and is not a good solution. |
||
} | ||
}); | ||
checkRemoveBackdrop(); | ||
|
||
|
@@ -463,6 +522,25 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap']) | |
appendToElement.append(modalDomEl); | ||
appendToElement.addClass(modalBodyClass); | ||
|
||
// 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 | ||
if (modal.appendTo === 'body' && document.body.scrollHeight > document.documentElement.clientHeight){ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto |
||
|
||
// set bodyOverflowing property | ||
modalDomEl.bodyOverflowing = true; | ||
|
||
// set right padding to body | ||
if(openedWindows.length() === 1){ | ||
if(!$$scrollbarHelper.scrollbarWidth){ | ||
$$scrollbarHelper.measureScrollbar(); | ||
} | ||
|
||
var body = $document.find('body'); | ||
// set originalBodyRightPadding property to reset it when the last modal closes | ||
modalDomEl.originalBodyRightPadding = body.css('padding-right'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is an unnecessarily verbose key name - please choose a better name, such as |
||
body.css('padding-right', ($$scrollbarHelper.scrollbarWidth + parseInt(modalDomEl.originalBodyRightPadding || '0'))+'px'); | ||
} | ||
} | ||
|
||
$modalStack.clearFocusListCache(); | ||
}; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this affect the tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Without bootstrap.css, the tests always pass. The tests rely on the modal related classes in bootstrap css which remove the scroll bar from the body when the modal is opened.