Skip to content

Commit f6b7c10

Browse files
committed
feat(overlay): add start and end positions to GlobalPositionStrategy
* Makes some things easier to follow in the `GlobalPositionStrategy`. * Adds the ability to position a global overlay to the start and end of the viewport, based on its layout direction. * Fixes the offset in the `center` position always being from the left. * Adds better docs for the various methods.
1 parent 444fb38 commit f6b7c10

File tree

3 files changed

+228
-83
lines changed

3 files changed

+228
-83
lines changed

src/cdk/overlay/position/global-position-strategy.spec.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,18 @@ describe('GlobalPositonStrategy', () => {
111111
expect(parentStyle.alignItems).toBe('flex-end');
112112
});
113113

114-
it('should center the element', () => {
114+
it('should center the element by default', () => {
115+
attachOverlay({
116+
positionStrategy: overlay.position().global()
117+
});
118+
119+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
120+
121+
expect(parentStyle.justifyContent).toBe('center');
122+
expect(parentStyle.alignItems).toBe('center');
123+
});
124+
125+
it('should center the element explicitly', () => {
115126
attachOverlay({
116127
positionStrategy: overlay.position()
117128
.global()
@@ -136,13 +147,35 @@ describe('GlobalPositonStrategy', () => {
136147
const elementStyle = overlayRef.overlayElement.style;
137148
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
138149

150+
expect(elementStyle.marginRight).toBe('');
151+
expect(elementStyle.marginBottom).toBe('');
139152
expect(elementStyle.marginLeft).toBe('10px');
140153
expect(elementStyle.marginTop).toBe('15px');
141154

142155
expect(parentStyle.justifyContent).toBe('center');
143156
expect(parentStyle.alignItems).toBe('center');
144157
});
145158

159+
it('should center the element with an offset in rtl', () => {
160+
attachOverlay({
161+
direction: 'rtl',
162+
positionStrategy: overlay.position()
163+
.global()
164+
.centerHorizontally('10px')
165+
.centerVertically('15px')
166+
});
167+
168+
const elementStyle = overlayRef.overlayElement.style;
169+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
170+
171+
expect(elementStyle.marginLeft).toBe('');
172+
expect(elementStyle.marginRight).toBe('10px');
173+
expect(elementStyle.marginTop).toBe('15px');
174+
175+
expect(parentStyle.justifyContent).toBe('center');
176+
expect(parentStyle.alignItems).toBe('center');
177+
});
178+
146179
it('should make the element position: static', () => {
147180
attachOverlay({
148181
positionStrategy: overlay.position().global()
@@ -303,6 +336,63 @@ describe('GlobalPositonStrategy', () => {
303336
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
304337
expect(parentStyle.justifyContent).toBe('flex-start');
305338
});
339+
340+
it('should position the overlay to the start in ltr', () => {
341+
attachOverlay({
342+
direction: 'ltr',
343+
positionStrategy: overlay.position().global().start('40px')
344+
});
345+
346+
const elementStyle = overlayRef.overlayElement.style;
347+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
348+
349+
expect(elementStyle.marginLeft).toBe('40px');
350+
expect(elementStyle.marginRight).toBe('');
351+
expect(parentStyle.justifyContent).toBe('flex-start');
352+
});
353+
354+
it('should position the overlay to the start in rtl', () => {
355+
attachOverlay({
356+
direction: 'rtl',
357+
positionStrategy: overlay.position().global().start('50px')
358+
});
359+
360+
const elementStyle = overlayRef.overlayElement.style;
361+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
362+
363+
expect(elementStyle.marginLeft).toBe('');
364+
expect(elementStyle.marginRight).toBe('50px');
365+
expect(parentStyle.justifyContent).toBe('flex-start');
366+
});
367+
368+
it('should position the overlay to the end in ltr', () => {
369+
attachOverlay({
370+
direction: 'ltr',
371+
positionStrategy: overlay.position().global().end('60px')
372+
});
373+
374+
const elementStyle = overlayRef.overlayElement.style;
375+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
376+
377+
expect(elementStyle.marginRight).toBe('60px');
378+
expect(elementStyle.marginLeft).toBe('');
379+
expect(parentStyle.justifyContent).toBe('flex-end');
380+
});
381+
382+
it('should position the overlay to the end in rtl', () => {
383+
attachOverlay({
384+
direction: 'rtl',
385+
positionStrategy: overlay.position().global().end('70px')
386+
});
387+
388+
const elementStyle = overlayRef.overlayElement.style;
389+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
390+
391+
expect(elementStyle.marginLeft).toBe('70px');
392+
expect(elementStyle.marginRight).toBe('');
393+
expect(parentStyle.justifyContent).toBe('flex-end');
394+
});
395+
306396
});
307397

308398

src/cdk/overlay/position/global-position-strategy.ts

Lines changed: 130 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,12 @@ import {OverlayReference} from '../overlay-reference';
1919
export class GlobalPositionStrategy implements PositionStrategy {
2020
/** The overlay to which this strategy is attached. */
2121
private _overlayRef: OverlayReference;
22-
private _cssPosition: string = 'static';
23-
private _topOffset: string = '';
24-
private _bottomOffset: string = '';
25-
private _leftOffset: string = '';
26-
private _rightOffset: string = '';
27-
private _alignItems: string = '';
28-
private _justifyContent: string = '';
2922
private _width: string = '';
3023
private _height: string = '';
24+
private _yPosition: 'top' | 'bottom' | 'center' = 'center';
25+
private _yOffset = '';
26+
private _xPosition: 'left' | 'right' | 'start' | 'end' | 'center' = 'center';
27+
private _xOffset = '';
3128

3229
attach(overlayRef: OverlayReference): void {
3330
const config = overlayRef.getConfig();
@@ -46,46 +43,64 @@ export class GlobalPositionStrategy implements PositionStrategy {
4643
}
4744

4845
/**
49-
* Sets the top position of the overlay. Clears any previously set vertical position.
50-
* @param value New top offset.
46+
* Positions the overlay to the top of the viewport.
47+
* @param offset Offset from the top of the viewport.
5148
*/
52-
top(value: string = ''): this {
53-
this._bottomOffset = '';
54-
this._topOffset = value;
55-
this._alignItems = 'flex-start';
49+
top(offset: string = ''): this {
50+
this._yOffset = offset;
51+
this._yPosition = 'top';
5652
return this;
5753
}
5854

5955
/**
60-
* Sets the left position of the overlay. Clears any previously set horizontal position.
61-
* @param value New left offset.
56+
* Positions the overlay to the left of the viewport, no matter what layout direction it has.
57+
* @param offset Offset from the left of the viewport.
6258
*/
63-
left(value: string = ''): this {
64-
this._rightOffset = '';
65-
this._leftOffset = value;
66-
this._justifyContent = 'flex-start';
59+
left(offset: string = ''): this {
60+
this._xOffset = offset;
61+
this._xPosition = 'left';
6762
return this;
6863
}
6964

7065
/**
71-
* Sets the bottom position of the overlay. Clears any previously set vertical position.
72-
* @param value New bottom offset.
66+
* Positions the overlay to the bottom of the viewport.
67+
* @param offset Offset from the bottom of the viewport.
7368
*/
74-
bottom(value: string = ''): this {
75-
this._topOffset = '';
76-
this._bottomOffset = value;
77-
this._alignItems = 'flex-end';
69+
bottom(offset: string = ''): this {
70+
this._yOffset = offset;
71+
this._yPosition = 'bottom';
7872
return this;
7973
}
8074

8175
/**
82-
* Sets the right position of the overlay. Clears any previously set horizontal position.
83-
* @param value New right offset.
76+
* Positions the overlay to the right of the viewport, no matter what layout direction it has.
77+
* @param offset Offset from the right of the viewport.
8478
*/
85-
right(value: string = ''): this {
86-
this._leftOffset = '';
87-
this._rightOffset = value;
88-
this._justifyContent = 'flex-end';
79+
right(offset: string = ''): this {
80+
this._xOffset = offset;
81+
this._xPosition = 'right';
82+
return this;
83+
}
84+
85+
/**
86+
* Sets the overlay to the start of the viewport, depending on the overlay direction.
87+
* This will be to the left in LTR layouts and to the right in RTL.
88+
* @param offset Offset from the edge of the screen.
89+
*/
90+
start(offset: string = ''): this {
91+
this._xOffset = offset;
92+
this._xPosition = 'start';
93+
return this;
94+
}
95+
96+
/**
97+
* Sets the overlay to the end of the viewport, depending on the overlay direction.
98+
* This will be to the right in LTR layouts and to the left in RTL.
99+
* @param offset Offset from the edge of the screen.
100+
*/
101+
end(offset: string = ''): this {
102+
this._xOffset = offset;
103+
this._xPosition = 'end';
89104
return this;
90105
}
91106

@@ -122,26 +137,22 @@ export class GlobalPositionStrategy implements PositionStrategy {
122137
}
123138

124139
/**
125-
* Centers the overlay horizontally with an optional offset.
126-
* Clears any previously set horizontal position.
127-
*
128-
* @param offset Overlay offset from the horizontal center.
140+
* Centers the overlay horizontally in the viewport.
141+
* @param offset Offset from the center of the viewport.
129142
*/
130143
centerHorizontally(offset: string = ''): this {
131-
this.left(offset);
132-
this._justifyContent = 'center';
144+
this._xOffset = offset;
145+
this._xPosition = 'center';
133146
return this;
134147
}
135148

136149
/**
137-
* Centers the overlay vertically with an optional offset.
138-
* Clears any previously set vertical position.
139-
*
140-
* @param offset Overlay offset from the vertical center.
150+
* Centers the overlay vertically in the viewport.
151+
* @param offset Offset from the center of the viewport.
141152
*/
142153
centerVertically(offset: string = ''): this {
143-
this.top(offset);
144-
this._alignItems = 'center';
154+
this._yPosition = 'center';
155+
this._yOffset = offset;
145156
return this;
146157
}
147158

@@ -150,42 +161,96 @@ export class GlobalPositionStrategy implements PositionStrategy {
150161
* @docs-private
151162
*/
152163
apply(): void {
153-
// Since the overlay ref applies the strategy asynchronously, it could
154-
// have been disposed before it ends up being applied. If that is the
155-
// case, we shouldn't do anything.
164+
// Since the overlay ref applies the strategy asynchronously, it could have been
165+
// disposed before it gets applied. If that is the case, we shouldn't do anything.
156166
if (!this._overlayRef.hasAttached()) {
157167
return;
158168
}
159169

170+
this._overlayRef.overlayElement.style.position = 'static';
171+
this._applyYPosition();
172+
this._applyXPosition();
173+
}
174+
175+
private _applyYPosition() {
160176
const styles = this._overlayRef.overlayElement.style;
161177
const parentStyles = this._overlayRef.hostElement.style;
162178
const config = this._overlayRef.getConfig();
163179

164-
styles.position = this._cssPosition;
165-
styles.marginLeft = config.width === '100%' ? '0' : this._leftOffset;
166-
styles.marginTop = config.height === '100%' ? '0' : this._topOffset;
167-
styles.marginBottom = this._bottomOffset;
168-
styles.marginRight = this._rightOffset;
180+
if (config.height === '100%') {
181+
parentStyles.alignItems = 'flex-start';
182+
styles.marginTop = styles.marginBottom = '0';
183+
return;
184+
}
185+
186+
switch (this._yPosition) {
187+
case 'top':
188+
case 'center':
189+
parentStyles.alignItems = this._yPosition === 'center' ? 'center' : 'flex-start';
190+
styles.marginTop = this._yOffset;
191+
styles.marginBottom = '';
192+
break;
193+
194+
case 'bottom':
195+
parentStyles.alignItems = 'flex-end';
196+
styles.marginTop = '';
197+
styles.marginBottom = this._yOffset;
198+
break;
199+
200+
default:
201+
throw Error(`Unsupported Y axis position ${this._yPosition}.`);
202+
}
203+
}
204+
205+
private _applyXPosition() {
206+
const styles = this._overlayRef.overlayElement.style;
207+
const parentStyles = this._overlayRef.hostElement.style;
208+
const config = this._overlayRef.getConfig();
209+
const isRtl = this._overlayRef.getConfig().direction === 'rtl';
169210

170211
if (config.width === '100%') {
171212
parentStyles.justifyContent = 'flex-start';
172-
} else if (this._justifyContent === 'center') {
213+
styles.marginLeft = styles.marginRight = '0';
214+
return;
215+
}
216+
217+
switch (this._xPosition) {
218+
// In RTL the browser will invert `flex-start` and `flex-end` automatically, but we don't
219+
// want that if the positioning is explicitly `left` and `right`, hence why we do another
220+
// inversion to ensure that the overlay stays in the same position.
221+
case 'left':
222+
parentStyles.justifyContent = isRtl ? 'flex-end' : 'flex-start';
223+
styles.marginLeft = this._xOffset;
224+
styles.marginRight = '';
225+
break;
226+
227+
case 'right':
228+
parentStyles.justifyContent = isRtl ? 'flex-start' : 'flex-end';
229+
styles.marginRight = this._xOffset;
230+
styles.marginLeft = '';
231+
break;
232+
233+
case 'center':
173234
parentStyles.justifyContent = 'center';
174-
} else if (this._overlayRef.getConfig().direction === 'rtl') {
175-
// In RTL the browser will invert `flex-start` and `flex-end` automatically, but we
176-
// don't want that because our positioning is explicitly `left` and `right`, hence
177-
// why we do another inversion to ensure that the overlay stays in the same position.
178-
// TODO: reconsider this if we add `start` and `end` methods.
179-
if (this._justifyContent === 'flex-start') {
180-
parentStyles.justifyContent = 'flex-end';
181-
} else if (this._justifyContent === 'flex-end') {
235+
styles.marginLeft = isRtl ? '' : this._xOffset;
236+
styles.marginRight = isRtl ? this._xOffset : '';
237+
break;
238+
239+
case 'start':
182240
parentStyles.justifyContent = 'flex-start';
183-
}
184-
} else {
185-
parentStyles.justifyContent = this._justifyContent;
186-
}
241+
styles.marginLeft = isRtl ? '' : this._xOffset;
242+
styles.marginRight = isRtl ? this._xOffset : '';
243+
break;
244+
245+
case 'end':
246+
parentStyles.justifyContent = 'flex-end';
247+
styles.marginLeft = isRtl ? this._xOffset : '';
248+
styles.marginRight = isRtl ? '' : this._xOffset;
249+
break;
187250

188-
parentStyles.alignItems = config.height === '100%' ? 'flex-start' : this._alignItems;
251+
default:
252+
throw Error(`Unsupported X axis position ${this._xPosition}.`);
253+
}
189254
}
190255

191256
/**

0 commit comments

Comments
 (0)