Skip to content

Commit 9f1ab9e

Browse files
committed
fix(tooltip): decouple removal logic from change detection
Currently the logic in the tooltip that removes it from the DOM is run either if the trigger is destroyed or the exit animation has finished. The problem is that if the trigger is detached from change detection, but hasn't been destroyed, the exit animation will never run and the element won't be cleaned up. These changes switch to using CSS animations and manipulating the DOM node directly to trigger the animation. Fixes #19365.
1 parent 294b8ee commit 9f1ab9e

File tree

7 files changed

+179
-130
lines changed

7 files changed

+179
-130
lines changed

src/material/tooltip/BUILD.bazel

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ ng_module(
2929
"//src/cdk/portal",
3030
"//src/cdk/scrolling",
3131
"//src/material/core",
32-
"@npm//@angular/animations",
3332
"@npm//@angular/common",
3433
"@npm//@angular/core",
3534
"@npm//rxjs",
@@ -65,7 +64,6 @@ ng_test_library(
6564
"//src/cdk/overlay",
6665
"//src/cdk/platform",
6766
"//src/cdk/testing/private",
68-
"@npm//@angular/animations",
6967
"@npm//@angular/platform-browser",
7068
],
7169
)

src/material/tooltip/testing/tooltip-harness.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,13 @@ export class MatTooltipHarness extends ComponentHarness {
3131

3232
/** Hides the tooltip. */
3333
async hide(): Promise<void> {
34-
const host = await this.host();
35-
await host.mouseAway();
36-
await this.forceStabilize(); // Needed in order to flush the `hide` animation.
34+
return (await this.host()).mouseAway();
3735
}
3836

3937
/** Gets whether the tooltip is open. */
4038
async isOpen(): Promise<boolean> {
41-
return !!(await this._optionalPanel());
39+
const panel = await this._optionalPanel();
40+
return !!panel && !(await panel.hasClass('mat-tooltip-hide'));
4241
}
4342

4443
/** Gets a promise for the tooltip panel's text. */

src/material/tooltip/tooltip.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
<div class="mat-tooltip"
1+
<div #tooltip
2+
class="mat-tooltip"
23
[ngClass]="tooltipClass"
4+
[class._mat-animation-noopable]="_animationMode === 'NoopAnimations'"
35
[class.mat-tooltip-handset]="(_isHandset | async)?.matches"
4-
[@state]="_visibility"
5-
(@state.start)="_animationStart()"
6-
(@state.done)="_animationDone($event)">{{message}}</div>
6+
(animationend)="_animationEnd($event)">{{message}}</div>

src/material/tooltip/tooltip.scss

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ $mat-tooltip-handset-margin: 24px;
2424
padding-right: $mat-tooltip-horizontal-padding;
2525
overflow: hidden;
2626
text-overflow: ellipsis;
27+
opacity: 0;
28+
transform: scale(0);
29+
30+
// Use a very short animation if animations are disabled so the `animationend` event still fires.
31+
&._mat-animation-noopable {
32+
animation-duration: 1ms;
33+
}
2734

2835
@include cdk-high-contrast(active, off) {
2936
outline: solid 1px;
@@ -35,3 +42,38 @@ $mat-tooltip-handset-margin: 24px;
3542
padding-left: $mat-tooltip-handset-horizontal-padding;
3643
padding-right: $mat-tooltip-handset-horizontal-padding;
3744
}
45+
46+
@keyframes mat-tooltip-show {
47+
0% {
48+
opacity: 0;
49+
transform: scale(0);
50+
}
51+
52+
50% {
53+
opacity: 0.5;
54+
transform: scale(0.99);
55+
}
56+
57+
100% {
58+
opacity: 1;
59+
transform: scale(1);
60+
}
61+
}
62+
63+
@keyframes mat-tooltip-hide {
64+
0% {
65+
opacity: 1;
66+
}
67+
68+
100% {
69+
opacity: 0;
70+
}
71+
}
72+
73+
.mat-tooltip-show {
74+
animation: mat-tooltip-show 200ms cubic-bezier(0, 0, 0.2, 1) forwards;
75+
}
76+
77+
.mat-tooltip-hide {
78+
animation: mat-tooltip-hide 100ms cubic-bezier(0, 0, 0.2, 1) forwards;
79+
}

0 commit comments

Comments
 (0)