diff --git a/src/e2e-app/block-scroll-strategy/block-scroll-strategy-e2e.ts b/src/e2e-app/block-scroll-strategy/block-scroll-strategy-e2e.ts index a9e8f4b7e3e3..927ba58b006b 100644 --- a/src/e2e-app/block-scroll-strategy/block-scroll-strategy-e2e.ts +++ b/src/e2e-app/block-scroll-strategy/block-scroll-strategy-e2e.ts @@ -1,5 +1,5 @@ import {Component} from '@angular/core'; -import {BlockScrollStrategy, ViewportRuler} from '@angular/material'; +import {Overlay, ScrollStrategy} from '@angular/material'; @Component({ moduleId: module.id, @@ -8,6 +8,6 @@ import {BlockScrollStrategy, ViewportRuler} from '@angular/material'; styleUrls: ['block-scroll-strategy-e2e.css'], }) export class BlockScrollStrategyE2E { - constructor(private _viewportRuler: ViewportRuler) { } - scrollStrategy = new BlockScrollStrategy(this._viewportRuler); + constructor(private _overlay: Overlay) { } + scrollStrategy: ScrollStrategy = this._overlay.scrollStrategies.block(); } diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 5dd5c1f8fce5..1ccf317ab1ae 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -13,7 +13,7 @@ import { } from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {DOCUMENT} from '@angular/platform-browser'; -import {Overlay, OverlayRef, OverlayState, TemplatePortal, RepositionScrollStrategy} from '../core'; +import {Overlay, OverlayRef, OverlayState, TemplatePortal} from '../core'; import {MdAutocomplete} from './autocomplete'; import {PositionStrategy} from '../core/overlay/position/position-strategy'; import {ConnectedPositionStrategy} from '../core/overlay/position/connected-position-strategy'; @@ -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 = this._overlay.scrollStrategies.reposition(); return overlayState; } diff --git a/src/lib/core/overlay/overlay-directives.ts b/src/lib/core/overlay/overlay-directives.ts index 06d2194d6117..a7a66df277b8 100644 --- a/src/lib/core/overlay/overlay-directives.ts +++ b/src/lib/core/overlay/overlay-directives.ts @@ -25,11 +25,9 @@ import {PortalModule} from '../portal/portal-directives'; import {ConnectedPositionStrategy} from './position/connected-position-strategy'; import {Dir, LayoutDirection} from '../rtl/dir'; import {Scrollable} from './scroll/scrollable'; -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 +123,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: ScrollStrategy = this._overlay.scrollStrategies.reposition(); /** Whether the overlay is open. */ @Input() open: boolean = false; @@ -157,7 +155,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 6a58b6a2b467..23b435e8d50d 100644 --- a/src/lib/core/overlay/overlay-ref.ts +++ b/src/lib/core/overlay/overlay-ref.ts @@ -20,9 +20,10 @@ 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); } /** The overlay's HTML element */ @@ -44,7 +45,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); @@ -71,7 +72,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(); @@ -85,9 +86,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 8b59c38aa0a6..930e4293f974 100644 --- a/src/lib/core/overlay/overlay-state.ts +++ b/src/lib/core/overlay/overlay-state.ts @@ -1,7 +1,6 @@ import {PositionStrategy} from './position/position-strategy'; import {LayoutDirection} from '../rtl/dir'; import {ScrollStrategy} from './scroll/scroll-strategy'; -import {NoopScrollStrategy} from './scroll/noop-scroll-strategy'; /** @@ -13,7 +12,7 @@ export class OverlayState { positionStrategy: PositionStrategy; /** Strategy to be used when handling scroll events while the overlay is open. */ - scrollStrategy: ScrollStrategy = new NoopScrollStrategy(); + scrollStrategy: ScrollStrategy; /** Custom class to add to the overlay pane. */ panelClass: string = ''; diff --git a/src/lib/core/overlay/overlay.spec.ts b/src/lib/core/overlay/overlay.spec.ts index ccc1424a563a..fd2d0140fb56 100644 --- a/src/lib/core/overlay/overlay.spec.ts +++ b/src/lib/core/overlay/overlay.spec.ts @@ -8,7 +8,8 @@ import {OverlayState} from './overlay-state'; import {OverlayRef} from './overlay-ref'; import {PositionStrategy} from './position/position-strategy'; import {OverlayModule} from './overlay-directives'; -import {ScrollStrategy} from './scroll/scroll-strategy'; +import {ViewportRuler} from './position/viewport-ruler'; +import {ScrollStrategy, ScrollDispatcher} from './scroll/index'; describe('Overlay', () => { @@ -21,15 +22,14 @@ describe('Overlay', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [OverlayModule, PortalModule, OverlayTestModule], - providers: [ - {provide: OverlayContainer, useFactory: () => { + providers: [{ + provide: OverlayContainer, + useFactory: () => { overlayContainerElement = document.createElement('div'); return {getContainerElement: () => overlayContainerElement}; - }} - ] - }); - - TestBed.compileComponents(); + } + }] + }).compileComponents(); })); beforeEach(inject([Overlay], (o: Overlay) => { @@ -354,30 +354,25 @@ describe('Overlay', () => { describe('scroll strategy', () => { let fakeScrollStrategy: FakeScrollStrategy; let config: OverlayState; + let overlayRef: OverlayRef; beforeEach(() => { config = new OverlayState(); - fakeScrollStrategy = new FakeScrollStrategy(); - config.scrollStrategy = fakeScrollStrategy; + fakeScrollStrategy = config.scrollStrategy = new FakeScrollStrategy(); + overlayRef = overlay.create(config); }); it('should attach the overlay ref to the scroll strategy', () => { - let overlayRef = overlay.create(config); - expect(fakeScrollStrategy.overlayRef).toBe(overlayRef, 'Expected scroll strategy to have been attached to the current overlay ref.'); }); it('should enable the scroll strategy when the overlay is attached', () => { - let overlayRef = overlay.create(config); - overlayRef.attach(componentPortal); expect(fakeScrollStrategy.isEnabled).toBe(true, 'Expected scroll strategy to be enabled.'); }); it('should disable the scroll strategy once the overlay is detached', () => { - let overlayRef = overlay.create(config); - overlayRef.attach(componentPortal); expect(fakeScrollStrategy.isEnabled).toBe(true, 'Expected scroll strategy to be enabled.'); @@ -386,8 +381,6 @@ describe('Overlay', () => { }); it('should disable the scroll strategy when the overlay is destroyed', () => { - let overlayRef = overlay.create(config); - overlayRef.dispose(); expect(fakeScrollStrategy.isEnabled).toBe(false, 'Expected scroll strategy to be disabled.'); }); @@ -466,6 +459,7 @@ class FakePositionStrategy implements PositionStrategy { dispose() {} } + class FakeScrollStrategy implements ScrollStrategy { isEnabled = false; overlayRef: OverlayRef; diff --git a/src/lib/core/overlay/overlay.ts b/src/lib/core/overlay/overlay.ts index 1693a830eaa1..0cdcfd1df12c 100644 --- a/src/lib/core/overlay/overlay.ts +++ b/src/lib/core/overlay/overlay.ts @@ -12,6 +12,7 @@ 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, ScrollStrategyOptions} from './scroll/index'; /** Next overlay unique ID. */ @@ -31,12 +32,13 @@ let defaultState = new OverlayState(); */ @Injectable() export class Overlay { - constructor(private _overlayContainer: OverlayContainer, + constructor(public scrollStrategies: ScrollStrategyOptions, + private _overlayContainer: OverlayContainer, private _componentFactoryResolver: ComponentFactoryResolver, private _positionBuilder: OverlayPositionBuilder, private _appRef: ApplicationRef, private _injector: Injector, - private _ngZone: NgZone) {} + private _ngZone: NgZone) { } /** * Creates an overlay. @@ -61,9 +63,9 @@ export class Overlay { */ 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 +86,9 @@ export class Overlay { * @param state */ private _createOverlayRef(pane: HTMLElement, state: OverlayState): OverlayRef { - return new OverlayRef(this._createPortalHost(pane), pane, state, this._ngZone); + let scrollStrategy = state.scrollStrategy || this.scrollStrategies.noop(); + let portalHost = this._createPortalHost(pane); + return new OverlayRef(portalHost, pane, state, scrollStrategy, this._ngZone); } } 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..434280144ec7 100644 --- a/src/lib/core/overlay/scroll/block-scroll-strategy.spec.ts +++ b/src/lib/core/overlay/scroll/block-scroll-strategy.spec.ts @@ -1,29 +1,46 @@ +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, + 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); - viewport = viewportRuler; - forceScrollElement = document.createElement('div'); - document.body.appendChild(forceScrollElement); - forceScrollElement.style.width = '100px'; - forceScrollElement.style.height = '3000px'; - })); + beforeEach(inject([Overlay, ViewportRuler], (overlay: Overlay, viewportRuler: ViewportRuler) => { + let overlayState = new OverlayState(); + + overlayState.scrollStrategy = overlay.scrollStrategies.block(); + overlayRef = overlay.create(overlayState); + componentPortal = new ComponentPortal(FocacciaMsg); + + viewport = viewportRuler; + forceScrollElement = document.createElement('div'); + document.body.appendChild(forceScrollElement); + forceScrollElement.style.width = '100px'; + forceScrollElement.style.height = '3000px'; + })); afterEach(() => { - strategy.disable(); + overlayRef.dispose(); document.body.removeChild(forceScrollElement); setScrollPosition(0, 0); }); @@ -33,7 +50,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 +58,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 +76,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 +84,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 +97,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 +110,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 +123,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 +133,7 @@ describe('BlockScrollStrategy', () => { const previousContentWidth = document.documentElement.getBoundingClientRect().width; - strategy.enable(); + overlayRef.attach(componentPortal); expect(document.documentElement.getBoundingClientRect().width).toBe(previousContentWidth); }); @@ -151,3 +168,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/close-scroll-strategy.spec.ts b/src/lib/core/overlay/scroll/close-scroll-strategy.spec.ts index aadfe8f209d5..91b55eca1094 100644 --- a/src/lib/core/overlay/scroll/close-scroll-strategy.spec.ts +++ b/src/lib/core/overlay/scroll/close-scroll-strategy.spec.ts @@ -10,7 +10,6 @@ import { OverlayModule, ScrollStrategy, ScrollDispatcher, - CloseScrollStrategy, } from '../../core'; @@ -34,11 +33,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 = overlay.scrollStrategies.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..7caca425cde3 100644 --- a/src/lib/core/overlay/scroll/close-scroll-strategy.ts +++ b/src/lib/core/overlay/scroll/close-scroll-strategy.ts @@ -1,4 +1,4 @@ -import {ScrollStrategy} from './scroll-strategy'; +import {ScrollStrategy, getMdScrollStrategyAlreadyAttachedError} from './scroll-strategy'; import {OverlayRef} from '../overlay-ref'; import {Subscription} from 'rxjs/Subscription'; import {ScrollDispatcher} from './scroll-dispatcher'; @@ -14,6 +14,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/index.ts b/src/lib/core/overlay/scroll/index.ts index ae291c6d5b9a..acf5039b6188 100644 --- a/src/lib/core/overlay/scroll/index.ts +++ b/src/lib/core/overlay/scroll/index.ts @@ -2,12 +2,14 @@ import {NgModule} from '@angular/core'; import {SCROLL_DISPATCHER_PROVIDER} from './scroll-dispatcher'; import {Scrollable} from './scrollable'; import {PlatformModule} from '../../platform/index'; +import {ScrollStrategyOptions} from './scroll-strategy-options'; export {Scrollable} from './scrollable'; export {ScrollDispatcher} from './scroll-dispatcher'; // Export pre-defined scroll strategies and interface to build custom ones. export {ScrollStrategy} from './scroll-strategy'; +export {ScrollStrategyOptions} from './scroll-strategy-options'; export {RepositionScrollStrategy} from './reposition-scroll-strategy'; export {CloseScrollStrategy} from './close-scroll-strategy'; export {NoopScrollStrategy} from './noop-scroll-strategy'; @@ -17,6 +19,6 @@ export {BlockScrollStrategy} from './block-scroll-strategy'; imports: [PlatformModule], exports: [Scrollable], declarations: [Scrollable], - providers: [SCROLL_DISPATCHER_PROVIDER], + providers: [SCROLL_DISPATCHER_PROVIDER, ScrollStrategyOptions], }) export class ScrollDispatchModule { } 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..1f2812a7b06c 100644 --- a/src/lib/core/overlay/scroll/reposition-scroll-strategy.spec.ts +++ b/src/lib/core/overlay/scroll/reposition-scroll-strategy.spec.ts @@ -10,7 +10,6 @@ import { OverlayModule, ScrollStrategy, ScrollDispatcher, - RepositionScrollStrategy, } from '../../core'; @@ -34,11 +33,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 = overlay.scrollStrategies.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..23a7e70d49e4 100644 --- a/src/lib/core/overlay/scroll/reposition-scroll-strategy.ts +++ b/src/lib/core/overlay/scroll/reposition-scroll-strategy.ts @@ -1,8 +1,14 @@ 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. @@ -11,15 +17,23 @@ export class RepositionScrollStrategy implements ScrollStrategy { private _scrollSubscription: Subscription|null = null; private _overlayRef: OverlayRef; - constructor(private _scrollDispatcher: ScrollDispatcher, private _scrollThrottle = 0) { } + constructor( + private _scrollDispatcher: ScrollDispatcher, + private _config: RepositionScrollStrategyConfig) { } attach(overlayRef: OverlayRef) { + if (this._overlayRef) { + throw getMdScrollStrategyAlreadyAttachedError(); + } + this._overlayRef = overlayRef; } 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-options.ts b/src/lib/core/overlay/scroll/scroll-strategy-options.ts new file mode 100644 index 000000000000..9123cfe27fcf --- /dev/null +++ b/src/lib/core/overlay/scroll/scroll-strategy-options.ts @@ -0,0 +1,42 @@ +import {Injectable} from '@angular/core'; +import {ScrollStrategy} from './scroll-strategy'; +import {CloseScrollStrategy} from './close-scroll-strategy'; +import {NoopScrollStrategy} from './noop-scroll-strategy'; +import {BlockScrollStrategy} from './block-scroll-strategy'; +import {ScrollDispatcher} from './scroll-dispatcher'; +import {ViewportRuler} from '../position/viewport-ruler'; +import { + RepositionScrollStrategy, + RepositionScrollStrategyConfig, +} from './reposition-scroll-strategy'; + + +/** + * Options for how an overlay will handle scrolling. + * + * Users can provide a custom value for `ScrollStrategyOptions` to replace the default + * behaviors. This class primarily acts as a factory for ScrollStrategy instances. + */ +@Injectable() +export class ScrollStrategyOptions { + constructor( + private _scrollDispatcher: ScrollDispatcher, + private _viewportRuler: ViewportRuler) { } + + /** Do nothing on scroll. */ + noop = () => new NoopScrollStrategy(); + + /** Close the overlay as soon as the user scrolls. */ + close = () => new CloseScrollStrategy(this._scrollDispatcher); + + /** Block scrolling. */ + block = () => new BlockScrollStrategy(this._viewportRuler); + + /** + * Update the overlay's position on scroll. + * @param config Configuration to be used inside the scroll strategy. + * Allows debouncing the reposition calls. + */ + reposition = (config?: RepositionScrollStrategyConfig) => + new RepositionScrollStrategy(this._scrollDispatcher, config) +} diff --git a/src/lib/core/overlay/scroll/scroll-strategy.md b/src/lib/core/overlay/scroll/scroll-strategy.md index bfb083cc1d79..b39772b0b431 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 a function, that returns a +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 = overlay.scrollStrategies.block(); this._overlay.create(overlayState).attach(yourPortal); ``` @@ -25,3 +25,15 @@ 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 you can pass in the new scroll strategy to your overlay state: + +```ts +// Your custom scroll strategy. +export class CustomScrollStrategy implements ScrollStrategy { + // your implementation +} + +overlayState.scrollStrategy = new CustomScrollStrategy(); +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..d8631ffafb0a 100644 --- a/src/lib/core/overlay/scroll/scroll-strategy.ts +++ b/src/lib/core/overlay/scroll/scroll-strategy.ts @@ -9,3 +9,10 @@ export interface ScrollStrategy { disable: () => void; attach: (overlayRef: OverlayRef) => void; } + +/** + * Returns an error to be thrown when attempting to attach an already-attached scroll strategy. + */ +export function getMdScrollStrategyAlreadyAttachedError(): Error { + return Error(`Scroll strategy has already been attached.`); +} diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index 41302c21c0e3..e5783aa6bdfa 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 = this._overlay.scrollStrategies.reposition(); this._popupRef = this._overlay.create(overlayState); } diff --git a/src/lib/dialog/dialog.ts b/src/lib/dialog/dialog.ts index 71bf25ace0c9..fa86910ad900 100644 --- a/src/lib/dialog/dialog.ts +++ b/src/lib/dialog/dialog.ts @@ -2,7 +2,13 @@ import {Injector, ComponentRef, Injectable, Optional, SkipSelf, TemplateRef} fro import {Location} from '@angular/common'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; -import {Overlay, OverlayRef, ComponentType, OverlayState, ComponentPortal} from '../core'; +import { + Overlay, + OverlayRef, + ComponentType, + OverlayState, + ComponentPortal, +} from '../core'; import {extendObject} from '../core/util/object-extend'; import {ESCAPE} from '../core/keyboard/keycodes'; import {DialogInjector} from './dialog-injector'; @@ -10,8 +16,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 +54,6 @@ export class MdDialog { constructor( private _overlay: Overlay, private _injector: Injector, - private _viewportRuler: ViewportRuler, @Optional() private _location: Location, @Optional() @SkipSelf() private _parentDialog: MdDialog) { @@ -123,7 +126,7 @@ export class MdDialog { let overlayState = new OverlayState(); overlayState.panelClass = dialogConfig.panelClass; overlayState.hasBackdrop = dialogConfig.hasBackdrop; - overlayState.scrollStrategy = new BlockScrollStrategy(this._viewportRuler); + overlayState.scrollStrategy = this._overlay.scrollStrategies.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..a54df373ef40 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 = this._overlay.scrollStrategies.reposition(); return overlayState; } diff --git a/src/lib/tooltip/tooltip.ts b/src/lib/tooltip/tooltip.ts index e32e1cb8d589..d8e77732d494 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,12 @@ 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 = this._overlay.scrollStrategies.reposition({ + scrollThrottle: SCROLL_THROTTLE_MS + }); this._overlayRef = this._overlay.create(config); }