chore(overlay): improve connected pos strategy api (#464)

This commit is contained in:
Jeremy Elbourn
2016-05-19 14:29:14 -07:00
parent d2c6cb86fa
commit 3513ff36e7
6 changed files with 128 additions and 139 deletions
@@ -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);
@@ -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<void> {
// 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;
}
@@ -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;
}
}
@@ -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<void> {
element.style.position = this._cssPosition;
element.style.top = this._top;
@@ -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);
}
}
+5 -10
View File
@@ -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;