Skip to content

Commit c296084

Browse files
authored
feat(cdk/overlay): add start and end positions to GlobalPositionStrategy (#12007)
* 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 c296084

File tree

3 files changed

+162
-37
lines changed

3 files changed

+162
-37
lines changed

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

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,17 @@ describe('GlobalPositonStrategy', () => {
9898
expect(parentStyle.alignItems).toBe('flex-end');
9999
});
100100

101+
it('should not set any alignment 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('');
109+
expect(parentStyle.alignItems).toBe('');
110+
});
111+
101112
it('should center the element', () => {
102113
attachOverlay({
103114
positionStrategy: overlay.position().global().centerHorizontally().centerVertically(),
@@ -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: 70 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,15 @@ 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 = '';
31-
private _width: string = '';
32-
private _height: string = '';
33-
private _isDisposed: boolean;
24+
private _cssPosition = 'static';
25+
private _topOffset = '';
26+
private _bottomOffset = '';
27+
private _alignItems = '';
28+
private _xPosition = '';
29+
private _xOffset = '';
30+
private _width = '';
31+
private _height = '';
32+
private _isDisposed = false;
3433

3534
attach(overlayRef: OverlayReference): void {
3635
const config = overlayRef.getConfig();
@@ -65,9 +64,8 @@ export class GlobalPositionStrategy implements PositionStrategy {
6564
* @param value New left offset.
6665
*/
6766
left(value: string = ''): this {
68-
this._rightOffset = '';
69-
this._leftOffset = value;
70-
this._justifyContent = 'flex-start';
67+
this._xOffset = value;
68+
this._xPosition = 'left';
7169
return this;
7270
}
7371

@@ -87,9 +85,30 @@ export class GlobalPositionStrategy implements PositionStrategy {
8785
* @param value New right offset.
8886
*/
8987
right(value: string = ''): this {
90-
this._leftOffset = '';
91-
this._rightOffset = value;
92-
this._justifyContent = 'flex-end';
88+
this._xOffset = value;
89+
this._xPosition = 'right';
90+
return this;
91+
}
92+
93+
/**
94+
* Sets the overlay to the start of the viewport, depending on the overlay direction.
95+
* This will be to the left in LTR layouts and to the right in RTL.
96+
* @param offset Offset from the edge of the screen.
97+
*/
98+
start(value: string = ''): this {
99+
this._xOffset = value;
100+
this._xPosition = 'start';
101+
return this;
102+
}
103+
104+
/**
105+
* Sets the overlay to the end of the viewport, depending on the overlay direction.
106+
* This will be to the right in LTR layouts and to the left in RTL.
107+
* @param offset Offset from the edge of the screen.
108+
*/
109+
end(value: string = ''): this {
110+
this._xOffset = value;
111+
this._xPosition = 'end';
93112
return this;
94113
}
95114

@@ -133,7 +152,7 @@ export class GlobalPositionStrategy implements PositionStrategy {
133152
*/
134153
centerHorizontally(offset: string = ''): this {
135154
this.left(offset);
136-
this._justifyContent = 'center';
155+
this._xPosition = 'center';
137156
return this;
138157
}
139158

@@ -171,31 +190,45 @@ export class GlobalPositionStrategy implements PositionStrategy {
171190
const shouldBeFlushVertically =
172191
(height === '100%' || height === '100vh') &&
173192
(!maxHeight || maxHeight === '100%' || maxHeight === '100vh');
174-
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;
193+
const xPosition = this._xPosition;
194+
const xOffset = this._xOffset;
195+
const isRtl = this._overlayRef.getConfig().direction === 'rtl';
196+
let marginLeft = '';
197+
let marginRight = '';
198+
let justifyContent = '';
180199

181200
if (shouldBeFlushHorizontally) {
182-
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';
201+
justifyContent = 'flex-start';
202+
} else if (xPosition === 'center') {
203+
justifyContent = 'center';
204+
205+
if (isRtl) {
206+
marginRight = xOffset;
207+
} else {
208+
marginLeft = xOffset;
194209
}
195-
} else {
196-
parentStyles.justifyContent = this._justifyContent;
210+
} else if (isRtl) {
211+
if (xPosition === 'left' || xPosition === 'end') {
212+
justifyContent = 'flex-end';
213+
marginLeft = xOffset;
214+
} else if (xPosition === 'right' || xPosition === 'start') {
215+
justifyContent = 'flex-start';
216+
marginRight = xOffset;
217+
}
218+
} else if (xPosition === 'left' || xPosition === 'start') {
219+
justifyContent = 'flex-start';
220+
marginLeft = xOffset;
221+
} else if (xPosition === 'right' || xPosition === 'end') {
222+
justifyContent = 'flex-end';
223+
marginRight = xOffset;
197224
}
198225

226+
styles.position = this._cssPosition;
227+
styles.marginLeft = shouldBeFlushHorizontally ? '0' : marginLeft;
228+
styles.marginTop = shouldBeFlushVertically ? '0' : this._topOffset;
229+
styles.marginBottom = this._bottomOffset;
230+
styles.marginRight = shouldBeFlushHorizontally ? '0' : marginRight;
231+
parentStyles.justifyContent = justifyContent;
199232
parentStyles.alignItems = shouldBeFlushVertically ? 'flex-start' : this._alignItems;
200233
}
201234

tools/public_api_guard/cdk/overlay.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,12 @@ export class GlobalPositionStrategy implements PositionStrategy {
230230
centerHorizontally(offset?: string): this;
231231
centerVertically(offset?: string): this;
232232
dispose(): void;
233+
end(value?: string): this;
233234
// @deprecated
234235
height(value?: string): this;
235236
left(value?: string): this;
236237
right(value?: string): this;
238+
start(value?: string): this;
237239
top(value?: string): this;
238240
// @deprecated
239241
width(value?: string): this;

0 commit comments

Comments
 (0)