From ea54c665c49d4c1eeeff0221053fef0a2e15f193 Mon Sep 17 00:00:00 2001 From: jeripeierSBB Date: Tue, 4 Jan 2022 15:42:24 +0100 Subject: [PATCH] fix(cdk/overlay): fix positioning when zooming in Safari 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. --- ...exible-connected-position-strategy.spec.ts | 40 +++++++++++++++ .../flexible-connected-position-strategy.ts | 49 ++++++++++++------- 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts index c13f6502583d..d9014316a338 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts @@ -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 diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 636efcc08756..df943e0d39cf 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -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; @@ -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[] = []; @@ -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 @@ -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); } @@ -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` @@ -491,6 +501,12 @@ 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; @@ -498,6 +514,15 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { 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}; } @@ -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) { @@ -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. @@ -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') { @@ -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;