|
8 | 8 |
|
9 | 9 | import {PositionStrategy} from './position-strategy'; |
10 | 10 | import {ElementRef} from '@angular/core'; |
11 | | -import {ViewportRuler, CdkScrollable} from '@angular/cdk/scrolling'; |
| 11 | +import {ViewportRuler, CdkScrollable, ViewportScrollPosition} from '@angular/cdk/scrolling'; |
12 | 12 | import { |
13 | 13 | ConnectedOverlayPositionChange, |
14 | 14 | ConnectionPositionPair, |
@@ -115,6 +115,9 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { |
115 | 115 | /** Keeps track of the CSS classes that the position strategy has applied on the overlay panel. */ |
116 | 116 | private _appliedPanelClasses: string[] = []; |
117 | 117 |
|
| 118 | + /** Amount by which the overlay was pushed in each axis during the last time it was positioned. */ |
| 119 | + private _previousPushAmount: {x: number, y: number} | null; |
| 120 | + |
118 | 121 | /** Observable sequence of position changes. */ |
119 | 122 | positionChanges: Observable<ConnectedOverlayPositionChange> = Observable.create(observer => { |
120 | 123 | const subscription = this._positionChanges.subscribe(observer); |
@@ -155,7 +158,13 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { |
155 | 158 | this._boundingBox = overlayRef.hostElement; |
156 | 159 | this._pane = overlayRef.overlayElement; |
157 | 160 | this._resizeSubscription.unsubscribe(); |
158 | | - this._resizeSubscription = this._viewportRuler.change().subscribe(() => this.apply()); |
| 161 | + this._resizeSubscription = this._viewportRuler.change().subscribe(() => { |
| 162 | + // When the window is resized, we want to trigger the next reposition as if it |
| 163 | + // was an initial render, in order for the strategy to pick a new optimal position, |
| 164 | + // otherwise position locking will cause it to stay at the old one. |
| 165 | + this._isInitialRender = true; |
| 166 | + this.apply(); |
| 167 | + }); |
159 | 168 | } |
160 | 169 |
|
161 | 170 | /** |
@@ -287,6 +296,8 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { |
287 | 296 |
|
288 | 297 | detach() { |
289 | 298 | this._clearPanelClasses(); |
| 299 | + this._lastPosition = null; |
| 300 | + this._previousPushAmount = null; |
290 | 301 | this._resizeSubscription.unsubscribe(); |
291 | 302 | } |
292 | 303 |
|
@@ -546,39 +557,55 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { |
546 | 557 | * the viewport, the top-left corner will be pushed on-screen (with overflow occuring on the |
547 | 558 | * right and bottom). |
548 | 559 | * |
549 | | - * @param start The starting point from which the overlay is pushed. |
550 | | - * @param overlay The overlay dimensions. |
| 560 | + * @param start Starting point from which the overlay is pushed. |
| 561 | + * @param overlay Dimensions of the overlay. |
| 562 | + * @param scrollPosition Current viewport scroll position. |
551 | 563 | * @returns The point at which to position the overlay after pushing. This is effectively a new |
552 | 564 | * originPoint. |
553 | 565 | */ |
554 | | - private _pushOverlayOnScreen(start: Point, overlay: ClientRect): Point { |
| 566 | + private _pushOverlayOnScreen(start: Point, |
| 567 | + overlay: ClientRect, |
| 568 | + scrollPosition: ViewportScrollPosition): Point { |
| 569 | + // If the position is locked and we've pushed the overlay already, reuse the previous push |
| 570 | + // amount, rather than pushing it again. If we were to continue pushing, the element would |
| 571 | + // remain in the viewport, which goes against the expectations when position locking is enabled. |
| 572 | + if (this._previousPushAmount && this._positionLocked) { |
| 573 | + return { |
| 574 | + x: start.x + this._previousPushAmount.x, |
| 575 | + y: start.y + this._previousPushAmount.y |
| 576 | + }; |
| 577 | + } |
| 578 | + |
555 | 579 | const viewport = this._viewportRect; |
556 | 580 |
|
557 | | - // Determine how much the overlay goes outside the viewport on each side, which we'll use to |
558 | | - // decide which direction to push it. |
| 581 | + // Determine how much the overlay goes outside the viewport on each |
| 582 | + // side, which we'll use to decide which direction to push it. |
559 | 583 | const overflowRight = Math.max(start.x + overlay.width - viewport.right, 0); |
560 | 584 | const overflowBottom = Math.max(start.y + overlay.height - viewport.bottom, 0); |
561 | | - const overflowTop = Math.max(viewport.top - start.y, 0); |
562 | | - const overflowLeft = Math.max(viewport.left - start.x, 0); |
| 585 | + const overflowTop = Math.max(viewport.top - scrollPosition.top - start.y, 0); |
| 586 | + const overflowLeft = Math.max(viewport.left - scrollPosition.left - start.x, 0); |
563 | 587 |
|
564 | | - // Amount by which to push the overlay in each direction such that it remains on-screen. |
565 | | - let pushX, pushY = 0; |
| 588 | + // Amount by which to push the overlay in each axis such that it remains on-screen. |
| 589 | + let pushX = 0; |
| 590 | + let pushY = 0; |
566 | 591 |
|
567 | 592 | // If the overlay fits completely within the bounds of the viewport, push it from whichever |
568 | 593 | // direction is goes off-screen. Otherwise, push the top-left corner such that its in the |
569 | 594 | // viewport and allow for the trailing end of the overlay to go out of bounds. |
570 | | - if (overlay.width <= viewport.width) { |
| 595 | + if (overlay.width < viewport.width) { |
571 | 596 | pushX = overflowLeft || -overflowRight; |
572 | 597 | } else { |
573 | | - pushX = viewport.left - start.x; |
| 598 | + pushX = start.x < this._viewportMargin ? (viewport.left - scrollPosition.left) - start.x : 0; |
574 | 599 | } |
575 | 600 |
|
576 | | - if (overlay.height <= viewport.height) { |
| 601 | + if (overlay.height < viewport.height) { |
577 | 602 | pushY = overflowTop || -overflowBottom; |
578 | 603 | } else { |
579 | | - pushY = viewport.top - start.y; |
| 604 | + pushY = start.y < this._viewportMargin ? (viewport.top - scrollPosition.top) - start.y : 0; |
580 | 605 | } |
581 | 606 |
|
| 607 | + this._previousPushAmount = {x: pushX, y: pushY}; |
| 608 | + |
582 | 609 | return { |
583 | 610 | x: start.x + pushX, |
584 | 611 | y: start.y + pushY, |
@@ -801,8 +828,9 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { |
801 | 828 | const styles = {} as CSSStyleDeclaration; |
802 | 829 |
|
803 | 830 | if (this._hasExactPosition()) { |
804 | | - extendStyles(styles, this._getExactOverlayY(position, originPoint)); |
805 | | - extendStyles(styles, this._getExactOverlayX(position, originPoint)); |
| 831 | + const scrollPosition = this._viewportRuler.getViewportScrollPosition(); |
| 832 | + extendStyles(styles, this._getExactOverlayY(position, originPoint, scrollPosition)); |
| 833 | + extendStyles(styles, this._getExactOverlayX(position, originPoint, scrollPosition)); |
806 | 834 | } else { |
807 | 835 | styles.position = 'static'; |
808 | 836 | } |
@@ -841,14 +869,16 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { |
841 | 869 | } |
842 | 870 |
|
843 | 871 | /** Gets the exact top/bottom for the overlay when not using flexible sizing or when pushing. */ |
844 | | - private _getExactOverlayY(position: ConnectedPosition, originPoint: Point) { |
| 872 | + private _getExactOverlayY(position: ConnectedPosition, |
| 873 | + originPoint: Point, |
| 874 | + scrollPosition: ViewportScrollPosition) { |
845 | 875 | // Reset any existing styles. This is necessary in case the |
846 | 876 | // preferred position has changed since the last `apply`. |
847 | 877 | let styles = {top: null, bottom: null} as CSSStyleDeclaration; |
848 | 878 | let overlayPoint = this._getOverlayPoint(originPoint, this._overlayRect, position); |
849 | 879 |
|
850 | 880 | if (this._isPushed) { |
851 | | - overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect); |
| 881 | + overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect, scrollPosition); |
852 | 882 | } |
853 | 883 |
|
854 | 884 | // @breaking-change 7.0.0 Currently the `_overlayContainer` is optional in order to avoid a |
@@ -878,14 +908,16 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { |
878 | 908 | } |
879 | 909 |
|
880 | 910 | /** Gets the exact left/right for the overlay when not using flexible sizing or when pushing. */ |
881 | | - private _getExactOverlayX(position: ConnectedPosition, originPoint: Point) { |
| 911 | + private _getExactOverlayX(position: ConnectedPosition, |
| 912 | + originPoint: Point, |
| 913 | + scrollPosition: ViewportScrollPosition) { |
882 | 914 | // Reset any existing styles. This is necessary in case the preferred position has |
883 | 915 | // changed since the last `apply`. |
884 | 916 | let styles = {left: null, right: null} as CSSStyleDeclaration; |
885 | 917 | let overlayPoint = this._getOverlayPoint(originPoint, this._overlayRect, position); |
886 | 918 |
|
887 | 919 | if (this._isPushed) { |
888 | | - overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect); |
| 920 | + overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect, scrollPosition); |
889 | 921 | } |
890 | 922 |
|
891 | 923 | // We want to set either `left` or `right` based on whether the overlay wants to appear "before" |
|
0 commit comments