Skip to content

Commit 27b8f4e

Browse files
crisbetojosephperrott
authored andcommitted
feat(overlay): add support for flexible connected positioning (angular#9153)
* 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 angular#6534. Fixes angular#2725. Fixes angular#5267.
1 parent aa9c7eb commit 27b8f4e

31 files changed

+3224
-778
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
/src/demo-app/card/** @jelbourn
101101
/src/demo-app/checkbox/** @tinayuangao @devversion
102102
/src/demo-app/chips/** @tinayuangao
103+
/src/demo-app/connected-overlay/** @jelbourn @crisbeto
103104
/src/demo-app/dataset/** @andrewseguin
104105
/src/demo-app/datepicker/** @mmalerba
105106
/src/demo-app/demo-app/** @jelbourn

src/cdk/overlay/_overlay.scss

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,16 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
5252
// A single overlay pane.
5353
.cdk-overlay-pane {
5454
position: absolute;
55+
5556
pointer-events: auto;
5657
box-sizing: border-box;
5758
z-index: $cdk-z-index-overlay;
59+
60+
// For connected-position overlays, we set `display: flex` in
61+
// order to force `max-width` and `max-height` to take effect.
62+
display: flex;
63+
max-width: 100%;
64+
max-height: 100%;
5865
}
5966

6067
.cdk-overlay-backdrop {
@@ -96,6 +103,24 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
96103
}
97104
}
98105

106+
// Overlay parent element used with the connected position strategy. Used to constrain the
107+
// overlay element's size to fit within the viewport.
108+
.cdk-overlay-connected-position-bounding-box {
109+
position: absolute;
110+
z-index: $cdk-z-index-overlay;
111+
112+
// We use `display: flex` on this element exclusively for centering connected overlays.
113+
// When *not* centering, a top/left/bottom/right will be set which overrides the normal
114+
// flex layout.
115+
display: flex;
116+
justify-content: center;
117+
align-items: center;
118+
119+
// Add some dimensions so the element has an `innerText` which some people depend on in tests.
120+
min-width: 1px;
121+
min-height: 1px;
122+
}
123+
99124
// Used when disabling global scrolling.
100125
.cdk-global-scrollblock {
101126
position: fixed;

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

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ 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';
109
import {
1110
ConnectedOverlayPositionChange,
1211
ConnectionPositionPair,
1312
} from './position/connected-position';
13+
import {FlexibleConnectedPositionStrategy} from './position/flexible-connected-position-strategy';
1414

1515

1616
describe('Overlay directives', () => {
@@ -79,13 +79,11 @@ describe('Overlay directives', () => {
7979
let testComponent: ConnectedOverlayDirectiveTest =
8080
fixture.debugElement.componentInstance;
8181
let overlayDirective = testComponent.connectedOverlayDirective;
82-
8382
let strategy =
84-
<ConnectedPositionStrategy> overlayDirective.overlayRef.getConfig().positionStrategy;
85-
expect(strategy instanceof ConnectedPositionStrategy).toBe(true);
83+
overlayDirective.overlayRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy;
8684

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

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

141-
const pane = overlayContainerElement.children[0] as HTMLElement;
139+
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
142140
expect(pane.style.width).toEqual('250px');
143141

144142
fixture.componentInstance.isOpen = false;
@@ -156,7 +154,7 @@ describe('Overlay directives', () => {
156154
fixture.componentInstance.isOpen = true;
157155
fixture.detectChanges();
158156

159-
const pane = overlayContainerElement.children[0] as HTMLElement;
157+
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
160158
expect(pane.style.height).toEqual('100vh');
161159

162160
fixture.componentInstance.isOpen = false;
@@ -174,7 +172,7 @@ describe('Overlay directives', () => {
174172
fixture.componentInstance.isOpen = true;
175173
fixture.detectChanges();
176174

177-
const pane = overlayContainerElement.children[0] as HTMLElement;
175+
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
178176
expect(pane.style.minWidth).toEqual('250px');
179177

180178
fixture.componentInstance.isOpen = false;
@@ -192,7 +190,7 @@ describe('Overlay directives', () => {
192190
fixture.componentInstance.isOpen = true;
193191
fixture.detectChanges();
194192

195-
const pane = overlayContainerElement.children[0] as HTMLElement;
193+
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
196194
expect(pane.style.minHeight).toEqual('500px');
197195

198196
fixture.componentInstance.isOpen = false;
@@ -233,18 +231,13 @@ describe('Overlay directives', () => {
233231
});
234232

235233
it('should set the offsetX', () => {
236-
const trigger = fixture.debugElement.query(By.css('button')).nativeElement;
237-
const startX = trigger.getBoundingClientRect().left;
238-
239234
fixture.componentInstance.offsetX = 5;
240235
fixture.componentInstance.isOpen = true;
241236
fixture.detectChanges();
242237

243-
const pane = overlayContainerElement.children[0] as HTMLElement;
238+
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
244239

245-
expect(pane.style.left)
246-
.toBe(startX + 5 + 'px',
247-
`Expected overlay translateX to equal the original X + the offsetX.`);
240+
expect(pane.style.transform).toContain('translateX(5px)');
248241

249242
fixture.componentInstance.isOpen = false;
250243
fixture.detectChanges();
@@ -253,9 +246,7 @@ describe('Overlay directives', () => {
253246
fixture.componentInstance.isOpen = true;
254247
fixture.detectChanges();
255248

256-
expect(pane.style.left)
257-
.toBe(startX + 15 + 'px',
258-
`Expected overlay directive to reflect new offsetX if it changes.`);
249+
expect(pane.style.transform).toContain('translateX(15px)');
259250
});
260251

261252
it('should set the offsetY', () => {
@@ -268,21 +259,17 @@ describe('Overlay directives', () => {
268259
fixture.componentInstance.isOpen = true;
269260
fixture.detectChanges();
270261

271-
// expected y value is the starting y + trigger height + offset y
272-
// 30 + 20 + 45 = 95px
273-
const pane = overlayContainerElement.children[0] as HTMLElement;
262+
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
274263

275-
expect(pane.style.top)
276-
.toBe('95px', `Expected overlay translateY to equal the start Y + height + offsetY.`);
264+
expect(pane.style.transform).toContain('translateY(45px)');
277265

278266
fixture.componentInstance.isOpen = false;
279267
fixture.detectChanges();
280268

281269
fixture.componentInstance.offsetY = 55;
282270
fixture.componentInstance.isOpen = true;
283271
fixture.detectChanges();
284-
expect(pane.style.top)
285-
.toBe('105px', `Expected overlay directive to reflect new offsetY if it changes.`);
272+
expect(pane.style.transform).toContain('translateY(55px)');
286273
});
287274

288275
it('should be able to update the origin after init', () => {

src/cdk/overlay/overlay-directives.ts

Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,39 @@ 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

3939

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

5668
/** Injection token that determines the scroll handling while the connected overlay is open. */
@@ -101,21 +113,22 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
101113
private _backdropSubscription = Subscription.EMPTY;
102114
private _offsetX: number = 0;
103115
private _offsetY: number = 0;
104-
private _position: ConnectedPositionStrategy;
116+
private _position: FlexibleConnectedPositionStrategy;
105117

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

109121
/** Registered connected position pairs. */
110-
@Input('cdkConnectedOverlayPositions') positions: ConnectionPositionPair[];
122+
@Input('cdkConnectedOverlayPositions') positions: ConnectedPosition[];
111123

112124
/** The offset in pixels for the overlay connection point on the x-axis */
113125
@Input('cdkConnectedOverlayOffsetX')
114126
get offsetX(): number { return this._offsetX; }
115127
set offsetX(offsetX: number) {
116128
this._offsetX = offsetX;
129+
117130
if (this._position) {
118-
this._position.withOffsetX(offsetX);
131+
this._setPositions(this._position);
119132
}
120133
}
121134

@@ -124,8 +137,9 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
124137
get offsetY() { return this._offsetY; }
125138
set offsetY(offsetY: number) {
126139
this._offsetY = offsetY;
140+
127141
if (this._position) {
128-
this._position.withOffsetY(offsetY);
142+
this._setPositions(this._position);
129143
}
130144
}
131145

@@ -264,28 +278,42 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
264278
}
265279

266280
/** Returns the position strategy of the overlay to be set on the overlay config */
267-
private _createPositionStrategy(): ConnectedPositionStrategy {
268-
const primaryPosition = this.positions[0];
269-
const originPoint = {originX: primaryPosition.originX, originY: primaryPosition.originY};
270-
const overlayPoint = {overlayX: primaryPosition.overlayX, overlayY: primaryPosition.overlayY};
281+
private _createPositionStrategy(): FlexibleConnectedPositionStrategy {
271282
const strategy = this._overlay.position()
272-
.connectedTo(this.origin.elementRef, originPoint, overlayPoint)
273-
.withOffsetX(this.offsetX)
274-
.withOffsetY(this.offsetY)
283+
.flexibleConnectedTo(this.origin.elementRef)
284+
// Turn off all of the flexible positioning features for now to have it behave
285+
// the same way as the old ConnectedPositionStrategy and to avoid breaking changes.
286+
// TODO(crisbeto): make these on by default and add inputs for them
287+
// next time we do breaking changes.
288+
.withFlexibleHeight(false)
289+
.withFlexibleWidth(false)
290+
.withPush(false)
291+
.withGrowAfterOpen(false)
275292
.withLockedPosition(this.lockPosition);
276293

277-
for (let i = 1; i < this.positions.length; i++) {
278-
strategy.withFallbackPosition(
279-
{originX: this.positions[i].originX, originY: this.positions[i].originY},
280-
{overlayX: this.positions[i].overlayX, overlayY: this.positions[i].overlayY}
281-
);
282-
}
283-
284-
strategy.onPositionChange.subscribe(pos => this.positionChange.emit(pos));
294+
this._setPositions(strategy);
295+
strategy.positionChanges.subscribe(p => this.positionChange.emit(p));
285296

286297
return strategy;
287298
}
288299

300+
/**
301+
* Sets the primary and fallback positions of a positions strategy,
302+
* based on the current directive inputs.
303+
*/
304+
private _setPositions(positionStrategy: FlexibleConnectedPositionStrategy) {
305+
const positions: ConnectedPosition[] = this.positions.map(pos => ({
306+
originX: pos.originX,
307+
originY: pos.originY,
308+
overlayX: pos.overlayX,
309+
overlayY: pos.overlayY,
310+
offsetX: this.offsetX,
311+
offsetY: this.offsetY
312+
}));
313+
314+
positionStrategy.withPositions(positions);
315+
}
316+
289317
/** Attaches the overlay and subscribes to backdrop clicks if backdrop exists */
290318
private _attachOverlay() {
291319
if (!this._overlayRef) {
@@ -306,7 +334,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
306334
});
307335
}
308336

309-
this._position.withDirection(this.dir);
310337
this._overlayRef.setDirection(this.dir);
311338

312339
if (!this._overlayRef.hasAttached()) {

0 commit comments

Comments
 (0)