Skip to content

Commit a332013

Browse files
committed
refactor(cdk/overlay): handle popovers outside position strategy
The code for inserting the popovers ended up being identical between position strategies so these changes switch to handling it at the overlay level instead. (cherry picked from commit 59812df)
1 parent b9dcae5 commit a332013

File tree

8 files changed

+73
-158
lines changed

8 files changed

+73
-158
lines changed

goldens/cdk/overlay/index.api.md

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ export class BlockScrollStrategy implements ScrollStrategy {
4040
// @public
4141
export class CdkConnectedOverlay implements OnDestroy, OnChanges {
4242
constructor(...args: unknown[]);
43-
asPopover: boolean;
4443
readonly attach: EventEmitter<void>;
4544
attachOverlay(): void;
4645
backdropClass: string | string[];
@@ -59,8 +58,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
5958
minHeight: number | string;
6059
minWidth: number | string;
6160
// (undocumented)
62-
static ngAcceptInputType_asPopover: unknown;
63-
// (undocumented)
6461
static ngAcceptInputType_disposeOnNavigation: unknown;
6562
// (undocumented)
6663
static ngAcceptInputType_flexibleDimensions: unknown;
@@ -73,6 +70,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
7370
// (undocumented)
7471
static ngAcceptInputType_push: unknown;
7572
// (undocumented)
73+
static ngAcceptInputType_usePopover: unknown;
74+
// (undocumented)
7675
ngOnChanges(changes: SimpleChanges): void;
7776
// (undocumented)
7877
ngOnDestroy(): void;
@@ -92,10 +91,11 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
9291
push: boolean;
9392
scrollStrategy: ScrollStrategy;
9493
transformOriginSelector: string;
94+
usePopover: boolean;
9595
viewportMargin: ViewportMargin;
9696
width: number | string;
9797
// (undocumented)
98-
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkConnectedOverlay, "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", ["cdkConnectedOverlay"], { "origin": { "alias": "cdkConnectedOverlayOrigin"; "required": false; }; "positions": { "alias": "cdkConnectedOverlayPositions"; "required": false; }; "positionStrategy": { "alias": "cdkConnectedOverlayPositionStrategy"; "required": false; }; "offsetX": { "alias": "cdkConnectedOverlayOffsetX"; "required": false; }; "offsetY": { "alias": "cdkConnectedOverlayOffsetY"; "required": false; }; "width": { "alias": "cdkConnectedOverlayWidth"; "required": false; }; "height": { "alias": "cdkConnectedOverlayHeight"; "required": false; }; "minWidth": { "alias": "cdkConnectedOverlayMinWidth"; "required": false; }; "minHeight": { "alias": "cdkConnectedOverlayMinHeight"; "required": false; }; "backdropClass": { "alias": "cdkConnectedOverlayBackdropClass"; "required": false; }; "panelClass": { "alias": "cdkConnectedOverlayPanelClass"; "required": false; }; "viewportMargin": { "alias": "cdkConnectedOverlayViewportMargin"; "required": false; }; "scrollStrategy": { "alias": "cdkConnectedOverlayScrollStrategy"; "required": false; }; "open": { "alias": "cdkConnectedOverlayOpen"; "required": false; }; "disableClose": { "alias": "cdkConnectedOverlayDisableClose"; "required": false; }; "transformOriginSelector": { "alias": "cdkConnectedOverlayTransformOriginOn"; "required": false; }; "hasBackdrop": { "alias": "cdkConnectedOverlayHasBackdrop"; "required": false; }; "lockPosition": { "alias": "cdkConnectedOverlayLockPosition"; "required": false; }; "flexibleDimensions": { "alias": "cdkConnectedOverlayFlexibleDimensions"; "required": false; }; "growAfterOpen": { "alias": "cdkConnectedOverlayGrowAfterOpen"; "required": false; }; "push": { "alias": "cdkConnectedOverlayPush"; "required": false; }; "disposeOnNavigation": { "alias": "cdkConnectedOverlayDisposeOnNavigation"; "required": false; }; "asPopover": { "alias": "cdkConnectedOverlayAsPopover"; "required": false; }; }, { "backdropClick": "backdropClick"; "positionChange": "positionChange"; "attach": "attach"; "detach": "detach"; "overlayKeydown": "overlayKeydown"; "overlayOutsideClick": "overlayOutsideClick"; }, never, never, true, never>;
98+
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkConnectedOverlay, "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", ["cdkConnectedOverlay"], { "origin": { "alias": "cdkConnectedOverlayOrigin"; "required": false; }; "positions": { "alias": "cdkConnectedOverlayPositions"; "required": false; }; "positionStrategy": { "alias": "cdkConnectedOverlayPositionStrategy"; "required": false; }; "offsetX": { "alias": "cdkConnectedOverlayOffsetX"; "required": false; }; "offsetY": { "alias": "cdkConnectedOverlayOffsetY"; "required": false; }; "width": { "alias": "cdkConnectedOverlayWidth"; "required": false; }; "height": { "alias": "cdkConnectedOverlayHeight"; "required": false; }; "minWidth": { "alias": "cdkConnectedOverlayMinWidth"; "required": false; }; "minHeight": { "alias": "cdkConnectedOverlayMinHeight"; "required": false; }; "backdropClass": { "alias": "cdkConnectedOverlayBackdropClass"; "required": false; }; "panelClass": { "alias": "cdkConnectedOverlayPanelClass"; "required": false; }; "viewportMargin": { "alias": "cdkConnectedOverlayViewportMargin"; "required": false; }; "scrollStrategy": { "alias": "cdkConnectedOverlayScrollStrategy"; "required": false; }; "open": { "alias": "cdkConnectedOverlayOpen"; "required": false; }; "disableClose": { "alias": "cdkConnectedOverlayDisableClose"; "required": false; }; "transformOriginSelector": { "alias": "cdkConnectedOverlayTransformOriginOn"; "required": false; }; "hasBackdrop": { "alias": "cdkConnectedOverlayHasBackdrop"; "required": false; }; "lockPosition": { "alias": "cdkConnectedOverlayLockPosition"; "required": false; }; "flexibleDimensions": { "alias": "cdkConnectedOverlayFlexibleDimensions"; "required": false; }; "growAfterOpen": { "alias": "cdkConnectedOverlayGrowAfterOpen"; "required": false; }; "push": { "alias": "cdkConnectedOverlayPush"; "required": false; }; "disposeOnNavigation": { "alias": "cdkConnectedOverlayDisposeOnNavigation"; "required": false; }; "usePopover": { "alias": "cdkConnectedOverlayUsePopover"; "required": false; }; }, { "backdropClick": "backdropClick"; "positionChange": "positionChange"; "attach": "attach"; "detach": "detach"; "overlayKeydown": "overlayKeydown"; "overlayOutsideClick": "overlayOutsideClick"; }, never, never, true, never>;
9999
// (undocumented)
100100
static ɵfac: i0.ɵɵFactoryDeclaration<CdkConnectedOverlay, never>;
101101
}
@@ -210,7 +210,7 @@ export function createCloseScrollStrategy(injector: Injector, config?: CloseScro
210210
export function createFlexibleConnectedPositionStrategy(injector: Injector, origin: FlexibleConnectedPositionStrategyOrigin): FlexibleConnectedPositionStrategy;
211211

212212
// @public
213-
export function createGlobalPositionStrategy(_injector: Injector): GlobalPositionStrategy;
213+
export function createGlobalPositionStrategy(injector: Injector): GlobalPositionStrategy;
214214

215215
// @public
216216
export function createNoopScrollStrategy(): NoopScrollStrategy;
@@ -225,24 +225,17 @@ export function createRepositionScrollStrategy(injector: Injector, config?: Repo
225225
export class FlexibleConnectedPositionStrategy implements PositionStrategy {
226226
constructor(connectedTo: FlexibleConnectedPositionStrategyOrigin, _viewportRuler: ViewportRuler, _document: Document, _platform: Platform, _overlayContainer: OverlayContainer);
227227
apply(): void;
228-
asPopover(isPopover: boolean): this;
229228
attach(overlayRef: OverlayRef): void;
230-
attachBackdrop(backdrop: HTMLElement, host: HTMLElement): boolean;
231-
attachHost(host: HTMLElement): boolean;
232-
createStructure(): {
233-
pane: HTMLDivElement;
234-
host: HTMLDivElement;
235-
} | null;
236229
// (undocumented)
237230
detach(): void;
238231
dispose(): void;
232+
getPopoverInsertionPoint(): Element;
239233
_origin: FlexibleConnectedPositionStrategyOrigin;
240234
positionChanges: Observable<ConnectedOverlayPositionChange>;
241235
get positions(): ConnectionPositionPair[];
242236
_preferredPositions: ConnectionPositionPair[];
243237
reapplyLastPosition(): void;
244238
setOrigin(origin: FlexibleConnectedPositionStrategyOrigin): this;
245-
updateStackingOrder(): boolean;
246239
withDefaultOffsetX(offset: number): this;
247240
withDefaultOffsetY(offset: number): this;
248241
withFlexibleDimensions(flexibleDimensions?: boolean): this;
@@ -277,6 +270,7 @@ export class FullscreenOverlayContainer extends OverlayContainer implements OnDe
277270

278271
// @public
279272
export class GlobalPositionStrategy implements PositionStrategy {
273+
constructor(injector: Injector);
280274
apply(): void;
281275
// (undocumented)
282276
attach(overlayRef: OverlayRef): void;
@@ -285,6 +279,7 @@ export class GlobalPositionStrategy implements PositionStrategy {
285279
centerVertically(offset?: string): this;
286280
dispose(): void;
287281
end(value?: string): this;
282+
getPopoverInsertionPoint(): Element;
288283
// @deprecated
289284
height(value?: string): this;
290285
left(value?: string): this;
@@ -342,6 +337,7 @@ export class OverlayConfig {
342337
panelClass?: string | string[];
343338
positionStrategy?: PositionStrategy;
344339
scrollStrategy?: ScrollStrategy;
340+
usePopover?: boolean;
345341
width?: number | string;
346342
}
347343

@@ -470,15 +466,9 @@ export interface OverlaySizeConfig {
470466
export interface PositionStrategy {
471467
apply(): void;
472468
attach(overlayRef: OverlayRef): void;
473-
attachBackdrop?(backdrop: HTMLElement, host: HTMLElement): boolean;
474-
attachHost?(host: HTMLElement): boolean;
475-
createStructure?(): {
476-
pane: HTMLElement;
477-
host: HTMLElement;
478-
} | null;
479469
detach?(): void;
480470
dispose(): void;
481-
updateStackingOrder?(host: HTMLElement): boolean;
471+
getPopoverInsertionPoint?(): Element;
482472
}
483473

484474
// @public

src/cdk/overlay/overlay-config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ export class OverlayConfig {
6161
*/
6262
disposeOnNavigation?: boolean = false;
6363

64+
/**
65+
* Whether the overlay should be rendered as a native popover element,
66+
* rather than placing it inside of the overlay container.
67+
*/
68+
usePopover?: boolean = false;
69+
6470
constructor(config?: OverlayConfig) {
6571
if (config) {
6672
// Use `Iterable` instead of `Array` because TypeScript, as of 3.6.3,

src/cdk/overlay/overlay-directives.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
222222
}
223223

224224
/** Whether the connected overlay should be rendered inside a popover element or the overlay container. */
225-
@Input({alias: 'cdkConnectedOverlayAsPopover', transform: booleanAttribute})
226-
asPopover: boolean = false;
225+
@Input({alias: 'cdkConnectedOverlayUsePopover', transform: booleanAttribute})
226+
usePopover: boolean = false;
227227

228228
/** Event emitted when the backdrop is clicked. */
229229
@Output() readonly backdropClick = new EventEmitter<MouseEvent>();
@@ -331,6 +331,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
331331
scrollStrategy: this.scrollStrategy,
332332
hasBackdrop: this.hasBackdrop,
333333
disposeOnNavigation: this.disposeOnNavigation,
334+
usePopover: this.usePopover,
334335
});
335336

336337
if (this.width || this.width === 0) {
@@ -380,8 +381,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
380381
.withGrowAfterOpen(this.growAfterOpen)
381382
.withViewportMargin(this.viewportMargin)
382383
.withLockedPosition(this.lockPosition)
383-
.withTransformOriginOn(this.transformOriginSelector)
384-
.asPopover(this.asPopover);
384+
.withTransformOriginOn(this.transformOriginSelector);
385385
}
386386

387387
/** Returns the position strategy of the overlay to be set on the overlay config */

src/cdk/overlay/overlay-ref.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -117,23 +117,11 @@ export class OverlayRef implements PortalOutlet {
117117
attach(portal: Portal<any>): any {
118118
// Insert the host into the DOM before attaching the portal, otherwise
119119
// the animations module will skip animations on repeat attachments.
120-
if (!this._host.parentElement) {
121-
const hasAttachedHost = this._positionStrategy?.attachHost?.(this._host);
122-
123-
if (!hasAttachedHost && this._previousHostParent) {
124-
this._previousHostParent.appendChild(this._host);
125-
}
126-
}
120+
this._attachHost();
127121

128122
const attachResult = this._portalOutlet.attach(portal);
129123
this._positionStrategy?.attach(this);
130-
131-
const hasUpdatedStackingOrder = this._positionStrategy?.updateStackingOrder?.(this._host);
132-
133-
if (!hasUpdatedStackingOrder) {
134-
this._updateStackingOrder();
135-
}
136-
124+
this._updateStackingOrder();
137125
this._updateElementSize();
138126
this._updateElementDirection();
139127

@@ -415,6 +403,20 @@ export class OverlayRef implements PortalOutlet {
415403
this._pane.style.pointerEvents = enablePointer ? '' : 'none';
416404
}
417405

406+
private _attachHost() {
407+
if (!this._host.parentElement) {
408+
if (this._config.usePopover && this._positionStrategy?.getPopoverInsertionPoint) {
409+
this._positionStrategy.getPopoverInsertionPoint().after(this._host);
410+
} else {
411+
this._previousHostParent?.appendChild(this._host);
412+
}
413+
}
414+
415+
if (this._config.usePopover) {
416+
this._host.showPopover();
417+
}
418+
}
419+
418420
/** Attaches a backdrop for this overlay. */
419421
private _attachBackdrop() {
420422
const showingClass = 'cdk-overlay-backdrop-showing';
@@ -432,12 +434,10 @@ export class OverlayRef implements PortalOutlet {
432434
this._toggleClasses(this._backdropRef.element, this._config.backdropClass, true);
433435
}
434436

435-
const strategyAttached = this._positionStrategy?.attachBackdrop?.(
436-
this._backdropRef.element,
437-
this._host,
438-
);
439-
440-
if (!strategyAttached) {
437+
if (this._config.usePopover) {
438+
// When using popovers, the backdrop needs to be inside the popover.
439+
this._host.prepend(this._backdropRef.element);
440+
} else {
441441
// Insert the backdrop before the pane in the DOM order,
442442
// in order to handle stacked overlays properly.
443443
this._host.parentElement!.insertBefore(this._backdropRef.element, this._host);
@@ -461,7 +461,7 @@ export class OverlayRef implements PortalOutlet {
461461
* in its original DOM position.
462462
*/
463463
private _updateStackingOrder() {
464-
if (this._host.nextSibling) {
464+
if (!this._config.usePopover && this._host.nextSibling) {
465465
this._host.parentNode!.appendChild(this._host);
466466
}
467467
}

src/cdk/overlay/overlay.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -47,34 +47,34 @@ export function createOverlayRef(injector: Injector, config?: OverlayConfig): Ov
4747
const idGenerator = injector.get(_IdGenerator);
4848
const appRef = injector.get(ApplicationRef);
4949
const directionality = injector.get(Directionality);
50-
const overlayConfig = new OverlayConfig(config);
51-
const customStructure = overlayConfig.positionStrategy?.createStructure?.();
50+
const renderer =
51+
injector.get(Renderer2, null, {optional: true}) ||
52+
injector.get(RendererFactory2).createRenderer(null, null);
5253

53-
let pane: HTMLElement;
54-
let host: HTMLElement;
54+
const overlayConfig = new OverlayConfig(config);
5555

56-
if (customStructure) {
57-
pane = customStructure.pane;
58-
host = customStructure.host;
59-
} else {
60-
host = doc.createElement('div');
61-
pane = doc.createElement('div');
62-
host.appendChild(pane);
63-
overlayContainer.getContainerElement().appendChild(host);
64-
}
56+
overlayConfig.direction = overlayConfig.direction || directionality.value;
57+
overlayConfig.usePopover = !!overlayConfig?.usePopover && 'showPopover' in doc.body;
6558

59+
const pane = doc.createElement('div');
60+
const host = doc.createElement('div');
6661
pane.id = idGenerator.getId('cdk-overlay-');
6762
pane.classList.add('cdk-overlay-pane');
63+
host.appendChild(pane);
6864

69-
const portalOutlet = new DomPortalOutlet(pane, appRef, injector);
70-
const renderer =
71-
injector.get(Renderer2, null, {optional: true}) ||
72-
injector.get(RendererFactory2).createRenderer(null, null);
65+
if (overlayConfig.usePopover) {
66+
host.setAttribute('popover', 'manual');
67+
host.classList.add('cdk-overlay-popover');
68+
}
7369

74-
overlayConfig.direction = overlayConfig.direction || directionality.value;
70+
if (overlayConfig.usePopover && overlayConfig.positionStrategy?.getPopoverInsertionPoint) {
71+
overlayConfig.positionStrategy.getPopoverInsertionPoint().after(host);
72+
} else {
73+
overlayContainer.getContainerElement().appendChild(host);
74+
}
7575

7676
return new OverlayRef(
77-
portalOutlet,
77+
new DomPortalOutlet(pane, appRef, injector),
7878
host,
7979
pane,
8080
overlayConfig,

src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2982,7 +2982,7 @@ describe('FlexibleConnectedPositionStrategy', () => {
29822982
});
29832983

29842984
it('should place the overlay inside the overlay container by default', () => {
2985-
attachOverlay({positionStrategy});
2985+
attachOverlay({positionStrategy, usePopover: false});
29862986
expect(containerElement.contains(overlayRef.hostElement)).toBe(true);
29872987
expect(overlayRef.hostElement.getAttribute('popover')).toBeFalsy();
29882988
});
@@ -2992,8 +2992,7 @@ describe('FlexibleConnectedPositionStrategy', () => {
29922992
return;
29932993
}
29942994

2995-
positionStrategy.asPopover(true);
2996-
attachOverlay({positionStrategy});
2995+
attachOverlay({positionStrategy, usePopover: true});
29972996

29982997
expect(containerElement.contains(overlayRef.hostElement)).toBe(false);
29992998
expect(originElement.nextElementSibling).toBe(overlayRef.hostElement);
@@ -3005,8 +3004,7 @@ describe('FlexibleConnectedPositionStrategy', () => {
30053004
return;
30063005
}
30073006

3008-
positionStrategy.asPopover(true);
3009-
attachOverlay({positionStrategy});
3007+
attachOverlay({positionStrategy, usePopover: true});
30103008
expect(originElement.nextElementSibling).toBe(overlayRef.hostElement);
30113009

30123010
overlayRef.detach();

0 commit comments

Comments
 (0)