Skip to content

Commit a6d21a1

Browse files
authored
fix(cdk/overlay): fix positioning when zooming in Safari (#24160)
Currently, when zooming in Safari in macOS and iOS the overlay is positioned at the wrong place, offset by the zoom offset (left/top). This fix corrects this by adding/subtracting the corresponding offset.
1 parent d93d9a3 commit a6d21a1

File tree

2 files changed

+72
-17
lines changed

2 files changed

+72
-17
lines changed

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,46 @@ describe('FlexibleConnectedPositionStrategy', () => {
164164
originElement.remove();
165165
});
166166

167+
it('should calculate position with simulated zoom in Safari', () => {
168+
let containerElement = overlayContainer.getContainerElement();
169+
spyOn(containerElement, 'getBoundingClientRect').and.returnValue({
170+
top: -200,
171+
bottom: 900,
172+
left: -200,
173+
right: 100,
174+
width: 100,
175+
height: 100,
176+
} as DOMRect);
177+
178+
const originElement = createPositionedBlockElement();
179+
document.body.appendChild(originElement);
180+
181+
// Position the element so it would have enough space to fit.
182+
originElement.style.top = '200px';
183+
originElement.style.left = '70px';
184+
185+
attachOverlay({
186+
positionStrategy: overlay
187+
.position()
188+
.flexibleConnectedTo(originElement)
189+
.withFlexibleDimensions(false)
190+
.withPush(false)
191+
.withPositions([
192+
{
193+
originX: 'start',
194+
originY: 'top',
195+
overlayX: 'start',
196+
overlayY: 'top',
197+
},
198+
]),
199+
});
200+
201+
expect(getComputedStyle(overlayRef.overlayElement).left).toBe('270px');
202+
expect(getComputedStyle(overlayRef.overlayElement).top).toBe('400px');
203+
204+
originElement.remove();
205+
});
206+
167207
it('should clean up after itself when disposed', () => {
168208
const origin = document.createElement('div');
169209
const positionStrategy = overlay

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

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
8585
/** Cached viewport dimensions */
8686
private _viewportRect: Dimensions;
8787

88+
/** Cached container dimensions */
89+
private _containerRect: Dimensions;
90+
8891
/** Amount of space that must be maintained between the overlay and the edge of the viewport. */
8992
private _viewportMargin = 0;
9093

@@ -213,16 +216,18 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
213216
this._resetOverlayElementStyles();
214217
this._resetBoundingBoxStyles();
215218

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

223227
const originRect = this._originRect;
224228
const overlayRect = this._overlayRect;
225229
const viewportRect = this._viewportRect;
230+
const containerRect = this._containerRect;
226231

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

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

363369
const lastPosition = this._lastPosition || this._preferredPositions[0];
364-
const originPoint = this._getOriginPoint(this._originRect, lastPosition);
370+
const originPoint = this._getOriginPoint(this._originRect, this._containerRect, lastPosition);
365371

366372
this._applyPosition(lastPosition, originPoint);
367373
}
@@ -479,7 +485,11 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
479485
/**
480486
* Gets the (x, y) coordinate of a connection point on the origin based on a relative position.
481487
*/
482-
private _getOriginPoint(originRect: Dimensions, pos: ConnectedPosition): Point {
488+
private _getOriginPoint(
489+
originRect: Dimensions,
490+
containerRect: Dimensions,
491+
pos: ConnectedPosition,
492+
): Point {
483493
let x: number;
484494
if (pos.originX == 'center') {
485495
// Note: when centering we should always use the `left`
@@ -491,13 +501,28 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
491501
x = pos.originX == 'start' ? startX : endX;
492502
}
493503

504+
// When zooming in Safari the container rectangle contains negative values for the position
505+
// and we need to re-add them to the calculated coordinates.
506+
if (containerRect.left < 0) {
507+
x -= containerRect.left;
508+
}
509+
494510
let y: number;
495511
if (pos.originY == 'center') {
496512
y = originRect.top + originRect.height / 2;
497513
} else {
498514
y = pos.originY == 'top' ? originRect.top : originRect.bottom;
499515
}
500516

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

@@ -580,7 +605,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
580605
/**
581606
* Whether the overlay can fit within the viewport when it may resize either its width or height.
582607
* @param fit How well the overlay fits in the viewport at some position.
583-
* @param point The (x, y) coordinates of the overlat at some position.
608+
* @param point The (x, y) coordinates of the overlay at some position.
584609
* @param viewport The geometry of the viewport.
585610
*/
586611
private _canFitWithFlexibleDimensions(fit: OverlayFit, point: Point, viewport: Dimensions) {
@@ -606,7 +631,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
606631
* right and bottom).
607632
*
608633
* @param start Starting point from which the overlay is pushed.
609-
* @param overlay Dimensions of the overlay.
634+
* @param rawOverlayRect Dimensions of the overlay.
610635
* @param scrollPosition Current viewport scroll position.
611636
* @returns The point at which to position the overlay after pushing. This is effectively a new
612637
* originPoint.
@@ -958,16 +983,6 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
958983
overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect, scrollPosition);
959984
}
960985

961-
let virtualKeyboardOffset = this._overlayContainer
962-
.getContainerElement()
963-
.getBoundingClientRect().top;
964-
965-
// Normally this would be zero, however when the overlay is attached to an input (e.g. in an
966-
// autocomplete), mobile browsers will shift everything in order to put the input in the middle
967-
// of the screen and to make space for the virtual keyboard. We need to account for this offset,
968-
// otherwise our positioning will be thrown off.
969-
overlayPoint.y -= virtualKeyboardOffset;
970-
971986
// We want to set either `top` or `bottom` based on whether the overlay wants to appear
972987
// above or below the origin and the direction in which the element will expand.
973988
if (position.overlayY === 'bottom') {
@@ -1183,7 +1198,7 @@ interface OverlayFit {
11831198
visibleArea: number;
11841199
}
11851200

1186-
/** Record of the measurments determining whether an overlay will fit in a specific position. */
1201+
/** Record of the measurements determining whether an overlay will fit in a specific position. */
11871202
interface FallbackPosition {
11881203
position: ConnectedPosition;
11891204
originPoint: Point;

0 commit comments

Comments
 (0)