Skip to content

Commit

Permalink
feat(overlay): add global position strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
jelbourn committed Apr 5, 2016
1 parent e58b76d commit c8f87a4
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 18 deletions.
13 changes: 11 additions & 2 deletions src/core/overlay/overlay-state.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import {PositionStrategy} from './position/position-strategy';


/**
* OverlayState is a bag of values for either the initial configuration or current state of an
* overlay.
*/
export class OverlayState {
// Not yet implemented.
// TODO(jelbourn): add overlay state / configuration.
/** Strategy with which to position the overlay. */
positionStrategy: PositionStrategy;

// TODO(jelbourn): configuration still to add
// - overlay size
// - focus trap
// - disable pointer events
// - z-index
}
1 change: 1 addition & 0 deletions src/core/overlay/overlay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
/** A single overlay pane. */
.md-overlay-pane {
position: absolute;
pointer-events: auto;
}
30 changes: 30 additions & 0 deletions src/core/overlay/overlay.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {TemplatePortal, ComponentPortal} from '../portal/portal';
import {Overlay, OVERLAY_CONTAINER_TOKEN} from './overlay';
import {DOM} from '../platform/dom/dom_adapter';
import {OverlayRef} from './overlay-ref';
import {OverlayState} from './overlay-state';
import {PositionStrategy} from './position/position-strategy';


export function main() {
Expand Down Expand Up @@ -121,6 +123,26 @@ export function main() {
expect(overlayContainerElement.childNodes.length).toBe(0);
expect(overlayContainerElement.textContent).toBe('');
}));

describe('applyState', () => {
let state: OverlayState;

beforeEach(() => {
state = new OverlayState();
});

it('should apply the positioning strategy', fakeAsyncTest(() => {
state.positionStrategy = new FakePositionStrategy();

overlay.create(state).then(ref => {
ref.attach(componentPortal);
});

flushMicrotasks();

expect(DOM.querySelectorAll(overlayContainerElement, '.fake-positioned').length).toBe(1);
}));
});
});
}

Expand All @@ -144,6 +166,14 @@ class TestComponentWithTemplatePortals {
constructor(public elementRef: ElementRef) { }
}

class FakePositionStrategy implements PositionStrategy {
apply(element: Element): Promise<void> {
DOM.addClass(element, 'fake-positioned');
return Promise.resolve();
}

}

function fakeAsyncTest(fn: () => void) {
return inject([], fakeAsync(fn));
}
44 changes: 37 additions & 7 deletions src/core/overlay/overlay.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import {
DynamicComponentLoader,
AppViewManager,
OpaqueToken,
Inject,
Injectable} from 'angular2/core';
DynamicComponentLoader,
AppViewManager,
OpaqueToken,
Inject,
Injectable, ElementRef
} from 'angular2/core';
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {OverlayState} from './overlay-state';
import {DomPortalHost} from '../portal/dom-portal-host';
import {OverlayRef} from './overlay-ref';
import {DOM} from '../platform/dom/dom_adapter';
import {GlobalPositionStrategy} from './position/global-position-strategy';
import {RelativePositionStrategy} from './position/relative-position-strategy';


// Re-export overlay-related modules so they can be imported directly from here.
export {OverlayState} from './overlay-state';
Expand Down Expand Up @@ -50,6 +54,14 @@ export class Overlay {
return this._createPaneElement(state).then(pane => this._createOverlayRef(pane));
}

/**
* Returns a position builder that can be used, via fluent API,
* to construct and configure a position strategy.
*/
position() {
return POSITION_BUILDER;
}

/**
* Creates the DOM element for an overlay.
* @param state State to apply to the created element.
Expand All @@ -72,8 +84,9 @@ export class Overlay {
* @param state The state to apply.
*/
applyState(pane: Element, state: OverlayState) {
// Not yet implemented.
// TODO(jelbourn): apply state to the pane element.
if (state.positionStrategy != null) {
state.positionStrategy.apply(pane);
}
}

/**
Expand All @@ -97,3 +110,20 @@ export class Overlay {
return new OverlayRef(this._createPortalHost(pane));
}
}


/** Builder for overlay position strategy. */
export class OverlayPositionBuilder {
/** Creates a global position strategy. */
global() {
return new GlobalPositionStrategy();
}

/** Creates a relative position strategy. */
relativeTo(elementRef: ElementRef) {
return new RelativePositionStrategy(elementRef);
}
}

// We only ever need one position builder.
let POSITION_BUILDER: OverlayPositionBuilder = new OverlayPositionBuilder();
118 changes: 118 additions & 0 deletions src/core/overlay/position/global-position-strategy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
inject,
fakeAsync,
flushMicrotasks,
} from 'angular2/testing';
import {
it,
describe,
expect,
beforeEach,
} from '../../../core/facade/testing';
import {BrowserDomAdapter} from '../../platform/browser/browser_adapter';
import {DOM} from '../../platform/dom/dom_adapter';
import {GlobalPositionStrategy} from './global-position-strategy';


export function main() {
describe('GlobalPositonStrategy', () => {
BrowserDomAdapter.makeCurrent();
let element: HTMLElement;
let strategy: GlobalPositionStrategy;

beforeEach(() => {
element = DOM.createElement('div');
strategy = new GlobalPositionStrategy();
});

it('should set explicit (top, left) position to the element', fakeAsyncTest(() => {
strategy.top('10px').left('40%').apply(element);

flushMicrotasks();

expect(element.style.top).toBe('10px');
expect(element.style.left).toBe('40%');
expect(element.style.bottom).toBe('');
expect(element.style.right).toBe('');
}));

it('should set explicit (bottom, right) position to the element', fakeAsyncTest(() => {
strategy.bottom('70px').right('15em').apply(element);

flushMicrotasks();

expect(element.style.top).toBe('');
expect(element.style.left).toBe('');
expect(element.style.bottom).toBe('70px');
expect(element.style.right).toBe('15em');
}));

it('should overwrite previously applied positioning', fakeAsyncTest(() => {
strategy.centerHorizontally().centerVertically().apply(element);
flushMicrotasks();

strategy.top('10px').left('40%').apply(element);
flushMicrotasks();

expect(element.style.top).toBe('10px');
expect(element.style.left).toBe('40%');
expect(element.style.bottom).toBe('');
expect(element.style.right).toBe('');
expect(element.style.transform).not.toContain('translate');

strategy.bottom('70px').right('15em').apply(element);

flushMicrotasks();

expect(element.style.top).toBe('');
expect(element.style.left).toBe('');
expect(element.style.bottom).toBe('70px');
expect(element.style.right).toBe('15em');
expect(element.style.transform).not.toContain('translate');
}));

it('should center the element', fakeAsyncTest(() => {
strategy.centerHorizontally().centerVertically().apply(element);

flushMicrotasks();

expect(element.style.top).toBe('50%');
expect(element.style.left).toBe('50%');
expect(element.style.transform).toContain('translateX(-50%)');
expect(element.style.transform).toContain('translateY(-50%)');
}));

it('should center the element with an offset', fakeAsyncTest(() => {
strategy.centerHorizontally('10px').centerVertically('15px').apply(element);

flushMicrotasks();

expect(element.style.top).toBe('50%');
expect(element.style.left).toBe('50%');
expect(element.style.transform).toContain('translateX(-50%)');
expect(element.style.transform).toContain('translateX(10px)');
expect(element.style.transform).toContain('translateY(-50%)');
expect(element.style.transform).toContain('translateY(15px)');
}));

it('should default the element to position: absolute', fakeAsyncTest(() => {
strategy.apply(element);

flushMicrotasks();

expect(element.style.position).toBe('absolute');
}));

it('should make the element position: fixed', fakeAsyncTest(() => {
strategy.fixed().apply(element);

flushMicrotasks();

expect(element.style.position).toBe('fixed');
}));
});
}

function fakeAsyncTest(fn: () => void) {
return inject([], fakeAsync(fn));
}
112 changes: 112 additions & 0 deletions src/core/overlay/position/global-position-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {PositionStrategy} from './position-strategy';
import {DOM} from '../../platform/dom/dom_adapter';


/**
* A strategy for positioning overlays. Using this strategy, an overlay is given an
* explicit position relative to the browser's viewport.
*/
export class GlobalPositionStrategy implements PositionStrategy {
private _cssPosition: string = 'absolute';
private _top: string = '';
private _bottom: string = '';
private _left: string = '';
private _right: string = '';

/** Array of individual applications of translateX(). Currently only for centering. */
private _translateX: string[] = [];

/** Array of individual applications of translateY(). Currently only for centering. */
private _translateY: string[] = [];

/** Sets the element to usee CSS position: fixed */
fixed() {
this._cssPosition = 'fixed';
return this;
}

/** Sets the element to usee CSS position: absolute. This is the default. */
absolute() {
this._cssPosition = 'absolute';
return this;
}

/** Sets the top position of the overlay. Clears any previously set vertical position. */
top(value: string) {
this._bottom = '';
this._translateY = [];
this._top = value;
return this;
}

/** Sets the left position of the overlay. Clears any previously set horizontal position. */
left(value: string) {
this._right = '';
this._translateX = [];
this._left = value;
return this;
}

/** Sets the bottom position of the overlay. Clears any previously set vertical position. */
bottom(value: string) {
this._top = '';
this._translateY = [];
this._bottom = value;
return this;
}

/** Sets the right position of the overlay. Clears any previously set horizontal position. */
right(value: string) {
this._left = '';
this._translateX = [];
this._right = value;
return this;
}

/**
* Centers the overlay horizontally with an optional offset.
* Clears any previously set horizontal position.
*/
centerHorizontally(offset = '0px') {
this._left = '50%';
this._right = '';
this._translateX = ['-50%', offset];
return this;
}

/**
* Centers the overlay vertically with an optional offset.
* Clears any previously set vertical position.
*/
centerVertically(offset = '0px') {
this._top = '50%';
this._bottom = '';
this._translateY = ['-50%', offset];
return this;
}

/** Apply the position to the element. */
apply(element: Element): Promise<void> {
DOM.setStyle(element, 'position', this._cssPosition);
DOM.setStyle(element, 'top', this._top);
DOM.setStyle(element, 'left', this._left);
DOM.setStyle(element, 'bottom', this._bottom);
DOM.setStyle(element, 'right', this._right);

// TODO(jelbourn): we don't want to always overwrite the transform property here,
// because it will need to be used for animations.
let tranlateX = this._reduceTranslateValues('translateX', this._translateX);
let translateY = this._reduceTranslateValues('translateY', this._translateY);

// It's important to trim the result, because the browser will ignore the set operation
// if the string contains only whitespace.
DOM.setStyle(element, 'transform', `${tranlateX} ${translateY}`.trim());

return Promise.resolve();
}

/** Reduce a list of translate values to a string that can be used in the transform property */
private _reduceTranslateValues(translateFn: string, values: string[]) {
return values.map(t => `${translateFn}(${t})`).join(' ');
}
}
6 changes: 6 additions & 0 deletions src/core/overlay/position/position-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** Strategy for setting the position on an overlay. */
export interface PositionStrategy {

/** Updates the position of the overlay element. */
apply(element: Element): Promise<void>;
}
11 changes: 11 additions & 0 deletions src/core/overlay/position/relative-position-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {PositionStrategy} from './position-strategy';
import {ElementRef} from 'angular2/core';

export class RelativePositionStrategy implements PositionStrategy {
constructor(private _relativeTo: ElementRef) { }

apply(element: Element): Promise<void> {
// Not yet implemented.
return null;
}
}
Loading

0 comments on commit c8f87a4

Please sign in to comment.