mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(scroll): provide directive and service to listen to scrolling (#2188)
* feat(scroll): provide directive and service to listen to scrolling * review response * fix lint * move scroll to overlay * rename scroll in tooltip * remove reference tot he live announcer
This commit is contained in:
committed by
Jeremy Elbourn
parent
a1f90283fd
commit
9b68e68168
+17
-2
@@ -47,6 +47,7 @@ export {
|
||||
} from './overlay/overlay-directives';
|
||||
export * from './overlay/position/connected-position-strategy';
|
||||
export * from './overlay/position/connected-position';
|
||||
export {ScrollDispatcher} from './overlay/scroll/scroll-dispatcher';
|
||||
|
||||
// Gestures
|
||||
export {GestureConfig} from './gestures/gesture-config';
|
||||
@@ -110,8 +111,22 @@ export {NoConflictStyleCompatibilityMode} from './compatibility/no-conflict-mode
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [MdLineModule, RtlModule, MdRippleModule, PortalModule, OverlayModule, A11yModule],
|
||||
exports: [MdLineModule, RtlModule, MdRippleModule, PortalModule, OverlayModule, A11yModule],
|
||||
imports: [
|
||||
MdLineModule,
|
||||
RtlModule,
|
||||
MdRippleModule,
|
||||
PortalModule,
|
||||
OverlayModule,
|
||||
A11yModule,
|
||||
],
|
||||
exports: [
|
||||
MdLineModule,
|
||||
RtlModule,
|
||||
MdRippleModule,
|
||||
PortalModule,
|
||||
OverlayModule,
|
||||
A11yModule,
|
||||
],
|
||||
})
|
||||
export class MdCoreModule {
|
||||
static forRoot(): ModuleWithProviders {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {PortalModule} from '../portal/portal-directives';
|
||||
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {Dir, LayoutDirection} from '../rtl/dir';
|
||||
import {Scrollable} from './scroll/scrollable';
|
||||
|
||||
/** Default set of positions for the overlay. Follows the behavior of a dropdown. */
|
||||
let defaultPositionList = [
|
||||
@@ -285,8 +286,8 @@ export class ConnectedOverlayDirective implements OnDestroy {
|
||||
|
||||
@NgModule({
|
||||
imports: [PortalModule],
|
||||
exports: [ConnectedOverlayDirective, OverlayOrigin],
|
||||
declarations: [ConnectedOverlayDirective, OverlayOrigin],
|
||||
exports: [ConnectedOverlayDirective, OverlayOrigin, Scrollable],
|
||||
declarations: [ConnectedOverlayDirective, OverlayOrigin, Scrollable],
|
||||
})
|
||||
export class OverlayModule {
|
||||
static forRoot(): ModuleWithProviders {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {OverlayRef} from './overlay-ref';
|
||||
import {OverlayPositionBuilder} from './position/overlay-position-builder';
|
||||
import {ViewportRuler} from './position/viewport-ruler';
|
||||
import {OverlayContainer} from './overlay-container';
|
||||
import {ScrollDispatcher} from './scroll/scroll-dispatcher';
|
||||
|
||||
/** Next overlay unique ID. */
|
||||
let nextUniqueId = 0;
|
||||
@@ -93,4 +94,5 @@ export const OVERLAY_PROVIDERS = [
|
||||
OverlayPositionBuilder,
|
||||
Overlay,
|
||||
OverlayContainer,
|
||||
ScrollDispatcher,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import {inject, TestBed, async, ComponentFixture} from '@angular/core/testing';
|
||||
import {NgModule, Component, ViewChild, ElementRef} from '@angular/core';
|
||||
import {ScrollDispatcher} from './scroll-dispatcher';
|
||||
import {OverlayModule} from '../overlay-directives';
|
||||
import {Scrollable} from './scrollable';
|
||||
|
||||
describe('Scroll Dispatcher', () => {
|
||||
let scroll: ScrollDispatcher;
|
||||
let fixture: ComponentFixture<ScrollingComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [OverlayModule.forRoot(), ScrollTestModule],
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(inject([ScrollDispatcher], (s: ScrollDispatcher) => {
|
||||
scroll = s;
|
||||
|
||||
fixture = TestBed.createComponent(ScrollingComponent);
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should be registered with the scrollable directive with the scroll service', () => {
|
||||
const componentScrollable = fixture.componentInstance.scrollable;
|
||||
expect(scroll.scrollableReferences.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);
|
||||
|
||||
fixture.destroy();
|
||||
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(false);
|
||||
});
|
||||
|
||||
it('should notify through the directive and service that a scroll event occurred', () => {
|
||||
let hasDirectiveScrollNotified = false;
|
||||
// Listen for notifications from scroll directive
|
||||
let scrollable = fixture.componentInstance.scrollable;
|
||||
scrollable.elementScrolled().subscribe(() => { hasDirectiveScrollNotified = true; });
|
||||
|
||||
// Listen for notifications from scroll service
|
||||
let hasServiceScrollNotified = false;
|
||||
scroll.scrolled().subscribe(() => { hasServiceScrollNotified = true; });
|
||||
|
||||
// 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.
|
||||
const scrollEvent = document.createEvent('UIEvents');
|
||||
scrollEvent.initUIEvent('scroll', true, true, window, 0);
|
||||
fixture.componentInstance.scrollingElement.nativeElement.dispatchEvent(scrollEvent);
|
||||
|
||||
expect(hasDirectiveScrollNotified).toBe(true);
|
||||
expect(hasServiceScrollNotified).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/** Simple component that contains a large div and can be scrolled. */
|
||||
@Component({
|
||||
template: `<div #scrollingElement cdk-scrollable style="height: 9999px"></div>`
|
||||
})
|
||||
class ScrollingComponent {
|
||||
@ViewChild(Scrollable) scrollable: Scrollable;
|
||||
@ViewChild('scrollingElement') scrollingElement: ElementRef;
|
||||
}
|
||||
|
||||
const TEST_COMPONENTS = [ScrollingComponent];
|
||||
@NgModule({
|
||||
imports: [OverlayModule],
|
||||
providers: [ScrollDispatcher],
|
||||
exports: TEST_COMPONENTS,
|
||||
declarations: TEST_COMPONENTS,
|
||||
entryComponents: TEST_COMPONENTS,
|
||||
})
|
||||
class ScrollTestModule { }
|
||||
@@ -0,0 +1,60 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Scrollable} from './scrollable';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import 'rxjs/add/observable/fromEvent';
|
||||
|
||||
|
||||
/**
|
||||
* Service contained all registered Scrollable references and emits an event when any one of the
|
||||
* Scrollable references emit a scrolled event.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ScrollDispatcher {
|
||||
/** Subject for notifying that a registered scrollable reference element has been scrolled. */
|
||||
_scrolled: Subject<void> = new Subject<void>();
|
||||
|
||||
/**
|
||||
* Map of all the scrollable references that are registered with the service and their
|
||||
* scroll event subscriptions.
|
||||
*/
|
||||
scrollableReferences: WeakMap<Scrollable, Subscription> = new WeakMap();
|
||||
|
||||
constructor() {
|
||||
// By default, notify a scroll event when the document is scrolled or the window is resized.
|
||||
Observable.fromEvent(window.document, 'scroll').subscribe(() => this._notify());
|
||||
Observable.fromEvent(window, 'resize').subscribe(() => this._notify());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
register(scrollable: Scrollable): void {
|
||||
const scrollSubscription = scrollable.elementScrolled().subscribe(() => this._notify());
|
||||
this.scrollableReferences.set(scrollable, scrollSubscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregisters a Scrollable reference and unsubscribes from its scroll event observable.
|
||||
*/
|
||||
deregister(scrollable: Scrollable): void {
|
||||
this.scrollableReferences.get(scrollable).unsubscribe();
|
||||
this.scrollableReferences.delete(scrollable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that emits an event whenever any of the registered Scrollable
|
||||
* references (or window, document, or body) fire a scrolled event.
|
||||
* TODO: Add an event limiter that includes throttle with the leading and trailing events.
|
||||
*/
|
||||
scrolled(): Observable<void> {
|
||||
return this._scrolled.asObservable();
|
||||
}
|
||||
|
||||
/** Sends a notification that a scroll event has been fired. */
|
||||
_notify() {
|
||||
this._scrolled.next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
Directive, ElementRef, OnInit, OnDestroy
|
||||
} from '@angular/core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {ScrollDispatcher} from './scroll-dispatcher';
|
||||
import 'rxjs/add/observable/fromEvent';
|
||||
|
||||
|
||||
/**
|
||||
* Sends an event when the directive's element is scrolled. Registers itself with the
|
||||
* ScrollDispatcher service to include itself as part of its collection of scrolling events that it
|
||||
* can be listened to through the service.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[cdk-scrollable]'
|
||||
})
|
||||
export class Scrollable implements OnInit, OnDestroy {
|
||||
constructor(private _elementRef: ElementRef, private _scroll: ScrollDispatcher) {}
|
||||
|
||||
ngOnInit() {
|
||||
this._scroll.register(this);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._scroll.deregister(this);
|
||||
}
|
||||
|
||||
/** Returns observable that emits when the scroll event is fired on the host element. */
|
||||
elementScrolled(): Observable<any> {
|
||||
return Observable.fromEvent(this._elementRef.nativeElement, 'scroll');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
<ng-content select="md-sidenav, mat-sidenav"></ng-content>
|
||||
|
||||
<div class="md-sidenav-content" [ngStyle]="_getStyles()">
|
||||
<div class="md-sidenav-content" [ngStyle]="_getStyles()" cdk-scrollable>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
||||
@@ -17,9 +17,12 @@ import {
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Dir, MdError, coerceBooleanProperty, DefaultStyleCompatibilityModeModule} from '../core';
|
||||
import {A11yModule, A11Y_PROVIDERS} from '../core/a11y/index';
|
||||
import {A11yModule} from '../core/a11y/index';
|
||||
import {FocusTrap} from '../core/a11y/focus-trap';
|
||||
import {ESCAPE} from '../core/keyboard/keycodes';
|
||||
import {OverlayModule} from '../core/overlay/overlay-directives';
|
||||
import {InteractivityChecker} from '../core/a11y/interactivity-checker';
|
||||
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
|
||||
|
||||
|
||||
/** Exception thrown when two MdSidenav are matching the same side. */
|
||||
@@ -503,7 +506,7 @@ export class MdSidenavContainer implements AfterContentInit {
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, DefaultStyleCompatibilityModeModule, A11yModule],
|
||||
imports: [CommonModule, DefaultStyleCompatibilityModeModule, A11yModule, OverlayModule],
|
||||
exports: [MdSidenavContainer, MdSidenav, DefaultStyleCompatibilityModeModule],
|
||||
declarations: [MdSidenavContainer, MdSidenav],
|
||||
})
|
||||
@@ -511,7 +514,7 @@ export class MdSidenavModule {
|
||||
static forRoot(): ModuleWithProviders {
|
||||
return {
|
||||
ngModule: MdSidenavModule,
|
||||
providers: [A11Y_PROVIDERS]
|
||||
providers: [InteractivityChecker, ScrollDispatcher]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
AnimationTransitionEvent,
|
||||
NgZone,
|
||||
Optional,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import {
|
||||
Overlay,
|
||||
@@ -23,16 +25,17 @@ import {
|
||||
ComponentPortal,
|
||||
OverlayConnectionPosition,
|
||||
OriginConnectionPosition,
|
||||
OVERLAY_PROVIDERS,
|
||||
DefaultStyleCompatibilityModeModule,
|
||||
DefaultStyleCompatibilityModeModule
|
||||
} from '../core';
|
||||
import {MdTooltipInvalidPositionError} from './tooltip-errors';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {Dir} from '../core/rtl/dir';
|
||||
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
|
||||
import {OverlayPositionBuilder} from '../core/overlay/position/overlay-position-builder';
|
||||
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
|
||||
import 'rxjs/add/operator/first';
|
||||
|
||||
|
||||
export type TooltipPosition = 'left' | 'right' | 'above' | 'below' | 'before' | 'after';
|
||||
|
||||
/** Time in ms to delay before changing the tooltip visibility to hidden */
|
||||
@@ -54,7 +57,7 @@ export const TOUCHEND_HIDE_DELAY = 1500;
|
||||
},
|
||||
exportAs: 'mdTooltip',
|
||||
})
|
||||
export class MdTooltip {
|
||||
export class MdTooltip implements OnInit, OnDestroy {
|
||||
_overlayRef: OverlayRef;
|
||||
_tooltipInstance: TooltipComponent;
|
||||
|
||||
@@ -104,10 +107,23 @@ export class MdTooltip {
|
||||
get _deprecatedMessage(): string { return this.message; }
|
||||
set _deprecatedMessage(v: string) { this.message = v; }
|
||||
|
||||
constructor(private _overlay: Overlay, private _elementRef: ElementRef,
|
||||
private _viewContainerRef: ViewContainerRef, private _ngZone: NgZone,
|
||||
constructor(private _overlay: Overlay,
|
||||
private _scrollDispatcher: ScrollDispatcher,
|
||||
private _elementRef: ElementRef,
|
||||
private _viewContainerRef: ViewContainerRef,
|
||||
private _ngZone: NgZone,
|
||||
@Optional() private _dir: Dir) {}
|
||||
|
||||
ngOnInit() {
|
||||
// When a scroll on the page occurs, update the position in case this tooltip needs
|
||||
// to be repositioned.
|
||||
this._scrollDispatcher.scrolled().subscribe(() => {
|
||||
if (this._overlayRef) {
|
||||
this._overlayRef.updatePosition();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Dispose the tooltip when destroyed */
|
||||
ngOnDestroy() {
|
||||
if (this._tooltipInstance) {
|
||||
@@ -370,7 +386,12 @@ export class MdTooltipModule {
|
||||
static forRoot(): ModuleWithProviders {
|
||||
return {
|
||||
ngModule: MdTooltipModule,
|
||||
providers: OVERLAY_PROVIDERS,
|
||||
providers: [
|
||||
Overlay,
|
||||
OverlayPositionBuilder,
|
||||
ViewportRuler,
|
||||
ScrollDispatcher
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ task(':build:components:rollup', () => {
|
||||
|
||||
// Rxjs dependencies
|
||||
'rxjs/Subject': 'Rx',
|
||||
'rxjs/add/observable/fromEvent': 'Rx.Observable',
|
||||
'rxjs/add/observable/forkJoin': 'Rx.Observable',
|
||||
'rxjs/add/observable/of': 'Rx.Observable',
|
||||
'rxjs/add/operator/toPromise': 'Rx.Observable.prototype',
|
||||
|
||||
Reference in New Issue
Block a user