Skip to content

Commit 627daf9

Browse files
authored
fix(cdk/overlay): detach overlay when portal is destroyed from the outside (#25212)
Fixes that some of the overlay elements were left behind when the portal is destroyed without going through the overlay API. We need to handle this case, because in many cases users don't have access to the `OverlayRef` (e.g. `MatDialog`) so that they can call `dispose` themselves. Fixes #25163.
1 parent 4342387 commit 627daf9

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

src/cdk/overlay/overlay-ref.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,26 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
158158
}
159159

160160
this._outsideClickDispatcher.add(this);
161+
162+
// TODO(crisbeto): the null check is here, because the portal outlet returns `any`.
163+
// We should be guaranteed for the result to be `ComponentRef | EmbeddedViewRef`, but
164+
// `instanceof EmbeddedViewRef` doesn't appear to work at the moment.
165+
if (typeof attachResult?.onDestroy === 'function') {
166+
// In most cases we control the portal and we know when it is being detached so that
167+
// we can finish the disposal process. The exception is if the user passes in a custom
168+
// `ViewContainerRef` that isn't destroyed through the overlay API. Note that we use
169+
// `detach` here instead of `dispose`, because we don't know if the user intends to
170+
// reattach the overlay at a later point. It also has the advantage of waiting for animations.
171+
attachResult.onDestroy(() => {
172+
if (this.hasAttached()) {
173+
// We have to delay the `detach` call, because detaching immediately prevents
174+
// other destroy hooks from running. This is likely a framework bug similar to
175+
// https://github.com/angular/angular/issues/46119
176+
this._ngZone.runOutsideAngular(() => Promise.resolve().then(() => this.detach()));
177+
}
178+
});
179+
}
180+
161181
return attachResult;
162182
}
163183

src/cdk/overlay/overlay.spec.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import {waitForAsync, fakeAsync, tick, ComponentFixture, TestBed} from '@angular/core/testing';
1+
import {
2+
waitForAsync,
3+
fakeAsync,
4+
tick,
5+
ComponentFixture,
6+
TestBed,
7+
flush,
8+
} from '@angular/core/testing';
29
import {
310
Component,
411
ViewChild,
@@ -463,6 +470,36 @@ describe('Overlay', () => {
463470
expect(() => overlayRef.removePanelClass([''])).not.toThrow();
464471
});
465472

473+
it('should detach a component-based overlay when the view is destroyed', fakeAsync(() => {
474+
const overlayRef = overlay.create();
475+
const paneElement = overlayRef.overlayElement;
476+
477+
overlayRef.attach(componentPortal);
478+
viewContainerFixture.detectChanges();
479+
480+
expect(paneElement.childNodes.length).not.toBe(0);
481+
482+
viewContainerFixture.destroy();
483+
flush();
484+
485+
expect(paneElement.childNodes.length).toBe(0);
486+
}));
487+
488+
it('should detach a template-based overlay when the view is destroyed', fakeAsync(() => {
489+
const overlayRef = overlay.create();
490+
const paneElement = overlayRef.overlayElement;
491+
492+
overlayRef.attach(templatePortal);
493+
viewContainerFixture.detectChanges();
494+
495+
expect(paneElement.childNodes.length).not.toBe(0);
496+
497+
viewContainerFixture.destroy();
498+
flush();
499+
500+
expect(paneElement.childNodes.length).toBe(0);
501+
}));
502+
466503
describe('positioning', () => {
467504
let config: OverlayConfig;
468505

0 commit comments

Comments
 (0)