diff --git a/karma.conf.js b/karma.conf.js index b53f7d382b..b189ffe816 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -19,6 +19,7 @@ module.exports = function(config) { 'node_modules/angular/angular.js', 'node_modules/angular-mocks/angular-mocks.js', 'node_modules/angular-sanitize/angular-sanitize.js', + 'node_modules/bootstrap/dist/css/bootstrap.css', 'misc/test-lib/helpers.js', 'src/**/*.js', 'template/**/*.js' diff --git a/package.json b/package.json index 7fab6bf604..42e3543c25 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "1.0.0-SNAPSHOT", "homepage": "http://angular-ui.github.io/bootstrap/", "dependencies": {}, - "scripts":{ + "scripts": { "test": "grunt" }, "repository": { @@ -15,6 +15,7 @@ "angular": "^1.4.4", "angular-mocks": "^1.4.4", "angular-sanitize": "^1.4.4", + "bootstrap": "^3.3.5", "grunt": "^0.4.5", "grunt-contrib-concat": "^0.5.1", "grunt-contrib-copy": "^0.8.0", diff --git a/src/modal/modal.js b/src/modal/modal.js index 4bc23c3947..3ac05c301e 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -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; + } + }) + .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; + + 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); + } }); 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){ + + // 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'); + body.css('padding-right', ($$scrollbarHelper.scrollbarWidth + parseInt(modalDomEl.originalBodyRightPadding || '0'))+'px'); + } + } + $modalStack.clearFocusListCache(); }; diff --git a/src/modal/test/modal.spec.js b/src/modal/test/modal.spec.js index efdd30e96a..ac0b7bf2e9 100644 --- a/src/modal/test/modal.spec.js +++ b/src/modal/test/modal.spec.js @@ -1225,4 +1225,56 @@ describe('$uibModal', function () { expect(called).toBeTruthy(); }); }); + + describe('body content when vertical scrollbar present', function() { + it('should not shift document elements - no body right padding specified', function() { + + var largeBlockWithButton = '

 

Block with large height to force vertical scroll

'; + var element = angular.element(largeBlockWithButton); + angular.element(document.body).append(element); + + var clickMeButton = $document.find('div #clickMeButton'); + var buttonLeftBeforeModalOpen = clickMeButton.offset().left; + + var modal = open({template: '
Content
'}); + expect($document).toHaveModalsOpen(1); + var buttonLeftAfterModalOpen = clickMeButton.offset().left; + expect(buttonLeftAfterModalOpen).toBe(buttonLeftBeforeModalOpen); + + dismiss(modal, 'closing in test'); + + expect($document).toHaveModalsOpen(0); + var buttonLeftAfterModalClose = clickMeButton.offset().left; + expect(buttonLeftAfterModalClose).toBe(buttonLeftBeforeModalOpen); + + element.remove(); + }); + + it('should not shift document elements - body has right padding specified', function() { + + // add right body padding + var body = $document.find('body').eq(0); + body.css('padding-right', '50px'); + + var largeBlockWithButton = '

 

Block with large height to force vertical scroll

'; + var element = angular.element(largeBlockWithButton); + angular.element(document.body).append(element); + + var clickMeButton = $document.find('div #clickMeButton'); + var buttonLeftBeforeModalOpen = clickMeButton.offset().left; + + var modal = open({template: '
Content
'}); + expect($document).toHaveModalsOpen(1); + var buttonLeftAfterModalOpen = clickMeButton.offset().left; + expect(buttonLeftAfterModalOpen).toBe(buttonLeftBeforeModalOpen); + + dismiss(modal, 'closing in test'); + + expect($document).toHaveModalsOpen(0); + var buttonLeftAfterModalClose = clickMeButton.offset().left; + expect(buttonLeftAfterModalClose).toBe(buttonLeftBeforeModalOpen); + + element.remove(); + }); + }); });