mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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.
This commit is contained in:
committed by
Kara
parent
3f0142b51e
commit
3b0915adf0
@@ -95,11 +95,15 @@ export class OverlayRef implements PortalHost {
|
||||
// pointer events therefore. Depends on the position strategy and the applied pane boundaries.
|
||||
this._togglePointerEvents(false);
|
||||
|
||||
if (this._config.positionStrategy && this._config.positionStrategy.detach) {
|
||||
this._config.positionStrategy.detach();
|
||||
}
|
||||
|
||||
if (this._config.scrollStrategy) {
|
||||
this._config.scrollStrategy.disable();
|
||||
}
|
||||
|
||||
const detachmentResult = this._portalHost.detach();
|
||||
const detachmentResult = this._portalHost.detach();
|
||||
|
||||
// Only emit after everything is detached.
|
||||
this._detachments.next();
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ScrollingVisibility,
|
||||
} from './connected-position';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Scrollable} from '@angular/cdk/scrolling';
|
||||
import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scroll-clip';
|
||||
@@ -35,6 +36,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
/** The overlay to which this strategy is attached. */
|
||||
private _overlayRef: OverlayRef;
|
||||
|
||||
/** Layout direction of the position strategy. */
|
||||
private _dir = 'ltr';
|
||||
|
||||
/** The offset in pixels for the overlay connection point on the x-axis */
|
||||
@@ -46,6 +48,9 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
/** The Scrollable containers used to check scrollable view properties on position change. */
|
||||
private scrollables: Scrollable[] = [];
|
||||
|
||||
/** Subscription to viewport resize events. */
|
||||
private _resizeSubscription = Subscription.EMPTY;
|
||||
|
||||
/** Whether the we're dealing with an RTL context */
|
||||
get _isRtl() {
|
||||
return this._dir === 'rtl';
|
||||
@@ -88,10 +93,19 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
attach(overlayRef: OverlayRef): void {
|
||||
this._overlayRef = overlayRef;
|
||||
this._pane = overlayRef.overlayElement;
|
||||
this._resizeSubscription.unsubscribe();
|
||||
this._resizeSubscription = this._viewportRuler.change().subscribe(() => this.apply());
|
||||
}
|
||||
|
||||
/** Performs any cleanup after the element is destroyed. */
|
||||
dispose() { }
|
||||
dispose() {
|
||||
this._resizeSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
/** @docs-private */
|
||||
detach() {
|
||||
this._resizeSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the position of the overlay element, using whichever preferred position relative
|
||||
|
||||
@@ -18,6 +18,9 @@ export interface PositionStrategy {
|
||||
/** Updates the position of the overlay element. */
|
||||
apply(): void;
|
||||
|
||||
/** Called when the overlay is detached. */
|
||||
detach?(): void;
|
||||
|
||||
/** Cleans up any DOM modifications made by the position strategy, if necessary. */
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ describe('Scroll Dispatcher', () => {
|
||||
|
||||
scroll.scrolled(0, () => {});
|
||||
dispatchFakeEvent(document, 'scroll');
|
||||
dispatchFakeEvent(window, 'resize');
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
subscription.unsubscribe();
|
||||
|
||||
@@ -11,7 +11,6 @@ import {Platform} from '@angular/cdk/platform';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {fromEvent} from 'rxjs/observable/fromEvent';
|
||||
import {merge} from 'rxjs/observable/merge';
|
||||
import {auditTime} from 'rxjs/operator/auditTime';
|
||||
import {Scrollable} from './scrollable';
|
||||
|
||||
@@ -87,10 +86,7 @@ export class ScrollDispatcher {
|
||||
|
||||
if (!this._globalSubscription) {
|
||||
this._globalSubscription = this._ngZone.runOutsideAngular(() => {
|
||||
return merge(
|
||||
fromEvent(window.document, 'scroll'),
|
||||
fromEvent(window, 'resize')
|
||||
).subscribe(() => this._notify());
|
||||
return fromEvent(window.document, 'scroll').subscribe(() => this._notify());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {TestBed, inject} from '@angular/core/testing';
|
||||
import {TestBed, inject, fakeAsync, tick} from '@angular/core/testing';
|
||||
import {ScrollDispatchModule} from './public-api';
|
||||
import {ViewportRuler, VIEWPORT_RULER_PROVIDER} from './viewport-ruler';
|
||||
import {dispatchFakeEvent} from '@angular/cdk/testing';
|
||||
|
||||
|
||||
// For all tests, we assume the browser window is 1024x786 (outerWidth x outerHeight).
|
||||
@@ -32,6 +33,10 @@ describe('ViewportRuler', () => {
|
||||
scrollTo(0, 0);
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
ruler.ngOnDestroy();
|
||||
});
|
||||
|
||||
it('should get the viewport bounds when the page is not scrolled', () => {
|
||||
let bounds = ruler.getViewportRect();
|
||||
expect(bounds.top).toBe(0);
|
||||
@@ -101,4 +106,37 @@ describe('ViewportRuler', () => {
|
||||
|
||||
document.body.removeChild(veryLargeElement);
|
||||
});
|
||||
|
||||
describe('changed event', () => {
|
||||
it('should dispatch an event when the window is resized', () => {
|
||||
const spy = jasmine.createSpy('viewport changed spy');
|
||||
const subscription = ruler.change(0).subscribe(spy);
|
||||
|
||||
dispatchFakeEvent(window, 'resize');
|
||||
expect(spy).toHaveBeenCalled();
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('should dispatch an event when the orientation is changed', () => {
|
||||
const spy = jasmine.createSpy('viewport changed spy');
|
||||
const subscription = ruler.change(0).subscribe(spy);
|
||||
|
||||
dispatchFakeEvent(window, 'orientationchange');
|
||||
expect(spy).toHaveBeenCalled();
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('should be able to throttle the callback', fakeAsync(() => {
|
||||
const spy = jasmine.createSpy('viewport changed spy');
|
||||
const subscription = ruler.change(1337).subscribe(spy);
|
||||
|
||||
dispatchFakeEvent(window, 'resize');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
|
||||
tick(1337);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
subscription.unsubscribe();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,23 +6,49 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable, Optional, SkipSelf} from '@angular/core';
|
||||
import {Injectable, Optional, SkipSelf, NgZone, OnDestroy} from '@angular/core';
|
||||
import {Platform} from '@angular/cdk/platform';
|
||||
import {ScrollDispatcher} from './scroll-dispatcher';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {fromEvent} from 'rxjs/observable/fromEvent';
|
||||
import {merge} from 'rxjs/observable/merge';
|
||||
import {auditTime} from 'rxjs/operator/auditTime';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {of as observableOf} from 'rxjs/observable/of';
|
||||
|
||||
/** Time in ms to throttle the resize events by default. */
|
||||
export const DEFAULT_RESIZE_TIME = 20;
|
||||
|
||||
/**
|
||||
* Simple utility for getting the bounds of the browser viewport.
|
||||
* @docs-private
|
||||
*/
|
||||
@Injectable()
|
||||
export class ViewportRuler {
|
||||
export class ViewportRuler implements OnDestroy {
|
||||
|
||||
/** Cached document client rectangle. */
|
||||
private _documentRect?: ClientRect;
|
||||
|
||||
constructor(scrollDispatcher: ScrollDispatcher) {
|
||||
/** Stream of viewport change events. */
|
||||
private _change: Observable<Event>;
|
||||
|
||||
/** Subscriptions to streams that invalidate the cached viewport dimensions. */
|
||||
private _invalidateCacheSubscriptions: Subscription[];
|
||||
|
||||
constructor(platform: Platform, ngZone: NgZone, scrollDispatcher: ScrollDispatcher) {
|
||||
this._change = platform.isBrowser ? ngZone.runOutsideAngular(() => {
|
||||
return merge<Event>(fromEvent(window, 'resize'), fromEvent(window, 'orientationchange'));
|
||||
}) : observableOf();
|
||||
|
||||
// Subscribe to scroll and resize events and update the document rectangle on changes.
|
||||
scrollDispatcher.scrolled(0, () => this._cacheViewportGeometry());
|
||||
this._invalidateCacheSubscriptions = [
|
||||
scrollDispatcher.scrolled(0, () => this._cacheViewportGeometry()),
|
||||
this.change().subscribe(() => this._cacheViewportGeometry())
|
||||
];
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._invalidateCacheSubscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
/** Gets a ClientRect for the viewport's bounds. */
|
||||
@@ -56,7 +82,6 @@ export class ViewportRuler {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the (top, left) scroll position of the viewport.
|
||||
* @param documentRect
|
||||
@@ -75,7 +100,7 @@ export class ViewportRuler {
|
||||
// `document.documentElement` works consistently, where the `top` and `left` values will
|
||||
// equal negative the scroll position.
|
||||
const top = -documentRect!.top || document.body.scrollTop || window.scrollY ||
|
||||
document.documentElement.scrollTop || 0;
|
||||
document.documentElement.scrollTop || 0;
|
||||
|
||||
const left = -documentRect!.left || document.body.scrollLeft || window.scrollX ||
|
||||
document.documentElement.scrollLeft || 0;
|
||||
@@ -83,23 +108,32 @@ export class ViewportRuler {
|
||||
return {top, left};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a stream that emits whenever the size of the viewport changes.
|
||||
* @param throttle Time in milliseconds to throttle the stream.
|
||||
*/
|
||||
change(throttleTime: number = DEFAULT_RESIZE_TIME): Observable<string> {
|
||||
return throttleTime > 0 ? auditTime.call(this._change, throttleTime) : this._change;
|
||||
}
|
||||
|
||||
/** Caches the latest client rectangle of the document element. */
|
||||
_cacheViewportGeometry() {
|
||||
this._documentRect = document.documentElement.getBoundingClientRect();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** @docs-private */
|
||||
export function VIEWPORT_RULER_PROVIDER_FACTORY(parentRuler: ViewportRuler,
|
||||
platform: Platform,
|
||||
ngZone: NgZone,
|
||||
scrollDispatcher: ScrollDispatcher) {
|
||||
return parentRuler || new ViewportRuler(scrollDispatcher);
|
||||
return parentRuler || new ViewportRuler(platform, ngZone, scrollDispatcher);
|
||||
}
|
||||
|
||||
/** @docs-private */
|
||||
export const VIEWPORT_RULER_PROVIDER = {
|
||||
// If there is already a ViewportRuler available, use that. Otherwise, provide a new one.
|
||||
provide: ViewportRuler,
|
||||
deps: [[new Optional(), new SkipSelf(), ViewportRuler], ScrollDispatcher],
|
||||
deps: [[new Optional(), new SkipSelf(), ViewportRuler], Platform, NgZone, ScrollDispatcher],
|
||||
useFactory: VIEWPORT_RULER_PROVIDER_FACTORY
|
||||
};
|
||||
|
||||
@@ -2,8 +2,7 @@ import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/t
|
||||
import {Component, QueryList, ViewChild, ViewChildren} from '@angular/core';
|
||||
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {ViewportRuler} from '@angular/cdk/scrolling';
|
||||
import {dispatchFakeEvent, FakeViewportRuler} from '@angular/cdk/testing';
|
||||
import {dispatchFakeEvent} from '@angular/cdk/testing';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {MatTab, MatTabGroup, MatTabHeaderPosition, MatTabsModule} from './index';
|
||||
|
||||
@@ -20,9 +19,6 @@ describe('MatTabGroup', () => {
|
||||
DisabledTabsTestApp,
|
||||
TabGroupWithSimpleApi,
|
||||
],
|
||||
providers: [
|
||||
{provide: ViewportRuler, useClass: FakeViewportRuler},
|
||||
]
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
|
||||
@@ -6,15 +6,14 @@ import {CommonModule} from '@angular/common';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
|
||||
import {PortalModule} from '@angular/cdk/portal';
|
||||
import {ViewportRuler} from '@angular/cdk/scrolling';
|
||||
import {Direction, Directionality} from '@angular/cdk/bidi';
|
||||
import {dispatchFakeEvent, dispatchKeyboardEvent, FakeViewportRuler} from '@angular/cdk/testing';
|
||||
import {dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing';
|
||||
import {MatTabHeader} from './tab-header';
|
||||
import {MatRippleModule} from '@angular/material/core';
|
||||
import {MatInkBar} from './ink-bar';
|
||||
import {MatTabLabelWrapper} from './tab-label-wrapper';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
|
||||
import {VIEWPORT_RULER_PROVIDER} from '@angular/cdk/scrolling';
|
||||
|
||||
|
||||
describe('MatTabHeader', () => {
|
||||
@@ -34,8 +33,8 @@ describe('MatTabHeader', () => {
|
||||
SimpleTabHeaderApp,
|
||||
],
|
||||
providers: [
|
||||
VIEWPORT_RULER_PROVIDER,
|
||||
{provide: Directionality, useFactory: () => ({value: dir, change: change.asObservable()})},
|
||||
{provide: ViewportRuler, useClass: FakeViewportRuler},
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import {Direction, Directionality} from '@angular/cdk/bidi';
|
||||
import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
|
||||
import {auditTime, startWith} from '@angular/cdk/rxjs';
|
||||
import {startWith} from '@angular/cdk/rxjs';
|
||||
import {
|
||||
AfterContentChecked,
|
||||
AfterContentInit,
|
||||
@@ -28,12 +28,12 @@ import {
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import {CanDisableRipple, mixinDisableRipple} from '@angular/material/core';
|
||||
import {fromEvent} from 'rxjs/observable/fromEvent';
|
||||
import {merge} from 'rxjs/observable/merge';
|
||||
import {of as observableOf} from 'rxjs/observable/of';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {MatInkBar} from './ink-bar';
|
||||
import {MatTabLabelWrapper} from './tab-label-wrapper';
|
||||
import {ViewportRuler} from '@angular/cdk/scrolling';
|
||||
|
||||
|
||||
/**
|
||||
@@ -134,6 +134,7 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
|
||||
constructor(private _elementRef: ElementRef,
|
||||
private _renderer: Renderer2,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _viewportRuler: ViewportRuler,
|
||||
@Optional() private _dir: Directionality) {
|
||||
super();
|
||||
}
|
||||
@@ -186,9 +187,7 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
|
||||
*/
|
||||
ngAfterContentInit() {
|
||||
const dirChange = this._dir ? this._dir.change : observableOf(null);
|
||||
const resize = typeof window !== 'undefined' ?
|
||||
auditTime.call(fromEvent(window, 'resize'), 150) :
|
||||
observableOf(null);
|
||||
const resize = this._viewportRuler.change(150);
|
||||
|
||||
this._realignInkBar = startWith.call(merge(dirChange, resize), null).subscribe(() => {
|
||||
this._updatePagination();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
|
||||
import {Component, ViewChild} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {ViewportRuler} from '@angular/cdk/scrolling';
|
||||
import {dispatchFakeEvent, dispatchMouseEvent, FakeViewportRuler} from '@angular/cdk/testing';
|
||||
import {dispatchFakeEvent, dispatchMouseEvent} from '@angular/cdk/testing';
|
||||
import {Direction, Directionality} from '@angular/cdk/bidi';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {MatTabNav, MatTabsModule, MatTabLink} from '../index';
|
||||
@@ -24,7 +23,6 @@ describe('MatTabNavBar', () => {
|
||||
value: dir,
|
||||
change: dirChange.asObservable()
|
||||
})},
|
||||
{provide: ViewportRuler, useClass: FakeViewportRuler},
|
||||
]
|
||||
});
|
||||
|
||||
@@ -173,7 +171,7 @@ describe('MatTabNavBar', () => {
|
||||
spyOn(inkBar, 'alignToElement');
|
||||
|
||||
dispatchFakeEvent(window, 'resize');
|
||||
tick(10);
|
||||
tick(150);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(inkBar.alignToElement).toHaveBeenCalled();
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
import {Directionality} from '@angular/cdk/bidi';
|
||||
import {coerceBooleanProperty} from '@angular/cdk/coercion';
|
||||
import {Platform} from '@angular/cdk/platform';
|
||||
import {auditTime, takeUntil} from '@angular/cdk/rxjs';
|
||||
import {takeUntil} from '@angular/cdk/rxjs';
|
||||
import {ViewportRuler} from '@angular/cdk/scrolling';
|
||||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
@@ -41,7 +42,6 @@ import {
|
||||
RippleGlobalOptions,
|
||||
ThemePalette,
|
||||
} from '@angular/material/core';
|
||||
import {fromEvent} from 'rxjs/observable/fromEvent';
|
||||
import {merge} from 'rxjs/observable/merge';
|
||||
import {of as observableOf} from 'rxjs/observable/of';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
@@ -114,7 +114,8 @@ export class MatTabNav extends _MatTabNavMixinBase implements AfterContentInit,
|
||||
elementRef: ElementRef,
|
||||
@Optional() private _dir: Directionality,
|
||||
private _ngZone: NgZone,
|
||||
private _changeDetectorRef: ChangeDetectorRef) {
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _viewportRuler: ViewportRuler) {
|
||||
super(renderer, elementRef);
|
||||
}
|
||||
|
||||
@@ -130,14 +131,10 @@ export class MatTabNav extends _MatTabNavMixinBase implements AfterContentInit,
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._ngZone.runOutsideAngular(() => {
|
||||
let dirChange = this._dir ? this._dir.change : observableOf(null);
|
||||
let resize = typeof window !== 'undefined' ?
|
||||
auditTime.call(fromEvent(window, 'resize'), 10) :
|
||||
observableOf(null);
|
||||
const dirChange = this._dir ? this._dir.change : observableOf(null);
|
||||
|
||||
return takeUntil.call(merge(dirChange, resize), this._onDestroy).subscribe(() => {
|
||||
this._alignInkBar();
|
||||
});
|
||||
return takeUntil.call(merge(dirChange, this._viewportRuler.change(10)), this._onDestroy)
|
||||
.subscribe(() => this._alignInkBar());
|
||||
});
|
||||
|
||||
this._setLinkDisableRipple();
|
||||
|
||||
Reference in New Issue
Block a user