From b13aff85f96dc8d5376c519af5199a02fe35f508 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 24 May 2017 19:07:45 +0200 Subject: [PATCH] feat(overlay): more flexible scroll strategy API and ability to define/override custom strategies * Refactors the overlay setup to allow for scroll strategies to be passed in by name, instead of by instance. * Handles the scroll strategy dependency injection automatically. * Adds an API for registering custom scroll strategies and overriding the existing ones. * Adds a second parameter to the `attach` method, allowing for a config object to be passed in. * Throws an error if there's an attempt to attach a scroll strategy multiple times. This is mostly a sanity check to ensure that we don't cache the scroll strategy instances. Relates to #4093. --- src/lib/autocomplete/autocomplete-trigger.ts | 4 +- src/lib/core/overlay/overlay-directives.ts | 6 +- src/lib/core/overlay/overlay-ref.ts | 14 ++-- src/lib/core/overlay/overlay-state.ts | 3 +- src/lib/core/overlay/overlay.spec.ts | 46 ++++++------- src/lib/core/overlay/overlay.ts | 54 ++++++++++++++- .../scroll/block-scroll-strategy.spec.ts | 66 ++++++++++++++----- .../overlay/scroll/block-scroll-strategy.ts | 2 + .../scroll/close-scroll-strategy.spec.ts | 6 +- .../overlay/scroll/close-scroll-strategy.ts | 8 ++- .../overlay/scroll/noop-scroll-strategy.ts | 2 + .../scroll/reposition-scroll-strategy.spec.ts | 6 +- .../scroll/reposition-scroll-strategy.ts | 24 +++++-- .../overlay/scroll/scroll-dispatcher.spec.ts | 2 +- .../core/overlay/scroll/scroll-strategy.md | 24 +++++-- .../core/overlay/scroll/scroll-strategy.ts | 9 ++- src/lib/datepicker/datepicker.ts | 4 +- src/lib/dialog/dialog.ts | 5 +- src/lib/menu/menu-trigger.ts | 7 +- src/lib/tooltip/tooltip.ts | 8 ++- 20 files changed, 211 insertions(+), 89 deletions(-) diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 5dd5c1f8fce5..d1164bc898f9 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -22,7 +22,6 @@ import {MdOptionSelectionChange, MdOption} from '../core/option/option'; import {ENTER, UP_ARROW, DOWN_ARROW, ESCAPE} from '../core/keyboard/keycodes'; import {Dir} from '../core/rtl/dir'; import {MdInputContainer} from '../input/input-container'; -import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher'; import {Subscription} from 'rxjs/Subscription'; import 'rxjs/add/observable/merge'; import 'rxjs/add/observable/fromEvent'; @@ -104,7 +103,6 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { constructor(private _element: ElementRef, private _overlay: Overlay, private _viewContainerRef: ViewContainerRef, private _changeDetectorRef: ChangeDetectorRef, - private _scrollDispatcher: ScrollDispatcher, @Optional() private _dir: Dir, private _zone: NgZone, @Optional() @Host() private _inputContainer: MdInputContainer, @Optional() @Inject(DOCUMENT) private _document: any) {} @@ -368,7 +366,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { overlayState.positionStrategy = this._getOverlayPosition(); overlayState.width = this._getHostWidth(); overlayState.direction = this._dir ? this._dir.value : 'ltr'; - overlayState.scrollStrategy = new RepositionScrollStrategy(this._scrollDispatcher); + overlayState.scrollStrategy = 'reposition'; return overlayState; } diff --git a/src/lib/core/overlay/overlay-directives.ts b/src/lib/core/overlay/overlay-directives.ts index 06d2194d6117..e09658cbd12d 100644 --- a/src/lib/core/overlay/overlay-directives.ts +++ b/src/lib/core/overlay/overlay-directives.ts @@ -16,7 +16,7 @@ import { import {Overlay, OVERLAY_PROVIDERS} from './overlay'; import {OverlayRef} from './overlay-ref'; import {TemplatePortal} from '../portal/portal'; -import {OverlayState} from './overlay-state'; +import {OverlayState, OverlayStateScrollStrategy} from './overlay-state'; import { ConnectionPositionPair, ConnectedOverlayPositionChange @@ -29,7 +29,6 @@ import {RepositionScrollStrategy} from './scroll/reposition-scroll-strategy'; import {ScrollStrategy} from './scroll/scroll-strategy'; import {coerceBooleanProperty} from '../coercion/boolean-property'; import {ESCAPE} from '../keyboard/keycodes'; -import {ScrollDispatcher} from './scroll/scroll-dispatcher'; import {Subscription} from 'rxjs/Subscription'; import {ScrollDispatchModule} from './scroll/index'; @@ -125,7 +124,7 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges { @Input() backdropClass: string; /** Strategy to be used when handling scroll events while the overlay is open. */ - @Input() scrollStrategy: ScrollStrategy = new RepositionScrollStrategy(this._scrollDispatcher); + @Input() scrollStrategy: OverlayStateScrollStrategy = 'reposition'; /** Whether the overlay is open. */ @Input() open: boolean = false; @@ -157,7 +156,6 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges { constructor( private _overlay: Overlay, private _renderer: Renderer2, - private _scrollDispatcher: ScrollDispatcher, templateRef: TemplateRef, viewContainerRef: ViewContainerRef, @Optional() private _dir: Dir) { diff --git a/src/lib/core/overlay/overlay-ref.ts b/src/lib/core/overlay/overlay-ref.ts index 00bc206aeaa1..760a53b94630 100644 --- a/src/lib/core/overlay/overlay-ref.ts +++ b/src/lib/core/overlay/overlay-ref.ts @@ -20,9 +20,11 @@ export class OverlayRef implements PortalHost { private _portalHost: PortalHost, private _pane: HTMLElement, private _state: OverlayState, + private _scrollStrategy: ScrollStrategy, private _ngZone: NgZone) { - this._state.scrollStrategy.attach(this); + _scrollStrategy.attach(this, + typeof _state.scrollStrategy === 'string' ? null : _state.scrollStrategy.config); } /** The overlay's HTML element */ @@ -44,7 +46,7 @@ export class OverlayRef implements PortalHost { this.updateDirection(); this.updatePosition(); this._attachments.next(); - this._state.scrollStrategy.enable(); + this._scrollStrategy.enable(); // Enable pointer events for the overlay pane element. this._togglePointerEvents(true); @@ -67,7 +69,7 @@ export class OverlayRef implements PortalHost { // This is necessary because otherwise the pane element will cover the page and disable // pointer events therefore. Depends on the position strategy and the applied pane boundaries. this._togglePointerEvents(false); - this._state.scrollStrategy.disable(); + this._scrollStrategy.disable(); this._detachments.next(); return this._portalHost.detach(); @@ -81,9 +83,13 @@ export class OverlayRef implements PortalHost { this._state.positionStrategy.dispose(); } + if (this._scrollStrategy) { + this._scrollStrategy.disable(); + this._scrollStrategy = null; + } + this.detachBackdrop(); this._portalHost.dispose(); - this._state.scrollStrategy.disable(); this._detachments.next(); this._detachments.complete(); this._attachments.complete(); diff --git a/src/lib/core/overlay/overlay-state.ts b/src/lib/core/overlay/overlay-state.ts index 6f397657ae62..2408587d197b 100644 --- a/src/lib/core/overlay/overlay-state.ts +++ b/src/lib/core/overlay/overlay-state.ts @@ -3,6 +3,7 @@ import {LayoutDirection} from '../rtl/dir'; import {ScrollStrategy} from './scroll/scroll-strategy'; import {NoopScrollStrategy} from './scroll/noop-scroll-strategy'; +export type OverlayStateScrollStrategy = string | {name: string; config: any}; /** * OverlayState is a bag of values for either the initial configuration or current state of an @@ -13,7 +14,7 @@ export class OverlayState { positionStrategy: PositionStrategy; /** Strategy to be used when handling scroll events while the overlay is open. */ - scrollStrategy: ScrollStrategy = new NoopScrollStrategy(); + scrollStrategy: OverlayStateScrollStrategy = 'noop'; /** Whether the overlay has a backdrop. */ hasBackdrop: boolean = false; diff --git a/src/lib/core/overlay/overlay.spec.ts b/src/lib/core/overlay/overlay.spec.ts index 681849b44f3c..ddf460d0e5c3 100644 --- a/src/lib/core/overlay/overlay.spec.ts +++ b/src/lib/core/overlay/overlay.spec.ts @@ -27,9 +27,7 @@ describe('Overlay', () => { return {getContainerElement: () => overlayContainerElement}; }} ] - }); - - TestBed.compileComponents(); + }).compileComponents(); })); beforeEach(inject([Overlay], (o: Overlay) => { @@ -340,10 +338,31 @@ describe('Overlay', () => { let fakeScrollStrategy: FakeScrollStrategy; let config: OverlayState; + class FakeScrollStrategy implements ScrollStrategy { + isEnabled = false; + overlayRef: OverlayRef; + + constructor() { + fakeScrollStrategy = this; + } + + attach(overlayRef: OverlayRef) { + this.overlayRef = overlayRef; + } + + enable() { + this.isEnabled = true; + } + + disable() { + this.isEnabled = false; + } + } + beforeEach(() => { config = new OverlayState(); - fakeScrollStrategy = new FakeScrollStrategy(); - config.scrollStrategy = fakeScrollStrategy; + overlay.registerScrollStrategy('fake', FakeScrollStrategy); + config.scrollStrategy = 'fake'; }); it('should attach the overlay ref to the scroll strategy', () => { @@ -450,20 +469,3 @@ class FakePositionStrategy implements PositionStrategy { dispose() {} } - -class FakeScrollStrategy implements ScrollStrategy { - isEnabled = false; - overlayRef: OverlayRef; - - attach(overlayRef: OverlayRef) { - this.overlayRef = overlayRef; - } - - enable() { - this.isEnabled = true; - } - - disable() { - this.isEnabled = false; - } -} diff --git a/src/lib/core/overlay/overlay.ts b/src/lib/core/overlay/overlay.ts index 1693a830eaa1..067a30b2d1d1 100644 --- a/src/lib/core/overlay/overlay.ts +++ b/src/lib/core/overlay/overlay.ts @@ -5,6 +5,7 @@ import { Injector, NgZone, Provider, + ReflectiveInjector, } from '@angular/core'; import {OverlayState} from './overlay-state'; import {DomPortalHost} from '../portal/dom-portal-host'; @@ -12,6 +13,11 @@ import {OverlayRef} from './overlay-ref'; import {OverlayPositionBuilder} from './position/overlay-position-builder'; import {VIEWPORT_RULER_PROVIDER} from './position/viewport-ruler'; import {OverlayContainer, OVERLAY_CONTAINER_PROVIDER} from './overlay-container'; +import {ScrollStrategy} from './scroll/scroll-strategy'; +import {RepositionScrollStrategy} from './scroll/reposition-scroll-strategy'; +import {BlockScrollStrategy} from './scroll/block-scroll-strategy'; +import {CloseScrollStrategy} from './scroll/close-scroll-strategy'; +import {NoopScrollStrategy} from './scroll/noop-scroll-strategy'; /** Next overlay unique ID. */ @@ -31,12 +37,22 @@ let defaultState = new OverlayState(); */ @Injectable() export class Overlay { + // Create a child ReflectiveInjector, allowing us to instantiate scroll + // strategies without going throught the injector cache. + private _reflectiveInjector = ReflectiveInjector.resolveAndCreate([], this._injector); + private _scrollStrategies = { + reposition: RepositionScrollStrategy, + block: BlockScrollStrategy, + close: CloseScrollStrategy, + noop: NoopScrollStrategy + }; + constructor(private _overlayContainer: OverlayContainer, private _componentFactoryResolver: ComponentFactoryResolver, private _positionBuilder: OverlayPositionBuilder, private _appRef: ApplicationRef, private _injector: Injector, - private _ngZone: NgZone) {} + private _ngZone: NgZone) { } /** * Creates an overlay. @@ -55,15 +71,26 @@ export class Overlay { return this._positionBuilder; } + /** + * Registers a scroll strategy to be available for use when creating an overlay. + * @param name Name of the scroll strategy. + * @param constructor Class to be used to instantiate the scroll strategy. + */ + registerScrollStrategy(name: string, constructor: Function): void { + if (name && constructor) { + this._scrollStrategies[name] = constructor; + } + } + /** * Creates the DOM element for an overlay and appends it to the overlay container. * @returns Newly-created pane element */ private _createPaneElement(): HTMLElement { let pane = document.createElement('div'); + pane.id = `cdk-overlay-${nextUniqueId++}`; pane.classList.add('cdk-overlay-pane'); - this._overlayContainer.getContainerElement().appendChild(pane); return pane; @@ -84,7 +111,28 @@ export class Overlay { * @param state */ private _createOverlayRef(pane: HTMLElement, state: OverlayState): OverlayRef { - return new OverlayRef(this._createPortalHost(pane), pane, state, this._ngZone); + let portalHost = this._createPortalHost(pane); + let scrollStrategy = this._createScrollStrategy(state); + return new OverlayRef(portalHost, pane, state, scrollStrategy, this._ngZone); + } + + /** + * Resolves the scroll strategy of an overlay state. + * @param state State for which to resolve the scroll strategy. + */ + private _createScrollStrategy(state: OverlayState): ScrollStrategy { + let strategyName = typeof state.scrollStrategy === 'string' ? + state.scrollStrategy : + state.scrollStrategy.name; + + if (!this._scrollStrategies.hasOwnProperty(strategyName)) { + throw new Error(`Unsupported scroll strategy "${strategyName}". The available scroll ` + + `strategies are ${Object.keys(this._scrollStrategies).join(', ')}.`); + } + + // Note that we use `resolveAndInstantiate` which will instantiate + // the scroll strategy without putting it in the injector cache. + return this._reflectiveInjector.resolveAndInstantiate(this._scrollStrategies[strategyName]); } } diff --git a/src/lib/core/overlay/scroll/block-scroll-strategy.spec.ts b/src/lib/core/overlay/scroll/block-scroll-strategy.spec.ts index e2aea1783df1..2ab9832e8009 100644 --- a/src/lib/core/overlay/scroll/block-scroll-strategy.spec.ts +++ b/src/lib/core/overlay/scroll/block-scroll-strategy.spec.ts @@ -1,20 +1,38 @@ +import {NgModule, Component} from '@angular/core'; import {inject, TestBed, async} from '@angular/core/testing'; -import {ComponentPortal, OverlayModule, BlockScrollStrategy, Platform} from '../../core'; -import {ViewportRuler} from '../position/viewport-ruler'; +import { + ComponentPortal, + OverlayModule, + PortalModule, + BlockScrollStrategy, + Platform, + ViewportRuler, + OverlayState, + Overlay, + OverlayRef, +} from '../../core'; describe('BlockScrollStrategy', () => { let platform = new Platform(); - let strategy: BlockScrollStrategy; let viewport: ViewportRuler; + let overlayRef: OverlayRef; + let componentPortal: ComponentPortal; let forceScrollElement: HTMLElement; beforeEach(async(() => { - TestBed.configureTestingModule({imports: [OverlayModule]}).compileComponents(); + TestBed.configureTestingModule({ + imports: [OverlayModule, PortalModule, OverlayTestModule] + }).compileComponents(); })); - beforeEach(inject([ViewportRuler], (viewportRuler: ViewportRuler) => { - strategy = new BlockScrollStrategy(viewportRuler); + beforeEach(inject([Overlay, ViewportRuler], (overlay: Overlay, viewportRuler: ViewportRuler) => { + let overlayState = new OverlayState(); + + overlayState.scrollStrategy = 'block'; + overlayRef = overlay.create(overlayState); + componentPortal = new ComponentPortal(FocacciaMsg); + viewport = viewportRuler; forceScrollElement = document.createElement('div'); document.body.appendChild(forceScrollElement); @@ -23,7 +41,7 @@ describe('BlockScrollStrategy', () => { })); afterEach(() => { - strategy.disable(); + overlayRef.dispose(); document.body.removeChild(forceScrollElement); setScrollPosition(0, 0); }); @@ -33,7 +51,7 @@ describe('BlockScrollStrategy', () => { expect(viewport.getViewportScrollPosition().top) .toBe(100, 'Expected viewport to be scrollable initially.'); - strategy.enable(); + overlayRef.attach(componentPortal); expect(document.documentElement.style.top) .toBe('-100px', 'Expected element to be offset by the previous scroll amount.'); @@ -41,7 +59,7 @@ describe('BlockScrollStrategy', () => { expect(viewport.getViewportScrollPosition().top) .toBe(100, 'Expected the viewport not to scroll.'); - strategy.disable(); + overlayRef.detach(); expect(viewport.getViewportScrollPosition().top) .toBe(100, 'Expected old scroll position to have bee restored after disabling.'); @@ -59,7 +77,7 @@ describe('BlockScrollStrategy', () => { expect(viewport.getViewportScrollPosition().left) .toBe(100, 'Expected viewport to be scrollable initially.'); - strategy.enable(); + overlayRef.attach(componentPortal); expect(document.documentElement.style.left) .toBe('-100px', 'Expected element to be offset by the previous scroll amount.'); @@ -67,7 +85,7 @@ describe('BlockScrollStrategy', () => { expect(viewport.getViewportScrollPosition().left) .toBe(100, 'Expected the viewport not to scroll.'); - strategy.disable(); + overlayRef.detach(); expect(viewport.getViewportScrollPosition().left) .toBe(100, 'Expected old scroll position to have bee restored after disabling.'); @@ -80,10 +98,10 @@ describe('BlockScrollStrategy', () => { it('should toggle the `cdk-global-scrollblock` class', skipIOS(() => { expect(document.documentElement.classList).not.toContain('cdk-global-scrollblock'); - strategy.enable(); + overlayRef.attach(componentPortal); expect(document.documentElement.classList).toContain('cdk-global-scrollblock'); - strategy.disable(); + overlayRef.detach(); expect(document.documentElement.classList).not.toContain('cdk-global-scrollblock'); })); @@ -93,12 +111,12 @@ describe('BlockScrollStrategy', () => { root.style.top = '13px'; root.style.left = '37px'; - strategy.enable(); + overlayRef.attach(componentPortal); expect(root.style.top).not.toBe('13px'); expect(root.style.left).not.toBe('37px'); - strategy.disable(); + overlayRef.detach(); expect(root.style.top).toBe('13px'); expect(root.style.left).toBe('37px'); @@ -106,7 +124,7 @@ describe('BlockScrollStrategy', () => { it(`should't do anything if the page isn't scrollable`, skipIOS(() => { forceScrollElement.style.display = 'none'; - strategy.enable(); + overlayRef.attach(componentPortal); expect(document.documentElement.classList).not.toContain('cdk-global-scrollblock'); })); @@ -116,7 +134,7 @@ describe('BlockScrollStrategy', () => { const previousContentWidth = document.documentElement.getBoundingClientRect().width; - strategy.enable(); + overlayRef.attach(componentPortal); expect(document.documentElement.getBoundingClientRect().width).toBe(previousContentWidth); }); @@ -151,3 +169,17 @@ describe('BlockScrollStrategy', () => { } }); + + +/** Simple component that we can attach to the overlay. */ +@Component({template: '

Focaccia

'}) +class FocacciaMsg { } + + +/** Test module to hold the component. */ +@NgModule({ + imports: [OverlayModule, PortalModule], + declarations: [FocacciaMsg], + entryComponents: [FocacciaMsg], +}) +class OverlayTestModule { } diff --git a/src/lib/core/overlay/scroll/block-scroll-strategy.ts b/src/lib/core/overlay/scroll/block-scroll-strategy.ts index 438db2efb560..c9aab0660a87 100644 --- a/src/lib/core/overlay/scroll/block-scroll-strategy.ts +++ b/src/lib/core/overlay/scroll/block-scroll-strategy.ts @@ -1,9 +1,11 @@ +import {Injectable} from '@angular/core'; import {ScrollStrategy} from './scroll-strategy'; import {ViewportRuler} from '../position/viewport-ruler'; /** * Strategy that will prevent the user from scrolling while the overlay is visible. */ +@Injectable() export class BlockScrollStrategy implements ScrollStrategy { private _previousHTMLStyles = { top: null, left: null }; private _previousScrollPosition: { top: number, left: number }; diff --git a/src/lib/core/overlay/scroll/close-scroll-strategy.spec.ts b/src/lib/core/overlay/scroll/close-scroll-strategy.spec.ts index aadfe8f209d5..8d87b512121b 100644 --- a/src/lib/core/overlay/scroll/close-scroll-strategy.spec.ts +++ b/src/lib/core/overlay/scroll/close-scroll-strategy.spec.ts @@ -34,11 +34,9 @@ describe('CloseScrollStrategy', () => { TestBed.compileComponents(); })); - beforeEach(inject([Overlay, ScrollDispatcher], (overlay: Overlay, - scrollDispatcher: ScrollDispatcher) => { - + beforeEach(inject([Overlay], (overlay: Overlay) => { let overlayState = new OverlayState(); - overlayState.scrollStrategy = new CloseScrollStrategy(scrollDispatcher); + overlayState.scrollStrategy = 'close'; overlayRef = overlay.create(overlayState); componentPortal = new ComponentPortal(MozarellaMsg); })); diff --git a/src/lib/core/overlay/scroll/close-scroll-strategy.ts b/src/lib/core/overlay/scroll/close-scroll-strategy.ts index 28f6a3d012c4..fafeea4c97b0 100644 --- a/src/lib/core/overlay/scroll/close-scroll-strategy.ts +++ b/src/lib/core/overlay/scroll/close-scroll-strategy.ts @@ -1,4 +1,5 @@ -import {ScrollStrategy} from './scroll-strategy'; +import {Injectable} from '@angular/core'; +import {ScrollStrategy, getMdScrollStrategyAlreadyAttachedError} from './scroll-strategy'; import {OverlayRef} from '../overlay-ref'; import {Subscription} from 'rxjs/Subscription'; import {ScrollDispatcher} from './scroll-dispatcher'; @@ -7,6 +8,7 @@ import {ScrollDispatcher} from './scroll-dispatcher'; /** * Strategy that will close the overlay as soon as the user starts scrolling. */ +@Injectable() export class CloseScrollStrategy implements ScrollStrategy { private _scrollSubscription: Subscription|null = null; private _overlayRef: OverlayRef; @@ -14,6 +16,10 @@ export class CloseScrollStrategy implements ScrollStrategy { constructor(private _scrollDispatcher: ScrollDispatcher) { } attach(overlayRef: OverlayRef) { + if (this._overlayRef) { + throw getMdScrollStrategyAlreadyAttachedError(); + } + this._overlayRef = overlayRef; } diff --git a/src/lib/core/overlay/scroll/noop-scroll-strategy.ts b/src/lib/core/overlay/scroll/noop-scroll-strategy.ts index 3d50a8f7743a..7f795f3532c0 100644 --- a/src/lib/core/overlay/scroll/noop-scroll-strategy.ts +++ b/src/lib/core/overlay/scroll/noop-scroll-strategy.ts @@ -1,8 +1,10 @@ +import {Injectable} from '@angular/core'; import {ScrollStrategy} from './scroll-strategy'; /** * Scroll strategy that doesn't do anything. */ +@Injectable() export class NoopScrollStrategy implements ScrollStrategy { enable() { } disable() { } diff --git a/src/lib/core/overlay/scroll/reposition-scroll-strategy.spec.ts b/src/lib/core/overlay/scroll/reposition-scroll-strategy.spec.ts index d210e20b8900..b8c4be1a2490 100644 --- a/src/lib/core/overlay/scroll/reposition-scroll-strategy.spec.ts +++ b/src/lib/core/overlay/scroll/reposition-scroll-strategy.spec.ts @@ -34,11 +34,9 @@ describe('RepositionScrollStrategy', () => { TestBed.compileComponents(); })); - beforeEach(inject([Overlay, ScrollDispatcher], (overlay: Overlay, - scrollDispatcher: ScrollDispatcher) => { - + beforeEach(inject([Overlay], (overlay: Overlay) => { let overlayState = new OverlayState(); - overlayState.scrollStrategy = new RepositionScrollStrategy(scrollDispatcher); + overlayState.scrollStrategy = 'reposition'; overlayRef = overlay.create(overlayState); componentPortal = new ComponentPortal(PastaMsg); })); diff --git a/src/lib/core/overlay/scroll/reposition-scroll-strategy.ts b/src/lib/core/overlay/scroll/reposition-scroll-strategy.ts index 924584ceb3a3..9bb90db73870 100644 --- a/src/lib/core/overlay/scroll/reposition-scroll-strategy.ts +++ b/src/lib/core/overlay/scroll/reposition-scroll-strategy.ts @@ -1,25 +1,41 @@ +import {Injectable} from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; -import {ScrollStrategy} from './scroll-strategy'; +import {ScrollStrategy, getMdScrollStrategyAlreadyAttachedError} from './scroll-strategy'; import {OverlayRef} from '../overlay-ref'; import {ScrollDispatcher} from './scroll-dispatcher'; +/** + * Config options for the RepositionScrollStrategy. + */ +export interface RepositionScrollStrategyConfig { + scrollThrottle?: number; +} /** * Strategy that will update the element position as the user is scrolling. */ +@Injectable() export class RepositionScrollStrategy implements ScrollStrategy { private _scrollSubscription: Subscription|null = null; private _overlayRef: OverlayRef; + private _config: RepositionScrollStrategyConfig; - constructor(private _scrollDispatcher: ScrollDispatcher, private _scrollThrottle = 0) { } + constructor(private _scrollDispatcher: ScrollDispatcher) { } + + attach(overlayRef: OverlayRef, config?: RepositionScrollStrategyConfig) { + if (this._overlayRef) { + throw getMdScrollStrategyAlreadyAttachedError(); + } - attach(overlayRef: OverlayRef) { this._overlayRef = overlayRef; + this._config = config; } enable() { if (!this._scrollSubscription) { - this._scrollSubscription = this._scrollDispatcher.scrolled(this._scrollThrottle, () => { + let throttle = this._config ? this._config.scrollThrottle : 0; + + this._scrollSubscription = this._scrollDispatcher.scrolled(throttle, () => { this._overlayRef.updatePosition(); }); } diff --git a/src/lib/core/overlay/scroll/scroll-dispatcher.spec.ts b/src/lib/core/overlay/scroll/scroll-dispatcher.spec.ts index 2ce254edc3d5..eb2da7ee1fde 100644 --- a/src/lib/core/overlay/scroll/scroll-dispatcher.spec.ts +++ b/src/lib/core/overlay/scroll/scroll-dispatcher.spec.ts @@ -9,7 +9,7 @@ describe('Scroll Dispatcher', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [OverlayModule, ScrollTestModule], + imports: [ScrollTestModule], }); TestBed.compileComponents(); diff --git a/src/lib/core/overlay/scroll/scroll-strategy.md b/src/lib/core/overlay/scroll/scroll-strategy.md index bfb083cc1d79..003aa61c3fb6 100644 --- a/src/lib/core/overlay/scroll/scroll-strategy.md +++ b/src/lib/core/overlay/scroll/scroll-strategy.md @@ -1,19 +1,19 @@ # Scroll strategies ## What is a scroll strategy? -A scroll strategy is a class that describes how an overlay should behave if the user scrolls +A scroll strategy is a way of describing how an overlay should behave if the user scrolls while the overlay is open. The strategy has a reference to the `OverlayRef`, allowing it to recalculate the position, close the overlay, block scrolling, etc. ## Usage -To associate an overlay with a scroll strategy, you have to pass in a `ScrollStrategy` instance -to the `OverlayState`. By default, all overlays will use the `NoopScrollStrategy` which doesn't -do anything: +To associate an overlay with a scroll strategy, you have to pass in the name of the scroll strategy +to the `OverlayState`. By default, all overlays will use the `noop` strategy which doesn't do +anything. The other available strategies are `reposition`, `block` and `close`: ```ts let overlayState = new OverlayState(); -overlayState.scrollStrategy = new BlockScrollStrategy(this._viewportRuler); +overlayState.scrollStrategy = 'block'; this._overlay.create(overlayState).attach(yourPortal); ``` @@ -25,3 +25,17 @@ interface. There are three stages of a scroll strategy's life cycle: 2. When an overlay is attached to the DOM, it'll call the `enable` method on its scroll strategy, 3. When an overlay is detached from the DOM or destroyed, it'll call the `disable` method on its scroll strategy, allowing it to clean up after itself. + +Afterwards the scroll strategy has to be registered with the `Overlay` service: + +```ts +overlay.registerScrollStrategy('custom', CustomScrollStrategy); +``` + +Finally, you can use the strategy by passing its name to the `OverlayState`: +```ts +let overlayState = new OverlayState(); + +overlayState.scrollStrategy = 'custom'; +this._overlay.create(overlayState).attach(yourPortal); +``` diff --git a/src/lib/core/overlay/scroll/scroll-strategy.ts b/src/lib/core/overlay/scroll/scroll-strategy.ts index 37fcada36e6d..3f71d48f206c 100644 --- a/src/lib/core/overlay/scroll/scroll-strategy.ts +++ b/src/lib/core/overlay/scroll/scroll-strategy.ts @@ -7,5 +7,12 @@ import {OverlayRef} from '../overlay-ref'; export interface ScrollStrategy { enable: () => void; disable: () => void; - attach: (overlayRef: OverlayRef) => void; + attach: (overlayRef: OverlayRef, config?: any) => void; +} + +/** + * Returns an error to be thrown when attempting to attach an already-attached scroll strategy. + */ +export function getMdScrollStrategyAlreadyAttachedError(): Error { + return new Error(`Scroll strategy has already been attached.`); } diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index bddb7c701adb..62a53bfa46d1 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -21,7 +21,6 @@ import {Dir} from '../core/rtl/dir'; import {MdDialog} from '../dialog/dialog'; import {MdDialogRef} from '../dialog/dialog-ref'; import {PositionStrategy} from '../core/overlay/position/position-strategy'; -import {RepositionScrollStrategy, ScrollDispatcher} from '../core/overlay/index'; import {MdDatepickerInput} from './datepicker-input'; import {Subscription} from 'rxjs/Subscription'; import {MdDialogConfig} from '../dialog/dialog-config'; @@ -157,7 +156,6 @@ export class MdDatepicker implements OnDestroy { private _overlay: Overlay, private _ngZone: NgZone, private _viewContainerRef: ViewContainerRef, - private _scrollDispatcher: ScrollDispatcher, @Optional() private _dateAdapter: DateAdapter, @Optional() private _dir: Dir) { if (!this._dateAdapter) { @@ -269,7 +267,7 @@ export class MdDatepicker implements OnDestroy { overlayState.hasBackdrop = true; overlayState.backdropClass = 'md-overlay-transparent-backdrop'; overlayState.direction = this._dir ? this._dir.value : 'ltr'; - overlayState.scrollStrategy = new RepositionScrollStrategy(this._scrollDispatcher); + overlayState.scrollStrategy = 'reposition'; this._popupRef = this._overlay.create(overlayState); } diff --git a/src/lib/dialog/dialog.ts b/src/lib/dialog/dialog.ts index a4c275bdf6e3..c58409087da3 100644 --- a/src/lib/dialog/dialog.ts +++ b/src/lib/dialog/dialog.ts @@ -10,8 +10,6 @@ import {MdDialogConfig} from './dialog-config'; import {MdDialogRef} from './dialog-ref'; import {MdDialogContainer} from './dialog-container'; import {TemplatePortal} from '../core/portal/portal'; -import {BlockScrollStrategy} from '../core/overlay/scroll/block-scroll-strategy'; -import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; import 'rxjs/add/operator/first'; @@ -50,7 +48,6 @@ export class MdDialog { constructor( private _overlay: Overlay, private _injector: Injector, - private _viewportRuler: ViewportRuler, @Optional() private _location: Location, @Optional() @SkipSelf() private _parentDialog: MdDialog) { @@ -122,7 +119,7 @@ export class MdDialog { private _getOverlayState(dialogConfig: MdDialogConfig): OverlayState { let overlayState = new OverlayState(); overlayState.hasBackdrop = dialogConfig.hasBackdrop; - overlayState.scrollStrategy = new BlockScrollStrategy(this._viewportRuler); + overlayState.scrollStrategy = 'block'; if (dialogConfig.backdropClass) { overlayState.backdropClass = dialogConfig.backdropClass; } diff --git a/src/lib/menu/menu-trigger.ts b/src/lib/menu/menu-trigger.ts index 22e5240d72d0..365bb1fc9e64 100644 --- a/src/lib/menu/menu-trigger.ts +++ b/src/lib/menu/menu-trigger.ts @@ -22,8 +22,6 @@ import { ConnectedPositionStrategy, HorizontalConnectionPos, VerticalConnectionPos, - RepositionScrollStrategy, - ScrollDispatcher, } from '../core'; import {Subscription} from 'rxjs/Subscription'; import {MenuPositionX, MenuPositionY} from './menu-positions'; @@ -80,8 +78,7 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy { @Output() onMenuClose = new EventEmitter(); constructor(private _overlay: Overlay, private _element: ElementRef, - private _viewContainerRef: ViewContainerRef, @Optional() private _dir: Dir, - private _scrollDispatcher: ScrollDispatcher) { } + private _viewContainerRef: ViewContainerRef, @Optional() private _dir: Dir) { } ngAfterViewInit() { this._checkMenu(); @@ -219,7 +216,7 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy { overlayState.hasBackdrop = true; overlayState.backdropClass = 'cdk-overlay-transparent-backdrop'; overlayState.direction = this.dir; - overlayState.scrollStrategy = new RepositionScrollStrategy(this._scrollDispatcher); + overlayState.scrollStrategy = 'reposition'; return overlayState; } diff --git a/src/lib/tooltip/tooltip.ts b/src/lib/tooltip/tooltip.ts index e32e1cb8d589..40f4f3305ce0 100644 --- a/src/lib/tooltip/tooltip.ts +++ b/src/lib/tooltip/tooltip.ts @@ -25,7 +25,6 @@ import { ComponentPortal, OverlayConnectionPosition, OriginConnectionPosition, - RepositionScrollStrategy, } from '../core'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; @@ -237,10 +236,13 @@ export class MdTooltip implements OnDestroy { }); let config = new OverlayState(); + config.direction = this._dir ? this._dir.value : 'ltr'; config.positionStrategy = strategy; - config.scrollStrategy = - new RepositionScrollStrategy(this._scrollDispatcher, SCROLL_THROTTLE_MS); + config.scrollStrategy = { + name: 'reposition', + config: { scrollThrottle: SCROLL_THROTTLE_MS } + }; this._overlayRef = this._overlay.create(config); }