Skip to content

Commit

Permalink
feat(overlay): add option to re-use last preferred position when re-a…
Browse files Browse the repository at this point in the history
…pplying 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.
  • Loading branch information
crisbeto authored and jelbourn committed Nov 20, 2017
1 parent a041253 commit f83beb8
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 4 deletions.
23 changes: 23 additions & 0 deletions src/cdk/overlay/position/connected-position-strategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 30 additions & 4 deletions src/cdk/overlay/position/connected-position-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConnectedOverlayPositionChange> = new Subject<ConnectedOverlayPositionChange>();
/** Whether the position strategy is applied currently. */
private _applied = false;

/** Whether the overlay position is locked. */
private _positionLocked = false;

private _onPositionChange = new Subject<ConnectedOverlayPositionChange>();

/** Emits an event when the connection point changes. */
get onPositionChange(): Observable<ConnectedOverlayPositionChange> {
Expand Down Expand Up @@ -101,22 +106,32 @@ 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();
}

/**
* 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;
Expand Down Expand Up @@ -230,6 +245,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
Expand Down

0 comments on commit f83beb8

Please sign in to comment.