Skip to content

Commit 59812df

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.
1 parent ecd9039 commit 59812df

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)