Skip to content

Commit 0d6d9cc

Browse files
crisbetoandrewseguin
authored andcommitted
fix(viewport-ruler): incorrectly caching viewport size (#7951)
* Currently the `ViewportRuler` caches the `ClientRect` of the `document.documentElement` which is only used for determining the scroll position and is being updated on demand anyway. These changes switch to caching the `innerWidth` and `innerHeight` which was the original intent. * Adds a `getViewportSize` method that returns the cached viewport width and height. The reasoning is that for all of the cases where we were using `getViewportRect`, we were only taking the `width` and `height`. `getViewportSize` helps us avoid the overhead from determining the `top`, `bottom`, `left` and `right` via the scroll position.
1 parent ff7a13b commit 0d6d9cc

File tree

7 files changed

+53
-64
lines changed

7 files changed

+53
-64
lines changed

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ export class ConnectedPositionStrategy implements PositionStrategy {
122122
const originRect = this._origin.getBoundingClientRect();
123123
const overlayRect = element.getBoundingClientRect();
124124

125-
// We use the viewport rect to determine whether a position would go off-screen.
126-
const viewportRect = this._viewportRuler.getViewportRect();
125+
// We use the viewport size to determine whether a position would go off-screen.
126+
const viewportSize = this._viewportRuler.getViewportSize();
127127

128128
// Fallback point if none of the fallbacks fit into the viewport.
129129
let fallbackPoint: OverlayPoint | undefined;
@@ -135,7 +135,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
135135
// Get the (x, y) point of connection on the origin, and then use that to get the
136136
// (top, left) coordinate for the overlay at `pos`.
137137
let originPoint = this._getOriginConnectionPoint(originRect, pos);
138-
let overlayPoint = this._getOverlayPoint(originPoint, overlayRect, viewportRect, pos);
138+
let overlayPoint = this._getOverlayPoint(originPoint, overlayRect, viewportSize, pos);
139139

140140
// If the overlay in the calculated position fits on-screen, put it there and we're done.
141141
if (overlayPoint.fitsInViewport) {
@@ -169,11 +169,11 @@ export class ConnectedPositionStrategy implements PositionStrategy {
169169

170170
const originRect = this._origin.getBoundingClientRect();
171171
const overlayRect = this._pane.getBoundingClientRect();
172-
const viewportRect = this._viewportRuler.getViewportRect();
172+
const viewportSize = this._viewportRuler.getViewportSize();
173173
const lastPosition = this._lastConnectedPosition || this._preferredPositions[0];
174174

175175
let originPoint = this._getOriginConnectionPoint(originRect, lastPosition);
176-
let overlayPoint = this._getOverlayPoint(originPoint, overlayRect, viewportRect, lastPosition);
176+
let overlayPoint = this._getOverlayPoint(originPoint, overlayRect, viewportSize, lastPosition);
177177
this._setElementPosition(this._pane, overlayRect, overlayPoint, lastPosition);
178178
}
179179

@@ -281,7 +281,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
281281
private _getOverlayPoint(
282282
originPoint: Point,
283283
overlayRect: ClientRect,
284-
viewportRect: ClientRect,
284+
viewportSize: {width: number; height: number},
285285
pos: ConnectionPositionPair): OverlayPoint {
286286
// Calculate the (overlayStartX, overlayStartY), the start of the potential overlay position
287287
// relative to the origin point.
@@ -311,9 +311,9 @@ export class ConnectedPositionStrategy implements PositionStrategy {
311311

312312
// How much the overlay would overflow at this position, on each side.
313313
let leftOverflow = 0 - x;
314-
let rightOverflow = (x + overlayRect.width) - viewportRect.width;
314+
let rightOverflow = (x + overlayRect.width) - viewportSize.width;
315315
let topOverflow = 0 - y;
316-
let bottomOverflow = (y + overlayRect.height) - viewportRect.height;
316+
let bottomOverflow = (y + overlayRect.height) - viewportSize.height;
317317

318318
// Visible parts of the element on each axis.
319319
let visibleWidth = this._subtractOverflows(overlayRect.width, leftOverflow, rightOverflow);

src/cdk/overlay/scroll/block-scroll-strategy.spec.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,28 +39,28 @@ describe('BlockScrollStrategy', () => {
3939
afterEach(inject([OverlayContainer], (container: OverlayContainer) => {
4040
overlayRef.dispose();
4141
document.body.removeChild(forceScrollElement);
42-
setScrollPosition(0, 0);
42+
window.scroll(0, 0);
4343
container.getContainerElement().parentNode!.removeChild(container.getContainerElement());
4444
}));
4545

4646
it('should toggle scroll blocking along the y axis', skipIOS(() => {
47-
setScrollPosition(0, 100);
47+
window.scroll(0, 100);
4848
expect(viewport.getViewportScrollPosition().top)
4949
.toBe(100, 'Expected viewport to be scrollable initially.');
5050

5151
overlayRef.attach(componentPortal);
5252
expect(document.documentElement.style.top)
5353
.toBe('-100px', 'Expected <html> element to be offset by the previous scroll amount.');
5454

55-
setScrollPosition(0, 300);
55+
window.scroll(0, 300);
5656
expect(viewport.getViewportScrollPosition().top)
5757
.toBe(100, 'Expected the viewport not to scroll.');
5858

5959
overlayRef.detach();
6060
expect(viewport.getViewportScrollPosition().top)
6161
.toBe(100, 'Expected old scroll position to have bee restored after disabling.');
6262

63-
setScrollPosition(0, 300);
63+
window.scroll(0, 300);
6464
expect(viewport.getViewportScrollPosition().top)
6565
.toBe(300, 'Expected user to be able to scroll after disabling.');
6666
}));
@@ -70,23 +70,23 @@ describe('BlockScrollStrategy', () => {
7070
forceScrollElement.style.height = '100px';
7171
forceScrollElement.style.width = '3000px';
7272

73-
setScrollPosition(100, 0);
73+
window.scroll(100, 0);
7474
expect(viewport.getViewportScrollPosition().left)
7575
.toBe(100, 'Expected viewport to be scrollable initially.');
7676

7777
overlayRef.attach(componentPortal);
7878
expect(document.documentElement.style.left)
7979
.toBe('-100px', 'Expected <html> element to be offset by the previous scroll amount.');
8080

81-
setScrollPosition(300, 0);
81+
window.scroll(300, 0);
8282
expect(viewport.getViewportScrollPosition().left)
8383
.toBe(100, 'Expected the viewport not to scroll.');
8484

8585
overlayRef.detach();
8686
expect(viewport.getViewportScrollPosition().left)
8787
.toBe(100, 'Expected old scroll position to have bee restored after disabling.');
8888

89-
setScrollPosition(300, 0);
89+
window.scroll(300, 0);
9090
expect(viewport.getViewportScrollPosition().left)
9191
.toBe(300, 'Expected user to be able to scroll after disabling.');
9292
}));
@@ -163,7 +163,6 @@ describe('BlockScrollStrategy', () => {
163163
* tests unusable. For example, something as basic as the following won't work:
164164
* ```
165165
* window.scroll(0, 100);
166-
* viewport._cacheViewportGeometry();
167166
* expect(viewport.getViewportScrollPosition().top).toBe(100);
168167
* ```
169168
* @param spec Test to be executed or skipped.
@@ -176,16 +175,6 @@ describe('BlockScrollStrategy', () => {
176175
};
177176
}
178177

179-
/**
180-
* Scrolls the viewport and clears the cache.
181-
* @param x Amount to scroll along the x axis.
182-
* @param y Amount to scroll along the y axis.
183-
*/
184-
function setScrollPosition(x: number, y: number) {
185-
window.scroll(x, y);
186-
viewport._cacheViewportGeometry();
187-
}
188-
189178
});
190179

191180

src/cdk/overlay/scroll/block-scroll-strategy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class BlockScrollStrategy implements ScrollStrategy {
7676
}
7777

7878
const body = document.body;
79-
const viewport = this._viewportRuler.getViewportRect();
79+
const viewport = this._viewportRuler.getViewportSize();
8080
return body.scrollHeight > viewport.height || body.scrollWidth > viewport.width;
8181
}
8282
}

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ describe('ViewportRuler', () => {
3737
ruler.ngOnDestroy();
3838
});
3939

40+
it('should get the viewport size', () => {
41+
let size = ruler.getViewportSize();
42+
expect(size.width).toBe(window.innerWidth);
43+
expect(size.height).toBe(window.innerHeight);
44+
});
45+
4046
it('should get the viewport bounds when the page is not scrolled', () => {
4147
let bounds = ruler.getViewportRect();
4248
expect(bounds.top).toBe(0);
@@ -49,8 +55,6 @@ describe('ViewportRuler', () => {
4955
document.body.appendChild(veryLargeElement);
5056

5157
scrollTo(1500, 2000);
52-
// Force an update of the cached viewport geometries because IE11 emits the scroll event later.
53-
ruler._cacheViewportGeometry();
5458

5559
let bounds = ruler.getViewportRect();
5660

@@ -87,8 +91,6 @@ describe('ViewportRuler', () => {
8791
document.body.appendChild(veryLargeElement);
8892

8993
scrollTo(1500, 2000);
90-
// Force an update of the cached viewport geometries because IE11 emits the scroll event later.
91-
ruler._cacheViewportGeometry();
9294

9395
// In the iOS simulator (BrowserStack & SauceLabs), adding the content to the
9496
// body causes karma's iframe for the test to stretch to fit that content once we attempt to

src/cdk/scrolling/viewport-ruler.ts

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,8 @@ export const DEFAULT_RESIZE_TIME = 20;
2424
*/
2525
@Injectable()
2626
export class ViewportRuler implements OnDestroy {
27-
28-
/** Cached document client rectangle. */
29-
private _documentRect?: ClientRect;
27+
/** Cached viewport dimensions. */
28+
private _viewportSize: {width: number; height: number};
3029

3130
/** Stream of viewport change events. */
3231
private _change: Observable<Event>;
@@ -39,21 +38,24 @@ export class ViewportRuler implements OnDestroy {
3938
return merge<Event>(fromEvent(window, 'resize'), fromEvent(window, 'orientationchange'));
4039
}) : observableOf();
4140

42-
this._invalidateCache = this.change().subscribe(() => this._cacheViewportGeometry());
41+
this._invalidateCache = this.change().subscribe(() => this._updateViewportSize());
4342
}
4443

4544
ngOnDestroy() {
4645
this._invalidateCache.unsubscribe();
4746
}
4847

49-
/** Gets a ClientRect for the viewport's bounds. */
50-
getViewportRect(documentRect: ClientRect | undefined = this._documentRect): ClientRect {
51-
// Cache the document bounding rect so that we don't recompute it for multiple calls.
52-
if (!documentRect) {
53-
this._cacheViewportGeometry();
54-
documentRect = this._documentRect;
48+
/** Returns the viewport's width and height. */
49+
getViewportSize(): Readonly<{width: number, height: number}> {
50+
if (!this._viewportSize) {
51+
this._updateViewportSize();
5552
}
5653

54+
return {width: this._viewportSize.width, height: this._viewportSize.height};
55+
}
56+
57+
/** Gets a ClientRect for the viewport's bounds. */
58+
getViewportRect(): ClientRect {
5759
// Use the document element's bounding rect rather than the window scroll properties
5860
// (e.g. pageYOffset, scrollY) due to in issue in Chrome and IE where window scroll
5961
// properties and client coordinates (boundingClientRect, clientX/Y, etc.) are in different
@@ -63,9 +65,8 @@ export class ViewportRuler implements OnDestroy {
6365
// We use the documentElement instead of the body because, by default (without a css reset)
6466
// browsers typically give the document body an 8px margin, which is not included in
6567
// getBoundingClientRect().
66-
const scrollPosition = this.getViewportScrollPosition(documentRect);
67-
const height = window.innerHeight;
68-
const width = window.innerWidth;
68+
const scrollPosition = this.getViewportScrollPosition();
69+
const {width, height} = this.getViewportSize();
6970

7071
return {
7172
top: scrollPosition.top,
@@ -77,27 +78,20 @@ export class ViewportRuler implements OnDestroy {
7778
};
7879
}
7980

80-
/**
81-
* Gets the (top, left) scroll position of the viewport.
82-
* @param documentRect
83-
*/
84-
getViewportScrollPosition(documentRect: ClientRect | undefined = this._documentRect) {
85-
// Cache the document bounding rect so that we don't recompute it for multiple calls.
86-
if (!documentRect) {
87-
this._cacheViewportGeometry();
88-
documentRect = this._documentRect;
89-
}
90-
81+
/** Gets the (top, left) scroll position of the viewport. */
82+
getViewportScrollPosition() {
9183
// The top-left-corner of the viewport is determined by the scroll position of the document
9284
// body, normally just (scrollLeft, scrollTop). However, Chrome and Firefox disagree about
9385
// whether `document.body` or `document.documentElement` is the scrolled element, so reading
9486
// `scrollTop` and `scrollLeft` is inconsistent. However, using the bounding rect of
9587
// `document.documentElement` works consistently, where the `top` and `left` values will
9688
// equal negative the scroll position.
97-
const top = -documentRect!.top || document.body.scrollTop || window.scrollY ||
89+
const documentRect = document.documentElement.getBoundingClientRect();
90+
91+
const top = -documentRect.top || document.body.scrollTop || window.scrollY ||
9892
document.documentElement.scrollTop || 0;
9993

100-
const left = -documentRect!.left || document.body.scrollLeft || window.scrollX ||
94+
const left = -documentRect.left || document.body.scrollLeft || window.scrollX ||
10195
document.documentElement.scrollLeft || 0;
10296

10397
return {top, left};
@@ -111,9 +105,9 @@ export class ViewportRuler implements OnDestroy {
111105
return throttleTime > 0 ? this._change.pipe(auditTime(throttleTime)) : this._change;
112106
}
113107

114-
/** Caches the latest client rectangle of the document element. */
115-
_cacheViewportGeometry() {
116-
this._documentRect = document.documentElement.getBoundingClientRect();
108+
/** Updates the cached viewport size. */
109+
private _updateViewportSize() {
110+
this._viewportSize = {width: window.innerWidth, height: window.innerHeight};
117111
}
118112
}
119113

src/cdk/testing/fake-viewport-ruler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export class FakeViewportRuler {
1414
};
1515
}
1616

17+
getViewportSize() {
18+
return {width: 1014, height: 686};
19+
}
20+
1721
getViewportScrollPosition() {
1822
return {top: 0, left: 0};
1923
}

src/lib/select/select.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,7 +1013,7 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
10131013
*/
10141014
private _calculateOverlayOffsetX(): void {
10151015
const overlayRect = this.overlayDir.overlayRef.overlayElement.getBoundingClientRect();
1016-
const viewportRect = this._viewportRuler.getViewportRect();
1016+
const viewportSize = this._viewportRuler.getViewportSize();
10171017
const isRtl = this._isRtl();
10181018
const paddingWidth = this.multiple ? SELECT_MULTIPLE_PANEL_PADDING_X + SELECT_PANEL_PADDING_X :
10191019
SELECT_PANEL_PADDING_X * 2;
@@ -1034,7 +1034,7 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
10341034

10351035
// Determine how much the select overflows on each side.
10361036
const leftOverflow = 0 - (overlayRect.left + offsetX - (isRtl ? paddingWidth : 0));
1037-
const rightOverflow = overlayRect.right + offsetX - viewportRect.width
1037+
const rightOverflow = overlayRect.right + offsetX - viewportSize.width
10381038
+ (isRtl ? 0 : paddingWidth);
10391039

10401040
// If the element overflows on either side, reduce the offset to allow it to fit.
@@ -1099,11 +1099,11 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
10991099
*/
11001100
private _checkOverlayWithinViewport(maxScroll: number): void {
11011101
const itemHeight = this._getItemHeight();
1102-
const viewportRect = this._viewportRuler.getViewportRect();
1102+
const viewportSize = this._viewportRuler.getViewportSize();
11031103

11041104
const topSpaceAvailable = this._triggerRect.top - SELECT_PANEL_VIEWPORT_PADDING;
11051105
const bottomSpaceAvailable =
1106-
viewportRect.height - this._triggerRect.bottom - SELECT_PANEL_VIEWPORT_PADDING;
1106+
viewportSize.height - this._triggerRect.bottom - SELECT_PANEL_VIEWPORT_PADDING;
11071107

11081108
const panelHeightTop = Math.abs(this._offsetY);
11091109
const totalPanelHeight =

0 commit comments

Comments
 (0)