mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(overlay): more flexible scroll strategy API and ability to define/override custom strategies (#4855)
* 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. * refactor: switch to new approach without ReflectiveInjector * refactor: switch to better approach * refactor: switch to even simpler approach * chore: address feedback
This commit is contained in:
committed by
Andrew Seguin
parent
70b31f177a
commit
7e912705c3
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -113,7 +112,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) {}
|
||||
@@ -381,7 +379,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<any>,
|
||||
viewContainerRef: ViewContainerRef,
|
||||
@Optional() private _dir: Dir) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<FocacciaMsg>;
|
||||
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 <html> 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 <html> 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: '<p>Focaccia</p>'})
|
||||
class FocacciaMsg { }
|
||||
|
||||
|
||||
/** Test module to hold the component. */
|
||||
@NgModule({
|
||||
imports: [OverlayModule, PortalModule],
|
||||
declarations: [FocacciaMsg],
|
||||
entryComponents: [FocacciaMsg],
|
||||
})
|
||||
class OverlayTestModule { }
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('Scroll Dispatcher', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [OverlayModule, ScrollTestModule],
|
||||
imports: [ScrollTestModule],
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
```
|
||||
|
||||
@@ -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.`);
|
||||
}
|
||||
|
||||
@@ -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<D> implements OnDestroy {
|
||||
private _overlay: Overlay,
|
||||
private _ngZone: NgZone,
|
||||
private _viewContainerRef: ViewContainerRef,
|
||||
private _scrollDispatcher: ScrollDispatcher,
|
||||
@Optional() private _dateAdapter: DateAdapter<D>,
|
||||
@Optional() private _dir: Dir) {
|
||||
if (!this._dateAdapter) {
|
||||
@@ -269,7 +267,7 @@ export class MdDatepicker<D> 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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<void>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user