Skip to content

Commit

Permalink
feat(overlay): add a utility for disabling body scroll
Browse files Browse the repository at this point in the history
Adds a `DisableBodyScroll` injectable that can toggle whether the body is scrollable.

Fixes angular#1662.
  • Loading branch information
crisbeto committed Nov 24, 2016
1 parent cf1b4b9 commit 1b66eea
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/lib/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export {Overlay, OVERLAY_PROVIDERS} from './overlay/overlay';
export {OverlayContainer} from './overlay/overlay-container';
export {OverlayRef} from './overlay/overlay-ref';
export {OverlayState} from './overlay/overlay-state';
export {DisableBodyScroll} from './overlay/disable-body-scroll';
export {
ConnectedOverlayDirective,
OverlayOrigin,
Expand Down
86 changes: 86 additions & 0 deletions src/lib/core/overlay/disable-body-scroll.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {DisableBodyScroll} from './disable-body-scroll';


describe('DisableBodyScroll', () => {
let service: DisableBodyScroll;
let forceScrollElement: HTMLElement;

beforeEach(() => {
forceScrollElement = document.createElement('div');
forceScrollElement.style.height = '3000px';
document.body.appendChild(forceScrollElement);
service = new DisableBodyScroll();
});

afterEach(() => {
forceScrollElement.parentNode.removeChild(forceScrollElement);
forceScrollElement = null;
service.deactivate();
});

it('should prevent scrolling', () => {
window.scroll(0, 0);

service.activate();

window.scroll(0, 500);

expect(window.pageYOffset).toBe(0);
});

it('should toggle the isActive property', () => {
service.activate();
expect(service.isActive).toBe(true);

service.deactivate();
expect(service.isActive).toBe(false);
});

it('should not disable scrolling if the content is shorter than the viewport height', () => {
forceScrollElement.style.height = '0';
service.activate();
expect(service.isActive).toBe(false);
});

it('should add the proper inline styles to the <body> and <html> nodes', () => {
let bodyCSS = document.body.style;
let htmlCSS = document.documentElement.style;

window.scroll(0, 500);
service.activate();

expect(bodyCSS.position).toBe('fixed');
expect(bodyCSS.width).toBe('100%');
expect(bodyCSS.top).toBe('-500px');
expect(bodyCSS.maxWidth).toBeTruthy();
expect(htmlCSS.overflowY).toBe('scroll');
});

it('should revert any previously-set inline styles', () => {
let bodyCSS = document.body.style;
let htmlCSS = document.documentElement.style;

bodyCSS.position = 'static';
bodyCSS.width = '1000px';
htmlCSS.overflowY = 'hidden';

service.activate();
service.deactivate();

expect(bodyCSS.position).toBe('static');
expect(bodyCSS.width).toBe('1000px');
expect(htmlCSS.overflowY).toBe('hidden');

bodyCSS.cssText = '';
htmlCSS.cssText = '';
});

it('should restore the scroll position when enabling scrolling', () => {
window.scroll(0, 1000);

service.activate();
service.deactivate();

expect(window.pageYOffset).toBe(1000);
});
});
56 changes: 56 additions & 0 deletions src/lib/core/overlay/disable-body-scroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {Injectable} from '@angular/core';

/**
* Utilitity that allows for toggling scrolling of the viewport on/off.
*/
@Injectable()
export class DisableBodyScroll {
private _bodyStyles: string = '';
private _htmlStyles: string = '';
private _previousScrollPosition: number = 0;
private _isActive: boolean = false;

/** Whether scrolling is disabled. */
public get isActive(): boolean {
return this._isActive;
}

/**
* Disables scrolling if it hasn't been disabled already and if the body is scrollable.
*/
activate(): void {
if (!this.isActive && document.body.scrollHeight > window.innerHeight) {
let body = document.body;
let html = document.documentElement;
let initialBodyWidth = body.clientWidth;

this._htmlStyles = html.style.cssText || '';
this._bodyStyles = body.style.cssText || '';
this._previousScrollPosition = window.scrollY || window.pageYOffset || 0;

body.style.position = 'fixed';
body.style.width = '100%';
body.style.top = -this._previousScrollPosition + 'px';
html.style.overflowY = 'scroll';

// TODO(crisbeto): this avoids issues if the body has a margin, however it prevents the
// body from adapting if the window is resized. check whether it's ok to reset the body
// margin in the core styles.
body.style.maxWidth = initialBodyWidth + 'px';

this._isActive = true;
}
}

/**
* Re-enables scrolling.
*/
deactivate(): void {
if (this.isActive) {
document.body.style.cssText = this._bodyStyles;
document.documentElement.style.cssText = this._htmlStyles;
window.scroll(0, this._previousScrollPosition);
this._isActive = false;
}
}
}
2 changes: 2 additions & 0 deletions src/lib/core/overlay/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {OverlayRef} from './overlay-ref';
import {OverlayPositionBuilder} from './position/overlay-position-builder';
import {ViewportRuler} from './position/viewport-ruler';
import {OverlayContainer} from './overlay-container';
import {DisableBodyScroll} from './disable-body-scroll';

/** Next overlay unique ID. */
let nextUniqueId = 0;
Expand Down Expand Up @@ -93,4 +94,5 @@ export const OVERLAY_PROVIDERS = [
OverlayPositionBuilder,
Overlay,
OverlayContainer,
DisableBodyScroll,
];

0 comments on commit 1b66eea

Please sign in to comment.