Skip to content

refactor(scroll-dispatcher): API improvements #7630

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/cdk/overlay/position/connected-position-strategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {ConnectedPositionStrategy} from './connected-position-strategy';
import {ViewportRuler, VIEWPORT_RULER_PROVIDER} from '@angular/cdk/scrolling';
import {OverlayPositionBuilder} from './overlay-position-builder';
import {ConnectedOverlayPositionChange} from './connected-position';
import {Scrollable} from '@angular/cdk/scrolling';
import {CdkScrollable} from '@angular/cdk/scrolling';
import {Subscription} from 'rxjs/Subscription';
import {ScrollDispatchModule} from '@angular/cdk/scrolling';
import {OverlayRef} from '../overlay-ref';
Expand Down Expand Up @@ -545,7 +545,7 @@ describe('ConnectedPositionStrategy', () => {
{overlayX: 'start', overlayY: 'top'});

strategy.withScrollableContainers([
new Scrollable(new FakeElementRef(scrollable), null!, null!, null!)]);
new CdkScrollable(new FakeElementRef(scrollable), null!, null!, null!)]);
strategy.attach(fakeOverlayRef(overlayElement));
positionChangeHandler = jasmine.createSpy('positionChangeHandler');
onPositionChangeSubscription = strategy.onPositionChange.subscribe(positionChangeHandler);
Expand Down
6 changes: 3 additions & 3 deletions src/cdk/overlay/position/connected-position-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import {Subject} from 'rxjs/Subject';
import {Subscription} from 'rxjs/Subscription';
import {Observable} from 'rxjs/Observable';
import {Scrollable} from '@angular/cdk/scrolling';
import {CdkScrollable} from '@angular/cdk/scrolling';
import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scroll-clip';
import {OverlayRef} from '../overlay-ref';

Expand All @@ -46,7 +46,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
private _offsetY: number = 0;

/** The Scrollable containers used to check scrollable view properties on position change. */
private scrollables: Scrollable[] = [];
private scrollables: CdkScrollable[] = [];

/** Subscription to viewport resize events. */
private _resizeSubscription = Subscription.EMPTY;
Expand Down Expand Up @@ -176,7 +176,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
* on reposition we can evaluate if it or the overlay has been clipped or outside view. Every
* Scrollable must be an ancestor element of the strategy's origin element.
*/
withScrollableContainers(scrollables: Scrollable[]) {
withScrollableContainers(scrollables: CdkScrollable[]) {
this.scrollables = scrollables;
}

Expand Down
2 changes: 1 addition & 1 deletion src/cdk/overlay/scroll/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

export {Scrollable, ScrollDispatcher} from '@angular/cdk/scrolling';
export {CdkScrollable, ScrollDispatcher} from '@angular/cdk/scrolling';

// Export pre-defined scroll strategies and interface to build custom ones.
export {ScrollStrategy} from './scroll-strategy';
Expand Down
50 changes: 38 additions & 12 deletions src/cdk/scrolling/scroll-dispatcher.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {inject, TestBed, async, fakeAsync, ComponentFixture, tick} from '@angular/core/testing';
import {NgModule, Component, ViewChild, ElementRef} from '@angular/core';
import {Scrollable, ScrollDispatcher, ScrollDispatchModule} from './public-api';
import {CdkScrollable, ScrollDispatcher, ScrollDispatchModule} from './public-api';
import {dispatchFakeEvent} from '@angular/cdk/testing';

describe('Scroll Dispatcher', () => {
Expand All @@ -26,15 +26,15 @@ describe('Scroll Dispatcher', () => {

it('should be registered with the scrollable directive with the scroll service', () => {
const componentScrollable = fixture.componentInstance.scrollable;
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(true);
expect(scroll.scrollContainers.has(componentScrollable)).toBe(true);
});

it('should have the scrollable directive deregistered when the component is destroyed', () => {
const componentScrollable = fixture.componentInstance.scrollable;
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(true);
expect(scroll.scrollContainers.has(componentScrollable)).toBe(true);

fixture.destroy();
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(false);
expect(scroll.scrollContainers.has(componentScrollable)).toBe(false);
});

it('should notify through the directive and service that a scroll event occurred',
Expand All @@ -52,7 +52,7 @@ describe('Scroll Dispatcher', () => {
// Emit a scroll event from the scrolling element in our component.
// This event should be picked up by the scrollable directive and notify.
// The notification should be picked up by the service.
dispatchFakeEvent(fixture.componentInstance.scrollingElement.nativeElement, 'scroll');
dispatchFakeEvent(fixture.componentInstance.scrollingElement.nativeElement, 'scroll', false);

// The scrollable directive should have notified the service immediately.
expect(directiveSpy).toHaveBeenCalled();
Expand All @@ -71,7 +71,7 @@ describe('Scroll Dispatcher', () => {
const subscription = fixture.ngZone!.onUnstable.subscribe(spy);

scroll.scrolled(0).subscribe(() => {});
dispatchFakeEvent(document, 'scroll');
dispatchFakeEvent(document, 'scroll', false);

expect(spy).not.toHaveBeenCalled();
subscription.unsubscribe();
Expand All @@ -81,7 +81,7 @@ describe('Scroll Dispatcher', () => {
const spy = jasmine.createSpy('zone unstable callback');
const subscription = fixture.ngZone!.onUnstable.subscribe(spy);

dispatchFakeEvent(fixture.componentInstance.scrollingElement.nativeElement, 'scroll');
dispatchFakeEvent(fixture.componentInstance.scrollingElement.nativeElement, 'scroll', false);

expect(spy).not.toHaveBeenCalled();
subscription.unsubscribe();
Expand All @@ -91,11 +91,11 @@ describe('Scroll Dispatcher', () => {
const spy = jasmine.createSpy('global scroll callback');
const subscription = scroll.scrolled(0).subscribe(spy);

dispatchFakeEvent(document, 'scroll');
dispatchFakeEvent(document, 'scroll', false);
expect(spy).toHaveBeenCalledTimes(1);

subscription.unsubscribe();
dispatchFakeEvent(document, 'scroll');
dispatchFakeEvent(document, 'scroll', false);

expect(spy).toHaveBeenCalledTimes(1);
});
Expand All @@ -104,22 +104,48 @@ describe('Scroll Dispatcher', () => {
describe('Nested scrollables', () => {
let scroll: ScrollDispatcher;
let fixture: ComponentFixture<NestedScrollingComponent>;
let element: ElementRef;

beforeEach(inject([ScrollDispatcher], (s: ScrollDispatcher) => {
scroll = s;

fixture = TestBed.createComponent(NestedScrollingComponent);
fixture.detectChanges();
element = fixture.componentInstance.interestingElement;
}));

it('should be able to identify the containing scrollables of an element', () => {
const interestingElement = fixture.componentInstance.interestingElement;
const scrollContainers = scroll.getScrollContainers(interestingElement);
const scrollContainers = scroll.getAncestorScrollContainers(element);
const scrollableElementIds =
scrollContainers.map(scrollable => scrollable.getElementRef().nativeElement.id);

expect(scrollableElementIds).toEqual(['scrollable-1', 'scrollable-1a']);
});

it('should emit when one of the ancestor scrollable containers is scrolled', () => {
const spy = jasmine.createSpy('scroll spy');
const subscription = scroll.ancestorScrolled(element, 0).subscribe(spy);
const grandparent = fixture.debugElement.nativeElement.querySelector('#scrollable-1');

dispatchFakeEvent(grandparent, 'scroll', false);
expect(spy).toHaveBeenCalledTimes(1);

dispatchFakeEvent(window.document, 'scroll', false);
expect(spy).toHaveBeenCalledTimes(2);

subscription.unsubscribe();
});

it('should not emit when a non-ancestor is scrolled', () => {
const spy = jasmine.createSpy('scroll spy');
const subscription = scroll.ancestorScrolled(element, 0).subscribe(spy);
const stranger = fixture.debugElement.nativeElement.querySelector('#scrollable-2');

dispatchFakeEvent(stranger, 'scroll', false);
expect(spy).not.toHaveBeenCalled();

subscription.unsubscribe();
});
});

describe('lazy subscription', () => {
Expand Down Expand Up @@ -172,7 +198,7 @@ describe('Scroll Dispatcher', () => {
template: `<div #scrollingElement cdk-scrollable style="height: 9999px"></div>`
})
class ScrollingComponent {
@ViewChild(Scrollable) scrollable: Scrollable;
@ViewChild(CdkScrollable) scrollable: CdkScrollable;
@ViewChild('scrollingElement') scrollingElement: ElementRef;
}

Expand Down
52 changes: 34 additions & 18 deletions src/cdk/scrolling/scroll-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {Subscription} from 'rxjs/Subscription';
import {Observable} from 'rxjs/Observable';
import {fromEvent} from 'rxjs/observable/fromEvent';
import {of as observableOf} from 'rxjs/observable/of';
import {auditTime} from 'rxjs/operator/auditTime';
import {Scrollable} from './scrollable';
import {auditTime, filter} from '@angular/cdk/rxjs';
import {CdkScrollable} from './scrollable';


/** Time in ms to throttle the scrolling events by default. */
Expand All @@ -29,7 +29,7 @@ export class ScrollDispatcher {
constructor(private _ngZone: NgZone, private _platform: Platform) { }

/** Subject for notifying that a registered scrollable reference element has been scrolled. */
private _scrolled: Subject<void> = new Subject<void>();
private _scrolled = new Subject<CdkScrollable|void>();

/** Keeps track of the global `scroll` and `resize` subscriptions. */
_globalSubscription: Subscription | null = null;
Expand All @@ -41,28 +41,30 @@ export class ScrollDispatcher {
* Map of all the scrollable references that are registered with the service and their
* scroll event subscriptions.
*/
scrollableReferences: Map<Scrollable, Subscription> = new Map();
scrollContainers: Map<CdkScrollable, Subscription> = new Map();

/**
* Registers a Scrollable with the service and listens for its scrolled events. When the
* scrollable is scrolled, the service emits the event in its scrolled observable.
* Registers a scrollable instance with the service and listens for its scrolled events. When the
* scrollable is scrolled, the service emits the event to its scrolled observable.
* @param scrollable Scrollable instance to be registered.
*/
register(scrollable: Scrollable): void {
const scrollSubscription = scrollable.elementScrolled().subscribe(() => this._scrolled.next());
this.scrollableReferences.set(scrollable, scrollSubscription);
register(scrollable: CdkScrollable): void {
const scrollSubscription = scrollable.elementScrolled()
.subscribe(() => this._scrolled.next(scrollable));

this.scrollContainers.set(scrollable, scrollSubscription);
}

/**
* Deregisters a Scrollable reference and unsubscribes from its scroll event observable.
* @param scrollable Scrollable instance to be deregistered.
*/
deregister(scrollable: Scrollable): void {
const scrollableReference = this.scrollableReferences.get(scrollable);
deregister(scrollable: CdkScrollable): void {
const scrollableReference = this.scrollContainers.get(scrollable);

if (scrollableReference) {
scrollableReference.unsubscribe();
this.scrollableReferences.delete(scrollable);
this.scrollContainers.delete(scrollable);
}
}

Expand All @@ -71,7 +73,7 @@ export class ScrollDispatcher {
* references (or window, document, or body) fire a scrolled event. Can provide a time in ms
* to override the default "throttle" time.
*/
scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME): Observable<void> {
scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME): Observable<CdkScrollable|void> {
return this._platform.isBrowser ? Observable.create(observer => {
if (!this._globalSubscription) {
this._addGlobalListener();
Expand All @@ -97,12 +99,26 @@ export class ScrollDispatcher {
}) : observableOf<void>();
}

/**
* Returns an observable that emits whenever any of the
* scrollable ancestors of an element are scrolled.
* @param elementRef Element whose ancestors to listen for.
* @param auditTimeInMs Time to throttle the scroll events.
*/
ancestorScrolled(elementRef: ElementRef, auditTimeInMs?: number): Observable<CdkScrollable> {
const ancestors = this.getAncestorScrollContainers(elementRef);

return filter.call(this.scrolled(auditTimeInMs), target => {
return !target || ancestors.indexOf(target) > -1;
});
}

/** Returns all registered Scrollables that contain the provided element. */
getScrollContainers(elementRef: ElementRef): Scrollable[] {
const scrollingContainers: Scrollable[] = [];
getAncestorScrollContainers(elementRef: ElementRef): CdkScrollable[] {
const scrollingContainers: CdkScrollable[] = [];

this.scrollableReferences.forEach((_subscription: Subscription, scrollable: Scrollable) => {
if (this.scrollableContainsElement(scrollable, elementRef)) {
this.scrollContainers.forEach((_subscription: Subscription, scrollable: CdkScrollable) => {
if (this._scrollableContainsElement(scrollable, elementRef)) {
scrollingContainers.push(scrollable);
}
});
Expand All @@ -111,7 +127,7 @@ export class ScrollDispatcher {
}

/** Returns true if the element is contained within the provided Scrollable. */
scrollableContainsElement(scrollable: Scrollable, elementRef: ElementRef): boolean {
private _scrollableContainsElement(scrollable: CdkScrollable, elementRef: ElementRef): boolean {
let element = elementRef.nativeElement;
let scrollableElement = scrollable.getElementRef().nativeElement;

Expand Down
2 changes: 1 addition & 1 deletion src/cdk/scrolling/scrollable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {ScrollDispatcher} from './scroll-dispatcher';
@Directive({
selector: '[cdk-scrollable], [cdkScrollable]'
})
export class Scrollable implements OnInit, OnDestroy {
export class CdkScrollable implements OnInit, OnDestroy {
private _elementScrolled: Subject<Event> = new Subject();
private _scrollListener: Function | null;

Expand Down
6 changes: 3 additions & 3 deletions src/cdk/scrolling/scrolling-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@

import {NgModule} from '@angular/core';
import {SCROLL_DISPATCHER_PROVIDER} from './scroll-dispatcher';
import {Scrollable} from './scrollable';
import {CdkScrollable} from './scrollable';
import {PlatformModule} from '@angular/cdk/platform';

@NgModule({
imports: [PlatformModule],
exports: [Scrollable],
declarations: [Scrollable],
exports: [CdkScrollable],
declarations: [CdkScrollable],
providers: [SCROLL_DISPATCHER_PROVIDER],
})
export class ScrollDispatchModule {}
4 changes: 2 additions & 2 deletions src/cdk/testing/dispatch-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export function dispatchEvent(node: Node | Window, event: Event): Event {
}

/** Shorthand to dispatch a fake event on a specified node. */
export function dispatchFakeEvent(node: Node | Window, type: string): Event {
return dispatchEvent(node, createFakeEvent(type));
export function dispatchFakeEvent(node: Node | Window, type: string, canBubble?: boolean): Event {
return dispatchEvent(node, createFakeEvent(type, canBubble));
}

/** Shorthand to dispatch a keyboard event with a specified key code. */
Expand Down
4 changes: 2 additions & 2 deletions src/cdk/testing/event-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ export function createKeyboardEvent(type: string, keyCode: number, target?: Elem
}

/** Creates a fake event object with any desired event type. */
export function createFakeEvent(type: string) {
export function createFakeEvent(type: string, canBubble = true, cancelable = true) {
const event = document.createEvent('Event');
event.initEvent(type, true, true);
event.initEvent(type, canBubble, cancelable);
return event;
}
4 changes: 2 additions & 2 deletions src/lib/tooltip/tooltip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {AnimationEvent} from '@angular/animations';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {OverlayContainer, OverlayModule, Scrollable} from '@angular/cdk/overlay';
import {OverlayContainer, OverlayModule, CdkScrollable} from '@angular/cdk/overlay';
import {Platform} from '@angular/cdk/platform';
import {dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing';
import {ESCAPE} from '@angular/cdk/keycodes';
Expand Down Expand Up @@ -686,7 +686,7 @@ class ScrollableTooltipDemo {
message: string = initialTooltipMessage;
showButton: boolean = true;

@ViewChild(Scrollable) scrollingContainer: Scrollable;
@ViewChild(CdkScrollable) scrollingContainer: CdkScrollable;

scrollDown() {
const scrollingContainerEl = this.scrollingContainer.getElementRef().nativeElement;
Expand Down
6 changes: 5 additions & 1 deletion src/lib/tooltip/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,11 @@ export class MatTooltip implements OnDestroy {
.connectedTo(this._elementRef, origin.main, overlay.main)
.withFallbackPosition(origin.fallback, overlay.fallback);

strategy.withScrollableContainers(this._scrollDispatcher.getScrollContainers(this._elementRef));
const scrollableAncestors = this._scrollDispatcher
.getAncestorScrollContainers(this._elementRef);

strategy.withScrollableContainers(scrollableAncestors);

strategy.onPositionChange.subscribe(change => {
if (this._tooltipInstance) {
if (change.scrollableViewProperties.isOverlayClipped && this._tooltipInstance.isVisible()) {
Expand Down