diff --git a/src/lib/core/overlay/overlay-directives.spec.ts b/src/lib/core/overlay/overlay-directives.spec.ts index 9fe8d4660ab3..daf545a9452c 100644 --- a/src/lib/core/overlay/overlay-directives.spec.ts +++ b/src/lib/core/overlay/overlay-directives.spec.ts @@ -1,5 +1,6 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {Component, ViewChild} from '@angular/core'; +import {By} from '@angular/platform-browser'; import {ConnectedOverlayDirective, OverlayModule} from './overlay-directives'; import {OverlayContainer} from './overlay-container'; import {ConnectedPositionStrategy} from './position/connected-position-strategy'; @@ -121,6 +122,36 @@ describe('Overlay directives', () => { expect(fixture.componentInstance.backdropClicked).toBe(true); }); + it('should set the offsetX', () => { + const trigger = fixture.debugElement.query(By.css('button')).nativeElement; + const startX = trigger.getBoundingClientRect().left; + + fixture.componentInstance.offsetX = 5; + fixture.componentInstance.isOpen = true; + fixture.detectChanges(); + + // expected x value is the starting x + offset x + const expectedX = startX + 5; + const pane = overlayContainerElement.children[0] as HTMLElement; + expect(pane.style.transform).toContain(`translateX(${expectedX}px)`); + }); + + it('should set the offsetY', () => { + const trigger = fixture.debugElement.query(By.css('button')).nativeElement; + trigger.style.position = 'absolute'; + trigger.style.top = '30px'; + trigger.style.height = '20px'; + + fixture.componentInstance.offsetY = 45; + fixture.componentInstance.isOpen = true; + fixture.detectChanges(); + + // expected y value is the starting y + trigger height + offset y + // 30 + 20 + 45 = 95px + const pane = overlayContainerElement.children[0] as HTMLElement; + expect(pane.style.transform).toContain(`translateY(95px)`); + }); + }); }); @@ -131,7 +162,7 @@ describe('Overlay directives', () => { `, }) @@ -139,6 +170,8 @@ class ConnectedOverlayDirectiveTest { isOpen = false; width: number | string; height: number | string; + offsetX: number = 0; + offsetY: number = 0; hasBackdrop: boolean; backdropClicked = false; diff --git a/src/lib/core/overlay/overlay-directives.ts b/src/lib/core/overlay/overlay-directives.ts index 86a73d0af341..1802ce536c24 100644 --- a/src/lib/core/overlay/overlay-directives.ts +++ b/src/lib/core/overlay/overlay-directives.ts @@ -67,6 +67,12 @@ export class ConnectedOverlayDirective implements OnDestroy { @Input() origin: OverlayOrigin; @Input() positions: ConnectionPositionPair[]; + /** The offset in pixels for the overlay connection point on the x-axis */ + @Input() offsetX: number = 0; + + /** The offset in pixels for the overlay connection point on the y-axis */ + @Input() offsetY: number = 0; + /** The width of the overlay panel. */ @Input() width: number | string; @@ -150,20 +156,27 @@ export class ConnectedOverlayDirective implements OnDestroy { overlayConfig.backdropClass = this.backdropClass; } - overlayConfig.positionStrategy = this._getPosition(); + overlayConfig.positionStrategy = this._setUpPosition(); overlayConfig.direction = this.dir; return overlayConfig; } + /** Sets up the position strategy with its initial configuration. */ + private _setUpPosition(): ConnectedPositionStrategy { + return this._getPosition() + .withDirection(this.dir) + .withOffsetX(this.offsetX) + .withOffsetY(this.offsetY); + } + /** Returns the position of the overlay to be set on the overlay config */ private _getPosition(): ConnectedPositionStrategy { return this._overlay.position().connectedTo( this.origin.elementRef, {originX: this.positions[0].overlayX, originY: this.positions[0].originY}, - {overlayX: this.positions[0].overlayX, overlayY: this.positions[0].overlayY}) - .setDirection(this.dir); + {overlayX: this.positions[0].overlayX, overlayY: this.positions[0].overlayY}); } /** Attaches the overlay and subscribes to backdrop clicks if backdrop exists */ diff --git a/src/lib/core/overlay/position/connected-position-strategy.spec.ts b/src/lib/core/overlay/position/connected-position-strategy.spec.ts index 1ef65ee3d7cb..7825170a98e6 100644 --- a/src/lib/core/overlay/position/connected-position-strategy.spec.ts +++ b/src/lib/core/overlay/position/connected-position-strategy.spec.ts @@ -215,7 +215,7 @@ describe('ConnectedPositionStrategy', () => { fakeElementRef, {originX: 'start', originY: 'bottom'}, {overlayX: 'start', overlayY: 'top'}) - .setDirection('rtl'); + .withDirection('rtl'); strategy.apply(overlayElement); @@ -223,6 +223,37 @@ describe('ConnectedPositionStrategy', () => { expect(overlayRect.top).toBe(originRect.bottom); expect(overlayRect.right).toBe(originRect.right); }); + + it('should position a panel with the x offset provided', () => { + originRect = originElement.getBoundingClientRect(); + strategy = positionBuilder.connectedTo( + fakeElementRef, + {originX: 'start', originY: 'top'}, + {overlayX: 'start', overlayY: 'top'}); + + strategy.withOffsetX(10); + strategy.apply(overlayElement); + + let overlayRect = overlayElement.getBoundingClientRect(); + expect(overlayRect.top).toBe(originRect.top); + expect(overlayRect.left).toBe(originRect.left + 10); + }); + + it('should position a panel with the y offset provided', () => { + originRect = originElement.getBoundingClientRect(); + strategy = positionBuilder.connectedTo( + fakeElementRef, + {originX: 'start', originY: 'top'}, + {overlayX: 'start', overlayY: 'top'}); + + strategy.withOffsetY(50); + strategy.apply(overlayElement); + + let overlayRect = overlayElement.getBoundingClientRect(); + expect(overlayRect.top).toBe(originRect.top + 50); + expect(overlayRect.left).toBe(originRect.left); + }); + }); diff --git a/src/lib/core/overlay/position/connected-position-strategy.ts b/src/lib/core/overlay/position/connected-position-strategy.ts index d46f1d48f353..a38f1dd08f32 100644 --- a/src/lib/core/overlay/position/connected-position-strategy.ts +++ b/src/lib/core/overlay/position/connected-position-strategy.ts @@ -11,7 +11,7 @@ import { /** * A strategy for positioning overlays. Using this strategy, an overlay is given an - * implict position relative some origin element. The relative position is defined in terms of + * implicit position relative some origin element. The relative position is defined in terms of * a point on the origin element that is connected to a point on the overlay element. For example, * a basic dropdown is connecting the bottom-left corner of the origin to the top-left corner * of the overlay. @@ -19,6 +19,12 @@ import { export class ConnectedPositionStrategy implements PositionStrategy { private _dir = 'ltr'; + /** The offset in pixels for the overlay connection point on the x-axis */ + private _offsetX: number = 0; + + /** The offset in pixels for the overlay connection point on the y-axis */ + private _offsetY: number = 0; + /** Whether the we're dealing with an RTL context */ get _isRtl() { return this._dir === 'rtl'; @@ -89,11 +95,23 @@ export class ConnectedPositionStrategy implements PositionStrategy { } /** Sets the layout direction so the overlay's position can be adjusted to match. */ - setDirection(dir: 'ltr' | 'rtl') { + withDirection(dir: 'ltr' | 'rtl'): ConnectedPositionStrategy { this._dir = dir; return this; } + /** Sets an offset for the overlay's connection point on the x-axis */ + withOffsetX(offset: number): ConnectedPositionStrategy { + this._offsetX = offset; + return this; + } + + /** Sets an offset for the overlay's connection point on the y-axis */ + withOffsetY(offset: number): ConnectedPositionStrategy { + this._offsetY = offset; + return this; + } + /** * Gets the horizontal (x) "start" dimension based on whether the overlay is in an RTL context. * @param rect @@ -168,8 +186,8 @@ export class ConnectedPositionStrategy implements PositionStrategy { } return { - x: originPoint.x + overlayStartX, - y: originPoint.y + overlayStartY + x: originPoint.x + overlayStartX + this._offsetX, + y: originPoint.y + overlayStartY + this._offsetY }; }