Skip to content

Commit 866cc99

Browse files
committed
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.
1 parent 93bdd11 commit 866cc99

File tree

7 files changed

+122
-4
lines changed

7 files changed

+122
-4
lines changed

goldens/cdk/overlay/index.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
297297
withGrowAfterOpen(growAfterOpen?: boolean): this;
298298
withLockedPosition(isLocked?: boolean): this;
299299
withPopoverLocation(location: FlexibleOverlayPopoverLocation): this;
300+
withCustomPopoverHostElement(element: FlexibleConnectedPositionStrategyOrigin): this;
301+
withAttachPopoverAsChild(withAttachPopoverAsChild?: boolean): this;
300302
withPositions(positions: ConnectedPosition[]): this;
301303
withPush(canPush?: boolean): this;
302304
withScrollableContainers(scrollables: CdkScrollable[]): this;

src/cdk/overlay/overlay-config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ export class OverlayConfig {
6767
*/
6868
usePopover?: boolean;
6969

70+
/**
71+
* Whether to attach the popover as a child of the popover host.
72+
* If true, the popover will be attached as a child of the host.
73+
* If false, the popover will be attached after the host.
74+
*/
75+
attachPopoverAsChild?: boolean;
76+
7077
constructor(config?: OverlayConfig) {
7178
if (config) {
7279
// Use `Iterable` instead of `Array` because TypeScript, as of 3.6.3,

src/cdk/overlay/overlay-directives.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ export interface CdkConnectedOverlayConfig {
129129
push?: boolean;
130130
disposeOnNavigation?: boolean;
131131
usePopover?: FlexibleOverlayPopoverLocation | null;
132+
customPopoverHostElement?: CdkOverlayOrigin | FlexibleConnectedPositionStrategyOrigin | null;
133+
attachPopoverAsChild?: boolean;
132134
matchWidth?: boolean;
133135
}
134136

@@ -259,6 +261,22 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
259261
@Input({alias: 'cdkConnectedOverlayMatchWidth', transform: booleanAttribute})
260262
matchWidth: boolean = false;
261263

264+
/**
265+
* A custom element to use as the host for the popover.
266+
* The popover will be inserted after this element in the DOM.
267+
* If null, the overlay will be inserted after the origin.
268+
*/
269+
@Input({alias: 'cdkCustomPopoverInsertionElement'})
270+
customPopoverHostElement: CdkOverlayOrigin | FlexibleConnectedPositionStrategyOrigin | null;
271+
272+
/**
273+
* Whether to attach the popover as a child of the popover host.
274+
* If true, the popover will be attached as a child of the host.
275+
* If false, the popover will be attached after the host.
276+
*/
277+
@Input({alias: 'cdkAttachPopoverAsChild', transform: booleanAttribute})
278+
attachPopoverAsChild: boolean = false;
279+
262280
/** Shorthand for setting multiple overlay options at once. */
263281
@Input('cdkConnectedOverlay')
264282
set _config(value: string | CdkConnectedOverlayConfig) {
@@ -338,6 +356,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
338356
}
339357

340358
if (changes['open']) {
359+
console.log('changes[open]');
360+
console.log(this.open);
341361
this.open ? this.attachOverlay() : this.detachOverlay();
342362
}
343363
}
@@ -381,6 +401,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
381401
hasBackdrop: this.hasBackdrop,
382402
disposeOnNavigation: this.disposeOnNavigation,
383403
usePopover: !!this.usePopover,
404+
attachPopoverAsChild: this.attachPopoverAsChild,
384405
});
385406

386407
if (this.height || this.height === 0) {
@@ -427,7 +448,9 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
427448
.withViewportMargin(this.viewportMargin)
428449
.withLockedPosition(this.lockPosition)
429450
.withTransformOriginOn(this.transformOriginSelector)
430-
.withPopoverLocation(this.usePopover === 'global' ? 'global' : 'inline');
451+
.withPopoverLocation(this.usePopover === 'global' ? 'global' : 'inline')
452+
.withCustomPopoverHostElement(this._getCustomPopoverHostElement())
453+
.withAttachPopoverAsChild(this.attachPopoverAsChild);
431454
}
432455

433456
/** Returns the position strategy of the overlay to be set on the overlay config */
@@ -445,6 +468,18 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
445468
}
446469
}
447470

471+
/**
472+
* Gets the custom popover host element from the origin input.
473+
* @docs-private
474+
*/
475+
private _getCustomPopoverHostElement(): FlexibleConnectedPositionStrategyOrigin | null {
476+
if (this.customPopoverHostElement instanceof CdkOverlayOrigin) {
477+
return this.customPopoverHostElement.elementRef;
478+
} else {
479+
return this.customPopoverHostElement;
480+
}
481+
}
482+
448483
private _getOriginElement(): Element | null {
449484
if (this.origin instanceof CdkOverlayOrigin) {
450485
return this.origin.elementRef.nativeElement;
@@ -544,6 +579,9 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
544579
this.push = config.push ?? this.push;
545580
this.disposeOnNavigation = config.disposeOnNavigation ?? this.disposeOnNavigation;
546581
this.usePopover = config.usePopover ?? this.usePopover;
582+
this.customPopoverHostElement =
583+
config.customPopoverHostElement ?? this.customPopoverHostElement;
584+
this.attachPopoverAsChild = config.attachPopoverAsChild ?? this.attachPopoverAsChild;
547585
this.matchWidth = config.matchWidth ?? this.matchWidth;
548586
}
549587
}

src/cdk/overlay/overlay-ref.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,9 @@ export class OverlayRef implements PortalOutlet {
410410
: null;
411411

412412
if (customInsertionPoint) {
413-
customInsertionPoint.after(this._host);
413+
this._config.attachPopoverAsChild
414+
? customInsertionPoint.appendChild(this._host)
415+
: customInsertionPoint.after(this._host);
414416
} else {
415417
this._previousHostParent?.appendChild(this._host);
416418
}

src/cdk/overlay/overlay.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,11 @@ export function createOverlayRef(injector: Injector, config?: OverlayConfig): Ov
9595
// it's going to end up at the custom insertion point anyways. We need to do it,
9696
// because some internal clients depend on the host passing through the container first.
9797
if (customInsertionPoint) {
98-
customInsertionPoint.after(host);
98+
if (overlayConfig.attachPopoverAsChild) {
99+
customInsertionPoint.appendChild(host);
100+
} else {
101+
customInsertionPoint.after(host);
102+
}
99103
}
100104

101105
return new OverlayRef(

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2959,11 +2959,14 @@ describe('FlexibleConnectedPositionStrategy', () => {
29592959
let positionStrategy: FlexibleConnectedPositionStrategy;
29602960
let containerElement: HTMLElement;
29612961
let originElement: HTMLElement;
2962+
let customHostElement: HTMLElement;
29622963

29632964
beforeEach(() => {
29642965
containerElement = overlayContainer.getContainerElement();
29652966
originElement = createPositionedBlockElement();
2967+
customHostElement = createBlockElement('span');
29662968
document.body.appendChild(originElement);
2969+
document.body.appendChild(customHostElement);
29672970

29682971
positionStrategy = createFlexibleConnectedPositionStrategy(injector, originElement)
29692972
.withPopoverLocation('inline')
@@ -2979,6 +2982,7 @@ describe('FlexibleConnectedPositionStrategy', () => {
29792982

29802983
afterEach(() => {
29812984
originElement.remove();
2985+
customHostElement.remove();
29822986
});
29832987

29842988
it('should place the overlay inside the overlay container by default', () => {
@@ -3014,6 +3018,32 @@ describe('FlexibleConnectedPositionStrategy', () => {
30143018
overlayRef.attach(portal);
30153019
expect(originElement.nextElementSibling).toBe(overlayRef.hostElement);
30163020
});
3021+
3022+
it('should insert the overlay after a custom element', () => {
3023+
if (!('showPopover' in document.body)) {
3024+
return;
3025+
}
3026+
3027+
positionStrategy.withCustomPopoverHostElement(customHostElement);
3028+
attachOverlay({positionStrategy, usePopover: true});
3029+
3030+
expect(containerElement.contains(overlayRef.hostElement)).toBe(false);
3031+
expect(customHostElement.nextElementSibling).toBe(overlayRef.hostElement);
3032+
expect(overlayRef.hostElement.getAttribute('popover')).toBe('manual');
3033+
});
3034+
3035+
it('should insert the overlay as a child of the origin', () => {
3036+
if (!('showPopover' in document.body)) {
3037+
return;
3038+
}
3039+
3040+
console.log(positionStrategy.getPopoverInsertionPoint());
3041+
attachOverlay({positionStrategy, usePopover: true, attachPopoverAsChild: true});
3042+
3043+
expect(containerElement.contains(overlayRef.hostElement)).toBe(false);
3044+
expect(originElement.contains(overlayRef.hostElement)).toBe(true);
3045+
expect(overlayRef.hostElement.getAttribute('popover')).toBe('manual');
3046+
});
30173047
});
30183048
});
30193049

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,18 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
164164
/** Configures where in the DOM to insert the overlay when popovers are enabled. */
165165
private _popoverLocation: FlexibleOverlayPopoverLocation = 'global';
166166

167+
/**
168+
* Defines a specific host element for the popover content. If provided, the popover will attach
169+
* to this element.
170+
* */
171+
private _customPopoverHostElement: FlexibleConnectedPositionStrategyOrigin | null;
172+
173+
/**
174+
* Whether the popover is attached directly as a child of the popover host element instead of
175+
* a sibling element.
176+
* */
177+
private _attachPopoverAsChild = false;
178+
167179
/** Observable sequence of position changes. */
168180
positionChanges: Observable<ConnectedOverlayPositionChange> = this._positionChanges;
169181

@@ -528,14 +540,26 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
528540
return this;
529541
}
530542

543+
/**
544+
* Sets a custom element to use as the host for the popover.
545+
* The popover will be inserted after this element in the DOM.
546+
* If null, the overlay will be inserted after the origin.
547+
* @param element The element to use as the host for the popover.
548+
*/
549+
withCustomPopoverHostElement(element: FlexibleConnectedPositionStrategyOrigin | null): this {
550+
this._customPopoverHostElement = element;
551+
return this;
552+
}
553+
531554
/** @docs-private */
532555
getPopoverInsertionPoint(): Element | null {
533556
// Return null so it falls back to inserting into the overlay container.
534557
if (this._popoverLocation === 'global') {
535558
return null;
536559
}
537560

538-
const origin = this._origin;
561+
const origin =
562+
this._customPopoverHostElement != null ? this._customPopoverHostElement : this._origin;
539563

540564
if (origin instanceof ElementRef) {
541565
return origin.nativeElement;
@@ -545,6 +569,17 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
545569
return null;
546570
}
547571

572+
/**
573+
* Whether to attach the popover as a child of the popover host.
574+
* If true, the popover will be attached as a child of the host.
575+
* If false, the popover will be attached after the host.
576+
* @param attachPopoverAsChild Whether to attach the popover as a child of the popover host.
577+
*/
578+
withAttachPopoverAsChild(attachPopoverAsChild = false): this {
579+
this._attachPopoverAsChild = attachPopoverAsChild;
580+
return this;
581+
}
582+
548583
/**
549584
* Gets the (x, y) coordinate of a connection point on the origin based on a relative position.
550585
*/

0 commit comments

Comments
 (0)