Skip to content

Commit 332f786

Browse files
authored
fix(cdk/scrolling): make it easier to provide custom scrollable (#33269)
Currently the `ScrollDispatcher` only accepts a `CdkScrollable` which makes it hard for users to register their own. These changes make it so only the `ScrollDispatcherTarget` interface needs to be implemented instead.
1 parent 40d7c5a commit 332f786

5 files changed

Lines changed: 67 additions & 51 deletions

File tree

goldens/cdk/overlay/index.api.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export class CdkOverlayOrigin {
168168
}
169169

170170
// @public
171-
export class CdkScrollable implements OnInit, OnDestroy {
171+
export class CdkScrollable implements ScrollDispatcherTarget, OnInit, OnDestroy {
172172
// (undocumented)
173173
protected readonly _destroyed: Subject<void>;
174174
// (undocumented)
@@ -302,7 +302,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
302302
withPopoverLocation(location: FlexibleOverlayPopoverLocation): this;
303303
withPositions(positions: ConnectedPosition[]): this;
304304
withPush(canPush?: boolean): this;
305-
withScrollableContainers(scrollables: CdkScrollable[]): this;
305+
withScrollableContainers(scrollables: ScrollDispatcherTarget[]): this;
306306
withTransformOriginOn(selector: string): this;
307307
withViewportMargin(margin: ViewportMargin): this;
308308
}
@@ -562,14 +562,14 @@ export interface RepositionScrollStrategyConfig {
562562

563563
// @public
564564
export class ScrollDispatcher implements OnDestroy {
565-
ancestorScrolled(elementOrElementRef: ElementRef | HTMLElement, auditTimeInMs?: number): Observable<CdkScrollable | void>;
566-
deregister(scrollable: CdkScrollable): void;
567-
getAncestorScrollContainers(elementOrElementRef: ElementRef | HTMLElement): CdkScrollable[];
565+
ancestorScrolled(elementOrElementRef: ElementRef | HTMLElement, auditTimeInMs?: number): Observable<ScrollDispatcherTarget | void>;
566+
deregister(target: ScrollDispatcherTarget): void;
567+
getAncestorScrollContainers(elementOrElementRef: ElementRef | HTMLElement): ScrollDispatcherTarget[];
568568
// (undocumented)
569569
ngOnDestroy(): void;
570-
register(scrollable: CdkScrollable): void;
571-
scrollContainers: Map<CdkScrollable, Subscription>;
572-
scrolled(auditTimeInMs?: number): Observable<CdkScrollable | void>;
570+
register(target: ScrollDispatcherTarget): void;
571+
readonly scrollContainers: Map<ScrollDispatcherTarget, Subscription>;
572+
scrolled(auditTimeInMs?: number): Observable<ScrollDispatcherTarget | void>;
573573
// (undocumented)
574574
static ɵfac: i0.ɵɵFactoryDeclaration<ScrollDispatcher, never>;
575575
// (undocumented)

goldens/cdk/scrolling/index.api.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class CdkFixedSizeVirtualScroll implements OnChanges {
5353
}
5454

5555
// @public
56-
export class CdkScrollable implements OnInit, OnDestroy {
56+
export class CdkScrollable implements ScrollDispatcherTarget, OnInit, OnDestroy {
5757
// (undocumented)
5858
protected readonly _destroyed: Subject<void>;
5959
// (undocumented)
@@ -257,20 +257,26 @@ export type _Right = {
257257

258258
// @public
259259
export class ScrollDispatcher implements OnDestroy {
260-
ancestorScrolled(elementOrElementRef: ElementRef | HTMLElement, auditTimeInMs?: number): Observable<CdkScrollable | void>;
261-
deregister(scrollable: CdkScrollable): void;
262-
getAncestorScrollContainers(elementOrElementRef: ElementRef | HTMLElement): CdkScrollable[];
260+
ancestorScrolled(elementOrElementRef: ElementRef | HTMLElement, auditTimeInMs?: number): Observable<ScrollDispatcherTarget | void>;
261+
deregister(target: ScrollDispatcherTarget): void;
262+
getAncestorScrollContainers(elementOrElementRef: ElementRef | HTMLElement): ScrollDispatcherTarget[];
263263
// (undocumented)
264264
ngOnDestroy(): void;
265-
register(scrollable: CdkScrollable): void;
266-
scrollContainers: Map<CdkScrollable, Subscription>;
267-
scrolled(auditTimeInMs?: number): Observable<CdkScrollable | void>;
265+
register(target: ScrollDispatcherTarget): void;
266+
readonly scrollContainers: Map<ScrollDispatcherTarget, Subscription>;
267+
scrolled(auditTimeInMs?: number): Observable<ScrollDispatcherTarget | void>;
268268
// (undocumented)
269269
static ɵfac: i0.ɵɵFactoryDeclaration<ScrollDispatcher, never>;
270270
// (undocumented)
271271
static ɵprov: i0.ɵɵInjectableDeclaration<ScrollDispatcher>;
272272
}
273273

274+
// @public
275+
export interface ScrollDispatcherTarget {
276+
elementScrolled(): Observable<Event>;
277+
getElementRef(): ElementRef<HTMLElement>;
278+
}
279+
274280
// @public
275281
export class ScrollingModule {
276282
// (undocumented)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {PositionStrategy} from './position-strategy';
1010
import {DOCUMENT, ElementRef, Injector} from '@angular/core';
11-
import {ViewportRuler, CdkScrollable, ViewportScrollPosition} from '../../scrolling';
11+
import {ViewportRuler, ScrollDispatcherTarget, ViewportScrollPosition} from '../../scrolling';
1212
import {
1313
ConnectedOverlayPositionChange,
1414
ConnectionPositionPair,
@@ -117,7 +117,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
117117
private _viewportMargin: ViewportMargin = 0;
118118

119119
/** The Scrollable containers used to check scrollable view properties on position change. */
120-
private _scrollables: CdkScrollable[] = [];
120+
private _scrollables: ScrollDispatcherTarget[] = [];
121121

122122
/** Ordered list of preferred positions, from most to least desirable. */
123123
_preferredPositions: ConnectionPositionPair[] = [];
@@ -416,7 +416,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
416416
* on reposition we can evaluate if it or the overlay has been clipped or outside view. Every
417417
* Scrollable must be an ancestor element of the strategy's origin element.
418418
*/
419-
withScrollableContainers(scrollables: CdkScrollable[]): this {
419+
withScrollableContainers(scrollables: ScrollDispatcherTarget[]): this {
420420
this._scrollables = scrollables;
421421
return this;
422422
}

src/cdk/scrolling/scroll-dispatcher.ts

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,22 @@ import {Platform} from '../platform';
1111
import {ElementRef, Service, NgZone, OnDestroy, RendererFactory2, inject} from '@angular/core';
1212
import {of as observableOf, Subject, Subscription, Observable, Observer} from 'rxjs';
1313
import {auditTime, filter} from 'rxjs/operators';
14-
import type {CdkScrollable} from './scrollable';
1514

1615
/** Time in ms to throttle the scrolling events by default. */
1716
export const DEFAULT_SCROLL_TIME = 20;
1817

18+
/** Scrollable instance that can be registered with the `ScrollDispatcher`. */
19+
export interface ScrollDispatcherTarget {
20+
/** Observable that emits when the element is scrolled. */
21+
elementScrolled(): Observable<Event>;
22+
23+
/** Gets the `ElementRef` representing the scrollable element. */
24+
getElementRef(): ElementRef<HTMLElement>;
25+
}
26+
1927
/**
20-
* Service contained all registered Scrollable references and emits an event when any one of the
21-
* Scrollable references emit a scrolled event.
28+
* Service contained all registered scroll targets and emits
29+
* an event when any one of them emits a scrolled event.
2230
*/
2331
@Service()
2432
export class ScrollDispatcher implements OnDestroy {
@@ -27,42 +35,42 @@ export class ScrollDispatcher implements OnDestroy {
2735
private _renderer = inject(RendererFactory2).createRenderer(null, null);
2836
private _cleanupGlobalListener: (() => void) | undefined;
2937

30-
/** Subject for notifying that a registered scrollable reference element has been scrolled. */
31-
private readonly _scrolled = new Subject<CdkScrollable | void>();
38+
/** Subject for notifying that a registered element has been scrolled. */
39+
private readonly _scrolled = new Subject<ScrollDispatcherTarget | void>();
3240

3341
/** Keeps track of the amount of subscriptions to `scrolled`. Used for cleaning up afterwards. */
3442
private _scrolledCount = 0;
3543

3644
/**
37-
* Map of all the scrollable references that are registered with the service and their
45+
* Map of all the scrollable targets that are registered with the service and their
3846
* scroll event subscriptions.
3947
*/
40-
scrollContainers: Map<CdkScrollable, Subscription> = new Map();
48+
readonly scrollContainers: Map<ScrollDispatcherTarget, Subscription> = new Map();
4149

4250
/**
4351
* Registers a scrollable instance with the service and listens for its scrolled events. When the
4452
* scrollable is scrolled, the service emits the event to its scrolled observable.
45-
* @param scrollable Scrollable instance to be registered.
53+
* @param target Scrollable instance to be registered.
4654
*/
47-
register(scrollable: CdkScrollable): void {
48-
if (!this.scrollContainers.has(scrollable)) {
55+
register(target: ScrollDispatcherTarget): void {
56+
if (!this.scrollContainers.has(target)) {
4957
this.scrollContainers.set(
50-
scrollable,
51-
scrollable.elementScrolled().subscribe(() => this._scrolled.next(scrollable)),
58+
target,
59+
target.elementScrolled().subscribe(() => this._scrolled.next(target)),
5260
);
5361
}
5462
}
5563

5664
/**
5765
* De-registers a Scrollable reference and unsubscribes from its scroll event observable.
58-
* @param scrollable Scrollable instance to be deregistered.
66+
* @param target Scrollable instance to be deregistered.
5967
*/
60-
deregister(scrollable: CdkScrollable): void {
61-
const scrollableReference = this.scrollContainers.get(scrollable);
68+
deregister(target: ScrollDispatcherTarget): void {
69+
const ref = this.scrollContainers.get(target);
6270

63-
if (scrollableReference) {
64-
scrollableReference.unsubscribe();
65-
this.scrollContainers.delete(scrollable);
71+
if (ref) {
72+
ref.unsubscribe();
73+
this.scrollContainers.delete(target);
6674
}
6775
}
6876

@@ -76,12 +84,12 @@ export class ScrollDispatcher implements OnDestroy {
7684
* If you need to update any data bindings as a result of a scroll event, you have
7785
* to run the callback using `NgZone.run`.
7886
*/
79-
scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME): Observable<CdkScrollable | void> {
87+
scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME): Observable<ScrollDispatcherTarget | void> {
8088
if (!this._platform.isBrowser) {
8189
return observableOf<void>();
8290
}
8391

84-
return new Observable((observer: Observer<CdkScrollable | void>) => {
92+
return new Observable((observer: Observer<ScrollDispatcherTarget | void>) => {
8593
if (!this._cleanupGlobalListener) {
8694
this._cleanupGlobalListener = this._ngZone.runOutsideAngular(() =>
8795
this._renderer.listen('document', 'scroll', () => this._scrolled.next()),
@@ -125,39 +133,41 @@ export class ScrollDispatcher implements OnDestroy {
125133
ancestorScrolled(
126134
elementOrElementRef: ElementRef | HTMLElement,
127135
auditTimeInMs?: number,
128-
): Observable<CdkScrollable | void> {
136+
): Observable<ScrollDispatcherTarget | void> {
129137
const ancestors = this.getAncestorScrollContainers(elementOrElementRef);
130138

131139
return this.scrolled(auditTimeInMs).pipe(
132140
filter(target => !target || ancestors.indexOf(target) > -1),
133141
);
134142
}
135143

136-
/** Returns all registered Scrollables that contain the provided element. */
137-
getAncestorScrollContainers(elementOrElementRef: ElementRef | HTMLElement): CdkScrollable[] {
138-
const scrollingContainers: CdkScrollable[] = [];
144+
/** Returns all registered containers that contain the provided element. */
145+
getAncestorScrollContainers(
146+
elementOrElementRef: ElementRef | HTMLElement,
147+
): ScrollDispatcherTarget[] {
148+
const scrollingContainers: ScrollDispatcherTarget[] = [];
139149

140-
this.scrollContainers.forEach((_subscription: Subscription, scrollable: CdkScrollable) => {
141-
if (this._scrollableContainsElement(scrollable, elementOrElementRef)) {
142-
scrollingContainers.push(scrollable);
150+
this.scrollContainers.forEach((_, target: ScrollDispatcherTarget) => {
151+
if (this._targetContainsElement(target, elementOrElementRef)) {
152+
scrollingContainers.push(target);
143153
}
144154
});
145155

146156
return scrollingContainers;
147157
}
148158

149159
/** Returns true if the element is contained within the provided Scrollable. */
150-
private _scrollableContainsElement(
151-
scrollable: CdkScrollable,
160+
private _targetContainsElement(
161+
scrollable: ScrollDispatcherTarget,
152162
elementOrElementRef: ElementRef | HTMLElement,
153163
): boolean {
154164
let element: HTMLElement | null = coerceElement(elementOrElementRef);
155-
let scrollableElement = scrollable.getElementRef().nativeElement;
165+
let targetElement = scrollable.getElementRef().nativeElement;
156166

157167
// Traverse through the element parents until we reach null, checking if any of the elements
158168
// are the scrollable's element.
159169
do {
160-
if (element == scrollableElement) {
170+
if (element == targetElement) {
161171
return true;
162172
}
163173
} while ((element = element!.parentElement));

src/cdk/scrolling/scrollable.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {Directionality} from '../bidi';
1010
import {getRtlScrollAxisType, RtlScrollAxisType, supportsScrollBehavior} from '../platform';
1111
import {Directive, ElementRef, NgZone, OnDestroy, OnInit, Renderer2, inject} from '@angular/core';
1212
import {Observable, Subject} from 'rxjs';
13-
import {ScrollDispatcher} from './scroll-dispatcher';
13+
import {ScrollDispatcher, ScrollDispatcherTarget} from './scroll-dispatcher';
1414

1515
export type _Without<T> = {[P in keyof T]?: never};
1616
export type _XOR<T, U> = (_Without<T> & U) | (_Without<U> & T);
@@ -39,7 +39,7 @@ export type ExtendedScrollToOptions = _XAxis & _YAxis & ScrollOptions;
3939
@Directive({
4040
selector: '[cdk-scrollable], [cdkScrollable]',
4141
})
42-
export class CdkScrollable implements OnInit, OnDestroy {
42+
export class CdkScrollable implements ScrollDispatcherTarget, OnInit, OnDestroy {
4343
protected elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
4444
protected scrollDispatcher = inject(ScrollDispatcher);
4545
protected ngZone = inject(NgZone);

0 commit comments

Comments
 (0)