Skip to content

Commit f83beb8

Browse files
crisbetojelbourn
authored andcommitted
feat(overlay): add option to re-use last preferred position when re-applying to open connected overlay (#7805)
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 a041253 commit f83beb8

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> {
@@ -101,22 +106,32 @@ export class ConnectedPositionStrategy implements PositionStrategy {
101106

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

107113
/** @docs-private */
108114
detach() {
115+
this._applied = false;
109116
this._resizeSubscription.unsubscribe();
110117
}
111118

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

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

0 commit comments

Comments
 (0)