mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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.
This commit is contained in:
committed by
Andrew Seguin
parent
80671bf2e8
commit
b2fbea7e8a
@@ -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';
|
||||
@@ -567,7 +567,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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -181,7 +181,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -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',
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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', () => {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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. */
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -97,12 +99,26 @@ export class ScrollDispatcher {
|
||||
}) : observableOf<void>();
|
||||
}
|
||||
|
||||
/** Returns all registered Scrollables that contain the provided element. */
|
||||
getScrollContainers(elementRef: ElementRef): Scrollable[] {
|
||||
const scrollingContainers: Scrollable[] = [];
|
||||
/**
|
||||
* 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);
|
||||
|
||||
this.scrollableReferences.forEach((_subscription: Subscription, scrollable: Scrollable) => {
|
||||
if (this.scrollableContainsElement(scrollable, elementRef)) {
|
||||
return filter.call(this.scrolled(auditTimeInMs), target => {
|
||||
return !target || ancestors.indexOf(target) > -1;
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns all registered Scrollables that contain the provided element. */
|
||||
getAncestorScrollContainers(elementRef: ElementRef): CdkScrollable[] {
|
||||
const scrollingContainers: CdkScrollable[] = [];
|
||||
|
||||
this.scrollContainers.forEach((_subscription: Subscription, scrollable: CdkScrollable) => {
|
||||
if (this._scrollableContainsElement(scrollable, elementRef)) {
|
||||
scrollingContainers.push(scrollable);
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user