diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts index d75aaafdcc81..c01ba5cf45dd 100644 --- a/src/lib/core/core.ts +++ b/src/lib/core/core.ts @@ -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, diff --git a/src/lib/core/overlay/disable-body-scroll.spec.ts b/src/lib/core/overlay/disable-body-scroll.spec.ts new file mode 100644 index 000000000000..aeb9cde43cd9 --- /dev/null +++ b/src/lib/core/overlay/disable-body-scroll.spec.ts @@ -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 and 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); + }); +}); diff --git a/src/lib/core/overlay/disable-body-scroll.ts b/src/lib/core/overlay/disable-body-scroll.ts new file mode 100644 index 000000000000..8f93655b1c97 --- /dev/null +++ b/src/lib/core/overlay/disable-body-scroll.ts @@ -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; + } + } +} diff --git a/src/lib/core/overlay/overlay.ts b/src/lib/core/overlay/overlay.ts index 22a4d43e546e..bb0114b6b1bb 100644 --- a/src/lib/core/overlay/overlay.ts +++ b/src/lib/core/overlay/overlay.ts @@ -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; @@ -93,4 +94,5 @@ export const OVERLAY_PROVIDERS = [ OverlayPositionBuilder, Overlay, OverlayContainer, + DisableBodyScroll, ];