Skip to content

Commit 1a708e8

Browse files
committed
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.
1 parent 520d83b commit 1a708e8

File tree

2 files changed

+53
-4
lines changed

2 files changed

+53
-4
lines changed

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,29 @@ describe('ConnectedPositionStrategy', () => {
440440
expect(Math.floor(overlayRect.left)).toBe(Math.floor(originRect.left));
441441
});
442442

443+
it('should re-use the preferred position when re-applying while locked in', () => {
444+
positionBuilder = new OverlayPositionBuilder(viewportRuler);
445+
strategy = positionBuilder.connectedTo(
446+
fakeElementRef,
447+
{originX: 'end', originY: 'center'},
448+
{overlayX: 'start', overlayY: 'center'})
449+
.withLockedPosition(true)
450+
.withFallbackPosition(
451+
{originX: 'start', originY: 'bottom'},
452+
{overlayX: 'end', overlayY: 'top'});
453+
454+
const recalcSpy = spyOn(strategy, 'recalculateLastPosition');
455+
456+
strategy.attach(fakeOverlayRef(overlayElement));
457+
strategy.apply();
458+
459+
expect(recalcSpy).not.toHaveBeenCalled();
460+
461+
strategy.apply();
462+
463+
expect(recalcSpy).toHaveBeenCalled();
464+
});
465+
443466
/**
444467
* Run all tests for connecting the overlay to the origin such that first preferred
445468
* position does not go off-screen. We do this because there are several cases where we

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

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,13 @@ export class ConnectedPositionStrategy implements PositionStrategy {
6868
/** The last position to have been calculated as the best fit position. */
6969
private _lastConnectedPosition: ConnectionPositionPair;
7070

71-
_onPositionChange:
72-
Subject<ConnectedOverlayPositionChange> = new Subject<ConnectedOverlayPositionChange>();
71+
/** Whether the position strategy is applied currently. */
72+
private _applied = false;
73+
74+
/** Whether the overlay position is locked. */
75+
private _positionLocked = false;
76+
77+
private _onPositionChange = new Subject<ConnectedOverlayPositionChange>();
7378

7479
/** Emits an event when the connection point changes. */
7580
get onPositionChange(): Observable<ConnectedOverlayPositionChange> {
@@ -100,22 +105,32 @@ export class ConnectedPositionStrategy implements PositionStrategy {
100105

101106
/** Disposes all resources used by the position strategy. */
102107
dispose() {
108+
this._applied = false;
103109
this._resizeSubscription.unsubscribe();
104110
}
105111

106112
/** @docs-private */
107113
detach() {
114+
this._applied = false;
108115
this._resizeSubscription.unsubscribe();
109116
}
110117

111118
/**
112119
* Updates the position of the overlay element, using whichever preferred position relative
113120
* to the origin fits on-screen.
114121
* @docs-private
115-
*
116-
* @returns Resolves when the styles have been applied.
117122
*/
118123
apply(): void {
124+
// If the position has been applied already (e.g. when the overlay was opened) and the
125+
// consumer opted into locking in the position, re-use the old position, in order to
126+
// prevent the overlay from jumping around.
127+
if (this._applied && this._positionLocked && this._lastConnectedPosition) {
128+
this.recalculateLastPosition();
129+
return;
130+
}
131+
132+
this._applied = true;
133+
119134
// We need the bounding rects for the origin and the overlay to determine how to position
120135
// the overlay relative to the origin.
121136
const element = this._pane;
@@ -229,6 +244,17 @@ export class ConnectedPositionStrategy implements PositionStrategy {
229244
return this;
230245
}
231246

247+
/**
248+
* Sets whether the overlay's position should be locked in after it is positioned
249+
* initially. When an overlay is locked in, it won't attempt to reposition itself
250+
* when the position is re-applied (e.g. when the user scrolls away).
251+
* @param isLocked Whether the overlay should locked in.
252+
*/
253+
withLockedPosition(isLocked: boolean): this {
254+
this._positionLocked = isLocked;
255+
return this;
256+
}
257+
232258
/**
233259
* Gets the horizontal (x) "start" dimension based on whether the overlay is in an RTL context.
234260
* @param rect

0 commit comments

Comments
 (0)