diff --git a/src/lib/core/overlay/position/connected-position-strategy.spec.ts b/src/lib/core/overlay/position/connected-position-strategy.spec.ts index c50217cd982e..add993fb18d3 100644 --- a/src/lib/core/overlay/position/connected-position-strategy.spec.ts +++ b/src/lib/core/overlay/position/connected-position-strategy.spec.ts @@ -1,11 +1,13 @@ import {ElementRef} from '@angular/core'; import {ConnectedPositionStrategy} from './connected-position-strategy'; -import {ViewportRuler} from './viewport-ruler'; +import {ViewportRuler, VIEWPORT_RULER_PROVIDER} from './viewport-ruler'; import {OverlayPositionBuilder} from './overlay-position-builder'; import {ConnectedOverlayPositionChange} from './connected-position'; import {Scrollable} from '../scroll/scrollable'; import {Subscription} from 'rxjs'; +import {TestBed, inject} from '@angular/core/testing'; import Spy = jasmine.Spy; +import {SCROLL_DISPATCHER_PROVIDER} from '../scroll/scroll-dispatcher'; // Default width and height of the overlay and origin panels throughout these tests. @@ -18,6 +20,16 @@ const DEFAULT_WIDTH = 60; describe('ConnectedPositionStrategy', () => { + let viewportRuler: ViewportRuler; + + beforeEach(() => TestBed.configureTestingModule({ + providers: [VIEWPORT_RULER_PROVIDER, SCROLL_DISPATCHER_PROVIDER] + })); + + beforeEach(inject([ViewportRuler], (_ruler: ViewportRuler) => { + viewportRuler = _ruler; + })); + describe('with origin on document body', () => { const ORIGIN_HEIGHT = DEFAULT_HEIGHT; const ORIGIN_WIDTH = DEFAULT_WIDTH; @@ -48,7 +60,7 @@ describe('ConnectedPositionStrategy', () => { overlayContainerElement.appendChild(overlayElement); fakeElementRef = new FakeElementRef(originElement); - positionBuilder = new OverlayPositionBuilder(new ViewportRuler()); + positionBuilder = new OverlayPositionBuilder(viewportRuler); }); afterEach(() => { @@ -457,7 +469,7 @@ describe('ConnectedPositionStrategy', () => { scrollable.appendChild(originElement); // Create a strategy with knowledge of the scrollable container - let positionBuilder = new OverlayPositionBuilder(new ViewportRuler()); + let positionBuilder = new OverlayPositionBuilder(viewportRuler); let fakeElementRef = new FakeElementRef(originElement); strategy = positionBuilder.connectedTo( fakeElementRef, diff --git a/src/lib/core/overlay/position/viewport-ruler.spec.ts b/src/lib/core/overlay/position/viewport-ruler.spec.ts index 36d2874efe13..999b9db1f8cb 100644 --- a/src/lib/core/overlay/position/viewport-ruler.spec.ts +++ b/src/lib/core/overlay/position/viewport-ruler.spec.ts @@ -1,5 +1,6 @@ -import {ViewportRuler} from './viewport-ruler'; - +import {ViewportRuler, VIEWPORT_RULER_PROVIDER} from './viewport-ruler'; +import {TestBed, inject} from '@angular/core/testing'; +import {SCROLL_DISPATCHER_PROVIDER} from '../scroll/scroll-dispatcher'; // For all tests, we assume the browser window is 1024x786 (outerWidth x outerHeight). // The karma config has been set to this for local tests, and it is the default size @@ -20,10 +21,14 @@ describe('ViewportRuler', () => { veryLargeElement.style.width = '6000px'; veryLargeElement.style.height = '6000px'; - beforeEach(() => { - ruler = new ViewportRuler(); + beforeEach(() => TestBed.configureTestingModule({ + providers: [VIEWPORT_RULER_PROVIDER, SCROLL_DISPATCHER_PROVIDER] + })); + + beforeEach(inject([ViewportRuler], (viewportRuler: ViewportRuler) => { + ruler = viewportRuler; scrollTo(0, 0); - }); + })); it('should get the viewport bounds when the page is not scrolled', () => { let bounds = ruler.getViewportRect(); @@ -35,7 +40,10 @@ describe('ViewportRuler', () => { it('should get the viewport bounds when the page is scrolled', () => { document.body.appendChild(veryLargeElement); + scrollTo(1500, 2000); + // Force an update of the cached viewport geometries because IE11 emits the scroll event later. + ruler._cacheViewportGeometry(); let bounds = ruler.getViewportRect(); @@ -63,14 +71,17 @@ describe('ViewportRuler', () => { }); it('should get the scroll position when the page is not scrolled', () => { - var scrollPos = ruler.getViewportScrollPosition(); + let scrollPos = ruler.getViewportScrollPosition(); expect(scrollPos.top).toBe(0); expect(scrollPos.left).toBe(0); }); it('should get the scroll position when the page is scrolled', () => { document.body.appendChild(veryLargeElement); + scrollTo(1500, 2000); + // Force an update of the cached viewport geometries because IE11 emits the scroll event later. + ruler._cacheViewportGeometry(); // In the iOS simulator (BrowserStack & SauceLabs), adding the content to the // body causes karma's iframe for the test to stretch to fit that content once we attempt to @@ -82,7 +93,7 @@ describe('ViewportRuler', () => { return; } - var scrollPos = ruler.getViewportScrollPosition(); + let scrollPos = ruler.getViewportScrollPosition(); expect(scrollPos.top).toBe(2000); expect(scrollPos.left).toBe(1500); diff --git a/src/lib/core/overlay/position/viewport-ruler.ts b/src/lib/core/overlay/position/viewport-ruler.ts index 91c21a18a541..d39271f75411 100644 --- a/src/lib/core/overlay/position/viewport-ruler.ts +++ b/src/lib/core/overlay/position/viewport-ruler.ts @@ -1,4 +1,5 @@ import {Injectable, Optional, SkipSelf} from '@angular/core'; +import {ScrollDispatcher} from '../scroll/scroll-dispatcher'; /** @@ -7,12 +8,20 @@ import {Injectable, Optional, SkipSelf} from '@angular/core'; */ @Injectable() export class ViewportRuler { - // TODO(jelbourn): cache the document's bounding rect and only update it when the window - // is resized (debounced). + /** Cached document client rectangle. */ + private _documentRect?: ClientRect; + + constructor(scrollDispatcher: ScrollDispatcher) { + // Initially cache the document rectangle. + this._cacheViewportGeometry(); + + // Subscribe to scroll and resize events and update the document rectangle on changes. + scrollDispatcher.scrolled().subscribe(() => this._cacheViewportGeometry()); + } /** Gets a ClientRect for the viewport's bounds. */ - getViewportRect(): ClientRect { + getViewportRect(documentRect = this._documentRect): ClientRect { // Use the document element's bounding rect rather than the window scroll properties // (e.g. pageYOffset, scrollY) due to in issue in Chrome and IE where window scroll // properties and client coordinates (boundingClientRect, clientX/Y, etc.) are in different @@ -22,7 +31,6 @@ export class ViewportRuler { // We use the documentElement instead of the body because, by default (without a css reset) // browsers typically give the document body an 8px margin, which is not included in // getBoundingClientRect(). - const documentRect = document.documentElement.getBoundingClientRect(); const scrollPosition = this.getViewportScrollPosition(documentRect); const height = window.innerHeight; const width = window.innerWidth; @@ -42,7 +50,7 @@ export class ViewportRuler { * Gets the (top, left) scroll position of the viewport. * @param documentRect */ - getViewportScrollPosition(documentRect = document.documentElement.getBoundingClientRect()) { + getViewportScrollPosition(documentRect = this._documentRect) { // The top-left-corner of the viewport is determined by the scroll position of the document // body, normally just (scrollLeft, scrollTop). However, Chrome and Firefox disagree about // whether `document.body` or `document.documentElement` is the scrolled element, so reading @@ -54,15 +62,22 @@ export class ViewportRuler { return {top, left}; } + + /** Caches the latest client rectangle of the document element. */ + _cacheViewportGeometry?() { + this._documentRect = document.documentElement.getBoundingClientRect(); + } + } -export function VIEWPORT_RULER_PROVIDER_FACTORY(parentDispatcher: ViewportRuler) { - return parentDispatcher || new ViewportRuler(); -}; +export function VIEWPORT_RULER_PROVIDER_FACTORY(parentRuler: ViewportRuler, + scrollDispatcher: ScrollDispatcher) { + return parentRuler || new ViewportRuler(scrollDispatcher); +} export const VIEWPORT_RULER_PROVIDER = { // If there is already a ViewportRuler available, use that. Otherwise, provide a new one. provide: ViewportRuler, - deps: [[new Optional(), new SkipSelf(), ViewportRuler]], + deps: [[new Optional(), new SkipSelf(), ViewportRuler], ScrollDispatcher], useFactory: VIEWPORT_RULER_PROVIDER_FACTORY }; diff --git a/src/lib/core/ripple/ripple.spec.ts b/src/lib/core/ripple/ripple.spec.ts index 66e52d24c481..449e6845e0f5 100644 --- a/src/lib/core/ripple/ripple.spec.ts +++ b/src/lib/core/ripple/ripple.spec.ts @@ -1,6 +1,7 @@ -import {TestBed, ComponentFixture, fakeAsync, tick} from '@angular/core/testing'; +import {TestBed, ComponentFixture, fakeAsync, tick, inject} from '@angular/core/testing'; import {Component, ViewChild} from '@angular/core'; import {MdRipple, MdRippleModule} from './ripple'; +import {ViewportRuler} from '../overlay/position/viewport-ruler'; /** Creates a DOM event to indicate that a CSS transition for the given property ended. */ @@ -60,6 +61,7 @@ describe('MdRipple', () => { let rippleElement: HTMLElement; let rippleBackground: Element; let originalBodyMargin: string; + let viewportRuler: ViewportRuler; beforeEach(() => { TestBed.configureTestingModule({ @@ -72,11 +74,13 @@ describe('MdRipple', () => { }); }); - beforeEach(() => { + beforeEach(inject([ViewportRuler], (ruler: ViewportRuler) => { + viewportRuler = ruler; + // Set body margin to 0 during tests so it doesn't mess up position calculations. originalBodyMargin = document.body.style.margin; document.body.style.margin = '0'; - }); + })); afterEach(() => { document.body.style.margin = originalBodyMargin; @@ -228,6 +232,9 @@ describe('MdRipple', () => { document.documentElement.scrollTop = pageScrollTop; // Mobile safari window.scrollTo(pageScrollLeft, pageScrollTop); + // Force an update of the cached viewport geometries because IE11 emits the + // scroll event later. + viewportRuler._cacheViewportGeometry(); }); afterEach(() => { @@ -239,6 +246,9 @@ describe('MdRipple', () => { document.documentElement.scrollTop = 0; // Mobile safari window.scrollTo(0, 0); + // Force an update of the cached viewport geometries because IE11 emits the + // scroll event later. + viewportRuler._cacheViewportGeometry(); }); it('create ripple with correct position', () => { diff --git a/src/lib/core/ripple/ripple.ts b/src/lib/core/ripple/ripple.ts index 6b1780364ae2..d456bc4ca7bd 100644 --- a/src/lib/core/ripple/ripple.ts +++ b/src/lib/core/ripple/ripple.ts @@ -18,6 +18,7 @@ import { } from './ripple-renderer'; import {DefaultStyleCompatibilityModeModule} from '../compatibility/default-mode'; import {ViewportRuler, VIEWPORT_RULER_PROVIDER} from '../overlay/position/viewport-ruler'; +import {SCROLL_DISPATCHER_PROVIDER} from '../overlay/scroll/scroll-dispatcher'; @Directive({ @@ -238,7 +239,7 @@ export class MdRipple implements OnInit, OnDestroy, OnChanges { imports: [DefaultStyleCompatibilityModeModule], exports: [MdRipple, DefaultStyleCompatibilityModeModule], declarations: [MdRipple], - providers: [VIEWPORT_RULER_PROVIDER], + providers: [VIEWPORT_RULER_PROVIDER, SCROLL_DISPATCHER_PROVIDER], }) export class MdRippleModule { /** @deprecated */ diff --git a/src/lib/tabs/tab-group.ts b/src/lib/tabs/tab-group.ts index 2e07efbe3e84..20a92a4b31f7 100644 --- a/src/lib/tabs/tab-group.ts +++ b/src/lib/tabs/tab-group.ts @@ -28,6 +28,7 @@ import {MdTab} from './tab'; import {MdTabBody} from './tab-body'; import {VIEWPORT_RULER_PROVIDER} from '../core/overlay/position/viewport-ruler'; import {MdTabHeader} from './tab-header'; +import {SCROLL_DISPATCHER_PROVIDER} from '../core/overlay/scroll/scroll-dispatcher'; /** Used to generate unique ID's for each tab component */ @@ -204,7 +205,7 @@ export class MdTabGroup { exports: [MdTabGroup, MdTabLabel, MdTab, MdTabNavBar, MdTabLink, MdTabLinkRipple], declarations: [MdTabGroup, MdTabLabel, MdTab, MdInkBar, MdTabLabelWrapper, MdTabNavBar, MdTabLink, MdTabBody, MdTabLinkRipple, MdTabHeader], - providers: [VIEWPORT_RULER_PROVIDER], + providers: [VIEWPORT_RULER_PROVIDER, SCROLL_DISPATCHER_PROVIDER], }) export class MdTabsModule { /** @deprecated */