6
6
* found in the LICENSE file at https://angular.dev/license
7
7
*/
8
8
9
- import { Direction , Directionality } from '../bidi' ;
10
- import { ComponentPortal , Portal , PortalOutlet , TemplatePortal } from '../portal' ;
9
+ import { Location } from '@angular/common' ;
11
10
import {
12
11
AfterRenderRef ,
13
12
ComponentRef ,
@@ -16,19 +15,17 @@ import {
16
15
NgZone ,
17
16
Renderer2 ,
18
17
afterNextRender ,
19
- afterRender ,
20
- untracked ,
21
18
} from '@angular/core' ;
22
- import { Location } from '@angular/common' ;
23
- import { Observable , Subject , merge , SubscriptionLike , Subscription } from 'rxjs' ;
24
- import { takeUntil } from 'rxjs/operators' ;
19
+ import { Observable , Subject , Subscription , SubscriptionLike } from 'rxjs' ;
20
+ import { Direction , Directionality } from '../bidi' ;
21
+ import { coerceArray , coerceCssPixelValue } from '../coercion' ;
22
+ import { ComponentPortal , Portal , PortalOutlet , TemplatePortal } from '../portal' ;
23
+ import { BackdropRef } from './backdrop-ref' ;
25
24
import { OverlayKeyboardDispatcher } from './dispatchers/overlay-keyboard-dispatcher' ;
26
25
import { OverlayOutsideClickDispatcher } from './dispatchers/overlay-outside-click-dispatcher' ;
27
26
import { OverlayConfig } from './overlay-config' ;
28
- import { coerceCssPixelValue , coerceArray } from '../coercion' ;
29
27
import { PositionStrategy } from './position/position-strategy' ;
30
28
import { ScrollStrategy } from './scroll' ;
31
- import { BackdropRef } from './backdrop-ref' ;
32
29
33
30
/** An object where all of its properties cannot be written. */
34
31
export type ImmutableObject < T > = {
@@ -47,6 +44,8 @@ export class OverlayRef implements PortalOutlet {
47
44
private _scrollStrategy : ScrollStrategy | undefined ;
48
45
private _locationChanges : SubscriptionLike = Subscription . EMPTY ;
49
46
private _backdropRef : BackdropRef | null = null ;
47
+ private _detachContentMutationObserver : MutationObserver | undefined ;
48
+ private _detachContentAfterRenderRef : AfterRenderRef | undefined ;
50
49
51
50
/**
52
51
* Reference to the parent of the `_host` at the time it was detached. Used to restore
@@ -60,10 +59,6 @@ export class OverlayRef implements PortalOutlet {
60
59
/** Stream of mouse outside events dispatched to this overlay. */
61
60
readonly _outsidePointerEvents = new Subject < MouseEvent > ( ) ;
62
61
63
- private _renders = new Subject < void > ( ) ;
64
-
65
- private _afterRenderRef : AfterRenderRef ;
66
-
67
62
/** Reference to the currently-running `afterNextRender` call. */
68
63
private _afterNextRenderRef : AfterRenderRef | undefined ;
69
64
@@ -87,18 +82,6 @@ export class OverlayRef implements PortalOutlet {
87
82
}
88
83
89
84
this . _positionStrategy = _config . positionStrategy ;
90
-
91
- // Users could open the overlay from an `effect`, in which case we need to
92
- // run the `afterRender` as `untracked`. We don't recommend that users do
93
- // this, but we also don't want to break users who are doing it.
94
- this . _afterRenderRef = untracked ( ( ) =>
95
- afterRender (
96
- ( ) => {
97
- this . _renders . next ( ) ;
98
- } ,
99
- { injector : this . _injector } ,
100
- ) ,
101
- ) ;
102
85
}
103
86
104
87
/** The overlay's HTML element */
@@ -182,6 +165,7 @@ export class OverlayRef implements PortalOutlet {
182
165
183
166
// Only emit the `attachments` event once all other setup is done.
184
167
this . _attachments . next ( ) ;
168
+ this . _completeDetachContent ( ) ;
185
169
186
170
// Track this overlay by the keyboard dispatcher
187
171
this . _keyboardDispatcher . add ( this ) ;
@@ -242,6 +226,7 @@ export class OverlayRef implements PortalOutlet {
242
226
243
227
// Only emit after everything is detached.
244
228
this . _detachments . next ( ) ;
229
+ this . _completeDetachContent ( ) ;
245
230
246
231
// Remove this overlay from keyboard dispatcher tracking.
247
232
this . _keyboardDispatcher . remove ( this ) ;
@@ -281,8 +266,7 @@ export class OverlayRef implements PortalOutlet {
281
266
}
282
267
283
268
this . _detachments . complete ( ) ;
284
- this . _afterRenderRef . destroy ( ) ;
285
- this . _renders . complete ( ) ;
269
+ this . _completeDetachContent ( ) ;
286
270
}
287
271
288
272
/** Whether the overlay has attached content. */
@@ -488,34 +472,42 @@ export class OverlayRef implements PortalOutlet {
488
472
}
489
473
}
490
474
491
- /** Detaches the overlay content next time the zone stabilizes . */
475
+ /** Detaches the overlay once the content finishes animating and is removed from the DOM . */
492
476
private _detachContentWhenEmpty ( ) {
493
- // Normally we wouldn't have to explicitly run this outside the `NgZone`, however
494
- // if the consumer is using `zone-patch-rxjs`, the `Subscription.unsubscribe` call will
495
- // be patched to run inside the zone, which will throw us into an infinite loop.
496
- this . _ngZone . runOutsideAngular ( ( ) => {
497
- // We can't remove the host here immediately, because the overlay pane's content
498
- // might still be animating. This stream helps us avoid interrupting the animation
499
- // by waiting for the pane to become empty.
500
- const subscription = this . _renders
501
- . pipe ( takeUntil ( merge ( this . _attachments , this . _detachments ) ) )
502
- . subscribe ( ( ) => {
503
- // Needs a couple of checks for the pane and host, because
504
- // they may have been removed by the time the zone stabilizes.
505
- if ( ! this . _pane || ! this . _host || this . _pane . children . length === 0 ) {
506
- if ( this . _pane && this . _config . panelClass ) {
507
- this . _toggleClasses ( this . _pane , this . _config . panelClass , false ) ;
508
- }
509
-
510
- if ( this . _host && this . _host . parentElement ) {
511
- this . _previousHostParent = this . _host . parentElement ;
512
- this . _host . remove ( ) ;
513
- }
514
-
515
- subscription . unsubscribe ( ) ;
516
- }
517
- } ) ;
477
+ // Attempt to detach on the next render.
478
+ this . _detachContentAfterRenderRef = afterNextRender ( ( ) => this . _detachContent ( ) , {
479
+ injector : this . _injector ,
518
480
} ) ;
481
+ // Otherwise wait until the content finishes animating out and detach.
482
+ if ( globalThis . MutationObserver && this . _pane ) {
483
+ this . _detachContentMutationObserver ||= new globalThis . MutationObserver ( ( ) => {
484
+ this . _detachContent ( ) ;
485
+ } ) ;
486
+ this . _detachContentMutationObserver . observe ( this . _pane , { childList : true } ) ;
487
+ }
488
+ }
489
+
490
+ private _detachContent ( ) {
491
+ // Needs a couple of checks for the pane and host, because
492
+ // they may have been removed by the time the zone stabilizes.
493
+ if ( ! this . _pane || ! this . _host || this . _pane . children . length === 0 ) {
494
+ if ( this . _pane && this . _config . panelClass ) {
495
+ this . _toggleClasses ( this . _pane , this . _config . panelClass , false ) ;
496
+ }
497
+
498
+ if ( this . _host && this . _host . parentElement ) {
499
+ this . _previousHostParent = this . _host . parentElement ;
500
+ this . _host . remove ( ) ;
501
+ }
502
+
503
+ this . _completeDetachContent ( ) ;
504
+ }
505
+ }
506
+
507
+ private _completeDetachContent ( ) {
508
+ this . _detachContentAfterRenderRef ?. destroy ( ) ;
509
+ this . _detachContentAfterRenderRef = undefined ;
510
+ this . _detachContentMutationObserver ?. disconnect ( ) ;
519
511
}
520
512
521
513
/** Disposes of a scroll strategy. */
0 commit comments