Skip to content

Commit bbe6355

Browse files
authored
fix(cdk/overlay): backdrop timeouts not being cleared in some cases (#23972)
When we start the detach sequence of a backdrop, we bind a `transitionend` event as well as a 500ms timeout in case the event doesn't fire. If the event fires before 500ms, we clear the timeout so users don't have to flush it in tests, however we had the following problems: 1. Transparent backdrops don't have a transition so they were always being cleaned up after the 500ms timeout. 2. We weren't clearing the timeout if the overlay was disposed while the backdrop transition is running. This meant that the timeout would still be in memory, even though the element was removed from the DOM. 3. We had a memory leak where the `click` and `transitionend` events weren't being removed from the backdrop if the overlay is disposed while detaching. Basically all Material components had one of these two issues. These changes resolve the problems by: 1. Clearing the timeout when the overlay is disposed. 2. Setting a 1ms transition on the transparent overlay so its `transitionend` can fire (almost) instantly. 3. Removing the `click` and `transitionend` events when the backdrop is disposed.
1 parent 5db1df0 commit bbe6355

File tree

3 files changed

+35
-19
lines changed

3 files changed

+35
-19
lines changed

src/cdk/overlay/_index.scss

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,18 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
9696
}
9797

9898
.cdk-overlay-transparent-backdrop {
99+
// Define a transition on the visibility so that the `transitionend` event can fire immediately.
100+
transition: visibility 1ms linear, opacity 1ms linear;
101+
visibility: hidden;
102+
opacity: 1;
103+
99104
// Note: as of Firefox 57, having the backdrop be `background: none` will prevent it from
100105
// capturing the user's mouse scroll events. Since we also can't use something like
101106
// `rgba(0, 0, 0, 0)`, we work around the inconsistency by not setting the background at
102107
// all and using `opacity` to make the element transparent.
103-
&, &.cdk-overlay-backdrop-showing {
108+
&.cdk-overlay-backdrop-showing {
104109
opacity: 0;
110+
visibility: visible;
105111
}
106112
}
107113

src/cdk/overlay/overlay-ref.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,17 @@ export type ImmutableObject<T> = {
3131
*/
3232
export class OverlayRef implements PortalOutlet, OverlayReference {
3333
private _backdropElement: HTMLElement | null = null;
34+
private _backdropTimeout: number | undefined;
3435
private readonly _backdropClick = new Subject<MouseEvent>();
3536
private readonly _attachments = new Subject<void>();
3637
private readonly _detachments = new Subject<void>();
3738
private _positionStrategy: PositionStrategy | undefined;
3839
private _scrollStrategy: ScrollStrategy | undefined;
3940
private _locationChanges: SubscriptionLike = Subscription.EMPTY;
4041
private _backdropClickHandler = (event: MouseEvent) => this._backdropClick.next(event);
42+
private _backdropTransitionendHandler = (event: TransitionEvent) => {
43+
this._disposeBackdrop(event.target as HTMLElement | null);
44+
};
4145

4246
/**
4347
* Reference to the parent of the `_host` at the time it was detached. Used to restore
@@ -418,26 +422,10 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
418422
return;
419423
}
420424

421-
let timeoutId: number;
422-
const finishDetach = () => {
423-
// It may not be attached to anything in certain cases (e.g. unit tests).
424-
if (backdropToDetach) {
425-
backdropToDetach.removeEventListener('click', this._backdropClickHandler);
426-
backdropToDetach.removeEventListener('transitionend', finishDetach);
427-
this._disposeBackdrop(backdropToDetach);
428-
}
429-
430-
if (this._config.backdropClass) {
431-
this._toggleClasses(backdropToDetach!, this._config.backdropClass, false);
432-
}
433-
434-
clearTimeout(timeoutId);
435-
};
436-
437425
backdropToDetach.classList.remove('cdk-overlay-backdrop-showing');
438426

439427
this._ngZone.runOutsideAngular(() => {
440-
backdropToDetach!.addEventListener('transitionend', finishDetach);
428+
backdropToDetach!.addEventListener('transitionend', this._backdropTransitionendHandler);
441429
});
442430

443431
// If the backdrop doesn't have a transition, the `transitionend` event won't fire.
@@ -447,7 +435,12 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
447435
// Run this outside the Angular zone because there's nothing that Angular cares about.
448436
// If it were to run inside the Angular zone, every test that used Overlay would have to be
449437
// either async or fakeAsync.
450-
timeoutId = this._ngZone.runOutsideAngular(() => setTimeout(finishDetach, 500));
438+
this._backdropTimeout = this._ngZone.runOutsideAngular(() =>
439+
setTimeout(() => {
440+
console.log('fallback');
441+
this._disposeBackdrop(backdropToDetach);
442+
}, 500),
443+
);
451444
}
452445

453446
/** Toggles a single CSS class or an array of classes on an element. */
@@ -505,6 +498,8 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
505498
/** Removes a backdrop element from the DOM. */
506499
private _disposeBackdrop(backdrop: HTMLElement | null) {
507500
if (backdrop) {
501+
backdrop.removeEventListener('click', this._backdropClickHandler);
502+
backdrop.removeEventListener('transitionend', this._backdropTransitionendHandler);
508503
backdrop.remove();
509504

510505
// It is possible that a new portal has been attached to this overlay since we started
@@ -514,6 +509,11 @@ export class OverlayRef implements PortalOutlet, OverlayReference {
514509
this._backdropElement = null;
515510
}
516511
}
512+
513+
if (this._backdropTimeout) {
514+
clearTimeout(this._backdropTimeout);
515+
this._backdropTimeout = undefined;
516+
}
517517
}
518518
}
519519

src/cdk/overlay/overlay.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,16 @@ describe('Overlay', () => {
348348
// `fakeAsync` will throw if we have an unflushed timer.
349349
}));
350350

351+
it('should clear the backdrop timeout if the overlay is disposed', fakeAsync(() => {
352+
const overlayRef = overlay.create({hasBackdrop: true});
353+
overlayRef.attach(componentPortal);
354+
overlayRef.detach();
355+
overlayRef.dispose();
356+
357+
// Note: we don't `tick` or `flush` here. The assertion is that
358+
// `fakeAsync` will throw if we have an unflushed timer.
359+
}));
360+
351361
it('should be able to use the `Overlay` provider during app initialization', () => {
352362
/** Dummy provider that depends on `Overlay`. */
353363
@Injectable()

0 commit comments

Comments
 (0)