Skip to content

Commit

Permalink
feat(scroll): provide directive and service to listen to scrolling
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewseguin committed Dec 13, 2016
1 parent 86123a3 commit 378281d
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 13 deletions.
27 changes: 24 additions & 3 deletions src/lib/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {PortalModule} from './portal/portal-directives';
import {OverlayModule} from './overlay/overlay-directives';
import {A11yModule, A11Y_PROVIDERS} from './a11y/index';
import {OVERLAY_PROVIDERS} from './overlay/overlay';
import {Scroll} from './scroll/scroll';
import {ScrollModule} from './scroll/scrollable';


// RTL
Expand Down Expand Up @@ -45,6 +47,8 @@ export {
} from './overlay/overlay-directives';
export * from './overlay/position/connected-position-strategy';
export * from './overlay/position/connected-position';
export * from './scroll/scrollable';
export * from './scroll/scroll';

// Gestures
export {MdGestureConfig} from './gestures/MdGestureConfig';
Expand Down Expand Up @@ -97,16 +101,33 @@ export {coerceNumberProperty} from './coersion/number-property';
export {DefaultStyleCompatibilityModeModule} from './compatibility/default-mode';
export {NoConflictStyleCompatibilityMode} from './compatibility/no-conflict-mode';

// Scroll
export {Scroll} from './scroll/scroll';
export {Scrollable} from './scroll/scrollable';

@NgModule({
imports: [MdLineModule, RtlModule, MdRippleModule, PortalModule, OverlayModule, A11yModule],
exports: [MdLineModule, RtlModule, MdRippleModule, PortalModule, OverlayModule, A11yModule],
imports: [MdLineModule,
RtlModule,
MdRippleModule,
PortalModule,
OverlayModule,
A11yModule,
ScrollModule
],
exports: [MdLineModule,
RtlModule,
MdRippleModule,
PortalModule,
OverlayModule,
A11yModule,
ScrollModule
],
})
export class MdCoreModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: MdCoreModule,
providers: [A11Y_PROVIDERS, OVERLAY_PROVIDERS],
providers: [A11Y_PROVIDERS, OVERLAY_PROVIDERS, Scroll],
};
}
}
76 changes: 76 additions & 0 deletions src/lib/core/scroll/scroll.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {inject, TestBed, async, ComponentFixture} from '@angular/core/testing';
import {NgModule, Component, ViewChild, ElementRef} from '@angular/core';
import {Scroll} from './scroll';
import {ScrollModule, Scrollable} from './scrollable';

describe('Scrollable', () => {
let scroll: Scroll;
let fixture: ComponentFixture<ScrollingComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ScrollModule.forRoot(), ScrollTestModule],
});

TestBed.compileComponents();
}));

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

fixture = TestBed.createComponent(ScrollingComponent);
fixture.detectChanges();
}));

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

it('should deregister the scrollable directive 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.
fixture.componentInstance.scrollingElement.nativeElement.dispatchEvent(new Event('scroll'));

expect(hasDirectiveScrollNotified).toBe(true);
expect(hasServiceScrollNotified).toBe(true);
});
});


/** Simple component that contains a large div and can be scrolled. */
@Component({
template: `<div #scrollingElement md-scrollable style="height: 9999px"></div>`
})
class ScrollingComponent {
@ViewChild(Scrollable) scrollable: Scrollable;
@ViewChild('scrollingElement') scrollingElement: ElementRef;
}

const TEST_COMPONENTS = [ScrollingComponent];
@NgModule({
imports: [ScrollModule],
providers: [Scroll],
exports: TEST_COMPONENTS,
declarations: TEST_COMPONENTS,
entryComponents: TEST_COMPONENTS,
})
class ScrollTestModule { }
59 changes: 59 additions & 0 deletions src/lib/core/scroll/scroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {Injectable} from '@angular/core';
import {Scrollable} from './scrollable';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';


/**
* Service contained all registered Scrollable references and emits an event when any one of the
* Scrollable references emit a scrolled event.
*/
@Injectable()
export class Scroll {
/** Subject for notifying that a registered scrollable reference element has been scrolled. */
_scrolled: Subject<Event> = new Subject();

/**
* Map of all the scrollable references that are registered with the service and their
* scroll event subscriptions.
*/
scrollableReferences: Map<Scrollable, Subscription> = new Map();

constructor() {
// By default, notify a scroll event when the document is scrolled or the window is resized.
window.document.addEventListener('scroll', this._notify.bind(this));
window.addEventListener('resize', this._notify.bind(this));
}

/**
* 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.bind(this));
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<Event> {
return this._scrolled.asObservable();
}

/** Sends a notification that a scroll event has been fired. */
_notify(e: Event) {
this._scrolled.next(e);
}
}
53 changes: 53 additions & 0 deletions src/lib/core/scroll/scrollable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
Directive, ElementRef, OnInit, OnDestroy, ModuleWithProviders,
NgModule
} from '@angular/core';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';
import {Scroll} from './scroll';


/**
* Sends an event when the directive's element is scrolled. Registers itself with the Scroll
* service to include itself as part of its collection of scrolling events that it can be listened
* to through the service.
*/
@Directive({
selector: '[md-scrollable]'
})
export class Scrollable implements OnInit, OnDestroy {
/** Subject for notifying that the element has been scrolled. */
private _elementScrolled: Subject<Event> = new Subject();

constructor(private _elementRef: ElementRef, private _scroll: Scroll) {}

ngOnInit() {
this._scroll.register(this);
this._elementRef.nativeElement.addEventListener('scroll', (e: Event) => {
this._elementScrolled.next(e);
});
}

ngOnDestroy() {
this._scroll.deregister(this);
}

/** Returns observable that emits an event when the scroll event is fired on the host element. */
elementScrolled(): Observable<Event> {
return this._elementScrolled.asObservable();
}
}


@NgModule({
exports: [Scrollable],
declarations: [Scrollable],
})
export class ScrollModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: ScrollModule,
providers: [Scroll]
};
}
}
2 changes: 1 addition & 1 deletion src/lib/sidenav/sidenav.html
Original file line number Diff line number Diff line change
Expand Up @@ -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()" md-scrollable>
<ng-content></ng-content>
</div>
15 changes: 13 additions & 2 deletions src/lib/sidenav/sidenav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import {Dir, MdError, coerceBooleanProperty, DefaultStyleCompatibilityModeModule
import {A11yModule, A11Y_PROVIDERS} 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 {ScrollModule} from '../core/scroll/scrollable';
import {InteractivityChecker} from '../core/a11y/interactivity-checker';
import {MdLiveAnnouncer} from '../core/a11y/live-announcer';
import {Scroll} from '../core/scroll/scroll';


/** Exception thrown when two MdSidenav are matching the same side. */
Expand Down Expand Up @@ -503,15 +508,21 @@ export class MdSidenavLayout implements AfterContentInit {


@NgModule({
imports: [CommonModule, DefaultStyleCompatibilityModeModule, A11yModule],
imports: [
CommonModule,
DefaultStyleCompatibilityModeModule,
A11yModule,
OverlayModule,
ScrollModule
],
exports: [MdSidenavLayout, MdSidenav, DefaultStyleCompatibilityModeModule],
declarations: [MdSidenavLayout, MdSidenav],
})
export class MdSidenavModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: MdSidenavModule,
providers: [A11Y_PROVIDERS]
providers: [MdLiveAnnouncer, InteractivityChecker, Scroll]
};
}
}
35 changes: 28 additions & 7 deletions src/lib/tooltip/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
animate,
AnimationTransitionEvent,
NgZone,
Optional,
Optional, OnDestroy, OnInit,
} from '@angular/core';
import {
Overlay,
Expand All @@ -23,13 +23,16 @@ import {
ComponentPortal,
OverlayConnectionPosition,
OriginConnectionPosition,
OVERLAY_PROVIDERS,
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 {Scroll} from '../core/scroll/scroll';
import {ScrollModule} from '../core/scroll/scrollable';
import {OverlayPositionBuilder} from '../core/overlay/position/overlay-position-builder';
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';

export type TooltipPosition = 'left' | 'right' | 'above' | 'below' | 'before' | 'after';

Expand All @@ -52,7 +55,7 @@ export const TOUCHEND_HIDE_DELAY = 1500;
},
exportAs: 'mdTooltip',
})
export class MdTooltip {
export class MdTooltip implements OnInit, OnDestroy {
_overlayRef: OverlayRef;
_tooltipInstance: TooltipComponent;

Expand Down Expand Up @@ -92,10 +95,23 @@ export class MdTooltip {
}
}

constructor(private _overlay: Overlay, private _elementRef: ElementRef,
private _viewContainerRef: ViewContainerRef, private _ngZone: NgZone,
constructor(private _overlay: Overlay,
private _scroll: Scroll,
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._scroll.scrolled().subscribe(() => {
if (this._overlayRef) {
this._overlayRef.updatePosition();
}
});
}

/** Dispose the tooltip when destroyed */
ngOnDestroy() {
if (this._tooltipInstance) {
Expand Down Expand Up @@ -349,7 +365,7 @@ export class TooltipComponent {


@NgModule({
imports: [OverlayModule, DefaultStyleCompatibilityModeModule],
imports: [OverlayModule, DefaultStyleCompatibilityModeModule, ScrollModule],
exports: [MdTooltip, TooltipComponent, DefaultStyleCompatibilityModeModule],
declarations: [MdTooltip, TooltipComponent],
entryComponents: [TooltipComponent],
Expand All @@ -358,7 +374,12 @@ export class MdTooltipModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: MdTooltipModule,
providers: OVERLAY_PROVIDERS,
providers: [
Overlay,
OverlayPositionBuilder,
ViewportRuler,
Scroll
]
};
}
}

0 comments on commit 378281d

Please sign in to comment.