Skip to content

Commit 60ea22f

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 36db1c0 commit 60ea22f

File tree

3 files changed

+226
-81
lines changed

3 files changed

+226
-81
lines changed

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

Lines changed: 90 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()
@@ -324,6 +357,62 @@ describe('GlobalPositonStrategy', () => {
324357
expect(parentStyle.alignItems).toBeFalsy();
325358
});
326359

360+
it('should position the overlay to the start in ltr', () => {
361+
attachOverlay({
362+
direction: 'ltr',
363+
positionStrategy: overlay.position().global().start('40px')
364+
});
365+
366+
const elementStyle = overlayRef.overlayElement.style;
367+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
368+
369+
expect(elementStyle.marginLeft).toBe('40px');
370+
expect(elementStyle.marginRight).toBe('');
371+
expect(parentStyle.justifyContent).toBe('flex-start');
372+
});
373+
374+
it('should position the overlay to the start in rtl', () => {
375+
attachOverlay({
376+
direction: 'rtl',
377+
positionStrategy: overlay.position().global().start('50px')
378+
});
379+
380+
const elementStyle = overlayRef.overlayElement.style;
381+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
382+
383+
expect(elementStyle.marginLeft).toBe('');
384+
expect(elementStyle.marginRight).toBe('50px');
385+
expect(parentStyle.justifyContent).toBe('flex-start');
386+
});
387+
388+
it('should position the overlay to the end in ltr', () => {
389+
attachOverlay({
390+
direction: 'ltr',
391+
positionStrategy: overlay.position().global().end('60px')
392+
});
393+
394+
const elementStyle = overlayRef.overlayElement.style;
395+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
396+
397+
expect(elementStyle.marginRight).toBe('60px');
398+
expect(elementStyle.marginLeft).toBe('');
399+
expect(parentStyle.justifyContent).toBe('flex-end');
400+
});
401+
402+
it('should position the overlay to the end in rtl', () => {
403+
attachOverlay({
404+
direction: 'rtl',
405+
positionStrategy: overlay.position().global().end('70px')
406+
});
407+
408+
const elementStyle = overlayRef.overlayElement.style;
409+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
410+
411+
expect(elementStyle.marginLeft).toBe('70px');
412+
expect(elementStyle.marginRight).toBe('');
413+
expect(parentStyle.justifyContent).toBe('flex-end');
414+
});
415+
327416
});
328417

329418

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

Lines changed: 129 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,12 @@ const wrapperClass = 'cdk-global-overlay-wrapper';
2121
export class GlobalPositionStrategy implements PositionStrategy {
2222
/** The overlay to which this strategy is attached. */
2323
private _overlayRef: OverlayReference;
24-
private _cssPosition: string = 'static';
25-
private _topOffset: string = '';
26-
private _bottomOffset: string = '';
27-
private _leftOffset: string = '';
28-
private _rightOffset: string = '';
29-
private _alignItems: string = '';
30-
private _justifyContent: string = '';
3124
private _width: string = '';
3225
private _height: string = '';
26+
private _yPosition: 'top' | 'bottom' | 'center' = 'center';
27+
private _yOffset = '';
28+
private _xPosition: 'left' | 'right' | 'start' | 'end' | 'center' = 'center';
29+
private _xOffset = '';
3330
private _isDisposed: boolean;
3431

3532
attach(overlayRef: OverlayReference): void {
@@ -50,46 +47,64 @@ export class GlobalPositionStrategy implements PositionStrategy {
5047
}
5148

5249
/**
53-
* Sets the top position of the overlay. Clears any previously set vertical position.
54-
* @param value New top offset.
50+
* Positions the overlay to the top of the viewport.
51+
* @param offset Offset from the top of the viewport.
5552
*/
56-
top(value: string = ''): this {
57-
this._bottomOffset = '';
58-
this._topOffset = value;
59-
this._alignItems = 'flex-start';
53+
top(offset: string = ''): this {
54+
this._yOffset = offset;
55+
this._yPosition = 'top';
6056
return this;
6157
}
6258

6359
/**
64-
* Sets the left position of the overlay. Clears any previously set horizontal position.
65-
* @param value New left offset.
60+
* Positions the overlay to the left of the viewport, no matter what layout direction it has.
61+
* @param offset Offset from the left of the viewport.
6662
*/
67-
left(value: string = ''): this {
68-
this._rightOffset = '';
69-
this._leftOffset = value;
70-
this._justifyContent = 'flex-start';
63+
left(offset: string = ''): this {
64+
this._xOffset = offset;
65+
this._xPosition = 'left';
7166
return this;
7267
}
7368

7469
/**
75-
* Sets the bottom position of the overlay. Clears any previously set vertical position.
76-
* @param value New bottom offset.
70+
* Positions the overlay to the bottom of the viewport.
71+
* @param offset Offset from the bottom of the viewport.
7772
*/
78-
bottom(value: string = ''): this {
79-
this._topOffset = '';
80-
this._bottomOffset = value;
81-
this._alignItems = 'flex-end';
73+
bottom(offset: string = ''): this {
74+
this._yOffset = offset;
75+
this._yPosition = 'bottom';
8276
return this;
8377
}
8478

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

@@ -126,26 +141,22 @@ export class GlobalPositionStrategy implements PositionStrategy {
126141
}
127142

128143
/**
129-
* Centers the overlay horizontally with an optional offset.
130-
* Clears any previously set horizontal position.
131-
*
132-
* @param offset Overlay offset from the horizontal center.
144+
* Centers the overlay horizontally in the viewport.
145+
* @param offset Offset from the center of the viewport.
133146
*/
134147
centerHorizontally(offset: string = ''): this {
135-
this.left(offset);
136-
this._justifyContent = 'center';
148+
this._xOffset = offset;
149+
this._xPosition = 'center';
137150
return this;
138151
}
139152

140153
/**
141-
* Centers the overlay vertically with an optional offset.
142-
* Clears any previously set vertical position.
143-
*
144-
* @param offset Overlay offset from the vertical center.
154+
* Centers the overlay vertically in the viewport.
155+
* @param offset Offset from the center of the viewport.
145156
*/
146157
centerVertically(offset: string = ''): this {
147-
this.top(offset);
148-
this._alignItems = 'center';
158+
this._yPosition = 'center';
159+
this._yOffset = offset;
149160
return this;
150161
}
151162

@@ -161,35 +172,90 @@ export class GlobalPositionStrategy implements PositionStrategy {
161172
return;
162173
}
163174

175+
this._overlayRef.overlayElement.style.position = 'static';
176+
this._applyYPosition();
177+
this._applyXPosition();
178+
}
179+
180+
private _applyYPosition() {
164181
const styles = this._overlayRef.overlayElement.style;
165182
const parentStyles = this._overlayRef.hostElement.style;
166183
const config = this._overlayRef.getConfig();
167184

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

174216
if (config.width === '100%') {
175217
parentStyles.justifyContent = 'flex-start';
176-
} else if (this._justifyContent === 'center') {
177-
parentStyles.justifyContent = 'center';
178-
} else if (this._overlayRef.getConfig().direction === 'rtl') {
179-
// In RTL the browser will invert `flex-start` and `flex-end` automatically, but we
180-
// don't want that because our positioning is explicitly `left` and `right`, hence
181-
// why we do another inversion to ensure that the overlay stays in the same position.
182-
// TODO: reconsider this if we add `start` and `end` methods.
183-
if (this._justifyContent === 'flex-start') {
184-
parentStyles.justifyContent = 'flex-end';
185-
} else if (this._justifyContent === 'flex-end') {
186-
parentStyles.justifyContent = 'flex-start';
187-
}
188-
} else {
189-
parentStyles.justifyContent = this._justifyContent;
218+
styles.marginLeft = styles.marginRight = '0';
219+
return;
190220
}
191221

192-
parentStyles.alignItems = config.height === '100%' ? 'flex-start' : this._alignItems;
222+
switch (this._xPosition) {
223+
// In RTL the browser will invert `flex-start` and `flex-end` automatically, but we don't
224+
// want that if the positioning is explicitly `left` and `right`, hence why we do another
225+
// inversion to ensure that the overlay stays in the same position.
226+
case 'left':
227+
parentStyles.justifyContent = isRtl ? 'flex-end' : 'flex-start';
228+
styles.marginLeft = this._xOffset;
229+
styles.marginRight = '';
230+
break;
231+
232+
case 'right':
233+
parentStyles.justifyContent = isRtl ? 'flex-start' : 'flex-end';
234+
styles.marginRight = this._xOffset;
235+
styles.marginLeft = '';
236+
break;
237+
238+
case 'center':
239+
parentStyles.justifyContent = 'center';
240+
styles.marginLeft = isRtl ? '' : this._xOffset;
241+
styles.marginRight = isRtl ? this._xOffset : '';
242+
break;
243+
244+
case 'start':
245+
parentStyles.justifyContent = 'flex-start';
246+
styles.marginLeft = isRtl ? '' : this._xOffset;
247+
styles.marginRight = isRtl ? this._xOffset : '';
248+
break;
249+
250+
case 'end':
251+
parentStyles.justifyContent = 'flex-end';
252+
styles.marginLeft = isRtl ? this._xOffset : '';
253+
styles.marginRight = isRtl ? '' : this._xOffset;
254+
break;
255+
256+
default:
257+
throw Error(`Unsupported X axis position ${this._xPosition}.`);
258+
}
193259
}
194260

195261
/**

0 commit comments

Comments
 (0)