diff --git a/src/cdk/overlay/overlay-ref.ts b/src/cdk/overlay/overlay-ref.ts index 25bd7a07e4dc..7aec70b29b3d 100644 --- a/src/cdk/overlay/overlay-ref.ts +++ b/src/cdk/overlay/overlay-ref.ts @@ -45,6 +45,10 @@ export class OverlayRef implements PortalHost { attach(portal: Portal): any { let attachResult = this._portalHost.attach(portal); + if (this._state.positionStrategy) { + this._state.positionStrategy.attach(this); + } + // Update the pane element with the given state configuration. this._updateStackingOrder(); this.updateSize(); @@ -146,7 +150,7 @@ export class OverlayRef implements PortalHost { /** Updates the position of the overlay based on the position strategy. */ updatePosition() { if (this._state.positionStrategy) { - this._state.positionStrategy.apply(this._pane); + this._state.positionStrategy.apply(); } } diff --git a/src/cdk/overlay/overlay.spec.ts b/src/cdk/overlay/overlay.spec.ts index 8599803ab103..690d6a20c0a6 100644 --- a/src/cdk/overlay/overlay.spec.ts +++ b/src/cdk/overlay/overlay.spec.ts @@ -532,9 +532,14 @@ class OverlayTestModule { } class OverlayContainerThemingTestModule { } class FakePositionStrategy implements PositionStrategy { - apply(element: Element): Promise { - element.classList.add('fake-positioned'); - return Promise.resolve(null); + element: HTMLElement; + + apply(): void { + this.element.classList.add('fake-positioned'); + } + + attach(overlayRef: OverlayRef) { + this.element = overlayRef.overlayElement; } dispose() {} diff --git a/src/cdk/overlay/position/connected-position-strategy.spec.ts b/src/cdk/overlay/position/connected-position-strategy.spec.ts index b1ef8baeb505..33d7b7bbe58b 100644 --- a/src/cdk/overlay/position/connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/connected-position-strategy.spec.ts @@ -7,6 +7,7 @@ import {ConnectedOverlayPositionChange} from './connected-position'; import {Scrollable} from '../scroll/scrollable'; import {Subscription} from 'rxjs/Subscription'; import {ScrollDispatchModule} from '../scroll/index'; +import {OverlayRef} from '../overlay-ref'; // Default width and height of the overlay and origin panels throughout these tests. @@ -142,7 +143,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'start', originY: 'bottom'}, {overlayX: 'start', overlayY: 'top'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect.bottom)); @@ -166,7 +168,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'end', originY: 'center'}, {overlayX: 'start', overlayY: 'center'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); expect(Math.floor(overlayRect.top)).toBe(Math.floor(originCenterY - (OVERLAY_HEIGHT / 2))); @@ -188,7 +191,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'end', originY: 'top'}, {overlayX: 'end', overlayY: 'bottom'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); expect(Math.floor(overlayRect.bottom)).toBe(Math.floor(originRect.top)); @@ -210,7 +214,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'start', originY: 'bottom'}, {overlayX: 'end', overlayY: 'top'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); @@ -234,7 +239,8 @@ describe('ConnectedPositionStrategy', () => { {overlayX: 'start', overlayY: 'bottom'}); // This should apply the fallback position, as the original position won't fit. - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); // Now make the overlay small enough to fit in the first preferred position. overlayElement.style.height = '15px'; @@ -260,7 +266,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'start', originY: 'top'}, {overlayX: 'start', overlayY: 'bottom'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); strategy.recalculateLastPosition(); let overlayRect = overlayElement.getBoundingClientRect(); @@ -279,7 +286,8 @@ describe('ConnectedPositionStrategy', () => { {overlayX: 'start', overlayY: 'top'}) .withDirection('rtl'); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect.bottom)); @@ -294,7 +302,8 @@ describe('ConnectedPositionStrategy', () => { {overlayX: 'start', overlayY: 'top'}); strategy.withOffsetX(10); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect.top)); @@ -309,7 +318,8 @@ describe('ConnectedPositionStrategy', () => { {overlayX: 'start', overlayY: 'top'}); strategy.withOffsetY(50); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect.top + 50)); @@ -334,7 +344,8 @@ describe('ConnectedPositionStrategy', () => { const positionChangeHandler = jasmine.createSpy('positionChangeHandler'); const subscription = strategy.onPositionChange.subscribe(positionChangeHandler); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); const latestCall = positionChangeHandler.calls.mostRecent(); @@ -346,7 +357,8 @@ describe('ConnectedPositionStrategy', () => { // the position change event should be emitted again. originElement.style.top = '200px'; originElement.style.left = '200px'; - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); expect(positionChangeHandler).toHaveBeenCalledTimes(2); @@ -369,7 +381,8 @@ describe('ConnectedPositionStrategy', () => { const positionChangeHandler = jasmine.createSpy('positionChangeHandler'); const subscription = strategy.onPositionChange.subscribe(positionChangeHandler); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); expect(positionChangeHandler).toHaveBeenCalled(); @@ -394,7 +407,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'end', originY: 'top'}, {overlayX: 'end', overlayY: 'top'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); @@ -415,7 +429,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'start', originY: 'bottom'}, {overlayX: 'start', overlayY: 'top'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect!.bottom)); @@ -428,7 +443,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'end', originY: 'center'}, {overlayX: 'start', overlayY: 'center'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); expect(Math.floor(overlayRect.top)).toBe(Math.floor(originCenterY! - (OVERLAY_HEIGHT / 2))); @@ -441,7 +457,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'start', originY: 'bottom'}, {overlayX: 'end', overlayY: 'top'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); @@ -455,7 +472,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'end', originY: 'top'}, {overlayX: 'end', overlayY: 'bottom'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); expect(Math.round(overlayRect.bottom)).toBe(Math.round(originRect!.top)); @@ -468,7 +486,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'center', originY: 'bottom'}, {overlayX: 'center', overlayY: 'top'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect!.bottom)); @@ -481,7 +500,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'center', originY: 'center'}, {overlayX: 'center', overlayY: 'center'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); let overlayRect = overlayElement.getBoundingClientRect(); expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect!.top)); @@ -526,7 +546,7 @@ describe('ConnectedPositionStrategy', () => { strategy.withScrollableContainers([ new Scrollable(new FakeElementRef(scrollable), null!, null!, null!)]); - + strategy.attach(fakeOverlayRef(overlayElement)); positionChangeHandler = jasmine.createSpy('positionChangeHandler'); onPositionChangeSubscription = strategy.onPositionChange.subscribe(positionChangeHandler); }); @@ -538,7 +558,7 @@ describe('ConnectedPositionStrategy', () => { }); it('should not have origin or overlay clipped or out of view without scroll', () => { - strategy.apply(overlayElement); + strategy.apply(); expect(positionChangeHandler).toHaveBeenCalled(); positionChange = positionChangeHandler.calls.mostRecent().args[0]; @@ -552,7 +572,7 @@ describe('ConnectedPositionStrategy', () => { it('should evaluate if origin is clipped if scrolled slightly down', () => { scrollable.scrollTop = 10; // Clip the origin by 10 pixels - strategy.apply(overlayElement); + strategy.apply(); expect(positionChangeHandler).toHaveBeenCalled(); positionChange = positionChangeHandler.calls.mostRecent().args[0]; @@ -566,7 +586,7 @@ describe('ConnectedPositionStrategy', () => { it('should evaluate if origin is out of view and overlay is clipped if scrolled enough', () => { scrollable.scrollTop = 31; // Origin is 30 pixels, move out of view and clip the overlay 1px - strategy.apply(overlayElement); + strategy.apply(); expect(positionChangeHandler).toHaveBeenCalled(); positionChange = positionChangeHandler.calls.mostRecent().args[0]; @@ -580,7 +600,7 @@ describe('ConnectedPositionStrategy', () => { it('should evaluate the overlay and origin are both out of the view', () => { scrollable.scrollTop = 61; // Scroll by overlay height + origin height + 1px buffer - strategy.apply(overlayElement); + strategy.apply(); expect(positionChangeHandler).toHaveBeenCalled(); positionChange = positionChangeHandler.calls.mostRecent().args[0]; @@ -626,7 +646,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'start', originY: 'top'}, {overlayX: 'start', overlayY: 'top'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); expect(overlayElement.style.left).toBeTruthy(); expect(overlayElement.style.right).toBeFalsy(); }); @@ -637,7 +658,8 @@ describe('ConnectedPositionStrategy', () => { {originX: 'end', originY: 'top'}, {overlayX: 'end', overlayY: 'top'}); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); expect(overlayElement.style.right).toBeTruthy(); expect(overlayElement.style.left).toBeFalsy(); }); @@ -653,7 +675,8 @@ describe('ConnectedPositionStrategy', () => { ) .withDirection('rtl'); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); expect(overlayElement.style.right).toBeTruthy(); expect(overlayElement.style.left).toBeFalsy(); }); @@ -665,7 +688,8 @@ describe('ConnectedPositionStrategy', () => { {overlayX: 'end', overlayY: 'top'} ).withDirection('rtl'); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); expect(overlayElement.style.left).toBeTruthy(); expect(overlayElement.style.right).toBeFalsy(); }); @@ -679,7 +703,8 @@ describe('ConnectedPositionStrategy', () => { {overlayX: 'start', overlayY: 'top'} ); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); expect(overlayElement.style.top).toBeTruthy(); expect(overlayElement.style.bottom).toBeFalsy(); }); @@ -691,7 +716,8 @@ describe('ConnectedPositionStrategy', () => { {overlayX: 'start', overlayY: 'bottom'} ); - strategy.apply(overlayElement); + strategy.attach(fakeOverlayRef(overlayElement)); + strategy.apply(); expect(overlayElement.style.bottom).toBeTruthy(); expect(overlayElement.style.top).toBeFalsy(); }); @@ -741,3 +767,7 @@ function createOverflowContainerElement() { class FakeElementRef implements ElementRef { constructor(public nativeElement: HTMLElement) { } } + +function fakeOverlayRef(overlayElement: HTMLElement) { + return {overlayElement} as OverlayRef; +} diff --git a/src/cdk/overlay/position/connected-position-strategy.ts b/src/cdk/overlay/position/connected-position-strategy.ts index 3472f6eac599..6f7fef8aaf98 100644 --- a/src/cdk/overlay/position/connected-position-strategy.ts +++ b/src/cdk/overlay/position/connected-position-strategy.ts @@ -20,6 +20,8 @@ import {Subject} from 'rxjs/Subject'; import {Observable} from 'rxjs/Observable'; import {Scrollable} from '../scroll/scrollable'; import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scroll-clip'; +import {OverlayRef} from '../overlay-ref'; + /** @@ -30,6 +32,9 @@ import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scrol * of the overlay. */ export class ConnectedPositionStrategy implements PositionStrategy { + /** The overlay to which this strategy is attached. */ + private _overlayRef: OverlayRef; + private _dir = 'ltr'; /** The offset in pixels for the overlay connection point on the x-axis */ @@ -80,9 +85,12 @@ export class ConnectedPositionStrategy implements PositionStrategy { return this._preferredPositions; } - /** - * To be used to for any cleanup after the element gets destroyed. - */ + attach(overlayRef: OverlayRef): void { + this._overlayRef = overlayRef; + this._pane = overlayRef.overlayElement; + } + + /** Performs any cleanup after the element is destroyed. */ dispose() { } /** @@ -90,15 +98,12 @@ export class ConnectedPositionStrategy implements PositionStrategy { * to the origin fits on-screen. * @docs-private * - * @param element Element to which to apply the CSS styles. * @returns Resolves when the styles have been applied. */ - apply(element: HTMLElement): void { - // Cache the overlay pane element in case re-calculating position is necessary - this._pane = element; - + apply(): void { // We need the bounding rects for the origin and the overlay to determine how to position // the overlay relative to the origin. + const element = this._pane; const originRect = this._origin.getBoundingClientRect(); const overlayRect = element.getBoundingClientRect(); diff --git a/src/cdk/overlay/position/global-position-strategy.spec.ts b/src/cdk/overlay/position/global-position-strategy.spec.ts index 79cb7a45f875..104757e17838 100644 --- a/src/cdk/overlay/position/global-position-strategy.spec.ts +++ b/src/cdk/overlay/position/global-position-strategy.spec.ts @@ -1,9 +1,6 @@ -import { - inject, - fakeAsync, - flushMicrotasks, -} from '@angular/core/testing'; +import {fakeAsync, flushMicrotasks, inject} from '@angular/core/testing'; import {GlobalPositionStrategy} from './global-position-strategy'; +import {OverlayRef} from '../overlay-ref'; describe('GlobalPositonStrategy', () => { @@ -14,6 +11,7 @@ describe('GlobalPositonStrategy', () => { element = document.createElement('div'); strategy = new GlobalPositionStrategy(); document.body.appendChild(element); + strategy.attach({overlayElement: element} as OverlayRef); }); afterEach(() => { @@ -22,7 +20,7 @@ describe('GlobalPositonStrategy', () => { }); it('should position the element to the (top, left) with an offset', fakeAsyncTest(() => { - strategy.top('10px').left('40px').apply(element); + strategy.top('10px').left('40px').apply(); flushMicrotasks(); @@ -39,7 +37,7 @@ describe('GlobalPositonStrategy', () => { })); it('should position the element to the (bottom, right) with an offset', fakeAsyncTest(() => { - strategy.bottom('70px').right('15em').apply(element); + strategy.bottom('70px').right('15em').apply(); flushMicrotasks(); @@ -56,10 +54,10 @@ describe('GlobalPositonStrategy', () => { })); it('should overwrite previously applied positioning', fakeAsyncTest(() => { - strategy.centerHorizontally().centerVertically().apply(element); + strategy.centerHorizontally().centerVertically().apply(); flushMicrotasks(); - strategy.top('10px').left('40%').apply(element); + strategy.top('10px').left('40%').apply(); flushMicrotasks(); let elementStyle = element.style; @@ -73,7 +71,7 @@ describe('GlobalPositonStrategy', () => { expect(parentStyle.justifyContent).toBe('flex-start'); expect(parentStyle.alignItems).toBe('flex-start'); - strategy.bottom('70px').right('15em').apply(element); + strategy.bottom('70px').right('15em').apply(); flushMicrotasks(); @@ -87,7 +85,7 @@ describe('GlobalPositonStrategy', () => { })); it('should center the element', fakeAsyncTest(() => { - strategy.centerHorizontally().centerVertically().apply(element); + strategy.centerHorizontally().centerVertically().apply(); flushMicrotasks(); @@ -98,7 +96,7 @@ describe('GlobalPositonStrategy', () => { })); it('should center the element with an offset', fakeAsyncTest(() => { - strategy.centerHorizontally('10px').centerVertically('15px').apply(element); + strategy.centerHorizontally('10px').centerVertically('15px').apply(); flushMicrotasks(); @@ -113,7 +111,7 @@ describe('GlobalPositonStrategy', () => { })); it('should make the element position: static', fakeAsyncTest(() => { - strategy.apply(element); + strategy.apply(); flushMicrotasks(); @@ -121,7 +119,7 @@ describe('GlobalPositonStrategy', () => { })); it('should wrap the element in a `cdk-global-overlay-wrapper`', fakeAsyncTest(() => { - strategy.apply(element); + strategy.apply(); flushMicrotasks(); @@ -132,7 +130,7 @@ describe('GlobalPositonStrategy', () => { it('should remove the parent wrapper from the DOM', fakeAsync(() => { - strategy.apply(element); + strategy.apply(); flushMicrotasks(); @@ -144,7 +142,7 @@ describe('GlobalPositonStrategy', () => { })); it('should set the element width', fakeAsync(() => { - strategy.width('100px').apply(element); + strategy.width('100px').apply(); flushMicrotasks(); @@ -152,7 +150,7 @@ describe('GlobalPositonStrategy', () => { })); it('should set the element height', fakeAsync(() => { - strategy.height('100px').apply(element); + strategy.height('100px').apply(); flushMicrotasks(); @@ -160,7 +158,7 @@ describe('GlobalPositonStrategy', () => { })); it('should reset the horizontal position and offset when the width is 100%', fakeAsync(() => { - strategy.centerHorizontally().width('100%').apply(element); + strategy.centerHorizontally().width('100%').apply(); flushMicrotasks(); @@ -169,7 +167,7 @@ describe('GlobalPositonStrategy', () => { })); it('should reset the vertical position and offset when the height is 100%', fakeAsync(() => { - strategy.centerVertically().height('100%').apply(element); + strategy.centerVertically().height('100%').apply(); flushMicrotasks(); diff --git a/src/cdk/overlay/position/global-position-strategy.ts b/src/cdk/overlay/position/global-position-strategy.ts index e5c09de78f41..69bcf5338576 100644 --- a/src/cdk/overlay/position/global-position-strategy.ts +++ b/src/cdk/overlay/position/global-position-strategy.ts @@ -7,6 +7,7 @@ */ import {PositionStrategy} from './position-strategy'; +import {OverlayRef} from '../overlay-ref'; /** @@ -16,6 +17,9 @@ import {PositionStrategy} from './position-strategy'; * element to become blurry. */ export class GlobalPositionStrategy implements PositionStrategy { + /** The overlay to which this strategy is attached. */ + private _overlayRef: OverlayRef; + private _cssPosition: string = 'static'; private _topOffset: string = ''; private _bottomOffset: string = ''; @@ -29,6 +33,10 @@ export class GlobalPositionStrategy implements PositionStrategy { /* A lazily-created wrapper for the overlay element that is used as a flex container. */ private _wrapper: HTMLElement | null = null; + attach(overlayRef: OverlayRef): void { + this._overlayRef = overlayRef; + } + /** * Sets the top position of the overlay. Clears any previously set vertical position. * @param value New top offset. @@ -133,10 +141,11 @@ export class GlobalPositionStrategy implements PositionStrategy { * Apply the position to the element. * @docs-private * - * @param element Element to which to apply the CSS. * @returns Resolved when the styles have been applied. */ - apply(element: HTMLElement): void { + apply(): void { + const element = this._overlayRef.overlayElement; + if (!this._wrapper && element.parentNode) { this._wrapper = document.createElement('div'); this._wrapper.classList.add('cdk-global-overlay-wrapper'); @@ -159,9 +168,7 @@ export class GlobalPositionStrategy implements PositionStrategy { parentStyles.alignItems = this._alignItems; } - /** - * Removes the wrapper element from the DOM. - */ + /** Removes the wrapper element from the DOM. */ dispose(): void { if (this._wrapper && this._wrapper.parentNode) { this._wrapper.parentNode.removeChild(this._wrapper); diff --git a/src/cdk/overlay/position/position-strategy.ts b/src/cdk/overlay/position/position-strategy.ts index abc90be73009..d848f4f4c51c 100644 --- a/src/cdk/overlay/position/position-strategy.ts +++ b/src/cdk/overlay/position/position-strategy.ts @@ -6,11 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ +import {OverlayRef} from '../overlay-ref'; + + /** Strategy for setting the position on an overlay. */ export interface PositionStrategy { + /** Attaches this position strategy to an overlay. */ + attach(overlay: OverlayRef): void; + /** Updates the position of the overlay element. */ - apply(element: Element): void; + apply(): void; /** Cleans up any DOM modifications made by the position strategy, if necessary. */ dispose(): void;