Skip to content

Commit a663938

Browse files
committed
feat(cdk/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 d011cae commit a663938

File tree

3 files changed

+231
-72
lines changed

3 files changed

+231
-72
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
@@ -98,7 +98,18 @@ describe('GlobalPositonStrategy', () => {
9898
expect(parentStyle.alignItems).toBe('flex-end');
9999
});
100100

101-
it('should center the element', () => {
101+
it('should center the element by default', () => {
102+
attachOverlay({
103+
positionStrategy: overlay.position().global(),
104+
});
105+
106+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
107+
108+
expect(parentStyle.justifyContent).toBe('center');
109+
expect(parentStyle.alignItems).toBe('center');
110+
});
111+
112+
it('should center the element explicitly', () => {
102113
attachOverlay({
103114
positionStrategy: overlay.position().global().centerHorizontally().centerVertically(),
104115
});
@@ -121,13 +132,36 @@ describe('GlobalPositonStrategy', () => {
121132
const elementStyle = overlayRef.overlayElement.style;
122133
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
123134

135+
expect(elementStyle.marginRight).toBe('');
136+
expect(elementStyle.marginBottom).toBe('');
124137
expect(elementStyle.marginLeft).toBe('10px');
125138
expect(elementStyle.marginTop).toBe('15px');
126139

127140
expect(parentStyle.justifyContent).toBe('center');
128141
expect(parentStyle.alignItems).toBe('center');
129142
});
130143

144+
it('should center the element with an offset in rtl', () => {
145+
attachOverlay({
146+
direction: 'rtl',
147+
positionStrategy: overlay
148+
.position()
149+
.global()
150+
.centerHorizontally('10px')
151+
.centerVertically('15px'),
152+
});
153+
154+
const elementStyle = overlayRef.overlayElement.style;
155+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
156+
157+
expect(elementStyle.marginLeft).toBe('');
158+
expect(elementStyle.marginRight).toBe('10px');
159+
expect(elementStyle.marginTop).toBe('15px');
160+
161+
expect(parentStyle.justifyContent).toBe('center');
162+
expect(parentStyle.alignItems).toBe('center');
163+
});
164+
131165
it('should make the element position: static', () => {
132166
attachOverlay({
133167
positionStrategy: overlay.position().global(),
@@ -367,6 +401,62 @@ describe('GlobalPositonStrategy', () => {
367401
expect(parentStyle.justifyContent).toBeFalsy();
368402
expect(parentStyle.alignItems).toBeFalsy();
369403
});
404+
405+
it('should position the overlay to the start in ltr', () => {
406+
attachOverlay({
407+
direction: 'ltr',
408+
positionStrategy: overlay.position().global().start('40px'),
409+
});
410+
411+
const elementStyle = overlayRef.overlayElement.style;
412+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
413+
414+
expect(elementStyle.marginLeft).toBe('40px');
415+
expect(elementStyle.marginRight).toBe('');
416+
expect(parentStyle.justifyContent).toBe('flex-start');
417+
});
418+
419+
it('should position the overlay to the start in rtl', () => {
420+
attachOverlay({
421+
direction: 'rtl',
422+
positionStrategy: overlay.position().global().start('50px'),
423+
});
424+
425+
const elementStyle = overlayRef.overlayElement.style;
426+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
427+
428+
expect(elementStyle.marginLeft).toBe('');
429+
expect(elementStyle.marginRight).toBe('50px');
430+
expect(parentStyle.justifyContent).toBe('flex-start');
431+
});
432+
433+
it('should position the overlay to the end in ltr', () => {
434+
attachOverlay({
435+
direction: 'ltr',
436+
positionStrategy: overlay.position().global().end('60px'),
437+
});
438+
439+
const elementStyle = overlayRef.overlayElement.style;
440+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
441+
442+
expect(elementStyle.marginRight).toBe('60px');
443+
expect(elementStyle.marginLeft).toBe('');
444+
expect(parentStyle.justifyContent).toBe('flex-end');
445+
});
446+
447+
it('should position the overlay to the end in rtl', () => {
448+
attachOverlay({
449+
direction: 'rtl',
450+
positionStrategy: overlay.position().global().end('70px'),
451+
});
452+
453+
const elementStyle = overlayRef.overlayElement.style;
454+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
455+
456+
expect(elementStyle.marginLeft).toBe('70px');
457+
expect(elementStyle.marginRight).toBe('');
458+
expect(parentStyle.justifyContent).toBe('flex-end');
459+
});
370460
});
371461

372462
@Component({template: ''})

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

Lines changed: 134 additions & 67 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,42 +172,98 @@ 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();
167-
const {width, height, maxWidth, maxHeight} = config;
168-
const shouldBeFlushHorizontally =
169-
(width === '100%' || width === '100vw') &&
170-
(!maxWidth || maxWidth === '100%' || maxWidth === '100vw');
184+
const {height, maxHeight} = config;
171185
const shouldBeFlushVertically =
172186
(height === '100%' || height === '100vh') &&
173187
(!maxHeight || maxHeight === '100%' || maxHeight === '100vh');
174188

175-
styles.position = this._cssPosition;
176-
styles.marginLeft = shouldBeFlushHorizontally ? '0' : this._leftOffset;
177-
styles.marginTop = shouldBeFlushVertically ? '0' : this._topOffset;
178-
styles.marginBottom = this._bottomOffset;
179-
styles.marginRight = this._rightOffset;
189+
if (shouldBeFlushVertically) {
190+
parentStyles.alignItems = 'flex-start';
191+
styles.marginTop = styles.marginBottom = '0';
192+
return;
193+
}
194+
195+
switch (this._yPosition) {
196+
case 'top':
197+
case 'center':
198+
parentStyles.alignItems = this._yPosition === 'center' ? 'center' : 'flex-start';
199+
styles.marginTop = shouldBeFlushVertically ? '0' : this._yOffset;
200+
styles.marginBottom = '';
201+
break;
202+
203+
case 'bottom':
204+
parentStyles.alignItems = 'flex-end';
205+
styles.marginTop = '';
206+
styles.marginBottom = shouldBeFlushVertically ? '0' : this._yOffset;
207+
break;
208+
209+
default:
210+
throw Error(`Unsupported Y axis position ${this._yPosition}.`);
211+
}
212+
}
213+
214+
private _applyXPosition() {
215+
const styles = this._overlayRef.overlayElement.style;
216+
const parentStyles = this._overlayRef.hostElement.style;
217+
const config = this._overlayRef.getConfig();
218+
const {width, maxWidth} = config;
219+
const isRtl = this._overlayRef.getConfig().direction === 'rtl';
220+
const shouldBeFlushHorizontally =
221+
(width === '100%' || width === '100vw') &&
222+
(!maxWidth || maxWidth === '100%' || maxWidth === '100vw');
180223

181224
if (shouldBeFlushHorizontally) {
182225
parentStyles.justifyContent = 'flex-start';
183-
} else if (this._justifyContent === 'center') {
184-
parentStyles.justifyContent = 'center';
185-
} else if (this._overlayRef.getConfig().direction === 'rtl') {
186-
// In RTL the browser will invert `flex-start` and `flex-end` automatically, but we
187-
// don't want that because our positioning is explicitly `left` and `right`, hence
188-
// why we do another inversion to ensure that the overlay stays in the same position.
189-
// TODO: reconsider this if we add `start` and `end` methods.
190-
if (this._justifyContent === 'flex-start') {
191-
parentStyles.justifyContent = 'flex-end';
192-
} else if (this._justifyContent === 'flex-end') {
193-
parentStyles.justifyContent = 'flex-start';
194-
}
195-
} else {
196-
parentStyles.justifyContent = this._justifyContent;
226+
styles.marginLeft = styles.marginRight = '0';
227+
return;
197228
}
198229

199-
parentStyles.alignItems = shouldBeFlushVertically ? 'flex-start' : this._alignItems;
230+
switch (this._xPosition) {
231+
// In RTL the browser will invert `flex-start` and `flex-end` automatically, but we don't
232+
// want that if the positioning is explicitly `left` and `right`, hence why we do another
233+
// inversion to ensure that the overlay stays in the same position.
234+
case 'left':
235+
parentStyles.justifyContent = isRtl ? 'flex-end' : 'flex-start';
236+
styles.marginLeft = this._xOffset;
237+
styles.marginRight = '';
238+
break;
239+
240+
case 'right':
241+
parentStyles.justifyContent = isRtl ? 'flex-start' : 'flex-end';
242+
styles.marginRight = this._xOffset;
243+
styles.marginLeft = '';
244+
break;
245+
246+
case 'center':
247+
parentStyles.justifyContent = 'center';
248+
styles.marginLeft = isRtl ? '' : this._xOffset;
249+
styles.marginRight = isRtl ? this._xOffset : '';
250+
break;
251+
252+
case 'start':
253+
parentStyles.justifyContent = 'flex-start';
254+
styles.marginLeft = isRtl ? '' : this._xOffset;
255+
styles.marginRight = isRtl ? this._xOffset : '';
256+
break;
257+
258+
case 'end':
259+
parentStyles.justifyContent = 'flex-end';
260+
styles.marginLeft = isRtl ? this._xOffset : '';
261+
styles.marginRight = isRtl ? '' : this._xOffset;
262+
break;
263+
264+
default:
265+
throw Error(`Unsupported X axis position ${this._xPosition}.`);
266+
}
200267
}
201268

202269
/**

0 commit comments

Comments
 (0)