Skip to content

fix(cdk/overlay): fix positioning when zooming in Safari #24160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,46 @@ describe('FlexibleConnectedPositionStrategy', () => {
originElement.remove();
});

it('should calculate position with simulated zoom in Safari', () => {
let containerElement = overlayContainer.getContainerElement();
spyOn(containerElement, 'getBoundingClientRect').and.returnValue({
top: -200,
bottom: 900,
left: -200,
right: 100,
width: 100,
height: 100,
} as DOMRect);

const originElement = createPositionedBlockElement();
document.body.appendChild(originElement);

// Position the element so it would have enough space to fit.
originElement.style.top = '200px';
originElement.style.left = '70px';

attachOverlay({
positionStrategy: overlay
.position()
.flexibleConnectedTo(originElement)
.withFlexibleDimensions(false)
.withPush(false)
.withPositions([
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'top',
},
]),
});

expect(getComputedStyle(overlayRef.overlayElement).left).toBe('270px');
expect(getComputedStyle(overlayRef.overlayElement).top).toBe('400px');

originElement.remove();
});

it('should clean up after itself when disposed', () => {
const origin = document.createElement('div');
const positionStrategy = overlay
Expand Down
49 changes: 32 additions & 17 deletions src/cdk/overlay/position/flexible-connected-position-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
/** Cached viewport dimensions */
private _viewportRect: Dimensions;

/** Cached container dimensions */
private _containerRect: Dimensions;

/** Amount of space that must be maintained between the overlay and the edge of the viewport. */
private _viewportMargin = 0;

Expand Down Expand Up @@ -213,16 +216,18 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
this._resetOverlayElementStyles();
this._resetBoundingBoxStyles();

// We need the bounding rects for the origin and the overlay to determine how to position
// We need the bounding rects for the origin, the overlay and the container to determine how to position
// the overlay relative to the origin.
// We use the viewport rect to determine whether a position would go off-screen.
this._viewportRect = this._getNarrowedViewportRect();
this._originRect = this._getOriginRect();
this._overlayRect = this._pane.getBoundingClientRect();
this._containerRect = this._overlayContainer.getContainerElement().getBoundingClientRect();

const originRect = this._originRect;
const overlayRect = this._overlayRect;
const viewportRect = this._viewportRect;
const containerRect = this._containerRect;

// Positions where the overlay will fit with flexible dimensions.
const flexibleFits: FlexibleFit[] = [];
Expand All @@ -234,7 +239,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
// If a good fit is found, it will be applied immediately.
for (let pos of this._preferredPositions) {
// Get the exact (x, y) coordinate for the point-of-origin on the origin element.
let originPoint = this._getOriginPoint(originRect, pos);
let originPoint = this._getOriginPoint(originRect, containerRect, pos);

// From that point-of-origin, get the exact (x, y) coordinate for the top-left corner of the
// overlay in this position. We use the top-left corner for calculations and later translate
Expand Down Expand Up @@ -359,9 +364,10 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
this._originRect = this._getOriginRect();
this._overlayRect = this._pane.getBoundingClientRect();
this._viewportRect = this._getNarrowedViewportRect();
this._containerRect = this._overlayContainer.getContainerElement().getBoundingClientRect();

const lastPosition = this._lastPosition || this._preferredPositions[0];
const originPoint = this._getOriginPoint(this._originRect, lastPosition);
const originPoint = this._getOriginPoint(this._originRect, this._containerRect, lastPosition);

this._applyPosition(lastPosition, originPoint);
}
Expand Down Expand Up @@ -479,7 +485,11 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
/**
* Gets the (x, y) coordinate of a connection point on the origin based on a relative position.
*/
private _getOriginPoint(originRect: Dimensions, pos: ConnectedPosition): Point {
private _getOriginPoint(
originRect: Dimensions,
containerRect: Dimensions,
pos: ConnectedPosition,
): Point {
let x: number;
if (pos.originX == 'center') {
// Note: when centering we should always use the `left`
Expand All @@ -491,13 +501,28 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
x = pos.originX == 'start' ? startX : endX;
}

// When zooming in Safari the container rectangle contains negative values for the position
// and we need to re-add them to the calculated coordinates.
if (containerRect.left < 0) {
x -= containerRect.left;
}

let y: number;
if (pos.originY == 'center') {
y = originRect.top + originRect.height / 2;
} else {
y = pos.originY == 'top' ? originRect.top : originRect.bottom;
}

// Normally the containerRect's top value would be zero, however when the overlay is attached to an input
// (e.g. in an autocomplete), mobile browsers will shift everything in order to put the input in the middle
// of the screen and to make space for the virtual keyboard. We need to account for this offset,
// otherwise our positioning will be thrown off.
// Additionally, when zooming in Safari this fixes the vertical position.
if (containerRect.top < 0) {
y -= containerRect.top;
}

return {x, y};
}

Expand Down Expand Up @@ -580,7 +605,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
/**
* Whether the overlay can fit within the viewport when it may resize either its width or height.
* @param fit How well the overlay fits in the viewport at some position.
* @param point The (x, y) coordinates of the overlat at some position.
* @param point The (x, y) coordinates of the overlay at some position.
* @param viewport The geometry of the viewport.
*/
private _canFitWithFlexibleDimensions(fit: OverlayFit, point: Point, viewport: Dimensions) {
Expand All @@ -606,7 +631,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
* right and bottom).
*
* @param start Starting point from which the overlay is pushed.
* @param overlay Dimensions of the overlay.
* @param rawOverlayRect Dimensions of the overlay.
* @param scrollPosition Current viewport scroll position.
* @returns The point at which to position the overlay after pushing. This is effectively a new
* originPoint.
Expand Down Expand Up @@ -958,16 +983,6 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect, scrollPosition);
}

let virtualKeyboardOffset = this._overlayContainer
.getContainerElement()
.getBoundingClientRect().top;

// Normally this would be zero, however when the overlay is attached to an input (e.g. in an
// autocomplete), mobile browsers will shift everything in order to put the input in the middle
// of the screen and to make space for the virtual keyboard. We need to account for this offset,
// otherwise our positioning will be thrown off.
overlayPoint.y -= virtualKeyboardOffset;

// We want to set either `top` or `bottom` based on whether the overlay wants to appear
// above or below the origin and the direction in which the element will expand.
if (position.overlayY === 'bottom') {
Expand Down Expand Up @@ -1183,7 +1198,7 @@ interface OverlayFit {
visibleArea: number;
}

/** Record of the measurments determining whether an overlay will fit in a specific position. */
/** Record of the measurements determining whether an overlay will fit in a specific position. */
interface FallbackPosition {
position: ConnectedPosition;
originPoint: Point;
Expand Down