mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(overlay): add scroll handling strategies (#4293)
* feat(overlay): add scroll handling strategies * Adds the `scrollStrategy` option to the overlay state, allowing the consumer to specify what scroll handling strategy they'd want to use. Also includes a `ScrollStrategy` interface that users can utilize to build their own strategies. * Adds the `RepositionScrollStrategy`, `CloseScrollStrategy` and `NoopScrollStrategy` as initial, out-of-the-box strategies. * Sets the `RepositionScrollStrategy` by default on all the connected overlays and removes some repetitive logic from the tooltip, autocomplete, menu and select. **Note:** I'll add a `BlockScrollStrategy` in a follow-up PR. I wanted to keep this one shorter. Relates to #4093. * fix: missing types on the scroll dispatcher * refactor: use class for fake scroll strategy * refactor: add onAttached and onDetached observables * chore: rename observables
This commit is contained in:
committed by
Andrew Seguin
parent
afaa2dc05b
commit
c8ec9814bc
@@ -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} from '../core';
|
||||
import {Overlay, OverlayRef, OverlayState, TemplatePortal, RepositionScrollStrategy} from '../core';
|
||||
import {MdAutocomplete} from './autocomplete';
|
||||
import {PositionStrategy} from '../core/overlay/position/position-strategy';
|
||||
import {ConnectedPositionStrategy} from '../core/overlay/position/connected-position-strategy';
|
||||
@@ -76,9 +76,6 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
|
||||
/** The subscription to positioning changes in the autocomplete panel. */
|
||||
private _panelPositionSubscription: Subscription;
|
||||
|
||||
/** Subscription to global scroll events. */
|
||||
private _scrollSubscription: Subscription;
|
||||
|
||||
/** Strategy that is used to position the panel. */
|
||||
private _positionStrategy: ConnectedPositionStrategy;
|
||||
|
||||
@@ -139,12 +136,6 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
|
||||
this._subscribeToClosingActions();
|
||||
}
|
||||
|
||||
if (!this._scrollSubscription) {
|
||||
this._scrollSubscription = this._scrollDispatcher.scrolled(0, () => {
|
||||
this._overlayRef.updatePosition();
|
||||
});
|
||||
}
|
||||
|
||||
this.autocomplete._setVisibility();
|
||||
this._floatPlaceholder();
|
||||
this._panelOpen = true;
|
||||
@@ -156,11 +147,6 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
|
||||
this._overlayRef.detach();
|
||||
}
|
||||
|
||||
if (this._scrollSubscription) {
|
||||
this._scrollSubscription.unsubscribe();
|
||||
this._scrollSubscription = null;
|
||||
}
|
||||
|
||||
this._panelOpen = false;
|
||||
this._resetPlaceholder();
|
||||
|
||||
@@ -374,6 +360,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);
|
||||
return overlayState;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ export * from './overlay/position/global-position-strategy';
|
||||
export * from './overlay/position/connected-position-strategy';
|
||||
export * from './overlay/position/connected-position';
|
||||
export {ScrollDispatcher} from './overlay/scroll/scroll-dispatcher';
|
||||
export {ScrollStrategy} from './overlay/scroll/scroll-strategy';
|
||||
export {RepositionScrollStrategy} from './overlay/scroll/reposition-scroll-strategy';
|
||||
export {CloseScrollStrategy} from './overlay/scroll/close-scroll-strategy';
|
||||
export {NoopScrollStrategy} from './overlay/scroll/noop-scroll-strategy';
|
||||
|
||||
// Gestures
|
||||
export {GestureConfig} from './gestures/gesture-config';
|
||||
|
||||
@@ -23,8 +23,11 @@ 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';
|
||||
|
||||
|
||||
@@ -119,6 +122,9 @@ export class ConnectedOverlayDirective implements OnDestroy {
|
||||
/** The custom class to be set on the backdrop element. */
|
||||
@Input() backdropClass: string;
|
||||
|
||||
/** Strategy to be used when handling scroll events while the overlay is open. */
|
||||
@Input() scrollStrategy: ScrollStrategy = new RepositionScrollStrategy(this._scrollDispatcher);
|
||||
|
||||
/** Whether or not the overlay should attach a backdrop. */
|
||||
@Input()
|
||||
get hasBackdrop() {
|
||||
@@ -156,6 +162,7 @@ export class ConnectedOverlayDirective implements OnDestroy {
|
||||
constructor(
|
||||
private _overlay: Overlay,
|
||||
private _renderer: Renderer2,
|
||||
private _scrollDispatcher: ScrollDispatcher,
|
||||
templateRef: TemplateRef<any>,
|
||||
viewContainerRef: ViewContainerRef,
|
||||
@Optional() private _dir: Dir) {
|
||||
@@ -213,6 +220,7 @@ export class ConnectedOverlayDirective implements OnDestroy {
|
||||
|
||||
this._position = this._createPositionStrategy() as ConnectedPositionStrategy;
|
||||
overlayConfig.positionStrategy = this._position;
|
||||
overlayConfig.scrollStrategy = this.scrollStrategy;
|
||||
|
||||
return overlayConfig;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {NgZone} from '@angular/core';
|
||||
import {PortalHost, Portal} from '../portal/portal';
|
||||
import {OverlayState} from './overlay-state';
|
||||
import {ScrollStrategy} from './scroll/scroll-strategy';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
|
||||
@@ -12,12 +13,17 @@ import {Subject} from 'rxjs/Subject';
|
||||
export class OverlayRef implements PortalHost {
|
||||
private _backdropElement: HTMLElement = null;
|
||||
private _backdropClick: Subject<any> = new Subject();
|
||||
private _attachments = new Subject<void>();
|
||||
private _detachments = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private _portalHost: PortalHost,
|
||||
private _pane: HTMLElement,
|
||||
private _state: OverlayState,
|
||||
private _ngZone: NgZone) { }
|
||||
private _ngZone: NgZone) {
|
||||
|
||||
this._state.scrollStrategy.attach(this);
|
||||
}
|
||||
|
||||
/** The overlay's HTML element */
|
||||
get overlayElement(): HTMLElement {
|
||||
@@ -37,6 +43,8 @@ export class OverlayRef implements PortalHost {
|
||||
this.updateSize();
|
||||
this.updateDirection();
|
||||
this.updatePosition();
|
||||
this._attachments.next();
|
||||
this._state.scrollStrategy.enable();
|
||||
|
||||
// Enable pointer events for the overlay pane element.
|
||||
this._togglePointerEvents(true);
|
||||
@@ -59,6 +67,8 @@ 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._detachments.next();
|
||||
|
||||
return this._portalHost.detach();
|
||||
}
|
||||
@@ -73,6 +83,10 @@ export class OverlayRef implements PortalHost {
|
||||
|
||||
this.detachBackdrop();
|
||||
this._portalHost.dispose();
|
||||
this._state.scrollStrategy.disable();
|
||||
this._detachments.next();
|
||||
this._detachments.complete();
|
||||
this._attachments.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,6 +103,16 @@ export class OverlayRef implements PortalHost {
|
||||
return this._backdropClick.asObservable();
|
||||
}
|
||||
|
||||
/** Returns an observable that emits when the overlay has been attached. */
|
||||
attachments(): Observable<void> {
|
||||
return this._attachments.asObservable();
|
||||
}
|
||||
|
||||
/** Returns an observable that emits when the overlay has been detached. */
|
||||
detachments(): Observable<void> {
|
||||
return this._detachments.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current state config of the overlay.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {PositionStrategy} from './position/position-strategy';
|
||||
import {LayoutDirection} from '../rtl/dir';
|
||||
import {ScrollStrategy} from './scroll/scroll-strategy';
|
||||
import {NoopScrollStrategy} from './scroll/noop-scroll-strategy';
|
||||
|
||||
|
||||
/**
|
||||
@@ -10,6 +12,9 @@ export class OverlayState {
|
||||
/** Strategy with which to position the overlay. */
|
||||
positionStrategy: PositionStrategy;
|
||||
|
||||
/** Strategy to be used when handling scroll events while the overlay is open. */
|
||||
scrollStrategy: ScrollStrategy = new NoopScrollStrategy();
|
||||
|
||||
/** Whether the overlay has a backdrop. */
|
||||
hasBackdrop: boolean = false;
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import {TemplatePortal, ComponentPortal} from '../portal/portal';
|
||||
import {Overlay} from './overlay';
|
||||
import {OverlayContainer} from './overlay-container';
|
||||
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';
|
||||
|
||||
|
||||
describe('Overlay', () => {
|
||||
@@ -135,6 +137,44 @@ describe('Overlay', () => {
|
||||
expect(pane.getAttribute('dir')).toEqual('rtl');
|
||||
});
|
||||
|
||||
it('should emit when an overlay is attached', () => {
|
||||
let overlayRef = overlay.create();
|
||||
let spy = jasmine.createSpy('attachments spy');
|
||||
|
||||
overlayRef.attachments().subscribe(spy);
|
||||
overlayRef.attach(componentPortal);
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit when an overlay is detached', () => {
|
||||
let overlayRef = overlay.create();
|
||||
let spy = jasmine.createSpy('detachments spy');
|
||||
|
||||
overlayRef.detachments().subscribe(spy);
|
||||
overlayRef.attach(componentPortal);
|
||||
overlayRef.detach();
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit and complete the observables when an overlay is disposed', () => {
|
||||
let overlayRef = overlay.create();
|
||||
let disposeSpy = jasmine.createSpy('dispose spy');
|
||||
let attachCompleteSpy = jasmine.createSpy('attachCompleteSpy spy');
|
||||
let detachCompleteSpy = jasmine.createSpy('detachCompleteSpy spy');
|
||||
|
||||
overlayRef.attachments().subscribe(null, null, attachCompleteSpy);
|
||||
overlayRef.detachments().subscribe(disposeSpy, null, detachCompleteSpy);
|
||||
|
||||
overlayRef.attach(componentPortal);
|
||||
overlayRef.dispose();
|
||||
|
||||
expect(disposeSpy).toHaveBeenCalled();
|
||||
expect(attachCompleteSpy).toHaveBeenCalled();
|
||||
expect(detachCompleteSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('positioning', () => {
|
||||
let state: OverlayState;
|
||||
|
||||
@@ -295,6 +335,48 @@ describe('Overlay', () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('scroll strategy', () => {
|
||||
let fakeScrollStrategy: FakeScrollStrategy;
|
||||
let config: OverlayState;
|
||||
|
||||
beforeEach(() => {
|
||||
config = new OverlayState();
|
||||
fakeScrollStrategy = new FakeScrollStrategy();
|
||||
config.scrollStrategy = fakeScrollStrategy;
|
||||
});
|
||||
|
||||
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.');
|
||||
|
||||
overlayRef.detach();
|
||||
expect(fakeScrollStrategy.isEnabled).toBe(false, 'Expected scroll strategy to be disabled.');
|
||||
});
|
||||
|
||||
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.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('OverlayContainer theming', () => {
|
||||
@@ -365,3 +447,19 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ let defaultState = new OverlayState();
|
||||
*
|
||||
* An overlay *is* a PortalHost, so any kind of Portal can be loaded into one.
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable()
|
||||
export class Overlay {
|
||||
constructor(private _overlayContainer: OverlayContainer,
|
||||
private _componentFactoryResolver: ComponentFactoryResolver,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import {inject, TestBed, async} from '@angular/core/testing';
|
||||
import {NgModule, Component} from '@angular/core';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {
|
||||
PortalModule,
|
||||
ComponentPortal,
|
||||
Overlay,
|
||||
OverlayState,
|
||||
OverlayRef,
|
||||
OverlayModule,
|
||||
ScrollStrategy,
|
||||
ScrollDispatcher,
|
||||
CloseScrollStrategy,
|
||||
} from '../../core';
|
||||
|
||||
|
||||
describe('CloseScrollStrategy', () => {
|
||||
let overlayRef: OverlayRef;
|
||||
let componentPortal: ComponentPortal<MozarellaMsg>;
|
||||
let scrolledSubject = new Subject();
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [OverlayModule, PortalModule, OverlayTestModule],
|
||||
providers: [
|
||||
{provide: ScrollDispatcher, useFactory: () => {
|
||||
return {scrolled: (delay: number, callback: () => any) => {
|
||||
return scrolledSubject.asObservable().subscribe(callback);
|
||||
}};
|
||||
}}
|
||||
]
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(inject([Overlay, ScrollDispatcher], (overlay: Overlay,
|
||||
scrollDispatcher: ScrollDispatcher) => {
|
||||
|
||||
let overlayState = new OverlayState();
|
||||
overlayState.scrollStrategy = new CloseScrollStrategy(scrollDispatcher);
|
||||
overlayRef = overlay.create(overlayState);
|
||||
componentPortal = new ComponentPortal(MozarellaMsg);
|
||||
}));
|
||||
|
||||
it('should detach the overlay as soon as the user scrolls', () => {
|
||||
overlayRef.attach(componentPortal);
|
||||
spyOn(overlayRef, 'detach');
|
||||
|
||||
scrolledSubject.next();
|
||||
expect(overlayRef.detach).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not attempt to detach the overlay after it has been detached', () => {
|
||||
overlayRef.attach(componentPortal);
|
||||
overlayRef.detach();
|
||||
|
||||
spyOn(overlayRef, 'detach');
|
||||
scrolledSubject.next();
|
||||
|
||||
expect(overlayRef.detach).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
/** Simple component that we can attach to the overlay. */
|
||||
@Component({template: '<p>Mozarella</p>'})
|
||||
class MozarellaMsg { }
|
||||
|
||||
|
||||
/** Test module to hold the component. */
|
||||
@NgModule({
|
||||
imports: [OverlayModule, PortalModule],
|
||||
declarations: [MozarellaMsg],
|
||||
entryComponents: [MozarellaMsg],
|
||||
})
|
||||
class OverlayTestModule { }
|
||||
@@ -0,0 +1,38 @@
|
||||
import {ScrollStrategy} from './scroll-strategy';
|
||||
import {OverlayRef} from '../overlay-ref';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {ScrollDispatcher} from './scroll-dispatcher';
|
||||
|
||||
|
||||
/**
|
||||
* Strategy that will close the overlay as soon as the user starts scrolling.
|
||||
*/
|
||||
export class CloseScrollStrategy implements ScrollStrategy {
|
||||
private _scrollSubscription: Subscription|null = null;
|
||||
private _overlayRef: OverlayRef;
|
||||
|
||||
constructor(private _scrollDispatcher: ScrollDispatcher) { }
|
||||
|
||||
attach(overlayRef: OverlayRef) {
|
||||
this._overlayRef = overlayRef;
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (!this._scrollSubscription) {
|
||||
this._scrollSubscription = this._scrollDispatcher.scrolled(null, () => {
|
||||
if (this._overlayRef.hasAttached()) {
|
||||
this._overlayRef.detach();
|
||||
}
|
||||
|
||||
this.disable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
disable() {
|
||||
if (this._scrollSubscription) {
|
||||
this._scrollSubscription.unsubscribe();
|
||||
this._scrollSubscription = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import {ScrollStrategy} from './scroll-strategy';
|
||||
|
||||
/**
|
||||
* Scroll strategy that doesn't do anything.
|
||||
*/
|
||||
export class NoopScrollStrategy implements ScrollStrategy {
|
||||
enable() { }
|
||||
disable() { }
|
||||
attach() { }
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import {inject, TestBed, async} from '@angular/core/testing';
|
||||
import {NgModule, Component} from '@angular/core';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {
|
||||
PortalModule,
|
||||
ComponentPortal,
|
||||
Overlay,
|
||||
OverlayState,
|
||||
OverlayRef,
|
||||
OverlayModule,
|
||||
ScrollStrategy,
|
||||
ScrollDispatcher,
|
||||
RepositionScrollStrategy,
|
||||
} from '../../core';
|
||||
|
||||
|
||||
describe('RepositionScrollStrategy', () => {
|
||||
let overlayRef: OverlayRef;
|
||||
let componentPortal: ComponentPortal<PastaMsg>;
|
||||
let scrolledSubject = new Subject();
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [OverlayModule, PortalModule, OverlayTestModule],
|
||||
providers: [
|
||||
{provide: ScrollDispatcher, useFactory: () => {
|
||||
return {scrolled: (delay: number, callback: () => any) => {
|
||||
return scrolledSubject.asObservable().subscribe(callback);
|
||||
}};
|
||||
}}
|
||||
]
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(inject([Overlay, ScrollDispatcher], (overlay: Overlay,
|
||||
scrollDispatcher: ScrollDispatcher) => {
|
||||
|
||||
let overlayState = new OverlayState();
|
||||
overlayState.scrollStrategy = new RepositionScrollStrategy(scrollDispatcher);
|
||||
overlayRef = overlay.create(overlayState);
|
||||
componentPortal = new ComponentPortal(PastaMsg);
|
||||
}));
|
||||
|
||||
it('should update the overlay position when the page is scrolled', () => {
|
||||
overlayRef.attach(componentPortal);
|
||||
spyOn(overlayRef, 'updatePosition');
|
||||
|
||||
scrolledSubject.next();
|
||||
expect(overlayRef.updatePosition).toHaveBeenCalledTimes(1);
|
||||
|
||||
scrolledSubject.next();
|
||||
expect(overlayRef.updatePosition).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should not be updating the position after the overlay is detached', () => {
|
||||
overlayRef.attach(componentPortal);
|
||||
spyOn(overlayRef, 'updatePosition');
|
||||
|
||||
overlayRef.detach();
|
||||
scrolledSubject.next();
|
||||
|
||||
expect(overlayRef.updatePosition).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not be updating the position after the overlay is destroyed', () => {
|
||||
overlayRef.attach(componentPortal);
|
||||
spyOn(overlayRef, 'updatePosition');
|
||||
|
||||
overlayRef.dispose();
|
||||
scrolledSubject.next();
|
||||
|
||||
expect(overlayRef.updatePosition).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
/** Simple component that we can attach to the overlay. */
|
||||
@Component({template: '<p>Pasta</p>'})
|
||||
class PastaMsg { }
|
||||
|
||||
|
||||
/** Test module to hold the component. */
|
||||
@NgModule({
|
||||
imports: [OverlayModule, PortalModule],
|
||||
declarations: [PastaMsg],
|
||||
entryComponents: [PastaMsg],
|
||||
})
|
||||
class OverlayTestModule { }
|
||||
@@ -0,0 +1,34 @@
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {ScrollStrategy} from './scroll-strategy';
|
||||
import {OverlayRef} from '../overlay-ref';
|
||||
import {ScrollDispatcher} from './scroll-dispatcher';
|
||||
|
||||
|
||||
/**
|
||||
* Strategy that will update the element position as the user is scrolling.
|
||||
*/
|
||||
export class RepositionScrollStrategy implements ScrollStrategy {
|
||||
private _scrollSubscription: Subscription|null = null;
|
||||
private _overlayRef: OverlayRef;
|
||||
|
||||
constructor(private _scrollDispatcher: ScrollDispatcher, private _scrollThrottle = 0) { }
|
||||
|
||||
attach(overlayRef: OverlayRef) {
|
||||
this._overlayRef = overlayRef;
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (!this._scrollSubscription) {
|
||||
this._scrollSubscription = this._scrollDispatcher.scrolled(this._scrollThrottle, () => {
|
||||
this._overlayRef.updatePosition();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
disable() {
|
||||
if (this._scrollSubscription) {
|
||||
this._scrollSubscription.unsubscribe();
|
||||
this._scrollSubscription = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {OverlayRef} from '../overlay-ref';
|
||||
|
||||
/**
|
||||
* Describes a strategy that will be used by an overlay
|
||||
* to handle scroll events while it is open.
|
||||
*/
|
||||
export interface ScrollStrategy {
|
||||
enable: () => void;
|
||||
disable: () => void;
|
||||
attach: (overlayRef: OverlayRef) => void;
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
ConnectedPositionStrategy,
|
||||
HorizontalConnectionPos,
|
||||
VerticalConnectionPos,
|
||||
RepositionScrollStrategy,
|
||||
ScrollDispatcher,
|
||||
} from '../core';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {MenuPositionX, MenuPositionY} from './menu-positions';
|
||||
@@ -78,7 +80,8 @@ 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 _viewContainerRef: ViewContainerRef, @Optional() private _dir: Dir,
|
||||
private _scrollDispatcher: ScrollDispatcher) { }
|
||||
|
||||
ngAfterViewInit() {
|
||||
this._checkMenu();
|
||||
@@ -216,6 +219,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);
|
||||
return overlayState;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ import {coerceBooleanProperty} from '../core/coercion/boolean-property';
|
||||
import {ConnectedOverlayDirective} from '../core/overlay/overlay-directives';
|
||||
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
|
||||
import {SelectionModel} from '../core/selection/selection';
|
||||
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
|
||||
import {MdSelectDynamicMultipleError, MdSelectNonArrayValueError} from './select-errors';
|
||||
import 'rxjs/add/observable/merge';
|
||||
import 'rxjs/add/operator/startWith';
|
||||
@@ -134,9 +133,6 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
|
||||
/** Subscription to tab events while overlay is focused. */
|
||||
private _tabSubscription: Subscription;
|
||||
|
||||
/** Subscription to global scrolled events while the select is open. */
|
||||
private _scrollSubscription: Subscription;
|
||||
|
||||
/** Whether filling out the select is required in the form. */
|
||||
private _required: boolean = false;
|
||||
|
||||
@@ -314,8 +310,7 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
|
||||
|
||||
constructor(private _element: ElementRef, private _renderer: Renderer2,
|
||||
private _viewportRuler: ViewportRuler, private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _scrollDispatcher: ScrollDispatcher, @Optional() private _dir: Dir,
|
||||
@Self() @Optional() public _control: NgControl,
|
||||
@Optional() private _dir: Dir, @Self() @Optional() public _control: NgControl,
|
||||
@Attribute('tabindex') tabIndex: string) {
|
||||
|
||||
if (this._control) {
|
||||
@@ -374,9 +369,6 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
|
||||
this._calculateOverlayPosition();
|
||||
this._placeholderState = this._floatPlaceholderState();
|
||||
this._panelOpen = true;
|
||||
this._scrollSubscription = this._scrollDispatcher.scrolled(0, () => {
|
||||
this.overlayDir.overlayRef.updatePosition();
|
||||
});
|
||||
}
|
||||
|
||||
/** Closes the overlay panel and focuses the host element. */
|
||||
@@ -388,11 +380,6 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
|
||||
this._placeholderState = '';
|
||||
}
|
||||
|
||||
if (this._scrollSubscription) {
|
||||
this._scrollSubscription.unsubscribe();
|
||||
this._scrollSubscription = null;
|
||||
}
|
||||
|
||||
this._focusHost();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,14 +444,6 @@ describe('MdTooltip', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
it('does not throw an error on destroy', () => {
|
||||
const fixture = TestBed.createComponent(BasicTooltipDemo);
|
||||
fixture.detectChanges();
|
||||
delete fixture.componentInstance.tooltip.scrollSubscription;
|
||||
expect(fixture.destroy.bind(fixture)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Optional,
|
||||
OnDestroy,
|
||||
Renderer2,
|
||||
OnInit,
|
||||
ChangeDetectorRef,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
ComponentPortal,
|
||||
OverlayConnectionPosition,
|
||||
OriginConnectionPosition,
|
||||
RepositionScrollStrategy,
|
||||
} from '../core';
|
||||
import {MdTooltipInvalidPositionError} from './tooltip-errors';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
@@ -34,13 +34,12 @@ import {Dir} from '../core/rtl/dir';
|
||||
import {Platform} from '../core/platform/index';
|
||||
import 'rxjs/add/operator/first';
|
||||
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
|
||||
|
||||
export type TooltipPosition = 'left' | 'right' | 'above' | 'below' | 'before' | 'after';
|
||||
|
||||
/** Time in ms to delay before changing the tooltip visibility to hidden */
|
||||
export const TOUCHEND_HIDE_DELAY = 1500;
|
||||
export const TOUCHEND_HIDE_DELAY = 1500;
|
||||
|
||||
/** Time in ms to throttle repositioning after scroll events. */
|
||||
export const SCROLL_THROTTLE_MS = 20;
|
||||
@@ -59,10 +58,9 @@ export const SCROLL_THROTTLE_MS = 20;
|
||||
},
|
||||
exportAs: 'mdTooltip',
|
||||
})
|
||||
export class MdTooltip implements OnInit, OnDestroy {
|
||||
export class MdTooltip implements OnDestroy {
|
||||
_overlayRef: OverlayRef;
|
||||
_tooltipInstance: TooltipComponent;
|
||||
scrollSubscription: Subscription;
|
||||
|
||||
private _position: TooltipPosition = 'below';
|
||||
private _disabled: boolean = false;
|
||||
@@ -164,16 +162,6 @@ export class MdTooltip implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// When a scroll on the page occurs, update the position in case this tooltip needs
|
||||
// to be repositioned.
|
||||
this.scrollSubscription = this._scrollDispatcher.scrolled(SCROLL_THROTTLE_MS, () => {
|
||||
if (this._overlayRef) {
|
||||
this._overlayRef.updatePosition();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the tooltip when destroyed.
|
||||
*/
|
||||
@@ -181,10 +169,6 @@ export class MdTooltip implements OnInit, OnDestroy {
|
||||
if (this._tooltipInstance) {
|
||||
this._disposeTooltip();
|
||||
}
|
||||
|
||||
if (this.scrollSubscription) {
|
||||
this.scrollSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the tooltip after the delay in ms, defaults to tooltip-delay-show or 0ms if no input */
|
||||
@@ -249,6 +233,8 @@ export class MdTooltip implements OnInit, OnDestroy {
|
||||
});
|
||||
let config = new OverlayState();
|
||||
config.positionStrategy = strategy;
|
||||
config.scrollStrategy =
|
||||
new RepositionScrollStrategy(this._scrollDispatcher, SCROLL_THROTTLE_MS);
|
||||
|
||||
this._overlayRef = this._overlay.create(config);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user