Skip to content
This repository has been archived by the owner on May 29, 2019. It is now read-only.

Commit

Permalink
fix(modal): body content shift
Browse files Browse the repository at this point in the history
- Implements TWBS body padding fix to keep content in an element
with a container class from shifting when the body overflow is
set to hidden with the modal-open class.

Fixes #2631
Closes #5711
  • Loading branch information
RobJacobs authored and deeg committed Mar 31, 2016
1 parent a3964d4 commit c83d0a8
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 18 deletions.
1 change: 1 addition & 0 deletions src/modal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ require('../stackedMap');
require('../../template/modal/backdrop.html.js');
require('../../template/modal/window.html.js');
require('./modal');
require('../position/position.css');

var MODULE_NAME = 'ui.bootstrap.module.modal';

Expand Down
27 changes: 18 additions & 9 deletions src/modal/modal.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.position'])
/**
* A helper, internal data structure that stores all references attached to key
*/
Expand Down Expand Up @@ -247,8 +247,8 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
})

.factory('$uibModalStack', ['$animate', '$animateCss', '$document',
'$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap',
function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap) {
'$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition',
function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap, $uibPosition) {
var OPENED_MODAL_CLASS = 'modal-open';

var backdropDomEl, backdropScope;
Expand All @@ -262,6 +262,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
var tabableSelector = 'a[href], area[href], input:not([disabled]), ' +
'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' +
'iframe, object, embed, *[tabindex], *[contenteditable=true]';
var scrollbarPadding;

function isVisible(element) {
return !!(element.offsetWidth ||
Expand Down Expand Up @@ -297,6 +298,14 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS;
openedClasses.remove(modalBodyClass, modalInstance);
appendToElement.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass));
if (scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
if (scrollbarPadding.originalRight) {
appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'});
} else {
appendToElement.css({paddingRight: ''});
}
scrollbarPadding = null;
}
toggleTopWindowClass(true);
}, modalWindow.closedDeferred);
checkRemoveBackdrop();
Expand Down Expand Up @@ -472,12 +481,12 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
angularDomEl.attr('modal-animation', 'true');
}

$animate.enter($compile(angularDomEl)(modal.scope), appendToElement)
.then(function() {
if (!modal.scope.$$uibDestructionScheduled) {
$animate.addClass(appendToElement, modalBodyClass);
}
});
scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement);
if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
appendToElement.css({paddingRight: scrollbarPadding.right + 'px'});
}
appendToElement.addClass(modalBodyClass);
$animate.enter($compile(angularDomEl)(modal.scope), appendToElement);

openedWindows.top().value.modalDomEl = angularDomEl;
openedWindows.top().value.modalOpener = modalOpener;
Expand Down
52 changes: 50 additions & 2 deletions src/position/docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,60 @@ Gets the closest positioned ancestor.
* _(Type: `element`)_ -
The closest positioned ancestor.

#### scrollbarWidth()
#### scrollbarWidth(isBody)

Calculates the browser scrollbar width and caches the result for future calls. Concept from the TWBS measureScrollbar() function in [modal.js](https://github.com/twbs/bootstrap/blob/master/js/modal.js).

##### parameters

* `isBody`
_(Type: `boolean`, Default: `false`, optional)_ - Is the requested scrollbar width for the body/html element. IE and Edge overlay the scrollbar on the body/html element and should be considered 0.

##### returns

* _(Type: `number`)_ -
The width of the browser scrollbar.

#### scrollbarPadding(element)

Calculates the padding required to replace the scrollbar on an element.

##### parameters

* 'element' _(Type: `element`)_ - The element to calculate the padding on (should be a scrollable element).

##### returns

An object with the following properties:

* `scrollbarWidth`
_(Type: `number`)_ -
The width of the scrollbar.

* `widthOverflow`
_(Type: `boolean`)_ -
Whether the width is overflowing.

* `right`
_(Type: `number`)_ -
The total right padding required to replace the scrollbar.

* `originalRight`
_(Type: `number`)_ -
The oringal right padding on the element.

* `heightOverflow`
_(Type: `boolean`)_ -
Whether the height is overflowing.

* `bottom`
_(Type: `number`)_ -
The total bottom padding required to replace the scrollbar.

* `originalBottom`
_(Type: `number`)_ -
The oringal bottom padding on the element.

#### isScrollable(element, includeHidden)

Determines if an element is scrollable.
Expand All @@ -72,7 +117,7 @@ Determines if an element is scrollable.
* _(Type: `boolean`)_ -
Whether the element is scrollable.

#### scrollParent(element, includeHidden)
#### scrollParent(element, includeHidden, includeSelf)

Gets the closest scrollable ancestor. Concept from the jQueryUI [scrollParent.js](https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js).

Expand All @@ -85,6 +130,9 @@ Gets the closest scrollable ancestor. Concept from the jQueryUI [scrollParent.j
* `includeHidden`
_(Type: `boolean`, Default: `false`, optional)_ - Should scroll style of 'hidden' be considered.

* `includeSelf`
_(Type: `boolean`, Default: `false`, optional)_ - Should the element passed in be included in the scrollable lookup.

##### returns

* _(Type: `element`)_ -
Expand Down
14 changes: 9 additions & 5 deletions src/position/position.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@
}

.uib-position-scrollbar-measure {
position: absolute;
top: -9999px;
width: 50px;
height: 50px;
overflow: scroll;
position: absolute !important;
top: -9999px !important;
width: 50px !important;
height: 50px !important;
overflow: scroll !important;
}

.uib-position-body-scrollbar-measure {
overflow: scroll !important;
}
62 changes: 60 additions & 2 deletions src/position/position.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ angular.module('ui.bootstrap.position', [])
* Do not access this variable directly, use scrollbarWidth() instead.
*/
var SCROLLBAR_WIDTH;
/**
* scrollbar on body and html element in IE and Edge overlay
* content and should be considered 0 width.
*/
var BODY_SCROLLBAR_WIDTH;
var OVERFLOW_REGEX = {
normal: /(auto|scroll)/,
hidden: /(auto|scroll|hidden)/
Expand All @@ -22,6 +27,7 @@ angular.module('ui.bootstrap.position', [])
secondary: /^(top|bottom|left|right|center)$/,
vertical: /^(top|bottom)$/
};
var BODY_REGEX = /(HTML|BODY)/;

return {

Expand Down Expand Up @@ -75,10 +81,23 @@ angular.module('ui.bootstrap.position', [])
/**
* Provides the scrollbar width, concept from TWBS measureScrollbar()
* function in https://github.com/twbs/bootstrap/blob/master/js/modal.js
* In IE and Edge, scollbar on body and html element overlay and should
* return a width of 0.
*
* @returns {number} The width of the browser scollbar.
*/
scrollbarWidth: function() {
scrollbarWidth: function(isBody) {
if (isBody) {
if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) {
var bodyElem = $document.find('body');
bodyElem.addClass('uib-position-body-scrollbar-measure');
BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth;
BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0;
bodyElem.removeClass('uib-position-body-scrollbar-measure');
}
return BODY_SCROLLBAR_WIDTH;
}

if (angular.isUndefined(SCROLLBAR_WIDTH)) {
var scrollElem = angular.element('<div class="uib-position-scrollbar-measure"></div>');
$document.find('body').append(scrollElem);
Expand All @@ -90,6 +109,40 @@ angular.module('ui.bootstrap.position', [])
return SCROLLBAR_WIDTH;
},

/**
* Provides the padding required on an element to replace the scrollbar.
*
* @returns {object} An object with the following properties:
* <ul>
* <li>**scrollbarWidth**: the width of the scrollbar</li>
* <li>**widthOverflow**: whether the the width is overflowing</li>
* <li>**right**: the amount of right padding on the element needed to replace the scrollbar</li>
* <li>**rightOriginal**: the amount of right padding currently on the element</li>
* <li>**heightOverflow**: whether the the height is overflowing</li>
* <li>**bottom**: the amount of bottom padding on the element needed to replace the scrollbar</li>
* <li>**bottomOriginal**: the amount of bottom padding currently on the element</li>
* </ul>
*/
scrollbarPadding: function(elem) {
elem = this.getRawNode(elem);

var elemStyle = $window.getComputedStyle(elem);
var paddingRight = this.parseStyle(elemStyle.paddingRight);
var paddingBottom = this.parseStyle(elemStyle.paddingBottom);
var scrollParent = this.scrollParent(elem, false, true);
var scrollbarWidth = this.scrollbarWidth(scrollParent, BODY_REGEX.test(scrollParent.tagName));

return {
scrollbarWidth: scrollbarWidth,
widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth,
right: paddingRight + scrollbarWidth,
originalRight: paddingRight,
heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight,
bottom: paddingBottom + scrollbarWidth,
originalBottom: paddingBottom
};
},

/**
* Checks to see if the element is scrollable.
*
Expand All @@ -115,15 +168,20 @@ angular.module('ui.bootstrap.position', [])
* @param {element} elem - The element to find the scroll parent of.
* @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered,
* default is false.
* @param {boolean=} [includeSelf=false] - Should the element being passed be
* included in the scrollable llokup.
*
* @returns {element} A HTML element.
*/
scrollParent: function(elem, includeHidden) {
scrollParent: function(elem, includeHidden, includeSelf) {
elem = this.getRawNode(elem);

var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal;
var documentEl = $document[0].documentElement;
var elemStyle = $window.getComputedStyle(elem);
if (includeSelf && overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX)) {
return elem;
}
var excludeStatic = elemStyle.position === 'absolute';
var scrollParent = elem.parentElement || documentEl;

Expand Down

0 comments on commit c83d0a8

Please sign in to comment.