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

Commit c83d0a8

Browse files
RobJacobsdeeg
authored andcommitted
fix(modal): body content shift
- 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
1 parent a3964d4 commit c83d0a8

File tree

5 files changed

+138
-18
lines changed

5 files changed

+138
-18
lines changed

src/modal/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ require('../stackedMap');
22
require('../../template/modal/backdrop.html.js');
33
require('../../template/modal/window.html.js');
44
require('./modal');
5+
require('../position/position.css');
56

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

src/modal/modal.js

+18-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
1+
angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.position'])
22
/**
33
* A helper, internal data structure that stores all references attached to key
44
*/
@@ -247,8 +247,8 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
247247
})
248248

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

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

266267
function isVisible(element) {
267268
return !!(element.offsetWidth ||
@@ -297,6 +298,14 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
297298
var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS;
298299
openedClasses.remove(modalBodyClass, modalInstance);
299300
appendToElement.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass));
301+
if (scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
302+
if (scrollbarPadding.originalRight) {
303+
appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'});
304+
} else {
305+
appendToElement.css({paddingRight: ''});
306+
}
307+
scrollbarPadding = null;
308+
}
300309
toggleTopWindowClass(true);
301310
}, modalWindow.closedDeferred);
302311
checkRemoveBackdrop();
@@ -472,12 +481,12 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
472481
angularDomEl.attr('modal-animation', 'true');
473482
}
474483

475-
$animate.enter($compile(angularDomEl)(modal.scope), appendToElement)
476-
.then(function() {
477-
if (!modal.scope.$$uibDestructionScheduled) {
478-
$animate.addClass(appendToElement, modalBodyClass);
479-
}
480-
});
484+
scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement);
485+
if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
486+
appendToElement.css({paddingRight: scrollbarPadding.right + 'px'});
487+
}
488+
appendToElement.addClass(modalBodyClass);
489+
$animate.enter($compile(angularDomEl)(modal.scope), appendToElement);
481490

482491
openedWindows.top().value.modalDomEl = angularDomEl;
483492
openedWindows.top().value.modalOpener = modalOpener;

src/position/docs/readme.md

+50-2
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,60 @@ Gets the closest positioned ancestor.
4545
* _(Type: `element`)_ -
4646
The closest positioned ancestor.
4747

48-
#### scrollbarWidth()
48+
#### scrollbarWidth(isBody)
4949

5050
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).
5151

52+
##### parameters
53+
54+
* `isBody`
55+
_(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.
56+
5257
##### returns
5358

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

62+
#### scrollbarPadding(element)
63+
64+
Calculates the padding required to replace the scrollbar on an element.
65+
66+
##### parameters
67+
68+
* 'element' _(Type: `element`)_ - The element to calculate the padding on (should be a scrollable element).
69+
70+
##### returns
71+
72+
An object with the following properties:
73+
74+
* `scrollbarWidth`
75+
_(Type: `number`)_ -
76+
The width of the scrollbar.
77+
78+
* `widthOverflow`
79+
_(Type: `boolean`)_ -
80+
Whether the width is overflowing.
81+
82+
* `right`
83+
_(Type: `number`)_ -
84+
The total right padding required to replace the scrollbar.
85+
86+
* `originalRight`
87+
_(Type: `number`)_ -
88+
The oringal right padding on the element.
89+
90+
* `heightOverflow`
91+
_(Type: `boolean`)_ -
92+
Whether the height is overflowing.
93+
94+
* `bottom`
95+
_(Type: `number`)_ -
96+
The total bottom padding required to replace the scrollbar.
97+
98+
* `originalBottom`
99+
_(Type: `number`)_ -
100+
The oringal bottom padding on the element.
101+
57102
#### isScrollable(element, includeHidden)
58103

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

75-
#### scrollParent(element, includeHidden)
120+
#### scrollParent(element, includeHidden, includeSelf)
76121

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

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

133+
* `includeSelf`
134+
_(Type: `boolean`, Default: `false`, optional)_ - Should the element passed in be included in the scrollable lookup.
135+
88136
##### returns
89137

90138
* _(Type: `element`)_ -

src/position/position.css

+9-5
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77
}
88

99
.uib-position-scrollbar-measure {
10-
position: absolute;
11-
top: -9999px;
12-
width: 50px;
13-
height: 50px;
14-
overflow: scroll;
10+
position: absolute !important;
11+
top: -9999px !important;
12+
width: 50px !important;
13+
height: 50px !important;
14+
overflow: scroll !important;
15+
}
16+
17+
.uib-position-body-scrollbar-measure {
18+
overflow: scroll !important;
1519
}

src/position/position.js

+60-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ angular.module('ui.bootstrap.position', [])
1212
* Do not access this variable directly, use scrollbarWidth() instead.
1313
*/
1414
var SCROLLBAR_WIDTH;
15+
/**
16+
* scrollbar on body and html element in IE and Edge overlay
17+
* content and should be considered 0 width.
18+
*/
19+
var BODY_SCROLLBAR_WIDTH;
1520
var OVERFLOW_REGEX = {
1621
normal: /(auto|scroll)/,
1722
hidden: /(auto|scroll|hidden)/
@@ -22,6 +27,7 @@ angular.module('ui.bootstrap.position', [])
2227
secondary: /^(top|bottom|left|right|center)$/,
2328
vertical: /^(top|bottom)$/
2429
};
30+
var BODY_REGEX = /(HTML|BODY)/;
2531

2632
return {
2733

@@ -75,10 +81,23 @@ angular.module('ui.bootstrap.position', [])
7581
/**
7682
* Provides the scrollbar width, concept from TWBS measureScrollbar()
7783
* function in https://github.com/twbs/bootstrap/blob/master/js/modal.js
84+
* In IE and Edge, scollbar on body and html element overlay and should
85+
* return a width of 0.
7886
*
7987
* @returns {number} The width of the browser scollbar.
8088
*/
81-
scrollbarWidth: function() {
89+
scrollbarWidth: function(isBody) {
90+
if (isBody) {
91+
if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) {
92+
var bodyElem = $document.find('body');
93+
bodyElem.addClass('uib-position-body-scrollbar-measure');
94+
BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth;
95+
BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0;
96+
bodyElem.removeClass('uib-position-body-scrollbar-measure');
97+
}
98+
return BODY_SCROLLBAR_WIDTH;
99+
}
100+
82101
if (angular.isUndefined(SCROLLBAR_WIDTH)) {
83102
var scrollElem = angular.element('<div class="uib-position-scrollbar-measure"></div>');
84103
$document.find('body').append(scrollElem);
@@ -90,6 +109,40 @@ angular.module('ui.bootstrap.position', [])
90109
return SCROLLBAR_WIDTH;
91110
},
92111

112+
/**
113+
* Provides the padding required on an element to replace the scrollbar.
114+
*
115+
* @returns {object} An object with the following properties:
116+
* <ul>
117+
* <li>**scrollbarWidth**: the width of the scrollbar</li>
118+
* <li>**widthOverflow**: whether the the width is overflowing</li>
119+
* <li>**right**: the amount of right padding on the element needed to replace the scrollbar</li>
120+
* <li>**rightOriginal**: the amount of right padding currently on the element</li>
121+
* <li>**heightOverflow**: whether the the height is overflowing</li>
122+
* <li>**bottom**: the amount of bottom padding on the element needed to replace the scrollbar</li>
123+
* <li>**bottomOriginal**: the amount of bottom padding currently on the element</li>
124+
* </ul>
125+
*/
126+
scrollbarPadding: function(elem) {
127+
elem = this.getRawNode(elem);
128+
129+
var elemStyle = $window.getComputedStyle(elem);
130+
var paddingRight = this.parseStyle(elemStyle.paddingRight);
131+
var paddingBottom = this.parseStyle(elemStyle.paddingBottom);
132+
var scrollParent = this.scrollParent(elem, false, true);
133+
var scrollbarWidth = this.scrollbarWidth(scrollParent, BODY_REGEX.test(scrollParent.tagName));
134+
135+
return {
136+
scrollbarWidth: scrollbarWidth,
137+
widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth,
138+
right: paddingRight + scrollbarWidth,
139+
originalRight: paddingRight,
140+
heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight,
141+
bottom: paddingBottom + scrollbarWidth,
142+
originalBottom: paddingBottom
143+
};
144+
},
145+
93146
/**
94147
* Checks to see if the element is scrollable.
95148
*
@@ -115,15 +168,20 @@ angular.module('ui.bootstrap.position', [])
115168
* @param {element} elem - The element to find the scroll parent of.
116169
* @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered,
117170
* default is false.
171+
* @param {boolean=} [includeSelf=false] - Should the element being passed be
172+
* included in the scrollable llokup.
118173
*
119174
* @returns {element} A HTML element.
120175
*/
121-
scrollParent: function(elem, includeHidden) {
176+
scrollParent: function(elem, includeHidden, includeSelf) {
122177
elem = this.getRawNode(elem);
123178

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

0 commit comments

Comments
 (0)