Skip to content

Commit b2fbea7

Browse files
crisbetoandrewseguin
authored andcommitted
refactor(scroll-dispatcher): API improvements (#7630)
* Adds the `ancestorScrolled` method that allows for the consumer to subscribe to scroll events coming from the element's parent scrollables. * The `scrolled` and `ancestorScrolled` streams now expose the scrollable that was scrolled. BREAKING CHANGES: * The `getScrollContainers` method has been renamed to `getAncestorScrollContainers` to better describe what it does. * The `ScrollDispatcher.scrollableReferences` property has been renamed to `scrollContainers`. * The `ScrollDispatcher.scrollableContainsElement` method has been removed. * The `Scrollable` class has been renamed to `CdkScrollable` for consistency.
1 parent 80671bf commit b2fbea7

11 files changed

+93
-47
lines changed

src/cdk/overlay/position/connected-position-strategy.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {ConnectedPositionStrategy} from './connected-position-strategy';
44
import {ViewportRuler, VIEWPORT_RULER_PROVIDER} from '@angular/cdk/scrolling';
55
import {OverlayPositionBuilder} from './overlay-position-builder';
66
import {ConnectedOverlayPositionChange} from './connected-position';
7-
import {Scrollable} from '@angular/cdk/scrolling';
7+
import {CdkScrollable} from '@angular/cdk/scrolling';
88
import {Subscription} from 'rxjs/Subscription';
99
import {ScrollDispatchModule} from '@angular/cdk/scrolling';
1010
import {OverlayRef} from '../overlay-ref';
@@ -567,7 +567,7 @@ describe('ConnectedPositionStrategy', () => {
567567
{overlayX: 'start', overlayY: 'top'});
568568

569569
strategy.withScrollableContainers([
570-
new Scrollable(new FakeElementRef(scrollable), null!, null!, null!)]);
570+
new CdkScrollable(new FakeElementRef(scrollable), null!, null!, null!)]);
571571
strategy.attach(fakeOverlayRef(overlayElement));
572572
positionChangeHandler = jasmine.createSpy('positionChangeHandler');
573573
onPositionChangeSubscription = strategy.onPositionChange.subscribe(positionChangeHandler);

src/cdk/overlay/position/connected-position-strategy.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import {Subject} from 'rxjs/Subject';
2020
import {Subscription} from 'rxjs/Subscription';
2121
import {Observable} from 'rxjs/Observable';
22-
import {Scrollable} from '@angular/cdk/scrolling';
22+
import {CdkScrollable} from '@angular/cdk/scrolling';
2323
import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scroll-clip';
2424
import {OverlayRef} from '../overlay-ref';
2525

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

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

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

src/cdk/overlay/scroll/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
export {Scrollable, ScrollDispatcher} from '@angular/cdk/scrolling';
9+
export {CdkScrollable, ScrollDispatcher} from '@angular/cdk/scrolling';
1010

1111
// Export pre-defined scroll strategies and interface to build custom ones.
1212
export {ScrollStrategy} from './scroll-strategy';

src/cdk/scrolling/scroll-dispatcher.spec.ts

+38-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {inject, TestBed, async, fakeAsync, ComponentFixture, tick} from '@angular/core/testing';
22
import {NgModule, Component, ViewChild, ElementRef} from '@angular/core';
3-
import {Scrollable, ScrollDispatcher, ScrollDispatchModule} from './public-api';
3+
import {CdkScrollable, ScrollDispatcher, ScrollDispatchModule} from './public-api';
44
import {dispatchFakeEvent} from '@angular/cdk/testing';
55

66
describe('Scroll Dispatcher', () => {
@@ -26,15 +26,15 @@ describe('Scroll Dispatcher', () => {
2626

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

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

3636
fixture.destroy();
37-
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(false);
37+
expect(scroll.scrollContainers.has(componentScrollable)).toBe(false);
3838
});
3939

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

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

7373
scroll.scrolled(0).subscribe(() => {});
74-
dispatchFakeEvent(document, 'scroll');
74+
dispatchFakeEvent(document, 'scroll', false);
7575

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

84-
dispatchFakeEvent(fixture.componentInstance.scrollingElement.nativeElement, 'scroll');
84+
dispatchFakeEvent(fixture.componentInstance.scrollingElement.nativeElement, 'scroll', false);
8585

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

94-
dispatchFakeEvent(document, 'scroll');
94+
dispatchFakeEvent(document, 'scroll', false);
9595
expect(spy).toHaveBeenCalledTimes(1);
9696

9797
subscription.unsubscribe();
98-
dispatchFakeEvent(document, 'scroll');
98+
dispatchFakeEvent(document, 'scroll', false);
9999

100100
expect(spy).toHaveBeenCalledTimes(1);
101101
});
@@ -104,22 +104,48 @@ describe('Scroll Dispatcher', () => {
104104
describe('Nested scrollables', () => {
105105
let scroll: ScrollDispatcher;
106106
let fixture: ComponentFixture<NestedScrollingComponent>;
107+
let element: ElementRef;
107108

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

111112
fixture = TestBed.createComponent(NestedScrollingComponent);
112113
fixture.detectChanges();
114+
element = fixture.componentInstance.interestingElement;
113115
}));
114116

115117
it('should be able to identify the containing scrollables of an element', () => {
116-
const interestingElement = fixture.componentInstance.interestingElement;
117-
const scrollContainers = scroll.getScrollContainers(interestingElement);
118+
const scrollContainers = scroll.getAncestorScrollContainers(element);
118119
const scrollableElementIds =
119120
scrollContainers.map(scrollable => scrollable.getElementRef().nativeElement.id);
120121

121122
expect(scrollableElementIds).toEqual(['scrollable-1', 'scrollable-1a']);
122123
});
124+
125+
it('should emit when one of the ancestor scrollable containers is scrolled', () => {
126+
const spy = jasmine.createSpy('scroll spy');
127+
const subscription = scroll.ancestorScrolled(element, 0).subscribe(spy);
128+
const grandparent = fixture.debugElement.nativeElement.querySelector('#scrollable-1');
129+
130+
dispatchFakeEvent(grandparent, 'scroll', false);
131+
expect(spy).toHaveBeenCalledTimes(1);
132+
133+
dispatchFakeEvent(window.document, 'scroll', false);
134+
expect(spy).toHaveBeenCalledTimes(2);
135+
136+
subscription.unsubscribe();
137+
});
138+
139+
it('should not emit when a non-ancestor is scrolled', () => {
140+
const spy = jasmine.createSpy('scroll spy');
141+
const subscription = scroll.ancestorScrolled(element, 0).subscribe(spy);
142+
const stranger = fixture.debugElement.nativeElement.querySelector('#scrollable-2');
143+
144+
dispatchFakeEvent(stranger, 'scroll', false);
145+
expect(spy).not.toHaveBeenCalled();
146+
147+
subscription.unsubscribe();
148+
});
123149
});
124150

125151
describe('lazy subscription', () => {
@@ -172,7 +198,7 @@ describe('Scroll Dispatcher', () => {
172198
template: `<div #scrollingElement cdk-scrollable style="height: 9999px"></div>`
173199
})
174200
class ScrollingComponent {
175-
@ViewChild(Scrollable) scrollable: Scrollable;
201+
@ViewChild(CdkScrollable) scrollable: CdkScrollable;
176202
@ViewChild('scrollingElement') scrollingElement: ElementRef;
177203
}
178204

src/cdk/scrolling/scroll-dispatcher.ts

+34-18
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import {Subscription} from 'rxjs/Subscription';
1313
import {Observable} from 'rxjs/Observable';
1414
import {fromEvent} from 'rxjs/observable/fromEvent';
1515
import {of as observableOf} from 'rxjs/observable/of';
16-
import {auditTime} from 'rxjs/operator/auditTime';
17-
import {Scrollable} from './scrollable';
16+
import {auditTime, filter} from '@angular/cdk/rxjs';
17+
import {CdkScrollable} from './scrollable';
1818

1919

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

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

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

4646
/**
47-
* Registers a Scrollable with the service and listens for its scrolled events. When the
48-
* scrollable is scrolled, the service emits the event in its scrolled observable.
47+
* Registers a scrollable instance with the service and listens for its scrolled events. When the
48+
* scrollable is scrolled, the service emits the event to its scrolled observable.
4949
* @param scrollable Scrollable instance to be registered.
5050
*/
51-
register(scrollable: Scrollable): void {
52-
const scrollSubscription = scrollable.elementScrolled().subscribe(() => this._scrolled.next());
53-
this.scrollableReferences.set(scrollable, scrollSubscription);
51+
register(scrollable: CdkScrollable): void {
52+
const scrollSubscription = scrollable.elementScrolled()
53+
.subscribe(() => this._scrolled.next(scrollable));
54+
55+
this.scrollContainers.set(scrollable, scrollSubscription);
5456
}
5557

5658
/**
5759
* Deregisters a Scrollable reference and unsubscribes from its scroll event observable.
5860
* @param scrollable Scrollable instance to be deregistered.
5961
*/
60-
deregister(scrollable: Scrollable): void {
61-
const scrollableReference = this.scrollableReferences.get(scrollable);
62+
deregister(scrollable: CdkScrollable): void {
63+
const scrollableReference = this.scrollContainers.get(scrollable);
6264

6365
if (scrollableReference) {
6466
scrollableReference.unsubscribe();
65-
this.scrollableReferences.delete(scrollable);
67+
this.scrollContainers.delete(scrollable);
6668
}
6769
}
6870

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

102+
/**
103+
* Returns an observable that emits whenever any of the
104+
* scrollable ancestors of an element are scrolled.
105+
* @param elementRef Element whose ancestors to listen for.
106+
* @param auditTimeInMs Time to throttle the scroll events.
107+
*/
108+
ancestorScrolled(elementRef: ElementRef, auditTimeInMs?: number): Observable<CdkScrollable> {
109+
const ancestors = this.getAncestorScrollContainers(elementRef);
110+
111+
return filter.call(this.scrolled(auditTimeInMs), target => {
112+
return !target || ancestors.indexOf(target) > -1;
113+
});
114+
}
115+
100116
/** Returns all registered Scrollables that contain the provided element. */
101-
getScrollContainers(elementRef: ElementRef): Scrollable[] {
102-
const scrollingContainers: Scrollable[] = [];
117+
getAncestorScrollContainers(elementRef: ElementRef): CdkScrollable[] {
118+
const scrollingContainers: CdkScrollable[] = [];
103119

104-
this.scrollableReferences.forEach((_subscription: Subscription, scrollable: Scrollable) => {
105-
if (this.scrollableContainsElement(scrollable, elementRef)) {
120+
this.scrollContainers.forEach((_subscription: Subscription, scrollable: CdkScrollable) => {
121+
if (this._scrollableContainsElement(scrollable, elementRef)) {
106122
scrollingContainers.push(scrollable);
107123
}
108124
});
@@ -111,7 +127,7 @@ export class ScrollDispatcher {
111127
}
112128

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

src/cdk/scrolling/scrollable.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {ScrollDispatcher} from './scroll-dispatcher';
2020
@Directive({
2121
selector: '[cdk-scrollable], [cdkScrollable]'
2222
})
23-
export class Scrollable implements OnInit, OnDestroy {
23+
export class CdkScrollable implements OnInit, OnDestroy {
2424
private _elementScrolled: Subject<Event> = new Subject();
2525
private _scrollListener: Function | null;
2626

src/cdk/scrolling/scrolling-module.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88

99
import {NgModule} from '@angular/core';
1010
import {SCROLL_DISPATCHER_PROVIDER} from './scroll-dispatcher';
11-
import {Scrollable} from './scrollable';
11+
import {CdkScrollable} from './scrollable';
1212
import {PlatformModule} from '@angular/cdk/platform';
1313

1414
@NgModule({
1515
imports: [PlatformModule],
16-
exports: [Scrollable],
17-
declarations: [Scrollable],
16+
exports: [CdkScrollable],
17+
declarations: [CdkScrollable],
1818
providers: [SCROLL_DISPATCHER_PROVIDER],
1919
})
2020
export class ScrollDispatchModule {}

src/cdk/testing/dispatch-events.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ export function dispatchEvent(node: Node | Window, event: Event): Event {
2020
}
2121

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

2727
/** Shorthand to dispatch a keyboard event with a specified key code. */

src/cdk/testing/event-objects.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ export function createKeyboardEvent(type: string, keyCode: number, target?: Elem
7474
}
7575

7676
/** Creates a fake event object with any desired event type. */
77-
export function createFakeEvent(type: string) {
77+
export function createFakeEvent(type: string, canBubble = true, cancelable = true) {
7878
const event = document.createEvent('Event');
79-
event.initEvent(type, true, true);
79+
event.initEvent(type, canBubble, cancelable);
8080
return event;
8181
}

src/lib/tooltip/tooltip.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {AnimationEvent} from '@angular/animations';
1717
import {By} from '@angular/platform-browser';
1818
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
1919
import {Direction, Directionality} from '@angular/cdk/bidi';
20-
import {OverlayContainer, OverlayModule, Scrollable} from '@angular/cdk/overlay';
20+
import {OverlayContainer, OverlayModule, CdkScrollable} from '@angular/cdk/overlay';
2121
import {Platform} from '@angular/cdk/platform';
2222
import {dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing';
2323
import {ESCAPE} from '@angular/cdk/keycodes';
@@ -686,7 +686,7 @@ class ScrollableTooltipDemo {
686686
message: string = initialTooltipMessage;
687687
showButton: boolean = true;
688688

689-
@ViewChild(Scrollable) scrollingContainer: Scrollable;
689+
@ViewChild(CdkScrollable) scrollingContainer: CdkScrollable;
690690

691691
scrollDown() {
692692
const scrollingContainerEl = this.scrollingContainer.getElementRef().nativeElement;

src/lib/tooltip/tooltip.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,11 @@ export class MatTooltip implements OnDestroy {
280280
.connectedTo(this._elementRef, origin.main, overlay.main)
281281
.withFallbackPosition(origin.fallback, overlay.fallback);
282282

283-
strategy.withScrollableContainers(this._scrollDispatcher.getScrollContainers(this._elementRef));
283+
const scrollableAncestors = this._scrollDispatcher
284+
.getAncestorScrollContainers(this._elementRef);
285+
286+
strategy.withScrollableContainers(scrollableAncestors);
287+
284288
strategy.onPositionChange.subscribe(change => {
285289
if (this._tooltipInstance) {
286290
if (change.scrollableViewProperties.isOverlayClipped && this._tooltipInstance.isVisible()) {

0 commit comments

Comments
 (0)