Skip to content

Commit 2a4bfb6

Browse files
committed
feat(scroll): provide directive and service to listen to scrolling
1 parent 3cf25a0 commit 2a4bfb6

File tree

7 files changed

+248
-13
lines changed

7 files changed

+248
-13
lines changed

src/lib/core/core.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {PortalModule} from './portal/portal-directives';
66
import {OverlayModule} from './overlay/overlay-directives';
77
import {A11yModule, A11Y_PROVIDERS} from './a11y/index';
88
import {OVERLAY_PROVIDERS} from './overlay/overlay';
9+
import {Scroll} from './scroll/scroll';
10+
import {ScrollModule} from './scroll/scrollable';
911

1012

1113
// RTL
@@ -47,6 +49,8 @@ export {
4749
} from './overlay/overlay-directives';
4850
export * from './overlay/position/connected-position-strategy';
4951
export * from './overlay/position/connected-position';
52+
export * from './scroll/scrollable';
53+
export * from './scroll/scroll';
5054

5155
// Gestures
5256
export {GestureConfig} from './gestures/gesture-config';
@@ -107,16 +111,33 @@ export {coerceNumberProperty} from './coercion/number-property';
107111
export {DefaultStyleCompatibilityModeModule} from './compatibility/default-mode';
108112
export {NoConflictStyleCompatibilityMode} from './compatibility/no-conflict-mode';
109113

114+
// Scroll
115+
export {Scroll} from './scroll/scroll';
116+
export {Scrollable} from './scroll/scrollable';
110117

111118
@NgModule({
112-
imports: [MdLineModule, RtlModule, MdRippleModule, PortalModule, OverlayModule, A11yModule],
113-
exports: [MdLineModule, RtlModule, MdRippleModule, PortalModule, OverlayModule, A11yModule],
119+
imports: [MdLineModule,
120+
RtlModule,
121+
MdRippleModule,
122+
PortalModule,
123+
OverlayModule,
124+
A11yModule,
125+
ScrollModule
126+
],
127+
exports: [MdLineModule,
128+
RtlModule,
129+
MdRippleModule,
130+
PortalModule,
131+
OverlayModule,
132+
A11yModule,
133+
ScrollModule
134+
],
114135
})
115136
export class MdCoreModule {
116137
static forRoot(): ModuleWithProviders {
117138
return {
118139
ngModule: MdCoreModule,
119-
providers: [A11Y_PROVIDERS, OVERLAY_PROVIDERS],
140+
providers: [A11Y_PROVIDERS, OVERLAY_PROVIDERS, Scroll],
120141
};
121142
}
122143
}

src/lib/core/scroll/scroll.spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {inject, TestBed, async, ComponentFixture} from '@angular/core/testing';
2+
import {NgModule, Component, ViewChild, ElementRef} from '@angular/core';
3+
import {Scroll} from './scroll';
4+
import {ScrollModule, Scrollable} from './scrollable';
5+
6+
describe('Scrollable', () => {
7+
let scroll: Scroll;
8+
let fixture: ComponentFixture<ScrollingComponent>;
9+
10+
beforeEach(async(() => {
11+
TestBed.configureTestingModule({
12+
imports: [ScrollModule.forRoot(), ScrollTestModule],
13+
});
14+
15+
TestBed.compileComponents();
16+
}));
17+
18+
beforeEach(inject([Scroll], (s: Scroll) => {
19+
scroll = s;
20+
21+
fixture = TestBed.createComponent(ScrollingComponent);
22+
fixture.detectChanges();
23+
}));
24+
25+
it('should register the scrollable directive with the scroll service', () => {
26+
const componentScrollable = fixture.componentInstance.scrollable;
27+
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(true);
28+
});
29+
30+
it('should deregister the scrollable directive when the component is destroyed', () => {
31+
const componentScrollable = fixture.componentInstance.scrollable;
32+
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(true);
33+
34+
fixture.destroy();
35+
expect(scroll.scrollableReferences.has(componentScrollable)).toBe(false);
36+
});
37+
38+
it('should notify through the directive and service that a scroll event occurred', () => {
39+
let hasDirectiveScrollNotified = false;
40+
// Listen for notifications from scroll directive
41+
let scrollable = fixture.componentInstance.scrollable;
42+
scrollable.elementScrolled().subscribe(() => { hasDirectiveScrollNotified = true; });
43+
44+
// Listen for notifications from scroll service
45+
let hasServiceScrollNotified = false;
46+
scroll.scrolled().subscribe(() => { hasServiceScrollNotified = true; });
47+
48+
// Emit a scroll event from the scrolling element in our component.
49+
// This event should be picked up by the scrollable directive and notify.
50+
// The notification should be picked up by the service.
51+
fixture.componentInstance.scrollingElement.nativeElement.dispatchEvent(new Event('scroll'));
52+
53+
expect(hasDirectiveScrollNotified).toBe(true);
54+
expect(hasServiceScrollNotified).toBe(true);
55+
});
56+
});
57+
58+
59+
/** Simple component that contains a large div and can be scrolled. */
60+
@Component({
61+
template: `<div #scrollingElement md-scrollable style="height: 9999px"></div>`
62+
})
63+
class ScrollingComponent {
64+
@ViewChild(Scrollable) scrollable: Scrollable;
65+
@ViewChild('scrollingElement') scrollingElement: ElementRef;
66+
}
67+
68+
const TEST_COMPONENTS = [ScrollingComponent];
69+
@NgModule({
70+
imports: [ScrollModule],
71+
providers: [Scroll],
72+
exports: TEST_COMPONENTS,
73+
declarations: TEST_COMPONENTS,
74+
entryComponents: TEST_COMPONENTS,
75+
})
76+
class ScrollTestModule { }

src/lib/core/scroll/scroll.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {Injectable} from '@angular/core';
2+
import {Scrollable} from './scrollable';
3+
import {Subject} from 'rxjs/Subject';
4+
import {Observable} from 'rxjs/Observable';
5+
import {Subscription} from 'rxjs/Subscription';
6+
7+
8+
/**
9+
* Service contained all registered Scrollable references and emits an event when any one of the
10+
* Scrollable references emit a scrolled event.
11+
*/
12+
@Injectable()
13+
export class Scroll {
14+
/** Subject for notifying that a registered scrollable reference element has been scrolled. */
15+
_scrolled: Subject<Event> = new Subject();
16+
17+
/**
18+
* Map of all the scrollable references that are registered with the service and their
19+
* scroll event subscriptions.
20+
*/
21+
scrollableReferences: Map<Scrollable, Subscription> = new Map();
22+
23+
constructor() {
24+
// By default, notify a scroll event when the document is scrolled or the window is resized.
25+
window.document.addEventListener('scroll', this._notify.bind(this));
26+
window.addEventListener('resize', this._notify.bind(this));
27+
}
28+
29+
/**
30+
* Registers a Scrollable with the service and listens for its scrolled events. When the
31+
* scrollable is scrolled, the service emits the event in its scrolled observable.
32+
*/
33+
register(scrollable: Scrollable): void {
34+
const scrollSubscription = scrollable.elementScrolled().subscribe(this._notify.bind(this));
35+
this.scrollableReferences.set(scrollable, scrollSubscription);
36+
}
37+
38+
/**
39+
* Deregisters a Scrollable reference and unsubscribes from its scroll event observable.
40+
*/
41+
deregister(scrollable: Scrollable): void {
42+
this.scrollableReferences.get(scrollable).unsubscribe();
43+
this.scrollableReferences.delete(scrollable);
44+
}
45+
46+
/**
47+
* Returns an observable that emits an event whenever any of the registered Scrollable
48+
* references (or window, document, or body) fire a scrolled event.
49+
* TODO: Add an event limiter that includes throttle with the leading and trailing events.
50+
*/
51+
scrolled(): Observable<Event> {
52+
return this._scrolled.asObservable();
53+
}
54+
55+
/** Sends a notification that a scroll event has been fired. */
56+
_notify(e: Event) {
57+
this._scrolled.next(e);
58+
}
59+
}

src/lib/core/scroll/scrollable.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
Directive, ElementRef, OnInit, OnDestroy, ModuleWithProviders,
3+
NgModule
4+
} from '@angular/core';
5+
import {Subject} from 'rxjs/Subject';
6+
import {Observable} from 'rxjs/Observable';
7+
import {Scroll} from './scroll';
8+
9+
10+
/**
11+
* Sends an event when the directive's element is scrolled. Registers itself with the Scroll
12+
* service to include itself as part of its collection of scrolling events that it can be listened
13+
* to through the service.
14+
*/
15+
@Directive({
16+
selector: '[md-scrollable]'
17+
})
18+
export class Scrollable implements OnInit, OnDestroy {
19+
/** Subject for notifying that the element has been scrolled. */
20+
private _elementScrolled: Subject<Event> = new Subject();
21+
22+
constructor(private _elementRef: ElementRef, private _scroll: Scroll) {}
23+
24+
ngOnInit() {
25+
this._scroll.register(this);
26+
this._elementRef.nativeElement.addEventListener('scroll', (e: Event) => {
27+
this._elementScrolled.next(e);
28+
});
29+
}
30+
31+
ngOnDestroy() {
32+
this._scroll.deregister(this);
33+
}
34+
35+
/** Returns observable that emits an event when the scroll event is fired on the host element. */
36+
elementScrolled(): Observable<Event> {
37+
return this._elementScrolled.asObservable();
38+
}
39+
}
40+
41+
42+
@NgModule({
43+
exports: [Scrollable],
44+
declarations: [Scrollable],
45+
})
46+
export class ScrollModule {
47+
static forRoot(): ModuleWithProviders {
48+
return {
49+
ngModule: ScrollModule,
50+
providers: [Scroll]
51+
};
52+
}
53+
}

src/lib/sidenav/sidenav-container.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33

44
<ng-content select="md-sidenav, mat-sidenav"></ng-content>
55

6-
<div class="md-sidenav-content" [ngStyle]="_getStyles()">
6+
<div class="md-sidenav-content" [ngStyle]="_getStyles()" md-scrollable>
77
<ng-content></ng-content>
88
</div>

src/lib/sidenav/sidenav.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import {Dir, MdError, coerceBooleanProperty, DefaultStyleCompatibilityModeModule
2020
import {A11yModule, A11Y_PROVIDERS} from '../core/a11y/index';
2121
import {FocusTrap} from '../core/a11y/focus-trap';
2222
import {ESCAPE} from '../core/keyboard/keycodes';
23+
import {OverlayModule} from '../core/overlay/overlay-directives';
24+
import {ScrollModule} from '../core/scroll/scrollable';
25+
import {InteractivityChecker} from '../core/a11y/interactivity-checker';
26+
import {MdLiveAnnouncer} from '../core/a11y/live-announcer';
27+
import {Scroll} from '../core/scroll/scroll';
2328

2429

2530
/** Exception thrown when two MdSidenav are matching the same side. */
@@ -503,15 +508,15 @@ export class MdSidenavContainer implements AfterContentInit {
503508

504509

505510
@NgModule({
506-
imports: [CommonModule, DefaultStyleCompatibilityModeModule, A11yModule],
511+
imports: [CommonModule, DefaultStyleCompatibilityModeModule, A11yModule, OverlayModule],
507512
exports: [MdSidenavContainer, MdSidenav, DefaultStyleCompatibilityModeModule],
508513
declarations: [MdSidenavContainer, MdSidenav],
509514
})
510515
export class MdSidenavModule {
511516
static forRoot(): ModuleWithProviders {
512517
return {
513518
ngModule: MdSidenavModule,
514-
providers: [A11Y_PROVIDERS]
519+
providers: [MdLiveAnnouncer, InteractivityChecker, Scroll]
515520
};
516521
}
517522
}

src/lib/tooltip/tooltip.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
animate,
1414
AnimationTransitionEvent,
1515
NgZone,
16-
Optional,
16+
Optional, OnDestroy, OnInit,
1717
} from '@angular/core';
1818
import {
1919
Overlay,
@@ -23,13 +23,16 @@ import {
2323
ComponentPortal,
2424
OverlayConnectionPosition,
2525
OriginConnectionPosition,
26-
OVERLAY_PROVIDERS,
2726
DefaultStyleCompatibilityModeModule,
2827
} from '../core';
2928
import {MdTooltipInvalidPositionError} from './tooltip-errors';
3029
import {Observable} from 'rxjs/Observable';
3130
import {Subject} from 'rxjs/Subject';
3231
import {Dir} from '../core/rtl/dir';
32+
import {Scroll} from '../core/scroll/scroll';
33+
import {ScrollModule} from '../core/scroll/scrollable';
34+
import {OverlayPositionBuilder} from '../core/overlay/position/overlay-position-builder';
35+
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
3336

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

@@ -52,7 +55,7 @@ export const TOUCHEND_HIDE_DELAY = 1500;
5255
},
5356
exportAs: 'mdTooltip',
5457
})
55-
export class MdTooltip {
58+
export class MdTooltip implements OnInit, OnDestroy {
5659
_overlayRef: OverlayRef;
5760
_tooltipInstance: TooltipComponent;
5861

@@ -102,10 +105,23 @@ export class MdTooltip {
102105
get _deprecatedMessage(): string { return this.message; }
103106
set _deprecatedMessage(v: string) { this.message = v; }
104107

105-
constructor(private _overlay: Overlay, private _elementRef: ElementRef,
106-
private _viewContainerRef: ViewContainerRef, private _ngZone: NgZone,
108+
constructor(private _overlay: Overlay,
109+
private _scroll: Scroll,
110+
private _elementRef: ElementRef,
111+
private _viewContainerRef: ViewContainerRef,
112+
private _ngZone: NgZone,
107113
@Optional() private _dir: Dir) {}
108114

115+
ngOnInit() {
116+
// When a scroll on the page occurs, update the position in case this tooltip needs
117+
// to be repositioned.
118+
this._scroll.scrolled().subscribe(() => {
119+
if (this._overlayRef) {
120+
this._overlayRef.updatePosition();
121+
}
122+
});
123+
}
124+
109125
/** Dispose the tooltip when destroyed */
110126
ngOnDestroy() {
111127
if (this._tooltipInstance) {
@@ -359,7 +375,7 @@ export class TooltipComponent {
359375

360376

361377
@NgModule({
362-
imports: [OverlayModule, DefaultStyleCompatibilityModeModule],
378+
imports: [OverlayModule, DefaultStyleCompatibilityModeModule, ScrollModule],
363379
exports: [MdTooltip, TooltipComponent, DefaultStyleCompatibilityModeModule],
364380
declarations: [MdTooltip, TooltipComponent],
365381
entryComponents: [TooltipComponent],
@@ -368,7 +384,12 @@ export class MdTooltipModule {
368384
static forRoot(): ModuleWithProviders {
369385
return {
370386
ngModule: MdTooltipModule,
371-
providers: OVERLAY_PROVIDERS,
387+
providers: [
388+
Overlay,
389+
OverlayPositionBuilder,
390+
ViewportRuler,
391+
Scroll
392+
]
372393
};
373394
}
374395
}

0 commit comments

Comments
 (0)