From 1a708e86c2a8601fbf89c2e7984b367359c2f832 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sun, 15 Oct 2017 12:27:00 +0200 Subject: [PATCH] feat(overlay): add option to re-use last preferred position when re-applying to open connected overlay Currently when updating the position of an open connected overlay (e.g. when the user is scrolling) we go through the same process for determining the preferred position as when the overlay was attached. This means that the preferred position could change, causing the overlay to jump. With these changes the consumer can decide to lock an overlay into its initial position, preventing it from jumping. This PR is a resubmit of #5471. --- .../connected-position-strategy.spec.ts | 23 +++++++++++++ .../position/connected-position-strategy.ts | 34 ++++++++++++++++--- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/cdk/overlay/position/connected-position-strategy.spec.ts b/src/cdk/overlay/position/connected-position-strategy.spec.ts index 93c3630e7a9e..ac88dbc6d951 100644 --- a/src/cdk/overlay/position/connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/connected-position-strategy.spec.ts @@ -440,6 +440,29 @@ describe('ConnectedPositionStrategy', () => { expect(Math.floor(overlayRect.left)).toBe(Math.floor(originRect.left)); }); + it('should re-use the preferred position when re-applying while locked in', () => { + positionBuilder = new OverlayPositionBuilder(viewportRuler); + strategy = positionBuilder.connectedTo( + fakeElementRef, + {originX: 'end', originY: 'center'}, + {overlayX: 'start', overlayY: 'center'}) + .withLockedPosition(true) + .withFallbackPosition( + {originX: 'start', originY: 'bottom'}, + {overlayX: 'end', overlayY: 'top'}); + + const recalcSpy = spyOn(strategy, 'recalculateLastPosition'); + + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); + + expect(recalcSpy).not.toHaveBeenCalled(); + + strategy.apply(); + + expect(recalcSpy).toHaveBeenCalled(); + }); + /** * Run all tests for connecting the overlay to the origin such that first preferred * position does not go off-screen. We do this because there are several cases where we diff --git a/src/cdk/overlay/position/connected-position-strategy.ts b/src/cdk/overlay/position/connected-position-strategy.ts index eb08712ba797..abfb4f709e10 100644 --- a/src/cdk/overlay/position/connected-position-strategy.ts +++ b/src/cdk/overlay/position/connected-position-strategy.ts @@ -68,8 +68,13 @@ export class ConnectedPositionStrategy implements PositionStrategy { /** The last position to have been calculated as the best fit position. */ private _lastConnectedPosition: ConnectionPositionPair; - _onPositionChange: - Subject = new Subject(); + /** Whether the position strategy is applied currently. */ + private _applied = false; + + /** Whether the overlay position is locked. */ + private _positionLocked = false; + + private _onPositionChange = new Subject(); /** Emits an event when the connection point changes. */ get onPositionChange(): Observable { @@ -100,11 +105,13 @@ export class ConnectedPositionStrategy implements PositionStrategy { /** Disposes all resources used by the position strategy. */ dispose() { + this._applied = false; this._resizeSubscription.unsubscribe(); } /** @docs-private */ detach() { + this._applied = false; this._resizeSubscription.unsubscribe(); } @@ -112,10 +119,18 @@ export class ConnectedPositionStrategy implements PositionStrategy { * Updates the position of the overlay element, using whichever preferred position relative * to the origin fits on-screen. * @docs-private - * - * @returns Resolves when the styles have been applied. */ apply(): void { + // If the position has been applied already (e.g. when the overlay was opened) and the + // consumer opted into locking in the position, re-use the old position, in order to + // prevent the overlay from jumping around. + if (this._applied && this._positionLocked && this._lastConnectedPosition) { + this.recalculateLastPosition(); + return; + } + + this._applied = true; + // We need the bounding rects for the origin and the overlay to determine how to position // the overlay relative to the origin. const element = this._pane; @@ -229,6 +244,17 @@ export class ConnectedPositionStrategy implements PositionStrategy { return this; } + /** + * Sets whether the overlay's position should be locked in after it is positioned + * initially. When an overlay is locked in, it won't attempt to reposition itself + * when the position is re-applied (e.g. when the user scrolls away). + * @param isLocked Whether the overlay should locked in. + */ + withLockedPosition(isLocked: boolean): this { + this._positionLocked = isLocked; + return this; + } + /** * Gets the horizontal (x) "start" dimension based on whether the overlay is in an RTL context. * @param rect