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 178297415e5d..f1e1565bacb1 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts @@ -163,6 +163,44 @@ describe('FlexibleConnectedPositionStrategy', () => { originElement.remove(); }); + it('should position a popover relative to the viewport instead of the overlay container', () => { + const originElement = createPositionedBlockElement(); + const container = overlayContainer.getContainerElement(); + const originalHeight = container.style.height; + + document.body.appendChild(originElement); + originElement.style.top = '200px'; + originElement.style.left = '70px'; + container.style.height = `${document.documentElement.clientHeight + 100}px`; + + const positionStrategy = createFlexibleConnectedPositionStrategy(injector, originElement) + .withFlexibleDimensions(false) + .withPush(false) + .withPositions([ + { + originX: 'start', + originY: 'top', + overlayX: 'start', + overlayY: 'bottom', + }, + ]); + + try { + attachOverlay({positionStrategy, usePopover: true}); + + expect(overlayRef.hostElement.getAttribute('popover')).toBe('manual'); + expect(Math.floor(container.getBoundingClientRect().height)).toBe( + document.documentElement.clientHeight + 100, + ); + expect(Math.floor(overlayRef.overlayElement.getBoundingClientRect().bottom)).toBe( + Math.floor(originElement.getBoundingClientRect().top), + ); + } finally { + container.style.height = originalHeight; + originElement.remove(); + } + }); + it('should calculate position with simulated zoom in Safari', () => { let containerElement = overlayContainer.getContainerElement(); spyOn(containerElement, 'getBoundingClientRect').and.returnValue({ @@ -312,6 +350,51 @@ describe('FlexibleConnectedPositionStrategy', () => { // Preconditions are set, now just run the full set of simple position tests. runSimplePositionTests(); + + it('should position an upward-flowing overlay relative to its container after scrolling', () => { + const container = overlayContainer.getContainerElement(); + const originalHeight = container.style.height; + const originalTop = container.style.top; + + // Simulate a mobile browser shifting and resizing the fixed overlay container while the + // page is scrolled. Its bottom remains aligned with the viewport, but its containing block + // is taller than the document element. + container.style.height = `${document.documentElement.clientHeight + 100}px`; + container.style.top = '-100px'; + + positionStrategy.withPositions([ + { + originX: 'start', + originY: 'top', + overlayX: 'start', + overlayY: 'bottom', + }, + ]); + + try { + attachOverlay({positionStrategy, usePopover: false}); + + expect(Math.floor(container.getBoundingClientRect().height)).toBe( + document.documentElement.clientHeight + 100, + ); + + let originRect = originElement.getBoundingClientRect(); + let overlayRect = overlayRef.overlayElement.getBoundingClientRect(); + + expect(Math.floor(overlayRect.bottom)).toBe(Math.floor(originRect.top)); + + window.scroll(2200, 2200); + overlayRef.updatePosition(); + + originRect = originElement.getBoundingClientRect(); + overlayRect = overlayRef.overlayElement.getBoundingClientRect(); + + expect(Math.floor(overlayRect.bottom)).toBe(Math.floor(originRect.top)); + } finally { + container.style.height = originalHeight; + container.style.top = originalTop; + } + }); }); describe('when near viewport edge', () => { diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 897e205915dc..bf3ca967f146 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -65,9 +65,7 @@ export function createFlexibleConnectedPositionStrategy( /** Supported locations in the DOM for connected overlays. */ export type FlexibleOverlayPopoverLocation = - | 'global' - | 'inline' - | {type: 'parent'; element: Element}; + 'global' | 'inline' | {type: 'parent'; element: Element}; /** * A strategy for positioning overlays. Using this strategy, an overlay is given an @@ -1073,9 +1071,15 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { // above or below the origin and the direction in which the element will expand. if (position.overlayY === 'bottom') { // When using `bottom`, we adjust the y position such that it is the distance - // from the bottom of the viewport rather than the top. - const documentHeight = this._document.documentElement!.clientHeight; - styles.bottom = `${documentHeight - (overlayPoint.y + this._overlayRect.height)}px`; + // from the bottom of the containing block rather than the top. Native popovers are + // positioned relative to the viewport, while regular overlays use the overlay container. + const containingBlockHeight = this._overlayRef.getConfig().usePopover + ? this._document.documentElement!.clientHeight + : this._containerRect.height; + + styles.bottom = coerceCssPixelValue( + containingBlockHeight - (overlayPoint.y + this._overlayRect.height), + ); } else { styles.top = coerceCssPixelValue(overlayPoint.y); }