Skip to content

Commit c8f87a4

Browse files
committed
feat(overlay): add global position strategy
1 parent e58b76d commit c8f87a4

11 files changed

+367
-18
lines changed

src/core/overlay/overlay-state.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1+
import {PositionStrategy} from './position/position-strategy';
2+
3+
14
/**
25
* OverlayState is a bag of values for either the initial configuration or current state of an
36
* overlay.
47
*/
58
export class OverlayState {
6-
// Not yet implemented.
7-
// TODO(jelbourn): add overlay state / configuration.
9+
/** Strategy with which to position the overlay. */
10+
positionStrategy: PositionStrategy;
11+
12+
// TODO(jelbourn): configuration still to add
13+
// - overlay size
14+
// - focus trap
15+
// - disable pointer events
16+
// - z-index
817
}

src/core/overlay/overlay.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@
1515
/** A single overlay pane. */
1616
.md-overlay-pane {
1717
position: absolute;
18+
pointer-events: auto;
1819
}

src/core/overlay/overlay.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {TemplatePortal, ComponentPortal} from '../portal/portal';
2323
import {Overlay, OVERLAY_CONTAINER_TOKEN} from './overlay';
2424
import {DOM} from '../platform/dom/dom_adapter';
2525
import {OverlayRef} from './overlay-ref';
26+
import {OverlayState} from './overlay-state';
27+
import {PositionStrategy} from './position/position-strategy';
2628

2729

2830
export function main() {
@@ -121,6 +123,26 @@ export function main() {
121123
expect(overlayContainerElement.childNodes.length).toBe(0);
122124
expect(overlayContainerElement.textContent).toBe('');
123125
}));
126+
127+
describe('applyState', () => {
128+
let state: OverlayState;
129+
130+
beforeEach(() => {
131+
state = new OverlayState();
132+
});
133+
134+
it('should apply the positioning strategy', fakeAsyncTest(() => {
135+
state.positionStrategy = new FakePositionStrategy();
136+
137+
overlay.create(state).then(ref => {
138+
ref.attach(componentPortal);
139+
});
140+
141+
flushMicrotasks();
142+
143+
expect(DOM.querySelectorAll(overlayContainerElement, '.fake-positioned').length).toBe(1);
144+
}));
145+
});
124146
});
125147
}
126148

@@ -144,6 +166,14 @@ class TestComponentWithTemplatePortals {
144166
constructor(public elementRef: ElementRef) { }
145167
}
146168

169+
class FakePositionStrategy implements PositionStrategy {
170+
apply(element: Element): Promise<void> {
171+
DOM.addClass(element, 'fake-positioned');
172+
return Promise.resolve();
173+
}
174+
175+
}
176+
147177
function fakeAsyncTest(fn: () => void) {
148178
return inject([], fakeAsync(fn));
149179
}

src/core/overlay/overlay.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import {
2-
DynamicComponentLoader,
3-
AppViewManager,
4-
OpaqueToken,
5-
Inject,
6-
Injectable} from 'angular2/core';
2+
DynamicComponentLoader,
3+
AppViewManager,
4+
OpaqueToken,
5+
Inject,
6+
Injectable, ElementRef
7+
} from 'angular2/core';
78
import {CONST_EXPR} from 'angular2/src/facade/lang';
89
import {OverlayState} from './overlay-state';
910
import {DomPortalHost} from '../portal/dom-portal-host';
1011
import {OverlayRef} from './overlay-ref';
1112
import {DOM} from '../platform/dom/dom_adapter';
13+
import {GlobalPositionStrategy} from './position/global-position-strategy';
14+
import {RelativePositionStrategy} from './position/relative-position-strategy';
15+
1216

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

57+
/**
58+
* Returns a position builder that can be used, via fluent API,
59+
* to construct and configure a position strategy.
60+
*/
61+
position() {
62+
return POSITION_BUILDER;
63+
}
64+
5365
/**
5466
* Creates the DOM element for an overlay.
5567
* @param state State to apply to the created element.
@@ -72,8 +84,9 @@ export class Overlay {
7284
* @param state The state to apply.
7385
*/
7486
applyState(pane: Element, state: OverlayState) {
75-
// Not yet implemented.
76-
// TODO(jelbourn): apply state to the pane element.
87+
if (state.positionStrategy != null) {
88+
state.positionStrategy.apply(pane);
89+
}
7790
}
7891

7992
/**
@@ -97,3 +110,20 @@ export class Overlay {
97110
return new OverlayRef(this._createPortalHost(pane));
98111
}
99112
}
113+
114+
115+
/** Builder for overlay position strategy. */
116+
export class OverlayPositionBuilder {
117+
/** Creates a global position strategy. */
118+
global() {
119+
return new GlobalPositionStrategy();
120+
}
121+
122+
/** Creates a relative position strategy. */
123+
relativeTo(elementRef: ElementRef) {
124+
return new RelativePositionStrategy(elementRef);
125+
}
126+
}
127+
128+
// We only ever need one position builder.
129+
let POSITION_BUILDER: OverlayPositionBuilder = new OverlayPositionBuilder();
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {
2+
inject,
3+
fakeAsync,
4+
flushMicrotasks,
5+
} from 'angular2/testing';
6+
import {
7+
it,
8+
describe,
9+
expect,
10+
beforeEach,
11+
} from '../../../core/facade/testing';
12+
import {BrowserDomAdapter} from '../../platform/browser/browser_adapter';
13+
import {DOM} from '../../platform/dom/dom_adapter';
14+
import {GlobalPositionStrategy} from './global-position-strategy';
15+
16+
17+
export function main() {
18+
describe('GlobalPositonStrategy', () => {
19+
BrowserDomAdapter.makeCurrent();
20+
let element: HTMLElement;
21+
let strategy: GlobalPositionStrategy;
22+
23+
beforeEach(() => {
24+
element = DOM.createElement('div');
25+
strategy = new GlobalPositionStrategy();
26+
});
27+
28+
it('should set explicit (top, left) position to the element', fakeAsyncTest(() => {
29+
strategy.top('10px').left('40%').apply(element);
30+
31+
flushMicrotasks();
32+
33+
expect(element.style.top).toBe('10px');
34+
expect(element.style.left).toBe('40%');
35+
expect(element.style.bottom).toBe('');
36+
expect(element.style.right).toBe('');
37+
}));
38+
39+
it('should set explicit (bottom, right) position to the element', fakeAsyncTest(() => {
40+
strategy.bottom('70px').right('15em').apply(element);
41+
42+
flushMicrotasks();
43+
44+
expect(element.style.top).toBe('');
45+
expect(element.style.left).toBe('');
46+
expect(element.style.bottom).toBe('70px');
47+
expect(element.style.right).toBe('15em');
48+
}));
49+
50+
it('should overwrite previously applied positioning', fakeAsyncTest(() => {
51+
strategy.centerHorizontally().centerVertically().apply(element);
52+
flushMicrotasks();
53+
54+
strategy.top('10px').left('40%').apply(element);
55+
flushMicrotasks();
56+
57+
expect(element.style.top).toBe('10px');
58+
expect(element.style.left).toBe('40%');
59+
expect(element.style.bottom).toBe('');
60+
expect(element.style.right).toBe('');
61+
expect(element.style.transform).not.toContain('translate');
62+
63+
strategy.bottom('70px').right('15em').apply(element);
64+
65+
flushMicrotasks();
66+
67+
expect(element.style.top).toBe('');
68+
expect(element.style.left).toBe('');
69+
expect(element.style.bottom).toBe('70px');
70+
expect(element.style.right).toBe('15em');
71+
expect(element.style.transform).not.toContain('translate');
72+
}));
73+
74+
it('should center the element', fakeAsyncTest(() => {
75+
strategy.centerHorizontally().centerVertically().apply(element);
76+
77+
flushMicrotasks();
78+
79+
expect(element.style.top).toBe('50%');
80+
expect(element.style.left).toBe('50%');
81+
expect(element.style.transform).toContain('translateX(-50%)');
82+
expect(element.style.transform).toContain('translateY(-50%)');
83+
}));
84+
85+
it('should center the element with an offset', fakeAsyncTest(() => {
86+
strategy.centerHorizontally('10px').centerVertically('15px').apply(element);
87+
88+
flushMicrotasks();
89+
90+
expect(element.style.top).toBe('50%');
91+
expect(element.style.left).toBe('50%');
92+
expect(element.style.transform).toContain('translateX(-50%)');
93+
expect(element.style.transform).toContain('translateX(10px)');
94+
expect(element.style.transform).toContain('translateY(-50%)');
95+
expect(element.style.transform).toContain('translateY(15px)');
96+
}));
97+
98+
it('should default the element to position: absolute', fakeAsyncTest(() => {
99+
strategy.apply(element);
100+
101+
flushMicrotasks();
102+
103+
expect(element.style.position).toBe('absolute');
104+
}));
105+
106+
it('should make the element position: fixed', fakeAsyncTest(() => {
107+
strategy.fixed().apply(element);
108+
109+
flushMicrotasks();
110+
111+
expect(element.style.position).toBe('fixed');
112+
}));
113+
});
114+
}
115+
116+
function fakeAsyncTest(fn: () => void) {
117+
return inject([], fakeAsync(fn));
118+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {PositionStrategy} from './position-strategy';
2+
import {DOM} from '../../platform/dom/dom_adapter';
3+
4+
5+
/**
6+
* A strategy for positioning overlays. Using this strategy, an overlay is given an
7+
* explicit position relative to the browser's viewport.
8+
*/
9+
export class GlobalPositionStrategy implements PositionStrategy {
10+
private _cssPosition: string = 'absolute';
11+
private _top: string = '';
12+
private _bottom: string = '';
13+
private _left: string = '';
14+
private _right: string = '';
15+
16+
/** Array of individual applications of translateX(). Currently only for centering. */
17+
private _translateX: string[] = [];
18+
19+
/** Array of individual applications of translateY(). Currently only for centering. */
20+
private _translateY: string[] = [];
21+
22+
/** Sets the element to usee CSS position: fixed */
23+
fixed() {
24+
this._cssPosition = 'fixed';
25+
return this;
26+
}
27+
28+
/** Sets the element to usee CSS position: absolute. This is the default. */
29+
absolute() {
30+
this._cssPosition = 'absolute';
31+
return this;
32+
}
33+
34+
/** Sets the top position of the overlay. Clears any previously set vertical position. */
35+
top(value: string) {
36+
this._bottom = '';
37+
this._translateY = [];
38+
this._top = value;
39+
return this;
40+
}
41+
42+
/** Sets the left position of the overlay. Clears any previously set horizontal position. */
43+
left(value: string) {
44+
this._right = '';
45+
this._translateX = [];
46+
this._left = value;
47+
return this;
48+
}
49+
50+
/** Sets the bottom position of the overlay. Clears any previously set vertical position. */
51+
bottom(value: string) {
52+
this._top = '';
53+
this._translateY = [];
54+
this._bottom = value;
55+
return this;
56+
}
57+
58+
/** Sets the right position of the overlay. Clears any previously set horizontal position. */
59+
right(value: string) {
60+
this._left = '';
61+
this._translateX = [];
62+
this._right = value;
63+
return this;
64+
}
65+
66+
/**
67+
* Centers the overlay horizontally with an optional offset.
68+
* Clears any previously set horizontal position.
69+
*/
70+
centerHorizontally(offset = '0px') {
71+
this._left = '50%';
72+
this._right = '';
73+
this._translateX = ['-50%', offset];
74+
return this;
75+
}
76+
77+
/**
78+
* Centers the overlay vertically with an optional offset.
79+
* Clears any previously set vertical position.
80+
*/
81+
centerVertically(offset = '0px') {
82+
this._top = '50%';
83+
this._bottom = '';
84+
this._translateY = ['-50%', offset];
85+
return this;
86+
}
87+
88+
/** Apply the position to the element. */
89+
apply(element: Element): Promise<void> {
90+
DOM.setStyle(element, 'position', this._cssPosition);
91+
DOM.setStyle(element, 'top', this._top);
92+
DOM.setStyle(element, 'left', this._left);
93+
DOM.setStyle(element, 'bottom', this._bottom);
94+
DOM.setStyle(element, 'right', this._right);
95+
96+
// TODO(jelbourn): we don't want to always overwrite the transform property here,
97+
// because it will need to be used for animations.
98+
let tranlateX = this._reduceTranslateValues('translateX', this._translateX);
99+
let translateY = this._reduceTranslateValues('translateY', this._translateY);
100+
101+
// It's important to trim the result, because the browser will ignore the set operation
102+
// if the string contains only whitespace.
103+
DOM.setStyle(element, 'transform', `${tranlateX} ${translateY}`.trim());
104+
105+
return Promise.resolve();
106+
}
107+
108+
/** Reduce a list of translate values to a string that can be used in the transform property */
109+
private _reduceTranslateValues(translateFn: string, values: string[]) {
110+
return values.map(t => `${translateFn}(${t})`).join(' ');
111+
}
112+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/** Strategy for setting the position on an overlay. */
2+
export interface PositionStrategy {
3+
4+
/** Updates the position of the overlay element. */
5+
apply(element: Element): Promise<void>;
6+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {PositionStrategy} from './position-strategy';
2+
import {ElementRef} from 'angular2/core';
3+
4+
export class RelativePositionStrategy implements PositionStrategy {
5+
constructor(private _relativeTo: ElementRef) { }
6+
7+
apply(element: Element): Promise<void> {
8+
// Not yet implemented.
9+
return null;
10+
}
11+
}

0 commit comments

Comments
 (0)