diff --git a/src/cdk/overlay/position/connected-position-strategy.ts b/src/cdk/overlay/position/connected-position-strategy.ts index 8b1a355a246b..48779988a8dc 100644 --- a/src/cdk/overlay/position/connected-position-strategy.ts +++ b/src/cdk/overlay/position/connected-position-strategy.ts @@ -174,12 +174,7 @@ export class ConnectedPositionStrategy implements PositionStrategy { * @param offset New offset in the X axis. */ withOffsetX(offset: number): this { - this._preferredPositions.forEach(position => { - if (position.offsetX == null) { - position.offsetX = offset; - } - }); - + this._positionStrategy.withDefaultOffsetX(offset); return this; } @@ -188,12 +183,7 @@ export class ConnectedPositionStrategy implements PositionStrategy { * @param offset New offset in the Y axis. */ withOffsetY(offset: number): this { - this._preferredPositions.forEach(position => { - if (position.offsetY == null) { - position.offsetY = offset; - } - }); - + this._positionStrategy.withDefaultOffsetY(offset); return this; } 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 201f68e9fe15..8d517776d2f5 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts @@ -385,6 +385,41 @@ describe('FlexibleConnectedPositionStrategy', () => { expect(Math.floor(overlayRect.left)).toBe(Math.floor(originRect.left + 10)); }); + it('should be able to set the default x offset', () => { + const originRect = originElement.getBoundingClientRect(); + + positionStrategy.withDefaultOffsetX(20).withPositions([{ + originX: 'start', + originY: 'top', + overlayX: 'start', + overlayY: 'top', + }]); + + attachOverlay({positionStrategy}); + + const overlayRect = overlayRef.overlayElement.getBoundingClientRect(); + expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect.top)); + expect(Math.floor(overlayRect.left)).toBe(Math.floor(originRect.left + 20)); + }); + + it('should have the position offset x take precedence over the default offset x', () => { + const originRect = originElement.getBoundingClientRect(); + + positionStrategy.withDefaultOffsetX(20).withPositions([{ + originX: 'start', + originY: 'top', + overlayX: 'start', + overlayY: 'top', + offsetX: 10 + }]); + + attachOverlay({positionStrategy}); + + const overlayRect = overlayRef.overlayElement.getBoundingClientRect(); + expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect.top)); + expect(Math.floor(overlayRect.left)).toBe(Math.floor(originRect.left + 10)); + }); + it('should position a panel with the y offset provided', () => { const originRect = originElement.getBoundingClientRect(); @@ -403,6 +438,41 @@ describe('FlexibleConnectedPositionStrategy', () => { expect(Math.floor(overlayRect.left)).toBe(Math.floor(originRect.left)); }); + it('should be able to set the default y offset', () => { + const originRect = originElement.getBoundingClientRect(); + + positionStrategy.withDefaultOffsetY(60).withPositions([{ + originX: 'start', + originY: 'top', + overlayX: 'start', + overlayY: 'top', + }]); + + attachOverlay({positionStrategy}); + + const overlayRect = overlayRef.overlayElement.getBoundingClientRect(); + expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect.top + 60)); + expect(Math.floor(overlayRect.left)).toBe(Math.floor(originRect.left)); + }); + + it('should have the position offset y take precedence over the default offset y', () => { + const originRect = originElement.getBoundingClientRect(); + + positionStrategy.withDefaultOffsetY(60).withPositions([{ + originX: 'start', + originY: 'top', + overlayX: 'start', + overlayY: 'top', + offsetY: 50 + }]); + + attachOverlay({positionStrategy}); + + const overlayRect = overlayRef.overlayElement.getBoundingClientRect(); + expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect.top + 50)); + expect(Math.floor(overlayRect.left)).toBe(Math.floor(originRect.left)); + }); + it('should allow for the fallback positions to specify their own offsets', () => { originElement.style.bottom = '0'; originElement.style.left = '50%'; diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index dfc26f74b3b8..4792318a89d4 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -69,7 +69,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { private _viewportRect: ClientRect; /** Amount of space that must be maintained between the overlay and the edge of the viewport. */ - private _viewportMargin: number = 0; + private _viewportMargin = 0; /** The Scrollable containers used to check scrollable view properties on position change. */ private scrollables: CdkScrollable[] = []; @@ -101,6 +101,12 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** Subscription to viewport size changes. */ private _resizeSubscription = Subscription.EMPTY; + /** Default offset for the overlay along the x axis. */ + private _offsetX = 0; + + /** Default offset for the overlay along the y axis. */ + private _offsetY = 0; + /** Observable sequence of position changes. */ positionChanges: Observable = this._positionChanges.asObservable(); @@ -366,6 +372,24 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { return this; } + /** + * Sets the default offset for the overlay's connection point on the x-axis. + * @param offset New offset in the X axis. + */ + withDefaultOffsetX(offset: number): this { + this._offsetX = offset; + return this; + } + + /** + * Sets the default offset for the overlay's connection point on the y-axis. + * @param offset New offset in the Y axis. + */ + withDefaultOffsetY(offset: number): this { + this._offsetY = offset; + return this; + } + /** * Gets the (x, y) coordinate of a connection point on the origin based on a relative position. */ @@ -431,14 +455,16 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { position: ConnectedPosition): OverlayFit { let {x, y} = point; + let offsetX = this._getOffset(position, 'x'); + let offsetY = this._getOffset(position, 'y'); // Account for the offsets since they could push the overlay out of the viewport. - if (position.offsetX) { - x += position.offsetX; + if (offsetX) { + x += offsetX; } - if (position.offsetY) { - y += position.offsetY; + if (offsetY) { + y += offsetY; } // How much the overlay would overflow at this position, on each side. @@ -721,13 +747,15 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { // cases where the element doesn't have anything to "push off of". Finally, this works // better both with flexible and non-flexible positioning. let transformString = ' '; + let offsetX = this._getOffset(position, 'x'); + let offsetY = this._getOffset(position, 'y'); - if (position.offsetX) { - transformString += `translateX(${position.offsetX}px)`; + if (offsetX) { + transformString += `translateX(${offsetX}px)`; } - if (position.offsetY) { - transformString += `translateY(${position.offsetY}px)`; + if (offsetY) { + transformString += `translateY(${offsetY}px)`; } styles.transform = transformString.trim(); @@ -869,6 +897,17 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { private _isRtl() { return this._overlayRef.getConfig().direction === 'rtl'; } + + /** Retrieves the offset of a position along the x or y axis. */ + private _getOffset(position: ConnectedPosition, axis: 'x' | 'y') { + if (axis === 'x') { + // We don't do something like `position['offset' + axis]` in + // order to avoid breking minifiers that rename properties. + return position.offsetX == null ? this._offsetX : position.offsetX; + } + + return position.offsetY == null ? this._offsetY : position.offsetY; + } } /** A simple (x, y) coordinate. */