diff --git a/src/core/overlay/position/connected-position-strategy.spec.ts b/src/core/overlay/position/connected-position-strategy.spec.ts index 70917d655f2..1adaa17c345 100644 --- a/src/core/overlay/position/connected-position-strategy.spec.ts +++ b/src/core/overlay/position/connected-position-strategy.spec.ts @@ -1,7 +1,8 @@ import {it, describe, expect, beforeEach} from '@angular/core/testing'; import {ElementRef} from '@angular/core'; -import {ConnectedPositionStrategy, RelativePosition} from './connected-position-strategy'; +import {ConnectedPositionStrategy} from './connected-position-strategy'; import {ViewportRuler} from './viewport-ruler'; +import {OverlayPositionBuilder} from './overlay-position-builder'; // Default width and height of the overlay and origin panels throughout these tests. @@ -25,6 +26,7 @@ export function main() { let strategy: ConnectedPositionStrategy; let fakeElementRef: ElementRef; let fakeViewportRuler: FakeViewportRuler; + let positionBuilder: OverlayPositionBuilder; let originRect: ClientRect; let originCenterX: number; @@ -40,8 +42,7 @@ export function main() { document.body.appendChild(overlayElement); fakeElementRef = new FakeElementRef(originElement); - - strategy = new ConnectedPositionStrategy(fakeElementRef, new ViewportRuler()); + positionBuilder = new OverlayPositionBuilder(new ViewportRuler()); }); afterEach(() => { @@ -118,22 +119,13 @@ export function main() { originElement.style.left = '200px'; originRect = originElement.getBoundingClientRect(); - // Above, right aligned. - let offscreenPos = new RelativePosition(); - offscreenPos.originX = 'end'; - offscreenPos.originY = 'start'; - offscreenPos.overlayX = 'end'; - offscreenPos.overlayY = 'end'; - - // Below, left aligned. - let fallbackPosition = new RelativePosition(); - fallbackPosition.originX = 'start'; - fallbackPosition.originY = 'end'; - fallbackPosition.overlayX = 'start'; - fallbackPosition.overlayY = 'start'; - - strategy.addPreferredPosition(offscreenPos); - strategy.addPreferredPosition(fallbackPosition); + strategy = positionBuilder.connectedTo( + fakeElementRef, + {originX: 'end', originY: 'top'}, + {overlayX: 'end', overlayY: 'bottom'}) + .withFallbackPosition( + {originX: 'start', originY: 'bottom'}, + {overlayX: 'start', overlayY: 'top'}); strategy.apply(overlayElement); @@ -151,22 +143,13 @@ export function main() { originRect = originElement.getBoundingClientRect(); originCenterY = originRect.top + (ORIGIN_HEIGHT / 2); - // To the left, below. - let offscreenPos = new RelativePosition(); - offscreenPos.originX = 'start'; - offscreenPos.originY = 'end'; - offscreenPos.overlayX = 'end'; - offscreenPos.overlayY = 'start'; - - // To the right, centered. - let fallbackPos = new RelativePosition(); - fallbackPos.originX = 'end'; - fallbackPos.originY = 'center'; - fallbackPos.overlayX = 'start'; - fallbackPos.overlayY = 'center'; - - strategy.addPreferredPosition(offscreenPos); - strategy.addPreferredPosition(fallbackPos); + strategy = positionBuilder.connectedTo( + fakeElementRef, + {originX: 'start', originY: 'bottom'}, + {overlayX: 'end', overlayY: 'top'}) + .withFallbackPosition( + {originX: 'end', originY: 'center'}, + {overlayX: 'start', overlayY: 'center'}); strategy.apply(overlayElement); @@ -180,28 +163,19 @@ export function main() { fakeViewportRuler.fakeRect = { top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500 }; - strategy = new ConnectedPositionStrategy(fakeElementRef, fakeViewportRuler); + positionBuilder = new OverlayPositionBuilder(fakeViewportRuler); originElement.style.top = '475px'; originElement.style.left = '200px'; originRect = originElement.getBoundingClientRect(); - // Below, left aligned. - let offscreenPos = new RelativePosition(); - offscreenPos.originX = 'start'; - offscreenPos.originY = 'end'; - offscreenPos.overlayX = 'start'; - offscreenPos.overlayY = 'start'; - - // Above, right aligned. - let fallbackPos = new RelativePosition(); - fallbackPos.originX = 'end'; - fallbackPos.originY = 'start'; - fallbackPos.overlayX = 'end'; - fallbackPos.overlayY = 'end'; - - strategy.addPreferredPosition(offscreenPos); - strategy.addPreferredPosition(fallbackPos); + strategy = positionBuilder.connectedTo( + fakeElementRef, + {originX: 'start', originY: 'bottom'}, + {overlayX: 'start', overlayY: 'top'}) + .withFallbackPosition( + {originX: 'end', originY: 'top'}, + {overlayX: 'end', overlayY: 'bottom'}); strategy.apply(overlayElement); @@ -215,28 +189,19 @@ export function main() { fakeViewportRuler.fakeRect = { top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500 }; - strategy = new ConnectedPositionStrategy(fakeElementRef, fakeViewportRuler); + positionBuilder = new OverlayPositionBuilder(fakeViewportRuler); originElement.style.top = '200px'; originElement.style.left = '475px'; originRect = originElement.getBoundingClientRect(); - // To the right, centered. - let offscreenPos = new RelativePosition(); - offscreenPos.originX = 'end'; - offscreenPos.originY = 'center'; - offscreenPos.overlayX = 'start'; - offscreenPos.overlayY = 'center'; - - // To the left, below. - let fallbackPos = new RelativePosition(); - fallbackPos.originX = 'start'; - fallbackPos.originY = 'end'; - fallbackPos.overlayX = 'end'; - fallbackPos.overlayY = 'start'; - - strategy.addPreferredPosition(offscreenPos); - strategy.addPreferredPosition(fallbackPos); + strategy = positionBuilder.connectedTo( + fakeElementRef, + {originX: 'end', originY: 'center'}, + {overlayX: 'start', overlayY: 'center'}) + .withFallbackPosition( + {originX: 'start', originY: 'bottom'}, + {overlayX: 'end', overlayY: 'top'}); strategy.apply(overlayElement); @@ -255,12 +220,10 @@ export function main() { */ function runSimplePositionTests() { it('should position a panel below, left-aligned', () => { - var pos = new RelativePosition(); - pos.originX = 'start'; - pos.originY = 'end'; - pos.overlayX = 'start'; - pos.overlayY = 'start'; - strategy.addPreferredPosition(pos); + strategy = positionBuilder.connectedTo( + fakeElementRef, + {originX: 'start', originY: 'bottom'}, + {overlayX: 'start', overlayY: 'top'}); strategy.apply(overlayElement); @@ -270,12 +233,10 @@ export function main() { }); it('should position to the right, center aligned vertically', () => { - var pos = new RelativePosition(); - pos.originX = 'end'; - pos.originY = 'center'; - pos.overlayX = 'start'; - pos.overlayY = 'center'; - strategy.addPreferredPosition(pos); + strategy = positionBuilder.connectedTo( + fakeElementRef, + {originX: 'end', originY: 'center'}, + {overlayX: 'start', overlayY: 'center'}); strategy.apply(overlayElement); @@ -285,12 +246,10 @@ export function main() { }); it('should position to the left, below', () => { - var pos = new RelativePosition(); - pos.originX = 'start'; - pos.originY = 'end'; - pos.overlayX = 'end'; - pos.overlayY = 'start'; - strategy.addPreferredPosition(pos); + strategy = positionBuilder.connectedTo( + fakeElementRef, + {originX: 'start', originY: 'bottom'}, + {overlayX: 'end', overlayY: 'top'}); strategy.apply(overlayElement); @@ -300,12 +259,10 @@ export function main() { }); it('should position above, right aligned', () => { - var pos = new RelativePosition(); - pos.originX = 'end'; - pos.originY = 'start'; - pos.overlayX = 'end'; - pos.overlayY = 'end'; - strategy.addPreferredPosition(pos); + strategy = positionBuilder.connectedTo( + fakeElementRef, + {originX: 'end', originY: 'top'}, + {overlayX: 'end', overlayY: 'bottom'}); strategy.apply(overlayElement); @@ -315,12 +272,10 @@ export function main() { }); it('should position below, centered', () => { - var pos = new RelativePosition(); - pos.originX = 'center'; - pos.originY = 'end'; - pos.overlayX = 'center'; - pos.overlayY = 'start'; - strategy.addPreferredPosition(pos); + strategy = positionBuilder.connectedTo( + fakeElementRef, + {originX: 'center', originY: 'bottom'}, + {overlayX: 'center', overlayY: 'top'}); strategy.apply(overlayElement); @@ -330,12 +285,10 @@ export function main() { }); it('should center the overlay on the origin', () => { - var pos = new RelativePosition(); - pos.originX = 'center'; - pos.originY = 'center'; - pos.overlayX = 'center'; - pos.overlayY = 'center'; - strategy.addPreferredPosition(pos); + strategy = positionBuilder.connectedTo( + fakeElementRef, + {originX: 'center', originY: 'center'}, + {overlayX: 'center', overlayY: 'center'}); strategy.apply(overlayElement); diff --git a/src/core/overlay/position/connected-position-strategy.ts b/src/core/overlay/position/connected-position-strategy.ts index 6ab6496f8c6..a727b590d9d 100644 --- a/src/core/overlay/position/connected-position-strategy.ts +++ b/src/core/overlay/position/connected-position-strategy.ts @@ -1,7 +1,8 @@ import {PositionStrategy} from './position-strategy'; import {ElementRef} from '@angular/core'; -import {applyCssTransform} from '@angular2-material/core/style/apply-transform'; import {ViewportRuler} from './viewport-ruler'; +import {applyCssTransform} from '@angular2-material/core/style/apply-transform'; +import {ConnectionPair, OriginPos, OverlayPos} from './connected-position'; @@ -18,20 +19,26 @@ export class ConnectedPositionStrategy implements PositionStrategy { _isRtl: boolean = false; /** Ordered list of preferred positions, from most to least desirable. */ - _preferredPositions: RelativePosition[] = []; + _preferredPositions: ConnectionPair[] = []; /** The origin element against which the overlay will be positioned. */ private _origin: HTMLElement; - constructor(private _connectedTo: ElementRef, private _viewportRuler: ViewportRuler) { + constructor( + private _connectedTo: ElementRef, + private _originPos: OriginPos, + private _overlayPos: OverlayPos, + private _viewportRuler: ViewportRuler) { this._origin = this._connectedTo.nativeElement; + this.withFallbackPosition(_originPos, _overlayPos); } /** * Updates the position of the overlay element, using whichever preferred position relative * to the origin fits on-screen. + * @internal */ apply(element: HTMLElement): Promise { // We need the bounding rects for the origin and the overlay to determine how to position @@ -67,16 +74,21 @@ export class ConnectedPositionStrategy implements PositionStrategy { /** Adds a preferred position to the end of the ordered preferred position list. */ - addPreferredPosition(pos: RelativePosition): void { + addPreferredPosition(pos: ConnectionPair): void { this._preferredPositions.push(pos); } + withFallbackPosition(originPos: OriginPos, overlayPos: OverlayPos): this { + this._preferredPositions.push(new ConnectionPair(originPos, overlayPos)); + return this; + } + /** * Gets the horizontal (x) "start" dimension based on whether the overlay is in an RTL context. * @param rect */ - _getStartX(rect: ClientRect): number { + private _getStartX(rect: ClientRect): number { return this._isRtl ? rect.right : rect.left; } @@ -84,7 +96,7 @@ export class ConnectedPositionStrategy implements PositionStrategy { * Gets the horizontal (x) "end" dimension based on whether the overlay is in an RTL context. * @param rect */ - _getEndX(rect: ClientRect): number { + private _getEndX(rect: ClientRect): number { return this._isRtl ? rect.left : rect.right; } @@ -94,7 +106,7 @@ export class ConnectedPositionStrategy implements PositionStrategy { * @param originRect * @param pos */ - private _getOriginConnectionPoint(originRect: ClientRect, pos: RelativePosition): Point { + private _getOriginConnectionPoint(originRect: ClientRect, pos: ConnectionPair): Point { const originStartX = this._getStartX(originRect); const originEndX = this._getEndX(originRect); @@ -109,7 +121,7 @@ export class ConnectedPositionStrategy implements PositionStrategy { if (pos.originY == 'center') { y = originRect.top + (originRect.height / 2); } else { - y = pos.originY == 'start' ? originRect.top : originRect.bottom; + y = pos.originY == 'top' ? originRect.top : originRect.bottom; } return {x, y}; @@ -126,7 +138,7 @@ export class ConnectedPositionStrategy implements PositionStrategy { private _getOverlayPoint( originPoint: Point, overlayRect: ClientRect, - pos: RelativePosition): Point { + pos: ConnectionPair): Point { // Calculate the (overlayStartX, overlayStartY), the start of the potential overlay position // relative to the origin point. let overlayStartX: number; @@ -140,7 +152,7 @@ export class ConnectedPositionStrategy implements PositionStrategy { if (pos.overlayY == 'center') { overlayStartY = -overlayRect.height / 2; } else { - overlayStartY = pos.overlayY == 'start' ? 0 : -overlayRect.height; + overlayStartY = pos.overlayY == 'top' ? 0 : -overlayRect.height; } return { @@ -191,14 +203,3 @@ export class ConnectedPositionStrategy implements PositionStrategy { type Point = {x: number, y: number}; -/** One dimension of a connection point on the perimeter of the origin or overlay element. */ -export type ConnectionPoint = 'start' | 'center' | 'end'; - - -/** The points of the origin element and the overlay element to connect. */ -export class RelativePosition { - originX: ConnectionPoint; - originY: ConnectionPoint; - overlayX: ConnectionPoint; - overlayY: ConnectionPoint; -} diff --git a/src/core/overlay/position/connected-position.ts b/src/core/overlay/position/connected-position.ts new file mode 100644 index 00000000000..16300a41fea --- /dev/null +++ b/src/core/overlay/position/connected-position.ts @@ -0,0 +1,36 @@ +/** Horizontal dimension of a connection point on the perimeter of the origin or overlay element. */ +export type HorizontalConnectionPos = 'start' | 'center' | 'end'; + +/** Vertical dimension of a connection point on the perimeter of the origin or overlay element. */ +export type VerticalConnectionPos = 'top' | 'center' | 'bottom'; + + +/** A connection point on the origin element. */ +export interface OriginPos { + originX: HorizontalConnectionPos; + originY: VerticalConnectionPos; +} + +/** A connection point on the overlay element. */ +export interface OverlayPos { + overlayX: HorizontalConnectionPos; + overlayY: VerticalConnectionPos; +} + +/** + * The points of the origin element and the overlay element to connect. + * @internal + */ +export class ConnectionPair { + originX: HorizontalConnectionPos; + originY: VerticalConnectionPos; + overlayX: HorizontalConnectionPos; + overlayY: VerticalConnectionPos; + + constructor(origin: OriginPos, overlay: OverlayPos) { + this.originX = origin.originX; + this.originY = origin.originY; + this.overlayX = overlay.overlayX; + this.overlayY = overlay.overlayY; + } +} diff --git a/src/core/overlay/position/global-position-strategy.ts b/src/core/overlay/position/global-position-strategy.ts index be97efeb1b9..f7413a3865f 100644 --- a/src/core/overlay/position/global-position-strategy.ts +++ b/src/core/overlay/position/global-position-strategy.ts @@ -85,7 +85,10 @@ export class GlobalPositionStrategy implements PositionStrategy { return this; } - /** Apply the position to the element. */ + /** + * Apply the position to the element. + * @internal + */ apply(element: HTMLElement): Promise { element.style.position = this._cssPosition; element.style.top = this._top; diff --git a/src/core/overlay/position/overlay-position-builder.ts b/src/core/overlay/position/overlay-position-builder.ts index 502d9d15e0b..212a1d9d849 100644 --- a/src/core/overlay/position/overlay-position-builder.ts +++ b/src/core/overlay/position/overlay-position-builder.ts @@ -2,6 +2,7 @@ import {ViewportRuler} from './viewport-ruler'; import {ConnectedPositionStrategy} from './connected-position-strategy'; import {ElementRef, Injectable} from '@angular/core'; import {GlobalPositionStrategy} from './global-position-strategy'; +import {OverlayPos, OriginPos} from './connected-position'; @@ -16,8 +17,8 @@ export class OverlayPositionBuilder { } /** Creates a relative position strategy. */ - connectedTo(elementRef: ElementRef) { - return new ConnectedPositionStrategy(elementRef, this._viewportRuler); + connectedTo(elementRef: ElementRef, originPos: OriginPos, overlayPos: OverlayPos) { + return new ConnectedPositionStrategy(elementRef, originPos, overlayPos, this._viewportRuler); } } diff --git a/src/demo-app/overlay/overlay-demo.ts b/src/demo-app/overlay/overlay-demo.ts index 3419002a4bd..195a3e6a821 100644 --- a/src/demo-app/overlay/overlay-demo.ts +++ b/src/demo-app/overlay/overlay-demo.ts @@ -11,8 +11,6 @@ import { import {Overlay, OverlayState, OVERLAY_PROVIDERS} from '@angular2-material/core/overlay/overlay'; import {ComponentPortal, Portal} from '@angular2-material/core/portal/portal'; import {TemplatePortalDirective} from '@angular2-material/core/portal/portal-directives'; -import {RelativePosition} from - '@angular2-material/core/overlay/position/connected-position-strategy'; @@ -70,15 +68,12 @@ export class OverlayDemo { } openSpaghettiPanel() { - // TODO(jelbourn): make a nice API for this. // TODO(jelbourn): separate overlay demo for connected positioning. - let strategy = this.overlay.position().connectedTo(this._overlayOrigin.elementRef); - var pos = new RelativePosition(); - pos.originX = 'start'; - pos.originY = 'end'; - pos.overlayX = 'start'; - pos.overlayY = 'start'; - strategy.addPreferredPosition(pos); + let strategy = this.overlay.position() + .connectedTo( + this._overlayOrigin.elementRef, + {originX: 'start', originY: 'bottom'}, + {overlayX: 'start', overlayY: 'top'} ); let config = new OverlayState(); config.positionStrategy = strategy;