Skip to content

Commit 1176bc4

Browse files
Copilotrenemadsen
andcommitted
Implement custom OverlayContainer for Angular 21 popover top layer compatibility
Co-authored-by: renemadsen <76994+renemadsen@users.noreply.github.com>
1 parent 49e54f6 commit 1176bc4

3 files changed

Lines changed: 137 additions & 0 deletions

File tree

eform-client/src/app/app.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ import {
6767
} from './state';
6868
import {NgxMaskDirective, NgxMaskPipe} from 'ngx-mask';
6969
import {NgxMaterialTimepickerModule} from 'ngx-material-timepicker';
70+
import {OverlayContainer} from '@angular/cdk/overlay';
71+
import {CustomOverlayContainer} from './common/services/custom-overlay-container.service';
7072

7173
// Factory function for APP_INITIALIZER to register icons
7274
export function registerIconsFactory(iconService: IconService) {
@@ -153,6 +155,10 @@ export function registerIconsFactory(iconService: IconService) {
153155
useFactory: registerIconsFactory,
154156
deps: [IconService],
155157
multi: true
158+
},
159+
{
160+
provide: OverlayContainer,
161+
useClass: CustomOverlayContainer
156162
}
157163
],
158164
bootstrap: [AppComponent],
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Injectable } from '@angular/core';
2+
import { OverlayContainer } from '@angular/cdk/overlay';
3+
4+
/**
5+
* Custom OverlayContainer that works with Angular 21's native popover API.
6+
*
7+
* Angular 21 uses the browser's top layer for modals (via popover="manual"),
8+
* which means CDK overlays (dropdowns, tooltips) appear behind modals.
9+
*
10+
* This service dynamically moves the overlay container inside the active modal
11+
* so overlays render above the modal content.
12+
*
13+
* Based on: https://qupaya.com/blog/using-the-angular-cdk-overlay-with-native-dialogs/
14+
* Issue: https://github.com/angular/components/issues/28133
15+
*/
16+
@Injectable({ providedIn: 'root' })
17+
export class CustomOverlayContainer extends OverlayContainer {
18+
private containerStack: HTMLElement[] = [];
19+
20+
/**
21+
* Set a custom parent element for the overlay container.
22+
* Call this when opening a modal to move overlays inside it.
23+
*
24+
* @param element The modal/dialog element to append overlays to
25+
*/
26+
public setContainerParent(element: HTMLElement): void {
27+
// Save current parent to stack for restoration
28+
if (this._containerElement) {
29+
this.containerStack.push(this._containerElement.parentElement as HTMLElement);
30+
}
31+
32+
// Move overlay container to the new parent (modal)
33+
if (this._containerElement && element) {
34+
element.appendChild(this._containerElement);
35+
}
36+
}
37+
38+
/**
39+
* Restore the overlay container to its previous parent.
40+
* Call this when closing a modal.
41+
*/
42+
public restoreContainerParent(): void {
43+
if (!this._containerElement) {
44+
return;
45+
}
46+
47+
const previousParent = this.containerStack.pop();
48+
if (previousParent) {
49+
previousParent.appendChild(this._containerElement);
50+
} else {
51+
// Fallback to body if no previous parent
52+
document.body.appendChild(this._containerElement);
53+
}
54+
}
55+
56+
/**
57+
* Override to ensure container is created in body by default
58+
*/
59+
protected override _createContainer(): void {
60+
super._createContainer();
61+
62+
// Ensure it starts in the body
63+
if (this._containerElement && !this._containerElement.parentElement) {
64+
document.body.appendChild(this._containerElement);
65+
}
66+
}
67+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Injectable, ComponentType, inject } from '@angular/core';
2+
import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
3+
import { CustomOverlayContainer } from './custom-overlay-container.service';
4+
import { filter, take } from 'rxjs/operators';
5+
6+
/**
7+
* Wrapper service for MatDialog that automatically manages overlay container
8+
* placement for Angular 21's popover top layer.
9+
*
10+
* This service ensures dropdowns and overlays within dialogs appear correctly
11+
* by moving the CDK overlay container inside the dialog element.
12+
*/
13+
@Injectable({ providedIn: 'root' })
14+
export class DialogWrapperService {
15+
private dialog = inject(MatDialog);
16+
private overlayContainer = inject(CustomOverlayContainer);
17+
18+
/**
19+
* Opens a modal dialog and manages overlay container placement.
20+
*
21+
* @param component The component to display in the dialog
22+
* @param config Dialog configuration options
23+
* @returns DialogRef for the opened dialog
24+
*/
25+
public open<T, D = any, R = any>(
26+
component: ComponentType<T>,
27+
config?: MatDialogConfig<D>
28+
): MatDialogRef<T, R> {
29+
const dialogRef = this.dialog.open(component, config);
30+
31+
// Wait for dialog to open and DOM to be ready
32+
setTimeout(() => {
33+
// Find the dialog container element
34+
const dialogElement = document.querySelector('.mat-mdc-dialog-container');
35+
if (dialogElement) {
36+
// Move overlay container inside this dialog
37+
this.overlayContainer.setContainerParent(dialogElement as HTMLElement);
38+
}
39+
}, 0);
40+
41+
// Restore overlay container when dialog closes
42+
dialogRef.afterClosed().pipe(
43+
take(1)
44+
).subscribe(() => {
45+
this.overlayContainer.restoreContainerParent();
46+
});
47+
48+
return dialogRef;
49+
}
50+
51+
/**
52+
* Closes all currently open dialogs
53+
*/
54+
public closeAll(): void {
55+
this.dialog.closeAll();
56+
}
57+
58+
/**
59+
* Gets a reference to an open dialog by ID
60+
*/
61+
public getDialogById(id: string): MatDialogRef<any> | undefined {
62+
return this.dialog.getDialogById(id);
63+
}
64+
}

0 commit comments

Comments
 (0)