Skip to content

Commit aeb9dbf

Browse files
committed
feat(cdk/overlay): Add option to insert overlay after an element instead of at root
1 parent c0945db commit aeb9dbf

File tree

5 files changed

+46
-4
lines changed

5 files changed

+46
-4
lines changed

goldens/cdk/overlay/index.api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
5454
growAfterOpen: boolean;
5555
hasBackdrop: boolean;
5656
height: number | string;
57+
insertOverlayAfter?: ElementRef;
5758
lockPosition: boolean;
5859
minHeight: number | string;
5960
minWidth: number | string;
@@ -92,7 +93,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
9293
viewportMargin: ViewportMargin;
9394
width: number | string;
9495
// (undocumented)
95-
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; }; }, { "backdropClick": "backdropClick"; "positionChange": "positionChange"; "attach": "attach"; "detach": "detach"; "overlayKeydown": "overlayKeydown"; "overlayOutsideClick": "overlayOutsideClick"; }, never, never, true, never>;
96+
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; }; "insertOverlayAfter": { "alias": "cdkConnectedOverlayInsertAfter"; "required": false; }; "push": { "alias": "cdkConnectedOverlayPush"; "required": false; }; "disposeOnNavigation": { "alias": "cdkConnectedOverlayDisposeOnNavigation"; "required": false; }; }, { "backdropClick": "backdropClick"; "positionChange": "positionChange"; "attach": "attach"; "detach": "detach"; "overlayKeydown": "overlayKeydown"; "overlayOutsideClick": "overlayOutsideClick"; }, never, never, true, never>;
9697
// (undocumented)
9798
static ɵfac: i0.ɵɵFactoryDeclaration<CdkConnectedOverlay, never>;
9899
}
@@ -324,6 +325,7 @@ export class OverlayConfig {
324325
disposeOnNavigation?: boolean;
325326
hasBackdrop?: boolean;
326327
height?: number | string;
328+
insertOverlayAfter?: ElementRef;
327329
maxHeight?: number | string;
328330
maxWidth?: number | string;
329331
minHeight?: number | string;

src/cdk/overlay/overlay-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import {ElementRef} from '@angular/core';
910
import {PositionStrategy} from './position/position-strategy';
1011
import {Direction, Directionality} from '../bidi';
1112
import {ScrollStrategy, NoopScrollStrategy} from './scroll/index';
@@ -30,6 +31,9 @@ export class OverlayConfig {
3031
/** Whether to disable any built-in animations. */
3132
disableAnimations?: boolean;
3233

34+
/** If specified, insert overlay after this element, instead of using the global overlay container. */
35+
insertOverlayAfter?: ElementRef;
36+
3337
/** The width of the overlay panel. If a number is provided, pixel units are assumed. */
3438
width?: number | string;
3539

src/cdk/overlay/overlay-directives.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
209209
@Input({alias: 'cdkConnectedOverlayGrowAfterOpen', transform: booleanAttribute})
210210
growAfterOpen: boolean = false;
211211

212+
/** Whether or not the overlay should attach a backdrop. */
213+
@Input('cdkConnectedOverlayInsertAfter')
214+
insertOverlayAfter?: ElementRef;
215+
212216
/** Whether the overlay can be pushed on-screen if none of the provided positions fit. */
213217
@Input({alias: 'cdkConnectedOverlayPush', transform: booleanAttribute}) push: boolean = false;
214218

@@ -327,6 +331,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
327331
scrollStrategy: this.scrollStrategy,
328332
hasBackdrop: this.hasBackdrop,
329333
disposeOnNavigation: this.disposeOnNavigation,
334+
insertOverlayAfter: this.insertOverlayAfter,
330335
});
331336

332337
if (this.width || this.width === 0) {

src/cdk/overlay/overlay.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,21 @@ export function createOverlayRef(injector: Injector, config?: OverlayConfig): Ov
4848
const appRef = injector.get(ApplicationRef);
4949
const directionality = injector.get(Directionality);
5050

51+
// Create the overlay pane and a parent which will then be attached to the document.
5152
const host = doc.createElement('div');
5253
const pane = doc.createElement('div');
53-
5454
pane.id = idGenerator.getId('cdk-overlay-');
5555
pane.classList.add('cdk-overlay-pane');
5656
host.appendChild(pane);
57-
overlayContainer.getContainerElement().appendChild(host);
57+
58+
// Insert after the specified element, or onto the global overlay container.
59+
if (config?.insertOverlayAfter) {
60+
const element = config.insertOverlayAfter.nativeElement;
61+
element.after(host);
62+
element.parentElement.style.position = 'relative';
63+
} else {
64+
overlayContainer.getContainerElement().appendChild(host);
65+
}
5866

5967
const portalOutlet = new DomPortalOutlet(pane, appRef, injector);
6068
const overlayConfig = new OverlayConfig(config);

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,14 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
581581
overlayStartY = pos.overlayY == 'top' ? 0 : -overlayRect.height;
582582
}
583583

584+
// Adjust the overly position when it is placed inline relative to its parent.
585+
const insertOverlayAfter = this._overlayRef.getConfig().insertOverlayAfter;
586+
if (insertOverlayAfter) {
587+
const rect = insertOverlayAfter!.nativeElement.parentElement.getBoundingClientRect();
588+
overlayStartX -= rect.left;
589+
overlayStartY -= rect.top;
590+
}
591+
584592
// The (x, y) coordinates of the overlay.
585593
return {
586594
x: originPoint.x + overlayStartX,
@@ -889,11 +897,26 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
889897
if (this._hasExactPosition()) {
890898
styles.top = styles.left = '0';
891899
styles.bottom = styles.right = styles.maxHeight = styles.maxWidth = '';
892-
styles.width = styles.height = '100%';
900+
901+
if (this._overlayRef.getConfig().insertOverlayAfter) {
902+
styles.width = coerceCssPixelValue(boundingBoxRect.width);
903+
styles.height = coerceCssPixelValue(boundingBoxRect.height);
904+
} else {
905+
// TODO(andreyd): can most likley remove this for common case
906+
styles.width = styles.height = '100%';
907+
}
893908
} else {
894909
const maxHeight = this._overlayRef.getConfig().maxHeight;
895910
const maxWidth = this._overlayRef.getConfig().maxWidth;
896911

912+
// Adjust the overly position when it is placed inline relative to its parent.
913+
const insertOverlayAfter = this._overlayRef.getConfig().insertOverlayAfter;
914+
if (insertOverlayAfter) {
915+
const rect = insertOverlayAfter!.nativeElement.parentElement.getBoundingClientRect();
916+
boundingBoxRect.left -= rect.left;
917+
boundingBoxRect.top -= rect.top;
918+
}
919+
897920
styles.height = coerceCssPixelValue(boundingBoxRect.height);
898921
styles.top = coerceCssPixelValue(boundingBoxRect.top);
899922
styles.bottom = coerceCssPixelValue(boundingBoxRect.bottom);

0 commit comments

Comments
 (0)