Skip to content

Commit 3b0915a

Browse files
crisbetokara
authored andcommitted
feat(viewport-ruler): add common window resize handler (#7113)
BREAKING CHANGE: Previously the `ScrollDispatcher.scrolled` subscription would react both on scroll events and on window resize events. Now it only reacts to scroll events. To react to resize events, subscribe to the `ViewportRuler.change()` stream.
1 parent 3f0142b commit 3b0915a

12 files changed

+124
-47
lines changed

src/cdk/overlay/overlay-ref.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,15 @@ export class OverlayRef implements PortalHost {
9595
// pointer events therefore. Depends on the position strategy and the applied pane boundaries.
9696
this._togglePointerEvents(false);
9797

98+
if (this._config.positionStrategy && this._config.positionStrategy.detach) {
99+
this._config.positionStrategy.detach();
100+
}
101+
98102
if (this._config.scrollStrategy) {
99103
this._config.scrollStrategy.disable();
100104
}
101105

102-
const detachmentResult = this._portalHost.detach();
106+
const detachmentResult = this._portalHost.detach();
103107

104108
// Only emit after everything is detached.
105109
this._detachments.next();

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
ScrollingVisibility,
1818
} from './connected-position';
1919
import {Subject} from 'rxjs/Subject';
20+
import {Subscription} from 'rxjs/Subscription';
2021
import {Observable} from 'rxjs/Observable';
2122
import {Scrollable} from '@angular/cdk/scrolling';
2223
import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scroll-clip';
@@ -35,6 +36,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
3536
/** The overlay to which this strategy is attached. */
3637
private _overlayRef: OverlayRef;
3738

39+
/** Layout direction of the position strategy. */
3840
private _dir = 'ltr';
3941

4042
/** The offset in pixels for the overlay connection point on the x-axis */
@@ -46,6 +48,9 @@ export class ConnectedPositionStrategy implements PositionStrategy {
4648
/** The Scrollable containers used to check scrollable view properties on position change. */
4749
private scrollables: Scrollable[] = [];
4850

51+
/** Subscription to viewport resize events. */
52+
private _resizeSubscription = Subscription.EMPTY;
53+
4954
/** Whether the we're dealing with an RTL context */
5055
get _isRtl() {
5156
return this._dir === 'rtl';
@@ -88,10 +93,19 @@ export class ConnectedPositionStrategy implements PositionStrategy {
8893
attach(overlayRef: OverlayRef): void {
8994
this._overlayRef = overlayRef;
9095
this._pane = overlayRef.overlayElement;
96+
this._resizeSubscription.unsubscribe();
97+
this._resizeSubscription = this._viewportRuler.change().subscribe(() => this.apply());
9198
}
9299

93100
/** Performs any cleanup after the element is destroyed. */
94-
dispose() { }
101+
dispose() {
102+
this._resizeSubscription.unsubscribe();
103+
}
104+
105+
/** @docs-private */
106+
detach() {
107+
this._resizeSubscription.unsubscribe();
108+
}
95109

96110
/**
97111
* Updates the position of the overlay element, using whichever preferred position relative

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export interface PositionStrategy {
1818
/** Updates the position of the overlay element. */
1919
apply(): void;
2020

21+
/** Called when the overlay is detached. */
22+
detach?(): void;
23+
2124
/** Cleans up any DOM modifications made by the position strategy, if necessary. */
2225
dispose(): void;
2326
}

src/cdk/scrolling/scroll-dispatcher.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ describe('Scroll Dispatcher', () => {
7272

7373
scroll.scrolled(0, () => {});
7474
dispatchFakeEvent(document, 'scroll');
75-
dispatchFakeEvent(window, 'resize');
7675

7776
expect(spy).not.toHaveBeenCalled();
7877
subscription.unsubscribe();

src/cdk/scrolling/scroll-dispatcher.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {Platform} from '@angular/cdk/platform';
1111
import {Subject} from 'rxjs/Subject';
1212
import {Subscription} from 'rxjs/Subscription';
1313
import {fromEvent} from 'rxjs/observable/fromEvent';
14-
import {merge} from 'rxjs/observable/merge';
1514
import {auditTime} from 'rxjs/operator/auditTime';
1615
import {Scrollable} from './scrollable';
1716

@@ -87,10 +86,7 @@ export class ScrollDispatcher {
8786

8887
if (!this._globalSubscription) {
8988
this._globalSubscription = this._ngZone.runOutsideAngular(() => {
90-
return merge(
91-
fromEvent(window.document, 'scroll'),
92-
fromEvent(window, 'resize')
93-
).subscribe(() => this._notify());
89+
return fromEvent(window.document, 'scroll').subscribe(() => this._notify());
9490
});
9591
}
9692

src/cdk/scrolling/viewport-ruler.spec.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import {TestBed, inject} from '@angular/core/testing';
1+
import {TestBed, inject, fakeAsync, tick} from '@angular/core/testing';
22
import {ScrollDispatchModule} from './public-api';
33
import {ViewportRuler, VIEWPORT_RULER_PROVIDER} from './viewport-ruler';
4+
import {dispatchFakeEvent} from '@angular/cdk/testing';
45

56

67
// For all tests, we assume the browser window is 1024x786 (outerWidth x outerHeight).
@@ -32,6 +33,10 @@ describe('ViewportRuler', () => {
3233
scrollTo(0, 0);
3334
}));
3435

36+
afterEach(() => {
37+
ruler.ngOnDestroy();
38+
});
39+
3540
it('should get the viewport bounds when the page is not scrolled', () => {
3641
let bounds = ruler.getViewportRect();
3742
expect(bounds.top).toBe(0);
@@ -101,4 +106,37 @@ describe('ViewportRuler', () => {
101106

102107
document.body.removeChild(veryLargeElement);
103108
});
109+
110+
describe('changed event', () => {
111+
it('should dispatch an event when the window is resized', () => {
112+
const spy = jasmine.createSpy('viewport changed spy');
113+
const subscription = ruler.change(0).subscribe(spy);
114+
115+
dispatchFakeEvent(window, 'resize');
116+
expect(spy).toHaveBeenCalled();
117+
subscription.unsubscribe();
118+
});
119+
120+
it('should dispatch an event when the orientation is changed', () => {
121+
const spy = jasmine.createSpy('viewport changed spy');
122+
const subscription = ruler.change(0).subscribe(spy);
123+
124+
dispatchFakeEvent(window, 'orientationchange');
125+
expect(spy).toHaveBeenCalled();
126+
subscription.unsubscribe();
127+
});
128+
129+
it('should be able to throttle the callback', fakeAsync(() => {
130+
const spy = jasmine.createSpy('viewport changed spy');
131+
const subscription = ruler.change(1337).subscribe(spy);
132+
133+
dispatchFakeEvent(window, 'resize');
134+
expect(spy).not.toHaveBeenCalled();
135+
136+
tick(1337);
137+
138+
expect(spy).toHaveBeenCalledTimes(1);
139+
subscription.unsubscribe();
140+
}));
141+
});
104142
});

src/cdk/scrolling/viewport-ruler.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,49 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Injectable, Optional, SkipSelf} from '@angular/core';
9+
import {Injectable, Optional, SkipSelf, NgZone, OnDestroy} from '@angular/core';
10+
import {Platform} from '@angular/cdk/platform';
1011
import {ScrollDispatcher} from './scroll-dispatcher';
12+
import {Observable} from 'rxjs/Observable';
13+
import {fromEvent} from 'rxjs/observable/fromEvent';
14+
import {merge} from 'rxjs/observable/merge';
15+
import {auditTime} from 'rxjs/operator/auditTime';
16+
import {Subscription} from 'rxjs/Subscription';
17+
import {of as observableOf} from 'rxjs/observable/of';
1118

19+
/** Time in ms to throttle the resize events by default. */
20+
export const DEFAULT_RESIZE_TIME = 20;
1221

1322
/**
1423
* Simple utility for getting the bounds of the browser viewport.
1524
* @docs-private
1625
*/
1726
@Injectable()
18-
export class ViewportRuler {
27+
export class ViewportRuler implements OnDestroy {
1928

2029
/** Cached document client rectangle. */
2130
private _documentRect?: ClientRect;
2231

23-
constructor(scrollDispatcher: ScrollDispatcher) {
32+
/** Stream of viewport change events. */
33+
private _change: Observable<Event>;
34+
35+
/** Subscriptions to streams that invalidate the cached viewport dimensions. */
36+
private _invalidateCacheSubscriptions: Subscription[];
37+
38+
constructor(platform: Platform, ngZone: NgZone, scrollDispatcher: ScrollDispatcher) {
39+
this._change = platform.isBrowser ? ngZone.runOutsideAngular(() => {
40+
return merge<Event>(fromEvent(window, 'resize'), fromEvent(window, 'orientationchange'));
41+
}) : observableOf();
42+
2443
// Subscribe to scroll and resize events and update the document rectangle on changes.
25-
scrollDispatcher.scrolled(0, () => this._cacheViewportGeometry());
44+
this._invalidateCacheSubscriptions = [
45+
scrollDispatcher.scrolled(0, () => this._cacheViewportGeometry()),
46+
this.change().subscribe(() => this._cacheViewportGeometry())
47+
];
48+
}
49+
50+
ngOnDestroy() {
51+
this._invalidateCacheSubscriptions.forEach(subscription => subscription.unsubscribe());
2652
}
2753

2854
/** Gets a ClientRect for the viewport's bounds. */
@@ -56,7 +82,6 @@ export class ViewportRuler {
5682
};
5783
}
5884

59-
6085
/**
6186
* Gets the (top, left) scroll position of the viewport.
6287
* @param documentRect
@@ -75,31 +100,40 @@ export class ViewportRuler {
75100
// `document.documentElement` works consistently, where the `top` and `left` values will
76101
// equal negative the scroll position.
77102
const top = -documentRect!.top || document.body.scrollTop || window.scrollY ||
78-
document.documentElement.scrollTop || 0;
103+
document.documentElement.scrollTop || 0;
79104

80105
const left = -documentRect!.left || document.body.scrollLeft || window.scrollX ||
81106
document.documentElement.scrollLeft || 0;
82107

83108
return {top, left};
84109
}
85110

111+
/**
112+
* Returns a stream that emits whenever the size of the viewport changes.
113+
* @param throttle Time in milliseconds to throttle the stream.
114+
*/
115+
change(throttleTime: number = DEFAULT_RESIZE_TIME): Observable<string> {
116+
return throttleTime > 0 ? auditTime.call(this._change, throttleTime) : this._change;
117+
}
118+
86119
/** Caches the latest client rectangle of the document element. */
87120
_cacheViewportGeometry() {
88121
this._documentRect = document.documentElement.getBoundingClientRect();
89122
}
90-
91123
}
92124

93125
/** @docs-private */
94126
export function VIEWPORT_RULER_PROVIDER_FACTORY(parentRuler: ViewportRuler,
127+
platform: Platform,
128+
ngZone: NgZone,
95129
scrollDispatcher: ScrollDispatcher) {
96-
return parentRuler || new ViewportRuler(scrollDispatcher);
130+
return parentRuler || new ViewportRuler(platform, ngZone, scrollDispatcher);
97131
}
98132

99133
/** @docs-private */
100134
export const VIEWPORT_RULER_PROVIDER = {
101135
// If there is already a ViewportRuler available, use that. Otherwise, provide a new one.
102136
provide: ViewportRuler,
103-
deps: [[new Optional(), new SkipSelf(), ViewportRuler], ScrollDispatcher],
137+
deps: [[new Optional(), new SkipSelf(), ViewportRuler], Platform, NgZone, ScrollDispatcher],
104138
useFactory: VIEWPORT_RULER_PROVIDER_FACTORY
105139
};

src/lib/tabs/tab-group.spec.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/t
22
import {Component, QueryList, ViewChild, ViewChildren} from '@angular/core';
33
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
44
import {By} from '@angular/platform-browser';
5-
import {ViewportRuler} from '@angular/cdk/scrolling';
6-
import {dispatchFakeEvent, FakeViewportRuler} from '@angular/cdk/testing';
5+
import {dispatchFakeEvent} from '@angular/cdk/testing';
76
import {Observable} from 'rxjs/Observable';
87
import {MatTab, MatTabGroup, MatTabHeaderPosition, MatTabsModule} from './index';
98

@@ -20,9 +19,6 @@ describe('MatTabGroup', () => {
2019
DisabledTabsTestApp,
2120
TabGroupWithSimpleApi,
2221
],
23-
providers: [
24-
{provide: ViewportRuler, useClass: FakeViewportRuler},
25-
]
2622
});
2723

2824
TestBed.compileComponents();

src/lib/tabs/tab-header.spec.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@ import {CommonModule} from '@angular/common';
66
import {By} from '@angular/platform-browser';
77
import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
88
import {PortalModule} from '@angular/cdk/portal';
9-
import {ViewportRuler} from '@angular/cdk/scrolling';
109
import {Direction, Directionality} from '@angular/cdk/bidi';
11-
import {dispatchFakeEvent, dispatchKeyboardEvent, FakeViewportRuler} from '@angular/cdk/testing';
10+
import {dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing';
1211
import {MatTabHeader} from './tab-header';
1312
import {MatRippleModule} from '@angular/material/core';
1413
import {MatInkBar} from './ink-bar';
1514
import {MatTabLabelWrapper} from './tab-label-wrapper';
1615
import {Subject} from 'rxjs/Subject';
17-
16+
import {VIEWPORT_RULER_PROVIDER} from '@angular/cdk/scrolling';
1817

1918

2019
describe('MatTabHeader', () => {
@@ -34,8 +33,8 @@ describe('MatTabHeader', () => {
3433
SimpleTabHeaderApp,
3534
],
3635
providers: [
36+
VIEWPORT_RULER_PROVIDER,
3737
{provide: Directionality, useFactory: () => ({value: dir, change: change.asObservable()})},
38-
{provide: ViewportRuler, useClass: FakeViewportRuler},
3938
]
4039
});
4140

src/lib/tabs/tab-header.ts

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

99
import {Direction, Directionality} from '@angular/cdk/bidi';
1010
import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
11-
import {auditTime, startWith} from '@angular/cdk/rxjs';
11+
import {startWith} from '@angular/cdk/rxjs';
1212
import {
1313
AfterContentChecked,
1414
AfterContentInit,
@@ -28,12 +28,12 @@ import {
2828
ViewEncapsulation,
2929
} from '@angular/core';
3030
import {CanDisableRipple, mixinDisableRipple} from '@angular/material/core';
31-
import {fromEvent} from 'rxjs/observable/fromEvent';
3231
import {merge} from 'rxjs/observable/merge';
3332
import {of as observableOf} from 'rxjs/observable/of';
3433
import {Subscription} from 'rxjs/Subscription';
3534
import {MatInkBar} from './ink-bar';
3635
import {MatTabLabelWrapper} from './tab-label-wrapper';
36+
import {ViewportRuler} from '@angular/cdk/scrolling';
3737

3838

3939
/**
@@ -134,6 +134,7 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
134134
constructor(private _elementRef: ElementRef,
135135
private _renderer: Renderer2,
136136
private _changeDetectorRef: ChangeDetectorRef,
137+
private _viewportRuler: ViewportRuler,
137138
@Optional() private _dir: Directionality) {
138139
super();
139140
}
@@ -186,9 +187,7 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
186187
*/
187188
ngAfterContentInit() {
188189
const dirChange = this._dir ? this._dir.change : observableOf(null);
189-
const resize = typeof window !== 'undefined' ?
190-
auditTime.call(fromEvent(window, 'resize'), 150) :
191-
observableOf(null);
190+
const resize = this._viewportRuler.change(150);
192191

193192
this._realignInkBar = startWith.call(merge(dirChange, resize), null).subscribe(() => {
194193
this._updatePagination();

0 commit comments

Comments
 (0)