Skip to content

Commit e5f1fdc

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 5bac828 commit e5f1fdc

File tree

4 files changed

+236
-88
lines changed

4 files changed

+236
-88
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()
@@ -392,6 +425,62 @@ describe('GlobalPositonStrategy', () => {
392425
expect(parentStyle.alignItems).toBeFalsy();
393426
});
394427

428+
it('should position the overlay to the start in ltr', () => {
429+
attachOverlay({
430+
direction: 'ltr',
431+
positionStrategy: overlay.position().global().start('40px')
432+
});
433+
434+
const elementStyle = overlayRef.overlayElement.style;
435+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
436+
437+
expect(elementStyle.marginLeft).toBe('40px');
438+
expect(elementStyle.marginRight).toBe('');
439+
expect(parentStyle.justifyContent).toBe('flex-start');
440+
});
441+
442+
it('should position the overlay to the start in rtl', () => {
443+
attachOverlay({
444+
direction: 'rtl',
445+
positionStrategy: overlay.position().global().start('50px')
446+
});
447+
448+
const elementStyle = overlayRef.overlayElement.style;
449+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
450+
451+
expect(elementStyle.marginLeft).toBe('');
452+
expect(elementStyle.marginRight).toBe('50px');
453+
expect(parentStyle.justifyContent).toBe('flex-start');
454+
});
455+
456+
it('should position the overlay to the end in ltr', () => {
457+
attachOverlay({
458+
direction: 'ltr',
459+
positionStrategy: overlay.position().global().end('60px')
460+
});
461+
462+
const elementStyle = overlayRef.overlayElement.style;
463+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
464+
465+
expect(elementStyle.marginRight).toBe('60px');
466+
expect(elementStyle.marginLeft).toBe('');
467+
expect(parentStyle.justifyContent).toBe('flex-end');
468+
});
469+
470+
it('should position the overlay to the end in rtl', () => {
471+
attachOverlay({
472+
direction: 'rtl',
473+
positionStrategy: overlay.position().global().end('70px')
474+
});
475+
476+
const elementStyle = overlayRef.overlayElement.style;
477+
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
478+
479+
expect(elementStyle.marginLeft).toBe('70px');
480+
expect(elementStyle.marginRight).toBe('');
481+
expect(parentStyle.justifyContent).toBe('flex-end');
482+
});
483+
395484
});
396485

397486

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

Lines changed: 133 additions & 66 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,40 +172,96 @@ 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 = (width === '100%' || width === '100vw') &&
169-
(!maxWidth || maxWidth === '100%' || maxWidth === '100vw');
184+
const {height, maxHeight} = config;
170185
const shouldBeFlushVertically = (height === '100%' || height === '100vh') &&
171186
(!maxHeight || maxHeight === '100%' || maxHeight === '100vh');
172187

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

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

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

200267
/**

0 commit comments

Comments
 (0)