I recently worked on a project where it was necessary to create some widgets due to the UI requirements. Among those were calendar and select widgets for which we used the Angular CDK overlay due to features like following the element when scolling the page. The project also required a variety of dialogs, where we decided to use the native browser dialog, because it’s easy to style and easy to use.
The Problem
What we didn’t know at the time of making those decisions was the fact, that the CDK overlay doens’t work well with the native Dialog. The problem, for which there has been a github issue since 2022, is that Material popups shown from native HTML dialog appear behind dialog
. We ran into this problem when we tried to use our calendar widget in a dialog. The root of the issue is the way that the two of them work.
The following picture shows what is required.
What initially happens is shown next.
Let’s start with the overlay. When using a CDK overlay in a web app, the CDK overlay requests a CDK overlay container from the OverlayContainer
class. The container is an HTML div
element with the class cdk-overlay-container
at the end of the HTML body
. The OverlayContainer
class creates this element the first time it is required. The container element has a default z-index
of 1000, which causes the overlay to appear usually over all other elements. The dialog is one exception to this.
The reason why the overlay is not diplayed over the dialog, even though it is part of it, is that the dialog is displayed in a rather special layer, the top layer. This layer and it’s content is really displayed on top of everything else on the page.
Because the overlay puts its container in the body
and not in the dialog
, it is displayed under the top layer and, therefore under the dialog.
The solution
At this point, I’d like to give thanks to the qupaya team because analysis and solution finding were a team effort, as well as the GitHub community for their work and ideas on this issue.
Our guiding idea was that we wanted to be able to make overlays open inside the dialog
element if a dialog is open. To achieve this, we needed to make the overlays use a different overlay container.
Therefore our first step was to override CDKs OverlayContainer
class. We created an extension that would manage different overlay container HTML elements. It adds a container when a dialog opens and removes it again when the dialog closes. It is also necessary to track the containers that are paused, so to say. An array is used as a stack for this purpose. Thankfully, OverlayContainer
has the overlay container as a protected writable property, so it can be changed. The following code shows the implementation.
@Injectable({ providedIn: 'root', }) export class QupayaOverlayContainer extends OverlayContainer { private readonly containerStack: HTMLElement[] = []; addContainer(container: HTMLElement): void { if (this._containerElement) { this.containerStack.push(this._containerElement); } this._containerElement = container; } removeContainer(): void { this._containerElement = this.containerStack.pop() as HTMLElement; } };
To replace the original container, it is necessary to override OverlayContainer
with QupayaOverlayContainer
. We created the following NgModule
to handle this. It needs to be added to the ApplicationConfig
providers using importProvidersFrom(QupayaModule)
.
@NgModule({ providers: [ { provide: OverlayContainer, useExisting: QupayaOverlayContainer, }, ], }) export class QupayaModule {}
Next, we needed to adjust the dialog. We already had a wrapper component for the dialog
element that handles opening and closing as well as some styling and uses ng-content
to project content into the dialog. We added another overlay container to the end of the dialog
element. The following code shows the simplified template. It’s important to use the same container class here as CDK uses because it has styling for it.
<dialog #dialogElement (close)="handleClose()"> <ng-content></ng-content> <div #dialogOverlayContainer class="cdk-overlay-container"></div> </dialog>
Finally, we needed to handle the adding and removing of containers. We attached that to the model property for opening and closing the dialog (open
). When it switches to true
, the dialog
is shown and its overlay container is added and made the current ‘active’ container. When it switches to false
, the dialog is closed and the top container element is removed from the stack. This is shown in the following code.
@Component({ selector: "qupaya-dialog", standalone: true, templateUrl: "./dialog.component.html", styleUrl: "./dialog.component.css", }) export class DialogComponent { private readonly overlayContainer = inject(QupayaOverlayContainer); private readonly dialogOverlayContainerRef = viewChild.required< ElementRef<HTMLDivElement> >("dialogOverlayContainer"); private readonly dialogElement = viewChild.required<ElementRef<HTMLDialogElement>>("dialogElement"); readonly open = model(false); readonly close = output<void>(); protected readonly openEvents = effect(() => { const dialog = untracked(this.dialogElement); const containerRef = untracked(this.dialogOverlayContainerRef); if (this.open()) { dialog.nativeElement.showModal(); this.overlayContainer.addContainer(containerRef.nativeElement); } else { dialog.nativeElement.close(); this.overlayContainer.removeContainer(); } }); handleClose(): void { this.close.emit(); this.open.set(false); } }
Potential issues
The shown solution covers a good amount of use cases. Things it doesn’t cover include
- What if there is something like a notification that pops up and should be shown under the dialog. All overlays that are added while the dialog is open will be shown in the dialog and will disappear when the dialog closes.
- We have an implementation for notifications that always uses the same overlay. It starts with the first notification and keeps the overlay open. When a dialog opens that overlay stays in the initial overlay container, so it is displayed under the dialog and it’s backdrop. Things like that could become an issue as well.
Issues like the ones mentioned here can be handled by different solutions, p.ex. moving the existing overlay container into the dialog, as mentioned in the GitHub issue comments. We used our solution to ensure that overlays opened on the page would stay under the dialog.
Summary
We learned a solution for how to use Angular CDK overlays in combination with the native dialog element. It works on the principle of overriding the CDKs OverlayContainer
class and creating a temporary overlay container in each dialog. You may encounter potential issues, as described in the last section.
We hope this article will help you if you want or need to use Angular CDK overlays with native dialogs.