Skip to content

Commit 1b66eea

Browse files
committed
feat(overlay): add a utility for disabling body scroll
Adds a `DisableBodyScroll` injectable that can toggle whether the body is scrollable. Fixes #1662.
1 parent cf1b4b9 commit 1b66eea

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed

src/lib/core/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export {Overlay, OVERLAY_PROVIDERS} from './overlay/overlay';
3434
export {OverlayContainer} from './overlay/overlay-container';
3535
export {OverlayRef} from './overlay/overlay-ref';
3636
export {OverlayState} from './overlay/overlay-state';
37+
export {DisableBodyScroll} from './overlay/disable-body-scroll';
3738
export {
3839
ConnectedOverlayDirective,
3940
OverlayOrigin,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {DisableBodyScroll} from './disable-body-scroll';
2+
3+
4+
describe('DisableBodyScroll', () => {
5+
let service: DisableBodyScroll;
6+
let forceScrollElement: HTMLElement;
7+
8+
beforeEach(() => {
9+
forceScrollElement = document.createElement('div');
10+
forceScrollElement.style.height = '3000px';
11+
document.body.appendChild(forceScrollElement);
12+
service = new DisableBodyScroll();
13+
});
14+
15+
afterEach(() => {
16+
forceScrollElement.parentNode.removeChild(forceScrollElement);
17+
forceScrollElement = null;
18+
service.deactivate();
19+
});
20+
21+
it('should prevent scrolling', () => {
22+
window.scroll(0, 0);
23+
24+
service.activate();
25+
26+
window.scroll(0, 500);
27+
28+
expect(window.pageYOffset).toBe(0);
29+
});
30+
31+
it('should toggle the isActive property', () => {
32+
service.activate();
33+
expect(service.isActive).toBe(true);
34+
35+
service.deactivate();
36+
expect(service.isActive).toBe(false);
37+
});
38+
39+
it('should not disable scrolling if the content is shorter than the viewport height', () => {
40+
forceScrollElement.style.height = '0';
41+
service.activate();
42+
expect(service.isActive).toBe(false);
43+
});
44+
45+
it('should add the proper inline styles to the <body> and <html> nodes', () => {
46+
let bodyCSS = document.body.style;
47+
let htmlCSS = document.documentElement.style;
48+
49+
window.scroll(0, 500);
50+
service.activate();
51+
52+
expect(bodyCSS.position).toBe('fixed');
53+
expect(bodyCSS.width).toBe('100%');
54+
expect(bodyCSS.top).toBe('-500px');
55+
expect(bodyCSS.maxWidth).toBeTruthy();
56+
expect(htmlCSS.overflowY).toBe('scroll');
57+
});
58+
59+
it('should revert any previously-set inline styles', () => {
60+
let bodyCSS = document.body.style;
61+
let htmlCSS = document.documentElement.style;
62+
63+
bodyCSS.position = 'static';
64+
bodyCSS.width = '1000px';
65+
htmlCSS.overflowY = 'hidden';
66+
67+
service.activate();
68+
service.deactivate();
69+
70+
expect(bodyCSS.position).toBe('static');
71+
expect(bodyCSS.width).toBe('1000px');
72+
expect(htmlCSS.overflowY).toBe('hidden');
73+
74+
bodyCSS.cssText = '';
75+
htmlCSS.cssText = '';
76+
});
77+
78+
it('should restore the scroll position when enabling scrolling', () => {
79+
window.scroll(0, 1000);
80+
81+
service.activate();
82+
service.deactivate();
83+
84+
expect(window.pageYOffset).toBe(1000);
85+
});
86+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {Injectable} from '@angular/core';
2+
3+
/**
4+
* Utilitity that allows for toggling scrolling of the viewport on/off.
5+
*/
6+
@Injectable()
7+
export class DisableBodyScroll {
8+
private _bodyStyles: string = '';
9+
private _htmlStyles: string = '';
10+
private _previousScrollPosition: number = 0;
11+
private _isActive: boolean = false;
12+
13+
/** Whether scrolling is disabled. */
14+
public get isActive(): boolean {
15+
return this._isActive;
16+
}
17+
18+
/**
19+
* Disables scrolling if it hasn't been disabled already and if the body is scrollable.
20+
*/
21+
activate(): void {
22+
if (!this.isActive && document.body.scrollHeight > window.innerHeight) {
23+
let body = document.body;
24+
let html = document.documentElement;
25+
let initialBodyWidth = body.clientWidth;
26+
27+
this._htmlStyles = html.style.cssText || '';
28+
this._bodyStyles = body.style.cssText || '';
29+
this._previousScrollPosition = window.scrollY || window.pageYOffset || 0;
30+
31+
body.style.position = 'fixed';
32+
body.style.width = '100%';
33+
body.style.top = -this._previousScrollPosition + 'px';
34+
html.style.overflowY = 'scroll';
35+
36+
// TODO(crisbeto): this avoids issues if the body has a margin, however it prevents the
37+
// body from adapting if the window is resized. check whether it's ok to reset the body
38+
// margin in the core styles.
39+
body.style.maxWidth = initialBodyWidth + 'px';
40+
41+
this._isActive = true;
42+
}
43+
}
44+
45+
/**
46+
* Re-enables scrolling.
47+
*/
48+
deactivate(): void {
49+
if (this.isActive) {
50+
document.body.style.cssText = this._bodyStyles;
51+
document.documentElement.style.cssText = this._htmlStyles;
52+
window.scroll(0, this._previousScrollPosition);
53+
this._isActive = false;
54+
}
55+
}
56+
}

src/lib/core/overlay/overlay.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {OverlayRef} from './overlay-ref';
1111
import {OverlayPositionBuilder} from './position/overlay-position-builder';
1212
import {ViewportRuler} from './position/viewport-ruler';
1313
import {OverlayContainer} from './overlay-container';
14+
import {DisableBodyScroll} from './disable-body-scroll';
1415

1516
/** Next overlay unique ID. */
1617
let nextUniqueId = 0;
@@ -93,4 +94,5 @@ export const OVERLAY_PROVIDERS = [
9394
OverlayPositionBuilder,
9495
Overlay,
9596
OverlayContainer,
97+
DisableBodyScroll,
9698
];

0 commit comments

Comments
 (0)