Skip to content

Commit 4b04504

Browse files
committed
feat(overlay): add support for flexible connected positioning
* Adds the `FlexibleConnectedPositionStrategy` that builds on top of the `ConnectedPositionStrategy` adds the following: * The ability to have overlays with flexible sizing. * Being able to push overlays into the viewport if they don't fit. * Having a margin between the overlay and the viewport edge. * Being able to assign weights to the overlay positions. * Refactors the `ConnectedPositionStrategy` to use the `FlexibleConnectedPositionStrategy` in order to avoid breaking API changes. * Switches all of the components to the new position strategy. * Adds an API to the `OverlayRef` that allows for the consumer to wrap the pane in a div. This is a common requirement between the `GlobalPositionStrategy` and the `FlexibleConnectedPositionStrategy`, and it's easier to keep track of the elements when the attaching and detaching is done in the `OverlayRef`. Fixes #6534. Fixes #2725. Fixes #5267.
1 parent c3d7cd9 commit 4b04504

29 files changed

+3041
-691
lines changed

src/cdk/overlay/_overlay.scss

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,17 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
4444
// A single overlay pane.
4545
.cdk-overlay-pane {
4646
position: absolute;
47+
4748
pointer-events: auto;
4849
box-sizing: border-box;
4950
z-index: $cdk-z-index-overlay;
51+
52+
// For connected-position overlays, we set `display: flex` in order to force `max-width`
53+
// and `max-height` to take effect.
54+
// todo: make sure this doesn't break existing stuff
55+
display: flex;
56+
max-width: 100%;
57+
max-height: 100%;
5058
}
5159

5260
.cdk-overlay-backdrop {
@@ -76,6 +84,20 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
7684
background: none;
7785
}
7886

87+
// Overlay parent element used with the connected position strategy. Used to constrain the
88+
// overlay element's size to fit within the viewport.
89+
.cdk-overlay-connected-pos-bounding-box {
90+
position: absolute;
91+
z-index: $cdk-z-index-overlay;
92+
93+
// We use `display: flex` on this element exclusively for centering connected overlays.
94+
// When *not* centering, a top/left/bottom/right will be set which overrides the normal
95+
// flex layout.
96+
display: flex;
97+
justify-content: center;
98+
align-items: center;
99+
}
100+
79101
// Used when disabling global scrolling.
80102
.cdk-global-scrollblock {
81103
position: fixed;

src/cdk/overlay/overlay-directives.spec.ts

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {dispatchKeyboardEvent} from '@angular/cdk/testing';
66
import {ESCAPE} from '@angular/cdk/keycodes';
77
import {CdkConnectedOverlay, OverlayModule, CdkOverlayOrigin} from './index';
88
import {OverlayContainer} from './overlay-container';
9-
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
9+
import {FlexibleConnectedPositionStrategy} from './position/flexible-connected-position-strategy';
1010
import {ConnectedOverlayPositionChange} from './position/connected-position';
1111

1212

@@ -80,13 +80,11 @@ describe('Overlay directives', () => {
8080
let testComponent: ConnectedOverlayDirectiveTest =
8181
fixture.debugElement.componentInstance;
8282
let overlayDirective = testComponent.connectedOverlayDirective;
83-
8483
let strategy =
85-
<ConnectedPositionStrategy> overlayDirective.overlayRef.getConfig().positionStrategy;
86-
expect(strategy instanceof ConnectedPositionStrategy).toBe(true);
84+
overlayDirective.overlayRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy;
8785

88-
let positions = strategy.positions;
89-
expect(positions.length).toBeGreaterThan(0);
86+
expect(strategy instanceof FlexibleConnectedPositionStrategy).toBe(true);
87+
expect(strategy.positions.length).toBeGreaterThan(0);
9088
});
9189

9290
it('should set and update the `dir` attribute', () => {
@@ -139,7 +137,7 @@ describe('Overlay directives', () => {
139137
fixture.componentInstance.isOpen = true;
140138
fixture.detectChanges();
141139

142-
const pane = overlayContainerElement.children[0] as HTMLElement;
140+
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
143141
expect(pane.style.width).toEqual('250px');
144142
});
145143

@@ -148,7 +146,7 @@ describe('Overlay directives', () => {
148146
fixture.componentInstance.isOpen = true;
149147
fixture.detectChanges();
150148

151-
const pane = overlayContainerElement.children[0] as HTMLElement;
149+
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
152150
expect(pane.style.height).toEqual('100vh');
153151
});
154152

@@ -157,7 +155,7 @@ describe('Overlay directives', () => {
157155
fixture.componentInstance.isOpen = true;
158156
fixture.detectChanges();
159157

160-
const pane = overlayContainerElement.children[0] as HTMLElement;
158+
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
161159
expect(pane.style.minWidth).toEqual('250px');
162160
});
163161

@@ -166,7 +164,7 @@ describe('Overlay directives', () => {
166164
fixture.componentInstance.isOpen = true;
167165
fixture.detectChanges();
168166

169-
const pane = overlayContainerElement.children[0] as HTMLElement;
167+
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
170168
expect(pane.style.minHeight).toEqual('500px');
171169
});
172170

@@ -198,18 +196,13 @@ describe('Overlay directives', () => {
198196
});
199197

200198
it('should set the offsetX', () => {
201-
const trigger = fixture.debugElement.query(By.css('button')).nativeElement;
202-
const startX = trigger.getBoundingClientRect().left;
203-
204199
fixture.componentInstance.offsetX = 5;
205200
fixture.componentInstance.isOpen = true;
206201
fixture.detectChanges();
207202

208-
const pane = overlayContainerElement.children[0] as HTMLElement;
203+
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
209204

210-
expect(pane.style.left)
211-
.toBe(startX + 5 + 'px',
212-
`Expected overlay translateX to equal the original X + the offsetX.`);
205+
expect(pane.style.transform).toContain('translateX(5px)');
213206

214207
fixture.componentInstance.isOpen = false;
215208
fixture.detectChanges();
@@ -218,9 +211,7 @@ describe('Overlay directives', () => {
218211
fixture.componentInstance.isOpen = true;
219212
fixture.detectChanges();
220213

221-
expect(pane.style.left)
222-
.toBe(startX + 15 + 'px',
223-
`Expected overlay directive to reflect new offsetX if it changes.`);
214+
expect(pane.style.transform).toContain('translateX(15px)');
224215
});
225216

226217
it('should set the offsetY', () => {
@@ -233,21 +224,17 @@ describe('Overlay directives', () => {
233224
fixture.componentInstance.isOpen = true;
234225
fixture.detectChanges();
235226

236-
// expected y value is the starting y + trigger height + offset y
237-
// 30 + 20 + 45 = 95px
238-
const pane = overlayContainerElement.children[0] as HTMLElement;
227+
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
239228

240-
expect(pane.style.top)
241-
.toBe('95px', `Expected overlay translateY to equal the start Y + height + offsetY.`);
229+
expect(pane.style.transform).toContain('translateY(45px)');
242230

243231
fixture.componentInstance.isOpen = false;
244232
fixture.detectChanges();
245233

246234
fixture.componentInstance.offsetY = 55;
247235
fixture.componentInstance.isOpen = true;
248236
fixture.detectChanges();
249-
expect(pane.style.top)
250-
.toBe('105px', `Expected overlay directive to reflect new offsetY if it changes.`);
237+
expect(pane.style.transform).toContain('translateY(55px)');
251238
});
252239

253240
});

src/cdk/overlay/overlay-directives.ts

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,40 @@ import {Overlay} from './overlay';
3030
import {OverlayConfig} from './overlay-config';
3131
import {OverlayRef} from './overlay-ref';
3232
import {
33-
ConnectedOverlayPositionChange,
34-
ConnectionPositionPair,
35-
} from './position/connected-position';
36-
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
33+
FlexibleConnectedPositionStrategy,
34+
ConnectedPosition,
35+
} from './position/flexible-connected-position-strategy';
36+
import {ConnectedOverlayPositionChange} from './position/connected-position';
3737
import {RepositionScrollStrategy, ScrollStrategy} from './scroll/index';
3838
import {DOCUMENT} from '@angular/common';
3939

4040

4141
/** Default set of positions for the overlay. Follows the behavior of a dropdown. */
42-
const defaultPositionList = [
43-
new ConnectionPositionPair(
44-
{originX: 'start', originY: 'bottom'},
45-
{overlayX: 'start', overlayY: 'top'}),
46-
new ConnectionPositionPair(
47-
{originX: 'start', originY: 'top'},
48-
{overlayX: 'start', overlayY: 'bottom'}),
49-
new ConnectionPositionPair(
50-
{originX: 'end', originY: 'top'},
51-
{overlayX: 'end', overlayY: 'bottom'}),
52-
new ConnectionPositionPair(
53-
{originX: 'end', originY: 'bottom'},
54-
{overlayX: 'end', overlayY: 'top'}),
42+
const defaultPositionList: ConnectedPosition[] = [
43+
{
44+
originX: 'start',
45+
originY: 'bottom',
46+
overlayX: 'start',
47+
overlayY: 'top'
48+
},
49+
{
50+
originX: 'start',
51+
originY: 'top',
52+
overlayX: 'start',
53+
overlayY: 'bottom'
54+
},
55+
{
56+
originX: 'end',
57+
originY: 'top',
58+
overlayX: 'end',
59+
overlayY: 'bottom'
60+
},
61+
{
62+
originX: 'end',
63+
originY: 'bottom',
64+
overlayX: 'end',
65+
overlayY: 'top'
66+
}
5567
];
5668

5769
/** Injection token that determines the scroll handling while the connected overlay is open. */
@@ -102,21 +114,22 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
102114
private _positionSubscription = Subscription.EMPTY;
103115
private _offsetX: number = 0;
104116
private _offsetY: number = 0;
105-
private _position: ConnectedPositionStrategy;
117+
private _position: FlexibleConnectedPositionStrategy;
106118

107119
/** Origin for the connected overlay. */
108120
@Input('cdkConnectedOverlayOrigin') origin: CdkOverlayOrigin;
109121

110122
/** Registered connected position pairs. */
111-
@Input('cdkConnectedOverlayPositions') positions: ConnectionPositionPair[];
123+
@Input('cdkConnectedOverlayPositions') positions: ConnectedPosition[];
112124

113125
/** The offset in pixels for the overlay connection point on the x-axis */
114126
@Input('cdkConnectedOverlayOffsetX')
115127
get offsetX(): number { return this._offsetX; }
116128
set offsetX(offsetX: number) {
117129
this._offsetX = offsetX;
130+
118131
if (this._position) {
119-
this._position.withOffsetX(offsetX);
132+
this._setPositions(this._position);
120133
}
121134
}
122135

@@ -125,8 +138,9 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
125138
get offsetY() { return this._offsetY; }
126139
set offsetY(offsetY: number) {
127140
this._offsetY = offsetY;
141+
128142
if (this._position) {
129-
this._position.withOffsetY(offsetY);
143+
this._setPositions(this._position);
130144
}
131145
}
132146

@@ -164,8 +178,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
164178

165179
/** @deprecated */
166180
@Input('positions')
167-
get _deprecatedPositions(): ConnectionPositionPair[] { return this.positions; }
168-
set _deprecatedPositions(_positions: ConnectionPositionPair[]) { this.positions = _positions; }
181+
get _deprecatedPositions(): ConnectedPosition[] { return this.positions; }
182+
set _deprecatedPositions(_positions: ConnectedPosition[]) { this.positions = _positions; }
169183

170184
/** @deprecated */
171185
@Input('offsetX')
@@ -305,31 +319,36 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
305319
}
306320

307321
/** Returns the position strategy of the overlay to be set on the overlay config */
308-
private _createPositionStrategy(): ConnectedPositionStrategy {
309-
const pos = this.positions[0];
310-
const originPoint = {originX: pos.originX, originY: pos.originY};
311-
const overlayPoint = {overlayX: pos.overlayX, overlayY: pos.overlayY};
312-
322+
private _createPositionStrategy(): FlexibleConnectedPositionStrategy {
313323
const strategy = this._overlay.position()
314-
.connectedTo(this.origin.elementRef, originPoint, overlayPoint)
315-
.withOffsetX(this.offsetX)
316-
.withOffsetY(this.offsetY);
324+
.flexibleConnectedTo(this.origin.elementRef)
325+
.withFlexibleHeight(false)
326+
.withFlexibleWidth(false)
327+
.withPush(false)
328+
.withGrowAfterOpen(false);
317329

318-
this._handlePositionChanges(strategy);
330+
this._setPositions(strategy);
331+
this._positionSubscription =
332+
strategy.positionChange.subscribe(p => this.positionChange.emit(p));
319333

320334
return strategy;
321335
}
322336

323-
private _handlePositionChanges(strategy: ConnectedPositionStrategy): void {
324-
for (let i = 1; i < this.positions.length; i++) {
325-
strategy.withFallbackPosition(
326-
{originX: this.positions[i].originX, originY: this.positions[i].originY},
327-
{overlayX: this.positions[i].overlayX, overlayY: this.positions[i].overlayY}
328-
);
329-
}
330-
331-
this._positionSubscription =
332-
strategy.onPositionChange.subscribe(pos => this.positionChange.emit(pos));
337+
/**
338+
* Sets the primary and fallback positions of a positions strategy,
339+
* based on the current directive inputs.
340+
*/
341+
private _setPositions(positionStrategy: FlexibleConnectedPositionStrategy) {
342+
const positions: ConnectedPosition[] = this.positions.map(pos => ({
343+
originX: pos.originX,
344+
originY: pos.originY,
345+
overlayX: pos.overlayX,
346+
overlayY: pos.overlayY,
347+
offsetX: this.offsetX,
348+
offsetY: this.offsetY
349+
}));
350+
351+
positionStrategy.withPositions(positions);
333352
}
334353

335354
/** Attaches the overlay and subscribes to backdrop clicks if backdrop exists */
@@ -338,7 +357,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
338357
this._createOverlay();
339358
}
340359

341-
this._position.withDirection(this.dir);
342360
this._overlayRef.setDirection(this.dir);
343361
this._document.addEventListener('keydown', this._escapeListener);
344362

0 commit comments

Comments
 (0)