From 4b5363d160ec639443ad83d3424c9e7b19c36f54 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sun, 6 Mar 2022 19:19:22 +0100 Subject: [PATCH] fix(material/tooltip): decouple removal logic from change detection (#19432) * fix(material/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. * fixup! fix(material/tooltip): decouple removal logic from change detection (cherry picked from commit a5ab8e9adf728b908e581720cb7b8ddf8d7c6f01) --- .../mdc-tooltip/testing/tooltip-harness.ts | 3 + .../mdc-tooltip/tooltip.html | 7 +- .../mdc-tooltip/tooltip.scss | 40 ++++++ .../mdc-tooltip/tooltip.spec.ts | 114 +++++++--------- .../mdc-tooltip/tooltip.ts | 25 +++- src/material/tooltip/BUILD.bazel | 2 - .../tooltip/testing/tooltip-harness.ts | 20 ++- src/material/tooltip/tooltip.html | 9 +- src/material/tooltip/tooltip.scss | 43 ++++++ src/material/tooltip/tooltip.spec.ts | 111 +++++++-------- src/material/tooltip/tooltip.ts | 129 ++++++++++++------ .../material/tooltip-testing.md | 12 ++ tools/public_api_guard/material/tooltip.md | 23 ++-- 13 files changed, 344 insertions(+), 194 deletions(-) diff --git a/src/material-experimental/mdc-tooltip/testing/tooltip-harness.ts b/src/material-experimental/mdc-tooltip/testing/tooltip-harness.ts index 62596cba1967..2761b4ffdf5a 100644 --- a/src/material-experimental/mdc-tooltip/testing/tooltip-harness.ts +++ b/src/material-experimental/mdc-tooltip/testing/tooltip-harness.ts @@ -14,6 +14,9 @@ export class MatTooltipHarness extends _MatTooltipHarnessBase { protected _optionalPanel = this.documentRootLocatorFactory().locatorForOptional('.mat-mdc-tooltip'); static hostSelector = '.mat-mdc-tooltip-trigger'; + protected _hiddenClass = 'mat-mdc-tooltip-hide'; + protected _showAnimationName = 'mat-mdc-tooltip-show'; + protected _hideAnimationName = 'mat-mdc-tooltip-hide'; /** * Gets a `HarnessPredicate` that can be used to search diff --git a/src/material-experimental/mdc-tooltip/tooltip.html b/src/material-experimental/mdc-tooltip/tooltip.html index 6eff2c6af2c9..28b374a88f8a 100644 --- a/src/material-experimental/mdc-tooltip/tooltip.html +++ b/src/material-experimental/mdc-tooltip/tooltip.html @@ -1,9 +1,8 @@
+ (animationend)="_handleAnimationEnd($event)" + [class.mdc-tooltip--multiline]="_isMultiline">
{{message}}
diff --git a/src/material-experimental/mdc-tooltip/tooltip.scss b/src/material-experimental/mdc-tooltip/tooltip.scss index 07c0a9af2dba..5901072294cb 100644 --- a/src/material-experimental/mdc-tooltip/tooltip.scss +++ b/src/material-experimental/mdc-tooltip/tooltip.scss @@ -6,6 +6,7 @@ .mat-mdc-tooltip { // We don't use MDC's positioning so this has to be relative. position: relative; + transform: scale(0); // Increases the area of the tooltip so the user's pointer can go from the trigger directly to it. &::before { @@ -18,8 +19,47 @@ z-index: -1; position: absolute; } + + &._mat-animation-noopable { + animation: none; + transform: scale(1); + } } .mat-mdc-tooltip-panel-non-interactive { pointer-events: none; } + +// TODO(crisbeto): we may be able to use MDC directly for these animations. + +@keyframes mat-mdc-tooltip-show { + 0% { + opacity: 0; + transform: scale(0.8); + } + + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes mat-mdc-tooltip-hide { + 0% { + opacity: 1; + transform: scale(1); + } + + 100% { + opacity: 0; + transform: scale(0.8); + } +} + +.mat-mdc-tooltip-show { + animation: mat-mdc-tooltip-show 150ms cubic-bezier(0, 0, 0.2, 1) forwards; +} + +.mat-mdc-tooltip-hide { + animation: mat-mdc-tooltip-hide 75ms cubic-bezier(0.4, 0, 1, 1) forwards; +} diff --git a/src/material-experimental/mdc-tooltip/tooltip.spec.ts b/src/material-experimental/mdc-tooltip/tooltip.spec.ts index 020a0f7674c0..a07f148c8e50 100644 --- a/src/material-experimental/mdc-tooltip/tooltip.spec.ts +++ b/src/material-experimental/mdc-tooltip/tooltip.spec.ts @@ -1,4 +1,3 @@ -import {AnimationEvent} from '@angular/animations'; import {FocusMonitor} from '@angular/cdk/a11y'; import {Direction, Directionality} from '@angular/cdk/bidi'; import {ESCAPE} from '@angular/cdk/keycodes'; @@ -26,14 +25,12 @@ import { ComponentFixture, fakeAsync, flush, - flushMicrotasks, inject, TestBed, tick, waitForAsync, } from '@angular/core/testing'; import {By} from '@angular/platform-browser'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {Subject} from 'rxjs'; import { MAT_TOOLTIP_DEFAULT_OPTIONS, @@ -43,6 +40,7 @@ import { TooltipPosition, TooltipTouchGestures, } from './index'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; const initialTooltipMessage = 'initial tooltip message'; @@ -55,7 +53,7 @@ describe('MDC-based MatTooltip', () => { beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ - imports: [MatTooltipModule, OverlayModule, NoopAnimationsModule], + imports: [MatTooltipModule, OverlayModule], declarations: [ BasicTooltipDemo, ScrollableTooltipDemo, @@ -111,19 +109,19 @@ describe('MDC-based MatTooltip', () => { fixture.detectChanges(); - // wait till animation has finished - tick(500); + // Wait until animation has finished + finishCurrentTooltipAnimation(overlayContainerElement, true); - // Make sure tooltip is shown to the user and animation has finished + // Make sure tooltip is shown to the user and animation has finished. const tooltipElement = overlayContainerElement.querySelector( '.mat-mdc-tooltip', ) as HTMLElement; expect(tooltipElement instanceof HTMLElement).toBe(true); - expect(tooltipElement.style.transform).toBe('scale(1)'); + expect(tooltipElement.classList).toContain('mat-mdc-tooltip-show'); expect(overlayContainerElement.textContent).toContain(initialTooltipMessage); - // After hide called, a timeout delay is created that will to hide the tooltip. + // After hide is called, a timeout delay is created that will to hide the tooltip. const tooltipDelay = 1000; tooltipDirective.hide(tooltipDelay); expect(tooltipDirective._isTooltipVisible()).toBe(true); @@ -134,7 +132,7 @@ describe('MDC-based MatTooltip', () => { expect(tooltipDirective._isTooltipVisible()).toBe(false); // On animation complete, should expect that the tooltip has been detached. - flushMicrotasks(); + finishCurrentTooltipAnimation(overlayContainerElement, false); assertTooltipInstance(tooltipDirective, false); })); @@ -143,17 +141,17 @@ describe('MDC-based MatTooltip', () => { tick(0); expect(tooltipDirective._isTooltipVisible()).toBe(true); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); tooltipDirective._overlayRef!.detach(); tick(0); fixture.detectChanges(); expect(tooltipDirective._isTooltipVisible()).toBe(false); - flushMicrotasks(); assertTooltipInstance(tooltipDirective, false); tooltipDirective.show(); tick(0); + finishCurrentTooltipAnimation(overlayContainerElement, true); expect(tooltipDirective._isTooltipVisible()).toBe(true); })); @@ -175,7 +173,7 @@ describe('MDC-based MatTooltip', () => { it('should be able to override the default show and hide delays', fakeAsync(() => { TestBed.resetTestingModule() .configureTestingModule({ - imports: [MatTooltipModule, OverlayModule, NoopAnimationsModule], + imports: [MatTooltipModule, OverlayModule], declarations: [BasicTooltipDemo], providers: [ { @@ -212,7 +210,7 @@ describe('MDC-based MatTooltip', () => { it('should be able to override the default position', fakeAsync(() => { TestBed.resetTestingModule() .configureTestingModule({ - imports: [MatTooltipModule, OverlayModule, NoopAnimationsModule], + imports: [MatTooltipModule, OverlayModule], declarations: [TooltipDemoWithoutPositionBinding], providers: [ { @@ -421,16 +419,7 @@ describe('MDC-based MatTooltip', () => { tooltipDirective.hide(0); fixture.detectChanges(); tick(); - - // At this point the animation should be able to complete itself and trigger the - // _animationDone function, but for unknown reasons in the test infrastructure, - // this does not occur. Manually call the hook so the animation subscriptions get invoked. - tooltipDirective._tooltipInstance!._animationDone({ - fromState: 'visible', - toState: 'hidden', - totalTime: 150, - phaseName: 'done', - } as AnimationEvent); + finishCurrentTooltipAnimation(overlayContainerElement, false); expect(() => { tooltipDirective.position = 'right'; @@ -444,7 +433,7 @@ describe('MDC-based MatTooltip', () => { tooltipDirective.show(); tick(0); // Tick for the show delay (default is 0) - expect(tooltipDirective._tooltipInstance!._visibility).toBe('visible'); + expect(tooltipDirective._tooltipInstance!.isVisible()).toBe(true); fixture.detectChanges(); expect(overlayContainerElement.textContent).toContain(initialTooltipMessage); @@ -530,33 +519,21 @@ describe('MDC-based MatTooltip', () => { it('should not try to dispose the tooltip when destroyed and done hiding', fakeAsync(() => { tooltipDirective.show(); fixture.detectChanges(); - tick(150); + finishCurrentTooltipAnimation(overlayContainerElement, true); const tooltipDelay = 1000; tooltipDirective.hide(); tick(tooltipDelay); // Change the tooltip state to hidden and trigger animation start + finishCurrentTooltipAnimation(overlayContainerElement, false); - // Store the tooltip instance, which will be set to null after the button is hidden. - const tooltipInstance = tooltipDirective._tooltipInstance!; fixture.componentInstance.showButton = false; fixture.detectChanges(); - - // At this point the animation should be able to complete itself and trigger the - // _animationDone function, but for unknown reasons in the test infrastructure, - // this does not occur. Manually call this and verify that doing so does not - // throw an error. - tooltipInstance._animationDone({ - fromState: 'visible', - toState: 'hidden', - totalTime: 150, - phaseName: 'done', - } as AnimationEvent); })); it('should complete the afterHidden stream when tooltip is destroyed', fakeAsync(() => { tooltipDirective.show(); fixture.detectChanges(); - tick(150); + finishCurrentTooltipAnimation(overlayContainerElement, true); const spy = jasmine.createSpy('complete spy'); const subscription = tooltipDirective @@ -566,7 +543,7 @@ describe('MDC-based MatTooltip', () => { tooltipDirective.hide(0); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, false); expect(spy).toHaveBeenCalled(); subscription.unsubscribe(); @@ -642,7 +619,7 @@ describe('MDC-based MatTooltip', () => { tooltipDirective.show(); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); let tooltipWrapper = overlayContainerElement.querySelector( '.cdk-overlay-connected-position-bounding-box', @@ -654,13 +631,13 @@ describe('MDC-based MatTooltip', () => { tooltipDirective.hide(0); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, false); dir.value = 'ltr'; tooltipDirective.show(); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); tooltipWrapper = overlayContainerElement.querySelector( '.cdk-overlay-connected-position-bounding-box', @@ -681,7 +658,7 @@ describe('MDC-based MatTooltip', () => { tooltipDirective.show(); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); expect(tooltipDirective._isTooltipVisible()).toBe(true); expect(overlayContainerElement.textContent).toContain(initialTooltipMessage); @@ -689,7 +666,7 @@ describe('MDC-based MatTooltip', () => { document.body.click(); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, false); fixture.detectChanges(); expect(tooltipDirective._isTooltipVisible()).toBe(false); @@ -700,7 +677,7 @@ describe('MDC-based MatTooltip', () => { tooltipDirective.show(); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); expect(tooltipDirective._isTooltipVisible()).toBe(true); expect(overlayContainerElement.textContent).toContain(initialTooltipMessage); @@ -708,7 +685,7 @@ describe('MDC-based MatTooltip', () => { dispatchFakeEvent(document.body, 'auxclick'); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, false); fixture.detectChanges(); expect(tooltipDirective._isTooltipVisible()).toBe(false); @@ -723,6 +700,7 @@ describe('MDC-based MatTooltip', () => { document.body.click(); fixture.detectChanges(); tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); expect(overlayContainerElement.textContent).toContain(initialTooltipMessage); })); @@ -741,6 +719,7 @@ describe('MDC-based MatTooltip', () => { fixture.detectChanges(); tick(500); fixture.detectChanges(); + finishCurrentTooltipAnimation(overlayContainerElement, false); expect(tooltipDirective._isTooltipVisible()).toBe(false); expect(overlayContainerElement.textContent).toBe(''); @@ -822,7 +801,7 @@ describe('MDC-based MatTooltip', () => { tick(0); expect(tooltipDirective._isTooltipVisible()).toBe(true); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); const overlayRef = tooltipDirective._overlayRef!; @@ -832,7 +811,7 @@ describe('MDC-based MatTooltip', () => { tick(0); expect(tooltipDirective._isTooltipVisible()).toBe(true); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); expect(overlayRef.detach).not.toHaveBeenCalled(); })); @@ -1176,14 +1155,14 @@ describe('MDC-based MatTooltip', () => { fixture.detectChanges(); // wait until animation has finished - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); // Make sure tooltip is shown to the user and animation has finished const tooltipElement = overlayContainerElement.querySelector( '.mat-mdc-tooltip', ) as HTMLElement; expect(tooltipElement instanceof HTMLElement).toBe(true); - expect(tooltipElement.style.transform).toBe('scale(1)'); + expect(tooltipElement.classList).toContain('mat-mdc-tooltip-show'); // After hide called, a timeout delay is created that will to hide the tooltip. const tooltipDelay = 1000; @@ -1196,7 +1175,7 @@ describe('MDC-based MatTooltip', () => { expect(tooltipDirective._isTooltipVisible()).toBe(false); // On animation complete, should expect that the tooltip has been detached. - flushMicrotasks(); + finishCurrentTooltipAnimation(overlayContainerElement, false); assertTooltipInstance(tooltipDirective, false); })); @@ -1233,9 +1212,9 @@ describe('MDC-based MatTooltip', () => { assertTooltipInstance(fixture.componentInstance.tooltip, false); - tick(250); // Finish the delay. + tick(500); // Finish the delay. fixture.detectChanges(); - tick(500); // Finish the animation. + finishCurrentTooltipAnimation(overlayContainerElement, true); // Finish the animation. assertTooltipInstance(fixture.componentInstance.tooltip, true); })); @@ -1274,7 +1253,7 @@ describe('MDC-based MatTooltip', () => { fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); - tick(500); // Finish the animation. + finishCurrentTooltipAnimation(overlayContainerElement, true); // Finish the animation. assertTooltipInstance(fixture.componentInstance.tooltip, true); dispatchFakeEvent(button, 'touchend'); @@ -1284,7 +1263,7 @@ describe('MDC-based MatTooltip', () => { tick(500); // Finish the delay. fixture.detectChanges(); - tick(500); // Finish the exit animation. + finishCurrentTooltipAnimation(overlayContainerElement, false); // Finish the exit animation. assertTooltipInstance(fixture.componentInstance.tooltip, false); })); @@ -1298,7 +1277,7 @@ describe('MDC-based MatTooltip', () => { fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); - tick(500); // Finish the animation. + finishCurrentTooltipAnimation(overlayContainerElement, true); // Finish the animation. assertTooltipInstance(fixture.componentInstance.tooltip, true); dispatchFakeEvent(button, 'touchcancel'); @@ -1308,7 +1287,7 @@ describe('MDC-based MatTooltip', () => { tick(500); // Finish the delay. fixture.detectChanges(); - tick(500); // Finish the exit animation. + finishCurrentTooltipAnimation(overlayContainerElement, false); // Finish the exit animation. assertTooltipInstance(fixture.componentInstance.tooltip, false); })); @@ -1431,7 +1410,7 @@ describe('MDC-based MatTooltip', () => { fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); - tick(500); // Finish the animation. + finishCurrentTooltipAnimation(overlayContainerElement, true); assertTooltipInstance(fixture.componentInstance.tooltip, true); // Simulate the pointer at the bottom/right of the page. @@ -1445,7 +1424,7 @@ describe('MDC-based MatTooltip', () => { fixture.detectChanges(); tick(1500); // Finish the delay. fixture.detectChanges(); - tick(500); // Finish the exit animation. + finishCurrentTooltipAnimation(overlayContainerElement, false); assertTooltipInstance(fixture.componentInstance.tooltip, false); })); @@ -1464,7 +1443,7 @@ describe('MDC-based MatTooltip', () => { fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); - tick(500); // Finish the animation. + finishCurrentTooltipAnimation(overlayContainerElement, true); assertTooltipInstance(fixture.componentInstance.tooltip, true); // Simulate the pointer over the trigger. @@ -1479,7 +1458,7 @@ describe('MDC-based MatTooltip', () => { fixture.detectChanges(); tick(1500); // Finish the delay. fixture.detectChanges(); - tick(500); // Finish the exit animation. + finishCurrentTooltipAnimation(overlayContainerElement, false); assertTooltipInstance(fixture.componentInstance.tooltip, true); })); @@ -1621,3 +1600,12 @@ function assertTooltipInstance(tooltip: MatTooltip, shouldExist: boolean): void // happens due to the `_tooltipInstance` having a circular structure. expect(!!tooltip._tooltipInstance).toBe(shouldExist); } + +function finishCurrentTooltipAnimation(overlayContainer: HTMLElement, isVisible: boolean) { + const tooltip = overlayContainer.querySelector('.mat-mdc-tooltip')!; + const event = createFakeEvent('animationend'); + Object.defineProperty(event, 'animationName', { + get: () => `mat-mdc-tooltip-${isVisible ? 'show' : 'hide'}`, + }); + dispatchEvent(tooltip, event); +} diff --git a/src/material-experimental/mdc-tooltip/tooltip.ts b/src/material-experimental/mdc-tooltip/tooltip.ts index cc0a49ba4d42..c08addbf7f2b 100644 --- a/src/material-experimental/mdc-tooltip/tooltip.ts +++ b/src/material-experimental/mdc-tooltip/tooltip.ts @@ -15,11 +15,13 @@ import { Inject, NgZone, Optional, + ViewChild, ViewContainerRef, ViewEncapsulation, } from '@angular/core'; import {DOCUMENT} from '@angular/common'; import {Platform} from '@angular/cdk/platform'; +import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; import {AriaDescriber, FocusMonitor} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {ConnectedPosition, Overlay, ScrollDispatcher} from '@angular/cdk/overlay'; @@ -31,7 +33,6 @@ import { _TooltipComponentBase, } from '@angular/material/tooltip'; import {numbers} from '@material/tooltip'; -import {matTooltipAnimations} from './tooltip-animations'; /** * CSS class that will be attached to the overlay panel. @@ -116,11 +117,10 @@ export class MatTooltip extends _MatTooltipBase { styleUrls: ['tooltip.css'], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, - animations: [matTooltipAnimations.tooltipState], host: { // Forces the element to have a layout in IE and Edge. This fixes issues where the element // won't be rendered if the animations are disabled or there is no web animations polyfill. - '[style.zoom]': '_visibility === "visible" ? 1 : null', + '[style.zoom]': 'isVisible() ? 1 : null', '(mouseleave)': '_handleMouseLeave($event)', 'aria-hidden': 'true', }, @@ -129,12 +129,27 @@ export class TooltipComponent extends _TooltipComponentBase { /* Whether the tooltip text overflows to multiple lines */ _isMultiline = false; - constructor(changeDetectorRef: ChangeDetectorRef, private _elementRef: ElementRef) { - super(changeDetectorRef); + /** Reference to the internal tooltip element. */ + @ViewChild('tooltip', { + // Use a static query here since we interact directly with + // the DOM which can happen before `ngAfterViewInit`. + static: true, + }) + _tooltip: ElementRef; + _showAnimation = 'mat-mdc-tooltip-show'; + _hideAnimation = 'mat-mdc-tooltip-hide'; + + constructor( + changeDetectorRef: ChangeDetectorRef, + private _elementRef: ElementRef, + @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string, + ) { + super(changeDetectorRef, animationMode); } protected override _onShow(): void { this._isMultiline = this._isTooltipMultiline(); + this._markForCheck(); } /** Whether the tooltip text has overflown to the next line */ diff --git a/src/material/tooltip/BUILD.bazel b/src/material/tooltip/BUILD.bazel index deb3e9d9fe9b..656cf3342018 100644 --- a/src/material/tooltip/BUILD.bazel +++ b/src/material/tooltip/BUILD.bazel @@ -29,7 +29,6 @@ ng_module( "//src/cdk/portal", "//src/cdk/scrolling", "//src/material/core", - "@npm//@angular/animations", "@npm//@angular/common", "@npm//@angular/core", "@npm//rxjs", @@ -65,7 +64,6 @@ ng_test_library( "//src/cdk/overlay", "//src/cdk/platform", "//src/cdk/testing/private", - "@npm//@angular/animations", "@npm//@angular/platform-browser", "@npm//rxjs", ], diff --git a/src/material/tooltip/testing/tooltip-harness.ts b/src/material/tooltip/testing/tooltip-harness.ts index 6940760d8752..bd1ce13c833f 100644 --- a/src/material/tooltip/testing/tooltip-harness.ts +++ b/src/material/tooltip/testing/tooltip-harness.ts @@ -16,6 +16,9 @@ import {TooltipHarnessFilters} from './tooltip-harness-filters'; export abstract class _MatTooltipHarnessBase extends ComponentHarness { protected abstract _optionalPanel: AsyncFactoryFn; + protected abstract _hiddenClass: string; + protected abstract _showAnimationName: string; + protected abstract _hideAnimationName: string; /** Shows the tooltip. */ async show(): Promise { @@ -24,9 +27,10 @@ export abstract class _MatTooltipHarnessBase extends ComponentHarness { // We need to dispatch both `touchstart` and a hover event, because the tooltip binds // different events depending on the device. The `changedTouches` is there in case the // element has ripples. - // @breaking-change 12.0.0 Remove null assertion from `dispatchEvent`. - await host.dispatchEvent?.('touchstart', {changedTouches: []}); + await host.dispatchEvent('touchstart', {changedTouches: []}); await host.hover(); + const panel = await this._optionalPanel(); + await panel?.dispatchEvent('animationend', {animationName: this._showAnimationName}); } /** Hides the tooltip. */ @@ -35,15 +39,16 @@ export abstract class _MatTooltipHarnessBase extends ComponentHarness { // We need to dispatch both `touchstart` and a hover event, because // the tooltip binds different events depending on the device. - // @breaking-change 12.0.0 Remove null assertion from `dispatchEvent`. - await host.dispatchEvent?.('touchend'); + await host.dispatchEvent('touchend'); await host.mouseAway(); - await this.forceStabilize(); // Needed in order to flush the `hide` animation. + const panel = await this._optionalPanel(); + await panel?.dispatchEvent('animationend', {animationName: this._hideAnimationName}); } /** Gets whether the tooltip is open. */ async isOpen(): Promise { - return !!(await this._optionalPanel()); + const panel = await this._optionalPanel(); + return !!panel && !(await panel.hasClass(this._hiddenClass)); } /** Gets a promise for the tooltip panel's text. */ @@ -56,6 +61,9 @@ export abstract class _MatTooltipHarnessBase extends ComponentHarness { /** Harness for interacting with a standard mat-tooltip in tests. */ export class MatTooltipHarness extends _MatTooltipHarnessBase { protected _optionalPanel = this.documentRootLocatorFactory().locatorForOptional('.mat-tooltip'); + protected _hiddenClass = 'mat-tooltip-hide'; + protected _showAnimationName = 'mat-tooltip-show'; + protected _hideAnimationName = 'mat-tooltip-hide'; static hostSelector = '.mat-tooltip-trigger'; /** diff --git a/src/material/tooltip/tooltip.html b/src/material/tooltip/tooltip.html index feaaaf53352f..cf6bd74a0592 100644 --- a/src/material/tooltip/tooltip.html +++ b/src/material/tooltip/tooltip.html @@ -1,6 +1,5 @@ -
{{message}}
+ [class.mat-tooltip-handset]="(_isHandset | async)?.matches">{{message}} diff --git a/src/material/tooltip/tooltip.scss b/src/material/tooltip/tooltip.scss index 1d4c9eeb4b64..1fadd1358a97 100644 --- a/src/material/tooltip/tooltip.scss +++ b/src/material/tooltip/tooltip.scss @@ -16,6 +16,12 @@ $handset-margin: 24px; padding-right: $horizontal-padding; overflow: hidden; text-overflow: ellipsis; + transform: scale(0); + + &._mat-animation-noopable { + animation: none; + transform: scale(1); + } @include a11y.high-contrast(active, off) { outline: solid 1px; @@ -31,3 +37,40 @@ $handset-margin: 24px; .mat-tooltip-panel-non-interactive { pointer-events: none; } + +@keyframes mat-tooltip-show { + 0% { + opacity: 0; + transform: scale(0); + } + + 50% { + opacity: 0.5; + transform: scale(0.99); + } + + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes mat-tooltip-hide { + 0% { + opacity: 1; + transform: scale(1); + } + + 100% { + opacity: 0; + transform: scale(1); + } +} + +.mat-tooltip-show { + animation: mat-tooltip-show 200ms cubic-bezier(0, 0, 0.2, 1) forwards; +} + +.mat-tooltip-hide { + animation: mat-tooltip-hide 100ms cubic-bezier(0, 0, 0.2, 1) forwards; +} diff --git a/src/material/tooltip/tooltip.spec.ts b/src/material/tooltip/tooltip.spec.ts index 759f642475a3..ec96e22c6c16 100644 --- a/src/material/tooltip/tooltip.spec.ts +++ b/src/material/tooltip/tooltip.spec.ts @@ -1,4 +1,3 @@ -import {AnimationEvent} from '@angular/animations'; import {FocusMonitor} from '@angular/cdk/a11y'; import {Direction, Directionality} from '@angular/cdk/bidi'; import {ESCAPE} from '@angular/cdk/keycodes'; @@ -26,14 +25,12 @@ import { ComponentFixture, fakeAsync, flush, - flushMicrotasks, inject, TestBed, tick, waitForAsync, } from '@angular/core/testing'; import {By} from '@angular/platform-browser'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {Subject} from 'rxjs'; import { MAT_TOOLTIP_DEFAULT_OPTIONS, @@ -43,6 +40,7 @@ import { TooltipPosition, TooltipTouchGestures, } from './index'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; const initialTooltipMessage = 'initial tooltip message'; @@ -55,7 +53,7 @@ describe('MatTooltip', () => { beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ - imports: [MatTooltipModule, OverlayModule, NoopAnimationsModule], + imports: [MatTooltipModule, OverlayModule], declarations: [ BasicTooltipDemo, ScrollableTooltipDemo, @@ -111,13 +109,13 @@ describe('MatTooltip', () => { fixture.detectChanges(); - // Wait until the animation has finished. - tick(500); + // Wait until animation has finished + finishCurrentTooltipAnimation(overlayContainerElement, true); // Make sure tooltip is shown to the user and animation has finished. const tooltipElement = overlayContainerElement.querySelector('.mat-tooltip') as HTMLElement; expect(tooltipElement instanceof HTMLElement).toBe(true); - expect(tooltipElement.style.transform).toBe('scale(1)'); + expect(tooltipElement.classList).toContain('mat-tooltip-show'); expect(overlayContainerElement.textContent).toContain(initialTooltipMessage); @@ -132,7 +130,7 @@ describe('MatTooltip', () => { expect(tooltipDirective._isTooltipVisible()).toBe(false); // On animation complete, should expect that the tooltip has been detached. - flushMicrotasks(); + finishCurrentTooltipAnimation(overlayContainerElement, false); assertTooltipInstance(tooltipDirective, false); })); @@ -141,17 +139,17 @@ describe('MatTooltip', () => { tick(0); expect(tooltipDirective._isTooltipVisible()).toBe(true); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); tooltipDirective._overlayRef!.detach(); tick(0); fixture.detectChanges(); expect(tooltipDirective._isTooltipVisible()).toBe(false); - flushMicrotasks(); assertTooltipInstance(tooltipDirective, false); tooltipDirective.show(); tick(0); + finishCurrentTooltipAnimation(overlayContainerElement, true); expect(tooltipDirective._isTooltipVisible()).toBe(true); })); @@ -173,7 +171,7 @@ describe('MatTooltip', () => { it('should be able to override the default show and hide delays', fakeAsync(() => { TestBed.resetTestingModule() .configureTestingModule({ - imports: [MatTooltipModule, OverlayModule, NoopAnimationsModule], + imports: [MatTooltipModule, OverlayModule], declarations: [BasicTooltipDemo], providers: [ { @@ -210,7 +208,7 @@ describe('MatTooltip', () => { it('should be able to override the default position', fakeAsync(() => { TestBed.resetTestingModule() .configureTestingModule({ - imports: [MatTooltipModule, OverlayModule, NoopAnimationsModule], + imports: [MatTooltipModule, OverlayModule], declarations: [TooltipDemoWithoutPositionBinding], providers: [ { @@ -419,16 +417,7 @@ describe('MatTooltip', () => { tooltipDirective.hide(0); fixture.detectChanges(); tick(); - - // At this point the animation should be able to complete itself and trigger the - // _animationDone function, but for unknown reasons in the test infrastructure, - // this does not occur. Manually call the hook so the animation subscriptions get invoked. - tooltipDirective._tooltipInstance!._animationDone({ - fromState: 'visible', - toState: 'hidden', - totalTime: 150, - phaseName: 'done', - } as AnimationEvent); + finishCurrentTooltipAnimation(overlayContainerElement, false); expect(() => { tooltipDirective.position = 'right'; @@ -442,7 +431,7 @@ describe('MatTooltip', () => { tooltipDirective.show(); tick(0); // Tick for the show delay (default is 0) - expect(tooltipDirective._tooltipInstance!._visibility).toBe('visible'); + expect(tooltipDirective._tooltipInstance!.isVisible()).toBe(true); fixture.detectChanges(); expect(overlayContainerElement.textContent).toContain(initialTooltipMessage); @@ -526,33 +515,21 @@ describe('MatTooltip', () => { it('should not try to dispose the tooltip when destroyed and done hiding', fakeAsync(() => { tooltipDirective.show(); fixture.detectChanges(); - tick(150); + finishCurrentTooltipAnimation(overlayContainerElement, true); const tooltipDelay = 1000; tooltipDirective.hide(); tick(tooltipDelay); // Change the tooltip state to hidden and trigger animation start + finishCurrentTooltipAnimation(overlayContainerElement, false); - // Store the tooltip instance, which will be set to null after the button is hidden. - const tooltipInstance = tooltipDirective._tooltipInstance!; fixture.componentInstance.showButton = false; fixture.detectChanges(); - - // At this point the animation should be able to complete itself and trigger the - // _animationDone function, but for unknown reasons in the test infrastructure, - // this does not occur. Manually call this and verify that doing so does not - // throw an error. - tooltipInstance._animationDone({ - fromState: 'visible', - toState: 'hidden', - totalTime: 150, - phaseName: 'done', - } as AnimationEvent); })); it('should complete the afterHidden stream when tooltip is destroyed', fakeAsync(() => { tooltipDirective.show(); fixture.detectChanges(); - tick(150); + finishCurrentTooltipAnimation(overlayContainerElement, true); const spy = jasmine.createSpy('complete spy'); const subscription = tooltipDirective @@ -562,7 +539,7 @@ describe('MatTooltip', () => { tooltipDirective.hide(0); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, false); expect(spy).toHaveBeenCalled(); subscription.unsubscribe(); @@ -638,7 +615,7 @@ describe('MatTooltip', () => { tooltipDirective.show(); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); let tooltipWrapper = overlayContainerElement.querySelector( '.cdk-overlay-connected-position-bounding-box', @@ -650,13 +627,13 @@ describe('MatTooltip', () => { tooltipDirective.hide(0); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, false); dir.value = 'ltr'; tooltipDirective.show(); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); tooltipWrapper = overlayContainerElement.querySelector( '.cdk-overlay-connected-position-bounding-box', @@ -677,7 +654,7 @@ describe('MatTooltip', () => { tooltipDirective.show(); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); expect(tooltipDirective._isTooltipVisible()).toBe(true); expect(overlayContainerElement.textContent).toContain(initialTooltipMessage); @@ -685,7 +662,7 @@ describe('MatTooltip', () => { document.body.click(); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, false); fixture.detectChanges(); expect(tooltipDirective._isTooltipVisible()).toBe(false); @@ -696,7 +673,7 @@ describe('MatTooltip', () => { tooltipDirective.show(); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); expect(tooltipDirective._isTooltipVisible()).toBe(true); expect(overlayContainerElement.textContent).toContain(initialTooltipMessage); @@ -704,7 +681,7 @@ describe('MatTooltip', () => { dispatchFakeEvent(document.body, 'auxclick'); tick(0); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, false); fixture.detectChanges(); expect(tooltipDirective._isTooltipVisible()).toBe(false); @@ -715,10 +692,10 @@ describe('MatTooltip', () => { tooltipDirective.show(); tick(0); fixture.detectChanges(); - document.body.click(); fixture.detectChanges(); tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); expect(overlayContainerElement.textContent).toContain(initialTooltipMessage); })); @@ -737,6 +714,7 @@ describe('MatTooltip', () => { fixture.detectChanges(); tick(500); fixture.detectChanges(); + finishCurrentTooltipAnimation(overlayContainerElement, false); expect(tooltipDirective._isTooltipVisible()).toBe(false); expect(overlayContainerElement.textContent).toBe(''); @@ -818,7 +796,7 @@ describe('MatTooltip', () => { tick(0); expect(tooltipDirective._isTooltipVisible()).toBe(true); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); const overlayRef = tooltipDirective._overlayRef!; @@ -828,7 +806,7 @@ describe('MatTooltip', () => { tick(0); expect(tooltipDirective._isTooltipVisible()).toBe(true); fixture.detectChanges(); - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); expect(overlayRef.detach).not.toHaveBeenCalled(); })); @@ -1147,12 +1125,12 @@ describe('MatTooltip', () => { fixture.detectChanges(); // wait until animation has finished - tick(500); + finishCurrentTooltipAnimation(overlayContainerElement, true); // Make sure tooltip is shown to the user and animation has finished const tooltipElement = overlayContainerElement.querySelector('.mat-tooltip') as HTMLElement; expect(tooltipElement instanceof HTMLElement).toBe(true); - expect(tooltipElement.style.transform).toBe('scale(1)'); + expect(tooltipElement.classList).toContain('mat-tooltip-show'); // After hide called, a timeout delay is created that will to hide the tooltip. const tooltipDelay = 1000; @@ -1165,7 +1143,7 @@ describe('MatTooltip', () => { expect(tooltipDirective._isTooltipVisible()).toBe(false); // On animation complete, should expect that the tooltip has been detached. - flushMicrotasks(); + finishCurrentTooltipAnimation(overlayContainerElement, false); assertTooltipInstance(tooltipDirective, false); })); @@ -1200,9 +1178,9 @@ describe('MatTooltip', () => { assertTooltipInstance(fixture.componentInstance.tooltip, false); - tick(250); // Finish the delay. + tick(500); // Finish the delay. fixture.detectChanges(); - tick(500); // Finish the animation. + finishCurrentTooltipAnimation(overlayContainerElement, true); // Finish the animation. assertTooltipInstance(fixture.componentInstance.tooltip, true); })); @@ -1241,7 +1219,7 @@ describe('MatTooltip', () => { fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); - tick(500); // Finish the animation. + finishCurrentTooltipAnimation(overlayContainerElement, true); // Finish the animation. assertTooltipInstance(fixture.componentInstance.tooltip, true); dispatchFakeEvent(button, 'touchend'); @@ -1251,7 +1229,7 @@ describe('MatTooltip', () => { tick(500); // Finish the delay. fixture.detectChanges(); - tick(500); // Finish the exit animation. + finishCurrentTooltipAnimation(overlayContainerElement, false); // Finish the exit animation. assertTooltipInstance(fixture.componentInstance.tooltip, false); })); @@ -1265,7 +1243,7 @@ describe('MatTooltip', () => { fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); - tick(500); // Finish the animation. + finishCurrentTooltipAnimation(overlayContainerElement, true); // Finish the animation. assertTooltipInstance(fixture.componentInstance.tooltip, true); dispatchFakeEvent(button, 'touchcancel'); @@ -1275,7 +1253,7 @@ describe('MatTooltip', () => { tick(500); // Finish the delay. fixture.detectChanges(); - tick(500); // Finish the exit animation. + finishCurrentTooltipAnimation(overlayContainerElement, false); // Finish the exit animation. assertTooltipInstance(fixture.componentInstance.tooltip, false); })); @@ -1398,7 +1376,7 @@ describe('MatTooltip', () => { fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); - tick(500); // Finish the animation. + finishCurrentTooltipAnimation(overlayContainerElement, true); assertTooltipInstance(fixture.componentInstance.tooltip, true); // Simulate the pointer at the bottom/right of the page. @@ -1412,7 +1390,7 @@ describe('MatTooltip', () => { fixture.detectChanges(); tick(1500); // Finish the delay. fixture.detectChanges(); - tick(500); // Finish the exit animation. + finishCurrentTooltipAnimation(overlayContainerElement, false); assertTooltipInstance(fixture.componentInstance.tooltip, false); })); @@ -1431,7 +1409,7 @@ describe('MatTooltip', () => { fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); - tick(500); // Finish the animation. + finishCurrentTooltipAnimation(overlayContainerElement, true); assertTooltipInstance(fixture.componentInstance.tooltip, true); // Simulate the pointer over the trigger. @@ -1446,7 +1424,7 @@ describe('MatTooltip', () => { fixture.detectChanges(); tick(1500); // Finish the delay. fixture.detectChanges(); - tick(500); // Finish the exit animation. + finishCurrentTooltipAnimation(overlayContainerElement, false); assertTooltipInstance(fixture.componentInstance.tooltip, true); })); @@ -1588,3 +1566,12 @@ function assertTooltipInstance(tooltip: MatTooltip, shouldExist: boolean): void // happens due to the `_tooltipInstance` having a circular structure. expect(!!tooltip._tooltipInstance).toBe(shouldExist); } + +function finishCurrentTooltipAnimation(overlayContainer: HTMLElement, isVisible: boolean) { + const tooltip = overlayContainer.querySelector('.mat-tooltip')!; + const event = createFakeEvent('animationend'); + Object.defineProperty(event, 'animationName', { + get: () => `mat-tooltip-${isVisible ? 'show' : 'hide'}`, + }); + dispatchEvent(tooltip, event); +} diff --git a/src/material/tooltip/tooltip.ts b/src/material/tooltip/tooltip.ts index 4299911474c7..77c9a665acb4 100644 --- a/src/material/tooltip/tooltip.ts +++ b/src/material/tooltip/tooltip.ts @@ -5,7 +5,6 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {AnimationEvent} from '@angular/animations'; import {AriaDescriber, FocusMonitor} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import { @@ -46,13 +45,13 @@ import { ViewContainerRef, ViewEncapsulation, AfterViewInit, + ViewChild, } from '@angular/core'; import {DOCUMENT} from '@angular/common'; +import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; import {Observable, Subject} from 'rxjs'; import {take, takeUntil} from 'rxjs/operators'; -import {matTooltipAnimations} from './tooltip-animations'; - /** Possible positions for a tooltip. */ export type TooltipPosition = 'left' | 'right' | 'above' | 'below' | 'before' | 'after'; @@ -860,13 +859,33 @@ export abstract class _TooltipComponentBase implements OnDestroy { /** Amount of milliseconds to delay the closing sequence. */ _mouseLeaveHideDelay: number; + /** Whether animations are currently disabled. */ + private _animationsDisabled: boolean; + + /** Reference to the internal tooltip element. */ + abstract _tooltip: ElementRef; + /** Whether interactions on the page should close the tooltip */ - private _closeOnInteraction: boolean = false; + private _closeOnInteraction = false; + + /** Whether the tooltip is currently visible. */ + private _isVisible = false; /** Subject for notifying that the tooltip has been hidden from the view */ private readonly _onHide: Subject = new Subject(); - constructor(private _changeDetectorRef: ChangeDetectorRef) {} + /** Name of the show animation and the class that toggles it. */ + protected abstract readonly _showAnimation: string; + + /** Name of the hide animation and the class that toggles it. */ + protected abstract readonly _hideAnimation: string; + + constructor( + private _changeDetectorRef: ChangeDetectorRef, + @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string, + ) { + this._animationsDisabled = animationMode === 'NoopAnimations'; + } /** * Shows the tooltip with an animation originating from the provided origin @@ -876,16 +895,9 @@ export abstract class _TooltipComponentBase implements OnDestroy { // Cancel the delayed hide if it is scheduled clearTimeout(this._hideTimeoutId); - // Body interactions should cancel the tooltip if there is a delay in showing. - this._closeOnInteraction = true; this._showTimeoutId = setTimeout(() => { - this._visibility = 'visible'; + this._toggleVisibility(true); this._showTimeoutId = undefined; - this._onShow(); - - // Mark for check so if any parent component has set the - // ChangeDetectionStrategy to OnPush it will be checked anyways - this._markForCheck(); }, delay); } @@ -898,12 +910,8 @@ export abstract class _TooltipComponentBase implements OnDestroy { clearTimeout(this._showTimeoutId); this._hideTimeoutId = setTimeout(() => { - this._visibility = 'hidden'; + this._toggleVisibility(false); this._hideTimeoutId = undefined; - - // Mark for check so if any parent component has set the - // ChangeDetectionStrategy to OnPush it will be checked anyways - this._markForCheck(); }, delay); } @@ -914,7 +922,7 @@ export abstract class _TooltipComponentBase implements OnDestroy { /** Whether the tooltip is being displayed. */ isVisible(): boolean { - return this._visibility === 'visible'; + return this._isVisible; } ngOnDestroy() { @@ -924,22 +932,6 @@ export abstract class _TooltipComponentBase implements OnDestroy { this._triggerElement = null!; } - _animationStart() { - this._closeOnInteraction = false; - } - - _animationDone(event: AnimationEvent): void { - const toState = event.toState as TooltipVisibility; - - if (toState === 'hidden' && !this.isVisible()) { - this._onHide.next(); - } - - if (toState === 'visible' || toState === 'hidden') { - this._closeOnInteraction = true; - } - } - /** * Interactions on the HTML body should close the tooltip immediately as defined in the * material design spec. @@ -972,6 +964,58 @@ export abstract class _TooltipComponentBase implements OnDestroy { * in the mdc-tooltip, not here. */ protected _onShow(): void {} + + /** Event listener dispatched when an animation on the tooltip finishes. */ + _handleAnimationEnd({animationName}: AnimationEvent) { + if (animationName === this._showAnimation || animationName === this._hideAnimation) { + this._finalizeAnimation(animationName === this._showAnimation); + } + } + + /** Handles the cleanup after an animation has finished. */ + private _finalizeAnimation(toVisible: boolean) { + if (toVisible) { + this._closeOnInteraction = true; + } else if (!this.isVisible()) { + this._onHide.next(); + } + } + + /** Toggles the visibility of the tooltip element. */ + private _toggleVisibility(isVisible: boolean) { + // We set the classes directly here ourselves so that toggling the tooltip state + // isn't bound by change detection. This allows us to hide it even if the + // view ref has been detached from the CD tree. + const tooltip = this._tooltip.nativeElement; + const showClass = this._showAnimation; + const hideClass = this._hideAnimation; + tooltip.classList.remove(isVisible ? hideClass : showClass); + tooltip.classList.add(isVisible ? showClass : hideClass); + this._isVisible = isVisible; + + // It's common for internal apps to disable animations using `* { animation: none !important }` + // which can break the opening sequence. Try to detect such cases and work around them. + if (isVisible && !this._animationsDisabled && typeof getComputedStyle === 'function') { + const styles = getComputedStyle(tooltip); + + // Use `getPropertyValue` to avoid issues with property renaming. + if ( + styles.getPropertyValue('animation-duration') === '0s' || + styles.getPropertyValue('animation-name') === 'none' + ) { + this._animationsDisabled = true; + } + } + + if (isVisible) { + this._onShow(); + } + + if (this._animationsDisabled) { + tooltip.classList.add('_mat-animation-noopable'); + this._finalizeAnimation(isVisible); + } + } } /** @@ -984,11 +1028,10 @@ export abstract class _TooltipComponentBase implements OnDestroy { styleUrls: ['tooltip.css'], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, - animations: [matTooltipAnimations.tooltipState], host: { // Forces the element to have a layout in IE and Edge. This fixes issues where the element // won't be rendered if the animations are disabled or there is no web animations polyfill. - '[style.zoom]': '_visibility === "visible" ? 1 : null', + '[style.zoom]': 'isVisible() ? 1 : null', '(mouseleave)': '_handleMouseLeave($event)', 'aria-hidden': 'true', }, @@ -996,11 +1039,21 @@ export abstract class _TooltipComponentBase implements OnDestroy { export class TooltipComponent extends _TooltipComponentBase { /** Stream that emits whether the user has a handset-sized display. */ _isHandset: Observable = this._breakpointObserver.observe(Breakpoints.Handset); + _showAnimation = 'mat-tooltip-show'; + _hideAnimation = 'mat-tooltip-hide'; + + @ViewChild('tooltip', { + // Use a static query here since we interact directly with + // the DOM which can happen before `ngAfterViewInit`. + static: true, + }) + _tooltip: ElementRef; constructor( changeDetectorRef: ChangeDetectorRef, private _breakpointObserver: BreakpointObserver, + @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string, ) { - super(changeDetectorRef); + super(changeDetectorRef, animationMode); } } diff --git a/tools/public_api_guard/material/tooltip-testing.md b/tools/public_api_guard/material/tooltip-testing.md index 805d4f1efa9b..f40f0eefdca8 100644 --- a/tools/public_api_guard/material/tooltip-testing.md +++ b/tools/public_api_guard/material/tooltip-testing.md @@ -12,21 +12,33 @@ import { TestElement } from '@angular/cdk/testing'; // @public export class MatTooltipHarness extends _MatTooltipHarnessBase { + // (undocumented) + protected _hiddenClass: string; + // (undocumented) + protected _hideAnimationName: string; // (undocumented) static hostSelector: string; // (undocumented) protected _optionalPanel: AsyncFactoryFn; + // (undocumented) + protected _showAnimationName: string; static with(options?: TooltipHarnessFilters): HarnessPredicate; } // @public (undocumented) export abstract class _MatTooltipHarnessBase extends ComponentHarness { getTooltipText(): Promise; + // (undocumented) + protected abstract _hiddenClass: string; hide(): Promise; + // (undocumented) + protected abstract _hideAnimationName: string; isOpen(): Promise; // (undocumented) protected abstract _optionalPanel: AsyncFactoryFn; show(): Promise; + // (undocumented) + protected abstract _showAnimationName: string; } // @public diff --git a/tools/public_api_guard/material/tooltip.md b/tools/public_api_guard/material/tooltip.md index 9b9dbcabe573..82bf91dddb88 100644 --- a/tools/public_api_guard/material/tooltip.md +++ b/tools/public_api_guard/material/tooltip.md @@ -5,7 +5,6 @@ ```ts import { AfterViewInit } from '@angular/core'; -import { AnimationEvent as AnimationEvent_2 } from '@angular/animations'; import { AnimationTriggerMetadata } from '@angular/animations'; import { AriaDescriber } from '@angular/cdk/a11y'; import { BooleanInput } from '@angular/cdk/coercion'; @@ -157,26 +156,30 @@ export const TOOLTIP_PANEL_CLASS = "mat-tooltip-panel"; // @public export class TooltipComponent extends _TooltipComponentBase { - constructor(changeDetectorRef: ChangeDetectorRef, _breakpointObserver: BreakpointObserver); + constructor(changeDetectorRef: ChangeDetectorRef, _breakpointObserver: BreakpointObserver, animationMode?: string); + // (undocumented) + _hideAnimation: string; _isHandset: Observable; // (undocumented) + _showAnimation: string; + // (undocumented) + _tooltip: ElementRef; + // (undocumented) static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵfac: i0.ɵɵFactoryDeclaration; } // @public (undocumented) export abstract class _TooltipComponentBase implements OnDestroy { - constructor(_changeDetectorRef: ChangeDetectorRef); + constructor(_changeDetectorRef: ChangeDetectorRef, animationMode?: string); afterHidden(): Observable; - // (undocumented) - _animationDone(event: AnimationEvent_2): void; - // (undocumented) - _animationStart(): void; + _handleAnimationEnd({ animationName }: AnimationEvent): void; _handleBodyInteraction(): void; // (undocumented) _handleMouseLeave({ relatedTarget }: MouseEvent): void; hide(delay: number): void; + protected abstract readonly _hideAnimation: string; _hideTimeoutId: number | undefined; isVisible(): boolean; _markForCheck(): void; @@ -186,7 +189,9 @@ export abstract class _TooltipComponentBase implements OnDestroy { ngOnDestroy(): void; protected _onShow(): void; show(delay: number): void; + protected abstract readonly _showAnimation: string; _showTimeoutId: number | undefined; + abstract _tooltip: ElementRef; tooltipClass: string | string[] | Set | { [key: string]: any; }; @@ -195,7 +200,7 @@ export abstract class _TooltipComponentBase implements OnDestroy { // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration<_TooltipComponentBase, never, never, {}, {}, never>; // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration<_TooltipComponentBase, never>; + static ɵfac: i0.ɵɵFactoryDeclaration<_TooltipComponentBase, [null, { optional: true; }]>; } // @public