From 866cc99b434b226c116effd5ee29e7a7a8d8ab23 Mon Sep 17 00:00:00 2001 From: tjshiu <35056071+tjshiu@users.noreply.github.com> Date: Mon, 17 Nov 2025 03:25:55 -0800 Subject: [PATCH 1/5] refactor(cdk/overlay): enhance popover insertion control with new inputs This commit introduces two new inputs to the CDK overlay, providing more granular control over how popovers are inserted into the DOM: - : Allows specifying a custom element to be used as the host for the popover. The popover will be inserted after this element in the DOM. - : A boolean that, when true, attaches the popover as a child of the popover host, rather than as a sibling. --- goldens/cdk/overlay/index.api.md | 2 + src/cdk/overlay/overlay-config.ts | 7 ++++ src/cdk/overlay/overlay-directives.ts | 40 ++++++++++++++++++- src/cdk/overlay/overlay-ref.ts | 4 +- src/cdk/overlay/overlay.ts | 6 ++- ...exible-connected-position-strategy.spec.ts | 30 ++++++++++++++ .../flexible-connected-position-strategy.ts | 37 ++++++++++++++++- 7 files changed, 122 insertions(+), 4 deletions(-) diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index 855db513f679..17ec61c1dc47 100644 --- a/goldens/cdk/overlay/index.api.md +++ b/goldens/cdk/overlay/index.api.md @@ -297,6 +297,8 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { withGrowAfterOpen(growAfterOpen?: boolean): this; withLockedPosition(isLocked?: boolean): this; withPopoverLocation(location: FlexibleOverlayPopoverLocation): this; + withCustomPopoverHostElement(element: FlexibleConnectedPositionStrategyOrigin): this; + withAttachPopoverAsChild(withAttachPopoverAsChild?: boolean): this; withPositions(positions: ConnectedPosition[]): this; withPush(canPush?: boolean): this; withScrollableContainers(scrollables: CdkScrollable[]): this; diff --git a/src/cdk/overlay/overlay-config.ts b/src/cdk/overlay/overlay-config.ts index c49ed9c1eb2b..40eac919e671 100644 --- a/src/cdk/overlay/overlay-config.ts +++ b/src/cdk/overlay/overlay-config.ts @@ -67,6 +67,13 @@ export class OverlayConfig { */ usePopover?: boolean; + /** + * Whether to attach the popover as a child of the popover host. + * If true, the popover will be attached as a child of the host. + * If false, the popover will be attached after the host. + */ + attachPopoverAsChild?: boolean; + constructor(config?: OverlayConfig) { if (config) { // Use `Iterable` instead of `Array` because TypeScript, as of 3.6.3, diff --git a/src/cdk/overlay/overlay-directives.ts b/src/cdk/overlay/overlay-directives.ts index 4fe99f3768b0..4f3aff2201ad 100644 --- a/src/cdk/overlay/overlay-directives.ts +++ b/src/cdk/overlay/overlay-directives.ts @@ -129,6 +129,8 @@ export interface CdkConnectedOverlayConfig { push?: boolean; disposeOnNavigation?: boolean; usePopover?: FlexibleOverlayPopoverLocation | null; + customPopoverHostElement?: CdkOverlayOrigin | FlexibleConnectedPositionStrategyOrigin | null; + attachPopoverAsChild?: boolean; matchWidth?: boolean; } @@ -259,6 +261,22 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { @Input({alias: 'cdkConnectedOverlayMatchWidth', transform: booleanAttribute}) matchWidth: boolean = false; + /** + * A custom element to use as the host for the popover. + * The popover will be inserted after this element in the DOM. + * If null, the overlay will be inserted after the origin. + */ + @Input({alias: 'cdkCustomPopoverInsertionElement'}) + customPopoverHostElement: CdkOverlayOrigin | FlexibleConnectedPositionStrategyOrigin | null; + + /** + * Whether to attach the popover as a child of the popover host. + * If true, the popover will be attached as a child of the host. + * If false, the popover will be attached after the host. + */ + @Input({alias: 'cdkAttachPopoverAsChild', transform: booleanAttribute}) + attachPopoverAsChild: boolean = false; + /** Shorthand for setting multiple overlay options at once. */ @Input('cdkConnectedOverlay') set _config(value: string | CdkConnectedOverlayConfig) { @@ -338,6 +356,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { } if (changes['open']) { + console.log('changes[open]'); + console.log(this.open); this.open ? this.attachOverlay() : this.detachOverlay(); } } @@ -381,6 +401,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { hasBackdrop: this.hasBackdrop, disposeOnNavigation: this.disposeOnNavigation, usePopover: !!this.usePopover, + attachPopoverAsChild: this.attachPopoverAsChild, }); if (this.height || this.height === 0) { @@ -427,7 +448,9 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { .withViewportMargin(this.viewportMargin) .withLockedPosition(this.lockPosition) .withTransformOriginOn(this.transformOriginSelector) - .withPopoverLocation(this.usePopover === 'global' ? 'global' : 'inline'); + .withPopoverLocation(this.usePopover === 'global' ? 'global' : 'inline') + .withCustomPopoverHostElement(this._getCustomPopoverHostElement()) + .withAttachPopoverAsChild(this.attachPopoverAsChild); } /** Returns the position strategy of the overlay to be set on the overlay config */ @@ -445,6 +468,18 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { } } + /** + * Gets the custom popover host element from the origin input. + * @docs-private + */ + private _getCustomPopoverHostElement(): FlexibleConnectedPositionStrategyOrigin | null { + if (this.customPopoverHostElement instanceof CdkOverlayOrigin) { + return this.customPopoverHostElement.elementRef; + } else { + return this.customPopoverHostElement; + } + } + private _getOriginElement(): Element | null { if (this.origin instanceof CdkOverlayOrigin) { return this.origin.elementRef.nativeElement; @@ -544,6 +579,9 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { this.push = config.push ?? this.push; this.disposeOnNavigation = config.disposeOnNavigation ?? this.disposeOnNavigation; this.usePopover = config.usePopover ?? this.usePopover; + this.customPopoverHostElement = + config.customPopoverHostElement ?? this.customPopoverHostElement; + this.attachPopoverAsChild = config.attachPopoverAsChild ?? this.attachPopoverAsChild; this.matchWidth = config.matchWidth ?? this.matchWidth; } } diff --git a/src/cdk/overlay/overlay-ref.ts b/src/cdk/overlay/overlay-ref.ts index 59971cf7b28b..3d7cbe65460c 100644 --- a/src/cdk/overlay/overlay-ref.ts +++ b/src/cdk/overlay/overlay-ref.ts @@ -410,7 +410,9 @@ export class OverlayRef implements PortalOutlet { : null; if (customInsertionPoint) { - customInsertionPoint.after(this._host); + this._config.attachPopoverAsChild + ? customInsertionPoint.appendChild(this._host) + : customInsertionPoint.after(this._host); } else { this._previousHostParent?.appendChild(this._host); } diff --git a/src/cdk/overlay/overlay.ts b/src/cdk/overlay/overlay.ts index a52742866812..40eaa9c0f031 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -95,7 +95,11 @@ export function createOverlayRef(injector: Injector, config?: OverlayConfig): Ov // it's going to end up at the custom insertion point anyways. We need to do it, // because some internal clients depend on the host passing through the container first. if (customInsertionPoint) { - customInsertionPoint.after(host); + if (overlayConfig.attachPopoverAsChild) { + customInsertionPoint.appendChild(host); + } else { + customInsertionPoint.after(host); + } } return new OverlayRef( diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts index 11b7326fb779..752ec49ffc51 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts @@ -2959,11 +2959,14 @@ describe('FlexibleConnectedPositionStrategy', () => { let positionStrategy: FlexibleConnectedPositionStrategy; let containerElement: HTMLElement; let originElement: HTMLElement; + let customHostElement: HTMLElement; beforeEach(() => { containerElement = overlayContainer.getContainerElement(); originElement = createPositionedBlockElement(); + customHostElement = createBlockElement('span'); document.body.appendChild(originElement); + document.body.appendChild(customHostElement); positionStrategy = createFlexibleConnectedPositionStrategy(injector, originElement) .withPopoverLocation('inline') @@ -2979,6 +2982,7 @@ describe('FlexibleConnectedPositionStrategy', () => { afterEach(() => { originElement.remove(); + customHostElement.remove(); }); it('should place the overlay inside the overlay container by default', () => { @@ -3014,6 +3018,32 @@ describe('FlexibleConnectedPositionStrategy', () => { overlayRef.attach(portal); expect(originElement.nextElementSibling).toBe(overlayRef.hostElement); }); + + it('should insert the overlay after a custom element', () => { + if (!('showPopover' in document.body)) { + return; + } + + positionStrategy.withCustomPopoverHostElement(customHostElement); + attachOverlay({positionStrategy, usePopover: true}); + + expect(containerElement.contains(overlayRef.hostElement)).toBe(false); + expect(customHostElement.nextElementSibling).toBe(overlayRef.hostElement); + expect(overlayRef.hostElement.getAttribute('popover')).toBe('manual'); + }); + + it('should insert the overlay as a child of the origin', () => { + if (!('showPopover' in document.body)) { + return; + } + + console.log(positionStrategy.getPopoverInsertionPoint()); + attachOverlay({positionStrategy, usePopover: true, attachPopoverAsChild: true}); + + expect(containerElement.contains(overlayRef.hostElement)).toBe(false); + expect(originElement.contains(overlayRef.hostElement)).toBe(true); + expect(overlayRef.hostElement.getAttribute('popover')).toBe('manual'); + }); }); }); diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 9552c9c7ee7d..168f756cf321 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -164,6 +164,18 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** Configures where in the DOM to insert the overlay when popovers are enabled. */ private _popoverLocation: FlexibleOverlayPopoverLocation = 'global'; + /** + * Defines a specific host element for the popover content. If provided, the popover will attach + * to this element. + * */ + private _customPopoverHostElement: FlexibleConnectedPositionStrategyOrigin | null; + + /** + * Whether the popover is attached directly as a child of the popover host element instead of + * a sibling element. + * */ + private _attachPopoverAsChild = false; + /** Observable sequence of position changes. */ positionChanges: Observable = this._positionChanges; @@ -528,6 +540,17 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { return this; } + /** + * Sets a custom element to use as the host for the popover. + * The popover will be inserted after this element in the DOM. + * If null, the overlay will be inserted after the origin. + * @param element The element to use as the host for the popover. + */ + withCustomPopoverHostElement(element: FlexibleConnectedPositionStrategyOrigin | null): this { + this._customPopoverHostElement = element; + return this; + } + /** @docs-private */ getPopoverInsertionPoint(): Element | null { // Return null so it falls back to inserting into the overlay container. @@ -535,7 +558,8 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { return null; } - const origin = this._origin; + const origin = + this._customPopoverHostElement != null ? this._customPopoverHostElement : this._origin; if (origin instanceof ElementRef) { return origin.nativeElement; @@ -545,6 +569,17 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { return null; } + /** + * Whether to attach the popover as a child of the popover host. + * If true, the popover will be attached as a child of the host. + * If false, the popover will be attached after the host. + * @param attachPopoverAsChild Whether to attach the popover as a child of the popover host. + */ + withAttachPopoverAsChild(attachPopoverAsChild = false): this { + this._attachPopoverAsChild = attachPopoverAsChild; + return this; + } + /** * Gets the (x, y) coordinate of a connection point on the origin based on a relative position. */ From cd8b63af44a09dc35dd7b6198d1d29284d99ead0 Mon Sep 17 00:00:00 2001 From: tjshiu <35056071+tjshiu@users.noreply.github.com> Date: Mon, 17 Nov 2025 10:51:40 -0800 Subject: [PATCH 2/5] refactor(cdk/overlay): simplify popover insertion logic This commit refactors the popover insertion logic in the CDK overlay by removing and in favor of a more flexible API. This change simplifies the API and provides more granular control over where popovers are inserted into the DOM. --- goldens/cdk/overlay/index.api.md | 4 +- src/cdk/overlay/overlay-config.ts | 7 -- src/cdk/overlay/overlay-directives.ts | 38 +---------- src/cdk/overlay/overlay-ref.ts | 8 ++- src/cdk/overlay/overlay.ts | 6 +- ...exible-connected-position-strategy.spec.ts | 10 +-- .../flexible-connected-position-strategy.ts | 65 +++++++------------ src/cdk/overlay/position/position-strategy.ts | 2 +- 8 files changed, 42 insertions(+), 98 deletions(-) diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index 17ec61c1dc47..f1a791618b91 100644 --- a/goldens/cdk/overlay/index.api.md +++ b/goldens/cdk/overlay/index.api.md @@ -284,7 +284,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { // (undocumented) detach(): void; dispose(): void; - getPopoverInsertionPoint(): Element | null; + getPopoverInsertionPoint(): Element | null | {parent: Element}; _origin: FlexibleConnectedPositionStrategyOrigin; positionChanges: Observable; get positions(): ConnectionPositionPair[]; @@ -536,7 +536,7 @@ export interface PositionStrategy { attach(overlayRef: OverlayRef): void; detach?(): void; dispose(): void; - getPopoverInsertionPoint?(): Element | null; + getPopoverInsertionPoint?(): Element | null | {parent: Element}; } // @public diff --git a/src/cdk/overlay/overlay-config.ts b/src/cdk/overlay/overlay-config.ts index 40eac919e671..c49ed9c1eb2b 100644 --- a/src/cdk/overlay/overlay-config.ts +++ b/src/cdk/overlay/overlay-config.ts @@ -67,13 +67,6 @@ export class OverlayConfig { */ usePopover?: boolean; - /** - * Whether to attach the popover as a child of the popover host. - * If true, the popover will be attached as a child of the host. - * If false, the popover will be attached after the host. - */ - attachPopoverAsChild?: boolean; - constructor(config?: OverlayConfig) { if (config) { // Use `Iterable` instead of `Array` because TypeScript, as of 3.6.3, diff --git a/src/cdk/overlay/overlay-directives.ts b/src/cdk/overlay/overlay-directives.ts index 4f3aff2201ad..57c6969a73ef 100644 --- a/src/cdk/overlay/overlay-directives.ts +++ b/src/cdk/overlay/overlay-directives.ts @@ -261,22 +261,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { @Input({alias: 'cdkConnectedOverlayMatchWidth', transform: booleanAttribute}) matchWidth: boolean = false; - /** - * A custom element to use as the host for the popover. - * The popover will be inserted after this element in the DOM. - * If null, the overlay will be inserted after the origin. - */ - @Input({alias: 'cdkCustomPopoverInsertionElement'}) - customPopoverHostElement: CdkOverlayOrigin | FlexibleConnectedPositionStrategyOrigin | null; - - /** - * Whether to attach the popover as a child of the popover host. - * If true, the popover will be attached as a child of the host. - * If false, the popover will be attached after the host. - */ - @Input({alias: 'cdkAttachPopoverAsChild', transform: booleanAttribute}) - attachPopoverAsChild: boolean = false; - /** Shorthand for setting multiple overlay options at once. */ @Input('cdkConnectedOverlay') set _config(value: string | CdkConnectedOverlayConfig) { @@ -356,8 +340,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { } if (changes['open']) { - console.log('changes[open]'); - console.log(this.open); this.open ? this.attachOverlay() : this.detachOverlay(); } } @@ -401,7 +383,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { hasBackdrop: this.hasBackdrop, disposeOnNavigation: this.disposeOnNavigation, usePopover: !!this.usePopover, - attachPopoverAsChild: this.attachPopoverAsChild, }); if (this.height || this.height === 0) { @@ -448,9 +429,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { .withViewportMargin(this.viewportMargin) .withLockedPosition(this.lockPosition) .withTransformOriginOn(this.transformOriginSelector) - .withPopoverLocation(this.usePopover === 'global' ? 'global' : 'inline') - .withCustomPopoverHostElement(this._getCustomPopoverHostElement()) - .withAttachPopoverAsChild(this.attachPopoverAsChild); + .withPopoverLocation(this.usePopover === null ? 'global' : this.usePopover); } /** Returns the position strategy of the overlay to be set on the overlay config */ @@ -468,18 +447,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { } } - /** - * Gets the custom popover host element from the origin input. - * @docs-private - */ - private _getCustomPopoverHostElement(): FlexibleConnectedPositionStrategyOrigin | null { - if (this.customPopoverHostElement instanceof CdkOverlayOrigin) { - return this.customPopoverHostElement.elementRef; - } else { - return this.customPopoverHostElement; - } - } - private _getOriginElement(): Element | null { if (this.origin instanceof CdkOverlayOrigin) { return this.origin.elementRef.nativeElement; @@ -579,9 +546,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { this.push = config.push ?? this.push; this.disposeOnNavigation = config.disposeOnNavigation ?? this.disposeOnNavigation; this.usePopover = config.usePopover ?? this.usePopover; - this.customPopoverHostElement = - config.customPopoverHostElement ?? this.customPopoverHostElement; - this.attachPopoverAsChild = config.attachPopoverAsChild ?? this.attachPopoverAsChild; this.matchWidth = config.matchWidth ?? this.matchWidth; } } diff --git a/src/cdk/overlay/overlay-ref.ts b/src/cdk/overlay/overlay-ref.ts index 3d7cbe65460c..9d6cedba5034 100644 --- a/src/cdk/overlay/overlay-ref.ts +++ b/src/cdk/overlay/overlay-ref.ts @@ -410,9 +410,11 @@ export class OverlayRef implements PortalOutlet { : null; if (customInsertionPoint) { - this._config.attachPopoverAsChild - ? customInsertionPoint.appendChild(this._host) - : customInsertionPoint.after(this._host); + if (customInsertionPoint instanceof Element) { + customInsertionPoint.after(this._host); + } else { + customInsertionPoint.parent?.appendChild(this._host); + } } else { this._previousHostParent?.appendChild(this._host); } diff --git a/src/cdk/overlay/overlay.ts b/src/cdk/overlay/overlay.ts index 40eaa9c0f031..df0911d67f4c 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -95,10 +95,10 @@ export function createOverlayRef(injector: Injector, config?: OverlayConfig): Ov // it's going to end up at the custom insertion point anyways. We need to do it, // because some internal clients depend on the host passing through the container first. if (customInsertionPoint) { - if (overlayConfig.attachPopoverAsChild) { - customInsertionPoint.appendChild(host); - } else { + if (customInsertionPoint instanceof Element) { customInsertionPoint.after(host); + } else { + customInsertionPoint.parent.appendChild(host); } } diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts index 752ec49ffc51..973932781383 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts @@ -3019,16 +3019,16 @@ describe('FlexibleConnectedPositionStrategy', () => { expect(originElement.nextElementSibling).toBe(overlayRef.hostElement); }); - it('should insert the overlay after a custom element', () => { + it('should insert the overlay as a child of a custom element', () => { if (!('showPopover' in document.body)) { return; } - positionStrategy.withCustomPopoverHostElement(customHostElement); + positionStrategy.withPopoverLocation({parent: customHostElement}); attachOverlay({positionStrategy, usePopover: true}); expect(containerElement.contains(overlayRef.hostElement)).toBe(false); - expect(customHostElement.nextElementSibling).toBe(overlayRef.hostElement); + expect(customHostElement.contains(overlayRef.hostElement)).toBe(true); expect(overlayRef.hostElement.getAttribute('popover')).toBe('manual'); }); @@ -3037,8 +3037,8 @@ describe('FlexibleConnectedPositionStrategy', () => { return; } - console.log(positionStrategy.getPopoverInsertionPoint()); - attachOverlay({positionStrategy, usePopover: true, attachPopoverAsChild: true}); + positionStrategy.withPopoverLocation({parent: originElement}); + attachOverlay({positionStrategy, usePopover: true}); expect(containerElement.contains(overlayRef.hostElement)).toBe(false); expect(originElement.contains(overlayRef.hostElement)).toBe(true); diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 168f756cf321..5db0244d9a92 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -45,6 +45,9 @@ export type FlexibleConnectedPositionStrategyOrigin = /** Equivalent of `DOMRect` without some of the properties we don't care about. */ type Dimensions = Omit; +/** Possible point to attach a popover to. */ +export type PopoverInsertionPoint = Element | {parent: Element} | null; + /** * Creates a flexible position strategy. * @param injector Injector used to resolve dependnecies for the position strategy. @@ -64,7 +67,10 @@ export function createFlexibleConnectedPositionStrategy( } /** Supported locations in the DOM for connected overlays. */ -export type FlexibleOverlayPopoverLocation = 'global' | 'inline'; +export type FlexibleOverlayPopoverLocation = + | 'global' + | 'inline' + | {parent: FlexibleConnectedPositionStrategyOrigin}; /** * A strategy for positioning overlays. Using this strategy, an overlay is given an @@ -164,18 +170,6 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** Configures where in the DOM to insert the overlay when popovers are enabled. */ private _popoverLocation: FlexibleOverlayPopoverLocation = 'global'; - /** - * Defines a specific host element for the popover content. If provided, the popover will attach - * to this element. - * */ - private _customPopoverHostElement: FlexibleConnectedPositionStrategyOrigin | null; - - /** - * Whether the popover is attached directly as a child of the popover host element instead of - * a sibling element. - * */ - private _attachPopoverAsChild = false; - /** Observable sequence of position changes. */ positionChanges: Observable = this._positionChanges; @@ -540,44 +534,35 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { return this; } - /** - * Sets a custom element to use as the host for the popover. - * The popover will be inserted after this element in the DOM. - * If null, the overlay will be inserted after the origin. - * @param element The element to use as the host for the popover. - */ - withCustomPopoverHostElement(element: FlexibleConnectedPositionStrategyOrigin | null): this { - this._customPopoverHostElement = element; - return this; - } - /** @docs-private */ - getPopoverInsertionPoint(): Element | null { - // Return null so it falls back to inserting into the overlay container. + getPopoverInsertionPoint(): PopoverInsertionPoint { if (this._popoverLocation === 'global') { return null; } const origin = - this._customPopoverHostElement != null ? this._customPopoverHostElement : this._origin; + this._popoverLocation === 'inline' + ? this._origin + : (this._popoverLocation as {parent: FlexibleConnectedPositionStrategyOrigin}).parent; + let element: Element | null = null; if (origin instanceof ElementRef) { - return origin.nativeElement; + element = origin.nativeElement; } else if (origin instanceof Element) { - return origin; + element = origin; } - return null; - } - /** - * Whether to attach the popover as a child of the popover host. - * If true, the popover will be attached as a child of the host. - * If false, the popover will be attached after the host. - * @param attachPopoverAsChild Whether to attach the popover as a child of the popover host. - */ - withAttachPopoverAsChild(attachPopoverAsChild = false): this { - this._attachPopoverAsChild = attachPopoverAsChild; - return this; + // If the location is 'inline', we're inserting as a sibling. + if (this._popoverLocation === 'inline') { + return element; + } + + // Otherwise we're inserting as a child. + if (element) { + return {parent: element}; + } + + return null; } /** diff --git a/src/cdk/overlay/position/position-strategy.ts b/src/cdk/overlay/position/position-strategy.ts index 1191acb2dc0e..6ad4206b66da 100644 --- a/src/cdk/overlay/position/position-strategy.ts +++ b/src/cdk/overlay/position/position-strategy.ts @@ -26,5 +26,5 @@ export interface PositionStrategy { * Gets the element in the DOM after which to insert * the overlay when it is rendered out as a popover. */ - getPopoverInsertionPoint?(): Element | null; + getPopoverInsertionPoint?(): Element | null | {parent: Element}; } From e885d6e43f22f68c4f4a37d37bc8697de7a0c8c7 Mon Sep 17 00:00:00 2001 From: tjshiu <35056071+tjshiu@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:33:59 -0800 Subject: [PATCH 3/5] refactor(cdk/overlay): add overlay changes --- goldens/cdk/overlay/index.api.md | 4 ++-- src/cdk/overlay/overlay-ref.ts | 4 +++- src/cdk/overlay/overlay.ts | 4 +++- .../flexible-connected-position-strategy.spec.ts | 4 ++-- .../flexible-connected-position-strategy.ts | 15 +++++++++++---- src/cdk/overlay/position/position-strategy.ts | 2 +- 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index f1a791618b91..43f12e0aca93 100644 --- a/goldens/cdk/overlay/index.api.md +++ b/goldens/cdk/overlay/index.api.md @@ -284,7 +284,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { // (undocumented) detach(): void; dispose(): void; - getPopoverInsertionPoint(): Element | null | {parent: Element}; + getPopoverInsertionPoint(): Element | null | {type: 'parent', element: Element}; _origin: FlexibleConnectedPositionStrategyOrigin; positionChanges: Observable; get positions(): ConnectionPositionPair[]; @@ -536,7 +536,7 @@ export interface PositionStrategy { attach(overlayRef: OverlayRef): void; detach?(): void; dispose(): void; - getPopoverInsertionPoint?(): Element | null | {parent: Element}; + getPopoverInsertionPoint?(): Element | null | {type: 'parent', element: Element}; } // @public diff --git a/src/cdk/overlay/overlay-ref.ts b/src/cdk/overlay/overlay-ref.ts index 9d6cedba5034..53b0b5e70ac3 100644 --- a/src/cdk/overlay/overlay-ref.ts +++ b/src/cdk/overlay/overlay-ref.ts @@ -413,7 +413,9 @@ export class OverlayRef implements PortalOutlet { if (customInsertionPoint instanceof Element) { customInsertionPoint.after(this._host); } else { - customInsertionPoint.parent?.appendChild(this._host); + if (customInsertionPoint.type === 'parent') { + customInsertionPoint.element?.appendChild(this._host); + } } } else { this._previousHostParent?.appendChild(this._host); diff --git a/src/cdk/overlay/overlay.ts b/src/cdk/overlay/overlay.ts index df0911d67f4c..c55886c67ea5 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -98,7 +98,9 @@ export function createOverlayRef(injector: Injector, config?: OverlayConfig): Ov if (customInsertionPoint instanceof Element) { customInsertionPoint.after(host); } else { - customInsertionPoint.parent.appendChild(host); + if (customInsertionPoint.type === 'parent') { + customInsertionPoint.element?.appendChild(host); + } } } diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts index 973932781383..45014d01ed85 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts @@ -3024,7 +3024,7 @@ describe('FlexibleConnectedPositionStrategy', () => { return; } - positionStrategy.withPopoverLocation({parent: customHostElement}); + positionStrategy.withPopoverLocation({type: 'parent', element: customHostElement}); attachOverlay({positionStrategy, usePopover: true}); expect(containerElement.contains(overlayRef.hostElement)).toBe(false); @@ -3037,7 +3037,7 @@ describe('FlexibleConnectedPositionStrategy', () => { return; } - positionStrategy.withPopoverLocation({parent: originElement}); + positionStrategy.withPopoverLocation({type: 'parent', element: originElement}); attachOverlay({positionStrategy, usePopover: true}); expect(containerElement.contains(overlayRef.hostElement)).toBe(false); diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 5db0244d9a92..956f8dacfd00 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -46,7 +46,7 @@ export type FlexibleConnectedPositionStrategyOrigin = type Dimensions = Omit; /** Possible point to attach a popover to. */ -export type PopoverInsertionPoint = Element | {parent: Element} | null; +export type PopoverInsertionPoint = Element | {type: 'parent'; element: Element} | null; /** * Creates a flexible position strategy. @@ -70,7 +70,7 @@ export function createFlexibleConnectedPositionStrategy( export type FlexibleOverlayPopoverLocation = | 'global' | 'inline' - | {parent: FlexibleConnectedPositionStrategyOrigin}; + | {type: 'parent'; element: FlexibleConnectedPositionStrategyOrigin}; /** * A strategy for positioning overlays. Using this strategy, an overlay is given an @@ -528,6 +528,8 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { * @param location Configures the location in the DOM. Supports the following values: * - `global` - The default which inserts the overlay inside the overlay container. * - `inline` - Inserts the overlay next to the trigger. + * - {type: 'parent', element: element} - Inserts the overlay to a child of a custom parent + * element. */ withPopoverLocation(location: FlexibleOverlayPopoverLocation): this { this._popoverLocation = location; @@ -543,7 +545,12 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { const origin = this._popoverLocation === 'inline' ? this._origin - : (this._popoverLocation as {parent: FlexibleConnectedPositionStrategyOrigin}).parent; + : ( + this._popoverLocation as { + type: 'parent'; + element: FlexibleConnectedPositionStrategyOrigin; + } + ).element; let element: Element | null = null; if (origin instanceof ElementRef) { @@ -559,7 +566,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { // Otherwise we're inserting as a child. if (element) { - return {parent: element}; + return {type: 'parent', element: element}; } return null; diff --git a/src/cdk/overlay/position/position-strategy.ts b/src/cdk/overlay/position/position-strategy.ts index 6ad4206b66da..34c548d1da13 100644 --- a/src/cdk/overlay/position/position-strategy.ts +++ b/src/cdk/overlay/position/position-strategy.ts @@ -26,5 +26,5 @@ export interface PositionStrategy { * Gets the element in the DOM after which to insert * the overlay when it is rendered out as a popover. */ - getPopoverInsertionPoint?(): Element | null | {parent: Element}; + getPopoverInsertionPoint?(): Element | null | {type: 'parent'; element: Element}; } From 99b17c46b59ebd054743dd7eb2c6e2a11b2cce26 Mon Sep 17 00:00:00 2001 From: tjshiu <35056071+tjshiu@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:06:42 -0800 Subject: [PATCH 4/5] refactor(cdk/overlay): streamline popover insertion point logic --- .../flexible-connected-position-strategy.ts | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 956f8dacfd00..18c271b4e601 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -45,9 +45,6 @@ export type FlexibleConnectedPositionStrategyOrigin = /** Equivalent of `DOMRect` without some of the properties we don't care about. */ type Dimensions = Omit; -/** Possible point to attach a popover to. */ -export type PopoverInsertionPoint = Element | {type: 'parent'; element: Element} | null; - /** * Creates a flexible position strategy. * @param injector Injector used to resolve dependnecies for the position strategy. @@ -70,7 +67,7 @@ export function createFlexibleConnectedPositionStrategy( export type FlexibleOverlayPopoverLocation = | 'global' | 'inline' - | {type: 'parent'; element: FlexibleConnectedPositionStrategyOrigin}; + | {type: 'parent'; element: Element}; /** * A strategy for positioning overlays. Using this strategy, an overlay is given an @@ -537,36 +534,32 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { } /** @docs-private */ - getPopoverInsertionPoint(): PopoverInsertionPoint { + getPopoverInsertionPoint(): Element | null | {type: 'parent'; element: Element} { if (this._popoverLocation === 'global') { return null; } - const origin = - this._popoverLocation === 'inline' - ? this._origin - : ( - this._popoverLocation as { - type: 'parent'; - element: FlexibleConnectedPositionStrategyOrigin; - } - ).element; - let element: Element | null = null; + let hostElement: Element | null = null; - if (origin instanceof ElementRef) { - element = origin.nativeElement; - } else if (origin instanceof Element) { - element = origin; + if (this._popoverLocation === 'inline') { + if (this._origin instanceof ElementRef) { + hostElement = this._origin.nativeElement; + } else if (this._origin instanceof Element) { + hostElement = this._origin; + } + } else { + // this._popoverLocation is {type: 'parent', element: Element} + hostElement = this._popoverLocation.element; } // If the location is 'inline', we're inserting as a sibling. if (this._popoverLocation === 'inline') { - return element; + return hostElement; } // Otherwise we're inserting as a child. - if (element) { - return {type: 'parent', element: element}; + if (hostElement) { + return {type: 'parent', element: hostElement}; } return null; From d5e3b254ca2af84851ed5dd3d52b8473643a3290 Mon Sep 17 00:00:00 2001 From: tjshiu <35056071+tjshiu@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:11:17 -0800 Subject: [PATCH 5/5] fix(cdk/overlay): update the insertionpoint type --- goldens/cdk/overlay/index.api.md | 4 +--- src/cdk/overlay/overlay-directives.ts | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index 43f12e0aca93..b40fc7e0ea37 100644 --- a/goldens/cdk/overlay/index.api.md +++ b/goldens/cdk/overlay/index.api.md @@ -297,8 +297,6 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { withGrowAfterOpen(growAfterOpen?: boolean): this; withLockedPosition(isLocked?: boolean): this; withPopoverLocation(location: FlexibleOverlayPopoverLocation): this; - withCustomPopoverHostElement(element: FlexibleConnectedPositionStrategyOrigin): this; - withAttachPopoverAsChild(withAttachPopoverAsChild?: boolean): this; withPositions(positions: ConnectedPosition[]): this; withPush(canPush?: boolean): this; withScrollableContainers(scrollables: CdkScrollable[]): this; @@ -313,7 +311,7 @@ export type FlexibleConnectedPositionStrategyOrigin = ElementRef | Element | (Po }); // @public -export type FlexibleOverlayPopoverLocation = 'global' | 'inline'; +export type FlexibleOverlayPopoverLocation = 'global' | 'inline' | {type: 'parent', element: Element }; // @public export class FullscreenOverlayContainer extends OverlayContainer implements OnDestroy { diff --git a/src/cdk/overlay/overlay-directives.ts b/src/cdk/overlay/overlay-directives.ts index 57c6969a73ef..f69d079035a9 100644 --- a/src/cdk/overlay/overlay-directives.ts +++ b/src/cdk/overlay/overlay-directives.ts @@ -129,8 +129,6 @@ export interface CdkConnectedOverlayConfig { push?: boolean; disposeOnNavigation?: boolean; usePopover?: FlexibleOverlayPopoverLocation | null; - customPopoverHostElement?: CdkOverlayOrigin | FlexibleConnectedPositionStrategyOrigin | null; - attachPopoverAsChild?: boolean; matchWidth?: boolean; }