diff --git a/src/dev-app/mdc-tooltip/mdc-tooltip-demo.scss b/src/dev-app/mdc-tooltip/mdc-tooltip-demo.scss
new file mode 100644
index 000000000000..d1298121ea59
--- /dev/null
+++ b/src/dev-app/mdc-tooltip/mdc-tooltip-demo.scss
@@ -0,0 +1,15 @@
+.demo-wrapper {
+ // Prevent the container from stretching to 100%.
+ display: inline-block;
+
+ .mat-mdc-tooltip-trigger {
+ // Center the trigger relative to the content so that it's not flush against one of the
+ // viewport edges. This will guarantee that we always get the specified demo position.
+ margin: 32px auto;
+ display: block;
+ }
+}
+
+.demo-form-field {
+ margin: 0 8px;
+}
diff --git a/src/dev-app/mdc-tooltip/mdc-tooltip-demo.ts b/src/dev-app/mdc-tooltip/mdc-tooltip-demo.ts
new file mode 100644
index 000000000000..fcb813b8a41a
--- /dev/null
+++ b/src/dev-app/mdc-tooltip/mdc-tooltip-demo.ts
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * 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 {Component} from '@angular/core';
+import {FormControl} from '@angular/forms';
+import {TooltipPosition} from '@angular/material-experimental/mdc-tooltip';
+
+@Component({
+ selector: 'mdc-tooltip-demo',
+ templateUrl: 'mdc-tooltip-demo.html',
+ styleUrls: ['mdc-tooltip-demo.css'],
+})
+export class MdcTooltipDemo {
+ message = new FormControl('Info about the action');
+ showDelay = new FormControl(0);
+ hideDelay = new FormControl(0);
+ positionOptions: TooltipPosition[] = ['below', 'after', 'before', 'above', 'left', 'right'];
+ position = new FormControl(this.positionOptions[0]);
+}
diff --git a/src/e2e-app/devserver-configure.js b/src/e2e-app/devserver-configure.js
index 4b5b619a7d11..37562c29178d 100644
--- a/src/e2e-app/devserver-configure.js
+++ b/src/e2e-app/devserver-configure.js
@@ -38,6 +38,7 @@ require.config({
'@material/tab-indicator': '@material/tab-indicator/dist/mdc.tabIndicator',
'@material/tab-scroller': '@material/tab-scroller/dist/mdc.tabScroller',
'@material/textfield': '@material/textfield/dist/mdc.textfield',
+ '@material/tooltip': '@material/tooltip/dist/mdc.tooltip',
'@material/top-app-bar': '@material/top-app-bar/dist/mdc.topAppBar',
}
});
diff --git a/src/material-experimental/config.bzl b/src/material-experimental/config.bzl
index 1006febdc775..3a3729896ab5 100644
--- a/src/material-experimental/config.bzl
+++ b/src/material-experimental/config.bzl
@@ -43,6 +43,7 @@ entryPoints = [
"mdc-table/testing",
"mdc-tabs",
"mdc-tabs/testing",
+ "mdc-tooltip",
"menubar",
"popover-edit",
"selection",
diff --git a/src/material-experimental/mdc-theming/BUILD.bazel b/src/material-experimental/mdc-theming/BUILD.bazel
index 8d16bd59be25..cb8ffe0b090b 100644
--- a/src/material-experimental/mdc-theming/BUILD.bazel
+++ b/src/material-experimental/mdc-theming/BUILD.bazel
@@ -37,6 +37,7 @@ sass_library(
"//src/material-experimental/mdc-snack-bar:mdc_snack_bar_scss_lib",
"//src/material-experimental/mdc-table:mdc_table_scss_lib",
"//src/material-experimental/mdc-tabs:mdc_tabs_scss_lib",
+ "//src/material-experimental/mdc-tooltip:mdc_tooltip_scss_lib",
"//src/material/core:core_scss_lib",
"//src/material/core:theming_scss_lib",
],
diff --git a/src/material-experimental/mdc-theming/_all-theme.scss b/src/material-experimental/mdc-theming/_all-theme.scss
index 65e5ba07c444..f979dce9cc02 100644
--- a/src/material-experimental/mdc-theming/_all-theme.scss
+++ b/src/material-experimental/mdc-theming/_all-theme.scss
@@ -13,6 +13,7 @@
@import '../mdc-snack-bar/snack-bar-theme';
@import '../mdc-tabs/tabs-theme';
@import '../mdc-table/table-theme';
+@import '../mdc-tooltip/tooltip-theme';
@import '../mdc-paginator/paginator-theme';
@import '../mdc-progress-bar/progress-bar-theme';
@import '../mdc-progress-spinner/progress-spinner-theme';
@@ -46,5 +47,6 @@
@include mat-mdc-form-field-theme($theme-or-color-config);
@include mat-mdc-input-theme($theme-or-color-config);
@include mat-mdc-tabs-theme($theme-or-color-config);
+ @include mat-mdc-tooltip-theme($theme-or-color-config);
}
}
diff --git a/src/material-experimental/mdc-tooltip/BUILD.bazel b/src/material-experimental/mdc-tooltip/BUILD.bazel
new file mode 100644
index 000000000000..fee31b478824
--- /dev/null
+++ b/src/material-experimental/mdc-tooltip/BUILD.bazel
@@ -0,0 +1,80 @@
+load(
+ "//tools:defaults.bzl",
+ "ng_module",
+ "ng_test_library",
+ "ng_web_test_suite",
+ "sass_binary",
+ "sass_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+ng_module(
+ name = "mdc-tooltip",
+ srcs = glob(
+ ["**/*.ts"],
+ exclude = [
+ "**/*.spec.ts",
+ ],
+ ),
+ assets = [
+ ":tooltip_scss",
+ ] + glob(["**/*.html"]),
+ module_name = "@angular/material-experimental/mdc-tooltip",
+ deps = [
+ "//src/material-experimental/mdc-core",
+ "//src/material/tooltip",
+ "@npm//@material/tooltip",
+ ],
+)
+
+sass_library(
+ name = "mdc_tooltip_scss_lib",
+ srcs = glob(["**/_*.scss"]),
+ deps = [
+ "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
+ "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
+ ],
+)
+
+sass_binary(
+ name = "tooltip_scss",
+ src = "tooltip.scss",
+ include_paths = [
+ "external/npm/node_modules",
+ ],
+ deps = [
+ "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
+ "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
+ ],
+)
+
+ng_test_library(
+ name = "tooltip_tests_lib",
+ srcs = glob(
+ ["**/*.spec.ts"],
+ exclude = [
+ "**/*.e2e.spec.ts",
+ ],
+ ),
+ deps = [
+ ":mdc-tooltip",
+ "//src/cdk/a11y",
+ "//src/cdk/bidi",
+ "//src/cdk/keycodes",
+ "//src/cdk/overlay",
+ "//src/cdk/platform",
+ "//src/cdk/testing/private",
+ "@npm//@angular/animations",
+ "@npm//@angular/platform-browser",
+ ],
+)
+
+ng_web_test_suite(
+ name = "unit_tests",
+ static_files = ["@npm//:node_modules/@material/tooltip/dist/mdc.tooltip.js"],
+ deps = [
+ ":tooltip_tests_lib",
+ "//src/material-experimental:mdc_require_config.js",
+ ],
+)
diff --git a/src/material-experimental/mdc-tooltip/README.md b/src/material-experimental/mdc-tooltip/README.md
new file mode 100644
index 000000000000..3d071b2234c0
--- /dev/null
+++ b/src/material-experimental/mdc-tooltip/README.md
@@ -0,0 +1,87 @@
+This is prototype of an alternate version of `MatTooltip` built on top of
+[MDC Web](https://github.com/material-components/material-components-web). It demonstrates how
+Angular Material could use MDC Web under the hood while still exposing the same API Angular users as
+the existing `MatTooltip`. This component is experimental and should not be used in production.
+
+## How to use
+Assuming your application is already up and running using Angular Material, you can add this
+component by following these steps:
+
+1. Install Angular Material Experimental & MDC WEB:
+
+ ```bash
+ npm i material-components-web @angular/material-experimental
+ ```
+
+2. In your `angular.json`, make sure `node_modules/` is listed as a Sass include path. This is
+ needed for the Sass compiler to be able to find the MDC Web Sass files.
+
+ ```json
+ ...
+ "styles": [
+ "src/styles.scss"
+ ],
+ "stylePreprocessorOptions": {
+ "includePaths": [
+ "node_modules/"
+ ]
+ },
+ ...
+ ```
+
+3. Import the experimental `MatTooltipModule` and add it to the module that declares your
+ component:
+
+ ```ts
+ import {MatTooltipModule} from '@angular/material-experimental/mdc-tooltip';
+
+ @NgModule({
+ declarations: [MyComponent],
+ imports: [MatTooltipModule],
+ })
+ export class MyModule {}
+ ```
+
+4. Use `matTooltip` in your component's template, just like you would the normal
+ `matTooltip`:
+
+ ```html
+
+ ```
+
+5. Add the theme and typography mixins to your Sass. (There is currently no pre-built CSS option for
+ the experimental `matTooltip`):
+
+ ```scss
+ @import '~@angular/material/theming';
+ @import '~@angular/material-experimental/mdc-tooltip/tooltip-theme';
+
+ $my-primary: mat-palette($mat-indigo);
+ $my-accent: mat-palette($mat-pink, A200, A100, A400);
+ $my-theme: mat-light-theme((
+ color: (
+ primary: $my-primary,
+ accent: $my-accent
+ )
+ ));
+
+ @include mat-mdc-tooltip-theme($my-theme);
+ @include mat-mdc-tooltip-typography($my-theme);
+ ```
+
+## API differences
+There are no API differences between the experimental and standard `matTooltip`.
+
+## Replacing the standard tooltip in an existing app
+Because the experimental API mirrors the API for the standard tooltip, it can easily be swapped in
+by just changing the import paths. There is currently no schematic for this, but you can run the
+following string replace across your TypeScript files:
+
+```bash
+grep -lr --include="*.ts" --exclude-dir="node_modules" \
+ --exclude="*.d.ts" "['\"]@angular/material/tooltip['\"]" | xargs sed -i \
+ "s/['\"]@angular\/material\/tooltip['\"]/'@angular\/material-experimental\/mdc-tooltip'/g"
+```
+
+CSS styles and tests that depend on implementation details of `matTooltip` (such as getting
+elements from the template by class name) will need to be manually updated.
diff --git a/src/material-experimental/mdc-tooltip/_tooltip-theme.scss b/src/material-experimental/mdc-tooltip/_tooltip-theme.scss
new file mode 100644
index 000000000000..dd7887a692c5
--- /dev/null
+++ b/src/material-experimental/mdc-tooltip/_tooltip-theme.scss
@@ -0,0 +1,39 @@
+@use '@material/tooltip/tooltip';
+@import '../mdc-helpers/mdc-helpers';
+
+@mixin mat-mdc-tooltip-color($config-or-theme) {
+ $config: mat-get-color-config($config-or-theme);
+ @include mat-using-mdc-theme($config) {
+ @include tooltip.core-styles($query: $mat-theme-styles-query);
+ }
+}
+
+@mixin mat-mdc-tooltip-typography($config-or-theme) {
+ $config: mat-get-typography-config($config-or-theme);
+ @include mat-using-mdc-typography($config) {
+ @include tooltip.core-styles($query: $mat-typography-styles-query);
+ }
+}
+
+@mixin mat-mdc-tooltip-density($config-or-theme) {
+ $density-scale: mat-get-density-config($config-or-theme);
+}
+
+@mixin mat-mdc-tooltip-theme($theme-or-color-config) {
+ $theme: mat-private-legacy-get-theme($theme-or-color-config);
+ @include mat-private-check-duplicate-theme-styles($theme, 'mat-mdc-tooltip') {
+ $color: mat-get-color-config($theme);
+ $density: mat-get-density-config($theme);
+ $typography: mat-get-typography-config($theme);
+
+ @if $color != null {
+ @include mat-mdc-tooltip-color($color);
+ }
+ @if $density != null {
+ @include mat-mdc-tooltip-density($density);
+ }
+ @if $typography != null {
+ @include mat-mdc-tooltip-typography($typography);
+ }
+ }
+}
diff --git a/src/material-experimental/mdc-tooltip/index.ts b/src/material-experimental/mdc-tooltip/index.ts
new file mode 100644
index 000000000000..676ca90f1ffa
--- /dev/null
+++ b/src/material-experimental/mdc-tooltip/index.ts
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * 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
+ */
+
+export * from './public-api';
diff --git a/src/material-experimental/mdc-tooltip/module.ts b/src/material-experimental/mdc-tooltip/module.ts
new file mode 100644
index 000000000000..8039d99ca91d
--- /dev/null
+++ b/src/material-experimental/mdc-tooltip/module.ts
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * 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 {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {A11yModule} from '@angular/cdk/a11y';
+import {OverlayModule} from '@angular/cdk/overlay';
+import {CdkScrollableModule} from '@angular/cdk/scrolling';
+import {MatCommonModule} from '@angular/material-experimental/mdc-core';
+import {MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY_PROVIDER} from '@angular/material/tooltip';
+import {MatTooltip, TooltipComponent} from './tooltip';
+
+@NgModule({
+ imports: [
+ A11yModule,
+ CommonModule,
+ OverlayModule,
+ MatCommonModule,
+ ],
+ exports: [MatTooltip, TooltipComponent, MatCommonModule, CdkScrollableModule],
+ declarations: [MatTooltip, TooltipComponent],
+ entryComponents: [TooltipComponent],
+ providers: [MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY_PROVIDER]
+})
+export class MatTooltipModule {
+}
diff --git a/src/material-experimental/mdc-tooltip/public-api.ts b/src/material-experimental/mdc-tooltip/public-api.ts
new file mode 100644
index 000000000000..7d68c4668005
--- /dev/null
+++ b/src/material-experimental/mdc-tooltip/public-api.ts
@@ -0,0 +1,26 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * 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
+ */
+
+export * from './tooltip';
+export * from './tooltip-animations';
+export * from './module';
+
+export {
+ getMatTooltipInvalidPositionError,
+ MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY,
+ MAT_TOOLTIP_DEFAULT_OPTIONS_FACTORY,
+ TooltipPosition,
+ TooltipTouchGestures,
+ TooltipVisibility,
+ SCROLL_THROTTLE_MS,
+ TOOLTIP_PANEL_CLASS,
+ MAT_TOOLTIP_SCROLL_STRATEGY,
+ MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY_PROVIDER,
+ MatTooltipDefaultOptions,
+ MAT_TOOLTIP_DEFAULT_OPTIONS
+} from '@angular/material/tooltip';
diff --git a/src/material-experimental/mdc-tooltip/tooltip-animations.ts b/src/material-experimental/mdc-tooltip/tooltip-animations.ts
new file mode 100644
index 000000000000..01319370e09d
--- /dev/null
+++ b/src/material-experimental/mdc-tooltip/tooltip-animations.ts
@@ -0,0 +1,33 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * 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 {
+ animate,
+ AnimationTriggerMetadata,
+ state,
+ style,
+ transition,
+ trigger,
+} from '@angular/animations';
+
+/**
+ * Animations used by MatTooltip.
+ * @docs-private
+ */
+export const matTooltipAnimations: {
+ readonly tooltipState: AnimationTriggerMetadata;
+} = {
+ /** Animation that transitions a tooltip in and out. */
+ tooltipState: trigger('state', [
+ // TODO(crisbeto): these values are based on MDC's CSS.
+ // We should be able to use their styles directly once we land #19432.
+ state('initial, void, hidden', style({opacity: 0, transform: 'scale(0.8)'})),
+ state('visible', style({transform: 'scale(1)'})),
+ transition('* => visible', animate('150ms cubic-bezier(0, 0, 0.2, 1)')),
+ transition('* => hidden', animate('75ms cubic-bezier(0.4, 0, 1, 1)')),
+ ])
+};
diff --git a/src/material-experimental/mdc-tooltip/tooltip.html b/src/material-experimental/mdc-tooltip/tooltip.html
new file mode 100644
index 000000000000..f5643e7221e9
--- /dev/null
+++ b/src/material-experimental/mdc-tooltip/tooltip.html
@@ -0,0 +1,8 @@
+
+
{{message}}
+
diff --git a/src/material-experimental/mdc-tooltip/tooltip.scss b/src/material-experimental/mdc-tooltip/tooltip.scss
new file mode 100644
index 000000000000..10c9cb2f2712
--- /dev/null
+++ b/src/material-experimental/mdc-tooltip/tooltip.scss
@@ -0,0 +1,14 @@
+@use '@material/tooltip/tooltip';
+
+// Only include the structural styles, because we handle the animation ourselves.
+@include tooltip.core-styles($query: structure);
+
+.mat-mdc-tooltip {
+ // We don't use MDC's positioning so this has to be static.
+ position: static;
+
+ // The overlay reference updates the pointer-events style property directly on the HTMLElement
+ // depending on the state of the overlay. For tooltips the overlay panel should never enable
+ // pointer events. To overwrite the inline CSS from the overlay reference `!important` is needed.
+ pointer-events: none !important;
+}
diff --git a/src/material-experimental/mdc-tooltip/tooltip.spec.ts b/src/material-experimental/mdc-tooltip/tooltip.spec.ts
new file mode 100644
index 000000000000..2bde17aa835c
--- /dev/null
+++ b/src/material-experimental/mdc-tooltip/tooltip.spec.ts
@@ -0,0 +1,1325 @@
+import {
+ waitForAsync,
+ ComponentFixture,
+ fakeAsync,
+ flush,
+ flushMicrotasks,
+ inject,
+ TestBed,
+ tick
+} from '@angular/core/testing';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ DebugElement,
+ ElementRef,
+ ViewChild,
+ NgZone,
+} from '@angular/core';
+import {AnimationEvent} from '@angular/animations';
+import {By} from '@angular/platform-browser';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {Direction, Directionality} from '@angular/cdk/bidi';
+import {OverlayContainer, OverlayModule, CdkScrollable} from '@angular/cdk/overlay';
+import {Platform} from '@angular/cdk/platform';
+import {
+ dispatchFakeEvent,
+ dispatchKeyboardEvent,
+ patchElementFocus,
+ dispatchMouseEvent,
+ createKeyboardEvent,
+ dispatchEvent,
+ createFakeEvent,
+} from '@angular/cdk/testing/private';
+import {ESCAPE} from '@angular/cdk/keycodes';
+import {FocusMonitor} from '@angular/cdk/a11y';
+import {
+ MatTooltip,
+ MatTooltipModule,
+ SCROLL_THROTTLE_MS,
+ TOOLTIP_PANEL_CLASS,
+ MAT_TOOLTIP_DEFAULT_OPTIONS,
+ TooltipTouchGestures,
+} from './index';
+
+
+const initialTooltipMessage = 'initial tooltip message';
+
+describe('MDC-based MatTooltip', () => {
+ let overlayContainer: OverlayContainer;
+ let overlayContainerElement: HTMLElement;
+ let dir: {value: Direction};
+ let platform: Platform;
+ let focusMonitor: FocusMonitor;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [MatTooltipModule, OverlayModule, NoopAnimationsModule],
+ declarations: [
+ BasicTooltipDemo,
+ ScrollableTooltipDemo,
+ OnPushTooltipDemo,
+ DynamicTooltipsDemo,
+ TooltipOnTextFields,
+ TooltipOnDraggableElement,
+ DataBoundAriaLabelTooltip,
+ ],
+ providers: [
+ {provide: Directionality, useFactory: () => {
+ return dir = {value: 'ltr'};
+ }}
+ ]
+ });
+
+ TestBed.compileComponents();
+
+ inject([OverlayContainer, FocusMonitor, Platform],
+ (oc: OverlayContainer, fm: FocusMonitor, pl: Platform) => {
+ overlayContainer = oc;
+ overlayContainerElement = oc.getContainerElement();
+ focusMonitor = fm;
+ platform = pl;
+ })();
+ }));
+
+ afterEach(inject([OverlayContainer], (currentOverlayContainer: OverlayContainer) => {
+ // Since we're resetting the testing module in some of the tests,
+ // we can potentially have multiple overlay containers.
+ currentOverlayContainer.ngOnDestroy();
+ overlayContainer.ngOnDestroy();
+ }));
+
+ describe('basic usage', () => {
+ let fixture: ComponentFixture;
+ let buttonDebugElement: DebugElement;
+ let buttonElement: HTMLButtonElement;
+ let tooltipDirective: MatTooltip;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BasicTooltipDemo);
+ fixture.detectChanges();
+ buttonDebugElement = fixture.debugElement.query(By.css('button'))!;
+ buttonElement = buttonDebugElement.nativeElement;
+ tooltipDirective = buttonDebugElement.injector.get(MatTooltip);
+ });
+
+ it('should show and hide the tooltip', fakeAsync(() => {
+ assertTooltipInstance(tooltipDirective, false);
+
+ tooltipDirective.show();
+ tick(0); // Tick for the show delay (default is 0)
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+
+ fixture.detectChanges();
+
+ // wait till animation has finished
+ tick(500);
+
+ // 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(overlayContainerElement.textContent).toContain(initialTooltipMessage);
+
+ // After hide called, a timeout delay is created that will to hide the tooltip.
+ const tooltipDelay = 1000;
+ tooltipDirective.hide(tooltipDelay);
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+
+ // After the tooltip delay elapses, expect that the tooltip is not visible.
+ tick(tooltipDelay);
+ fixture.detectChanges();
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+
+ // On animation complete, should expect that the tooltip has been detached.
+ flushMicrotasks();
+ assertTooltipInstance(tooltipDirective, false);
+ }));
+
+ it('should be able to re-open a tooltip if it was closed by detaching the overlay',
+ fakeAsync(() => {
+ tooltipDirective.show();
+ tick(0);
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+ fixture.detectChanges();
+ tick(500);
+
+ tooltipDirective._overlayRef!.detach();
+ tick(0);
+ fixture.detectChanges();
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+ flushMicrotasks();
+ assertTooltipInstance(tooltipDirective, false);
+
+ tooltipDirective.show();
+ tick(0);
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+ }));
+
+ it('should show with delay', fakeAsync(() => {
+ assertTooltipInstance(tooltipDirective, false);
+
+ const tooltipDelay = 1000;
+ tooltipDirective.show(tooltipDelay);
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+
+ fixture.detectChanges();
+ expect(overlayContainerElement.textContent).toContain('');
+
+ tick(tooltipDelay);
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+ expect(overlayContainerElement.textContent).toContain(initialTooltipMessage);
+ }));
+
+ it('should be able to override the default show and hide delays', fakeAsync(() => {
+ TestBed
+ .resetTestingModule()
+ .configureTestingModule({
+ imports: [MatTooltipModule, OverlayModule, NoopAnimationsModule],
+ declarations: [BasicTooltipDemo],
+ providers: [{
+ provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
+ useValue: {showDelay: 1337, hideDelay: 7331}
+ }]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(BasicTooltipDemo);
+ fixture.detectChanges();
+ tooltipDirective = fixture.debugElement.query(By.css('button'))!
+ .injector.get(MatTooltip);
+
+ tooltipDirective.show();
+ fixture.detectChanges();
+ tick();
+
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+ tick(1337);
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+
+ tooltipDirective.hide();
+ fixture.detectChanges();
+ tick();
+
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+ tick(7331);
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+ }));
+
+ it('should be able to override the default position', fakeAsync(() => {
+ TestBed
+ .resetTestingModule()
+ .configureTestingModule({
+ imports: [MatTooltipModule, OverlayModule, NoopAnimationsModule],
+ declarations: [TooltipDemoWithoutPositionBinding],
+ providers: [{
+ provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
+ useValue: {position: 'right'}
+ }]
+ })
+ .compileComponents();
+
+ const newFixture = TestBed.createComponent(TooltipDemoWithoutPositionBinding);
+ newFixture.detectChanges();
+ tooltipDirective = newFixture.debugElement.query(By.css('button'))!
+ .injector.get(MatTooltip);
+
+ tooltipDirective.show();
+ newFixture.detectChanges();
+ tick();
+
+ expect(tooltipDirective.position).toBe('right');
+ expect(tooltipDirective._getOverlayPosition().main.overlayX).toBe('start');
+ expect(tooltipDirective._getOverlayPosition().fallback.overlayX).toBe('end');
+ }));
+
+ it('should set a css class on the overlay panel element', fakeAsync(() => {
+ tooltipDirective.show();
+ fixture.detectChanges();
+ tick(0);
+
+ const overlayRef = tooltipDirective._overlayRef;
+
+ expect(!!overlayRef).toBeTruthy();
+ expect(overlayRef!.overlayElement.classList).toContain(TOOLTIP_PANEL_CLASS,
+ 'Expected the overlay panel element to have the tooltip panel class set.');
+ }));
+
+ it('should not show if disabled', fakeAsync(() => {
+ // Test that disabling the tooltip will not set the tooltip visible
+ tooltipDirective.disabled = true;
+ tooltipDirective.show();
+ fixture.detectChanges();
+ tick(0);
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+
+ // Test to make sure setting disabled to false will show the tooltip
+ // Sanity check to make sure everything was correct before (detectChanges, tick)
+ tooltipDirective.disabled = false;
+ tooltipDirective.show();
+ fixture.detectChanges();
+ tick(0);
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+ }));
+
+ it('should hide if disabled while visible', fakeAsync(() => {
+ // Display the tooltip with a timeout before hiding.
+ tooltipDirective.hideDelay = 1000;
+ tooltipDirective.show();
+ fixture.detectChanges();
+ tick(0);
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+
+ // Set tooltip to be disabled and verify that the tooltip hides.
+ tooltipDirective.disabled = true;
+ tick(0);
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+ }));
+
+ it('should hide if the message is cleared while the tooltip is open', fakeAsync(() => {
+ tooltipDirective.show();
+ fixture.detectChanges();
+ tick(0);
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+
+ fixture.componentInstance.message = '';
+ fixture.detectChanges();
+ tick(0);
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+ }));
+
+ it('should not show if hide is called before delay finishes', waitForAsync(() => {
+ assertTooltipInstance(tooltipDirective, false);
+
+ const tooltipDelay = 1000;
+
+ tooltipDirective.show(tooltipDelay);
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+
+ fixture.detectChanges();
+ expect(overlayContainerElement.textContent).toContain('');
+ tooltipDirective.hide();
+
+ fixture.whenStable().then(() => {
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+ });
+ }));
+
+ it('should not show tooltip if message is not present or empty', () => {
+ assertTooltipInstance(tooltipDirective, false);
+
+ tooltipDirective.message = undefined!;
+ fixture.detectChanges();
+ tooltipDirective.show();
+ assertTooltipInstance(tooltipDirective, false);
+
+ tooltipDirective.message = null!;
+ fixture.detectChanges();
+ tooltipDirective.show();
+ assertTooltipInstance(tooltipDirective, false);
+
+ tooltipDirective.message = '';
+ fixture.detectChanges();
+ tooltipDirective.show();
+ assertTooltipInstance(tooltipDirective, false);
+
+ tooltipDirective.message = ' ';
+ fixture.detectChanges();
+ tooltipDirective.show();
+ assertTooltipInstance(tooltipDirective, false);
+ });
+
+ it('should not follow through with hide if show is called after', fakeAsync(() => {
+ tooltipDirective.show();
+ tick(0); // Tick for the show delay (default is 0)
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+
+ // After hide called, a timeout delay is created that will to hide the tooltip.
+ const tooltipDelay = 1000;
+ tooltipDirective.hide(tooltipDelay);
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+
+ // Before delay time has passed, call show which should cancel intent to hide tooltip.
+ tooltipDirective.show();
+ tick(tooltipDelay);
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+ }));
+
+ it('should be able to update the tooltip position while open', fakeAsync(() => {
+ tooltipDirective.position = 'below';
+ tooltipDirective.show();
+ tick();
+
+ assertTooltipInstance(tooltipDirective, true);
+
+ tooltipDirective.position = 'above';
+ spyOn(tooltipDirective._overlayRef!, 'updatePosition').and.callThrough();
+ fixture.detectChanges();
+ tick();
+
+ assertTooltipInstance(tooltipDirective, true);
+ expect(tooltipDirective._overlayRef!.updatePosition).toHaveBeenCalled();
+ }));
+
+ it('should not throw when updating the position for a closed tooltip', fakeAsync(() => {
+ tooltipDirective.position = 'left';
+ tooltipDirective.show(0);
+ fixture.detectChanges();
+ tick();
+
+ 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);
+
+ expect(() => {
+ tooltipDirective.position = 'right';
+ fixture.detectChanges();
+ tick();
+ }).not.toThrow();
+ }));
+
+ it('should be able to modify the tooltip message', fakeAsync(() => {
+ assertTooltipInstance(tooltipDirective, false);
+
+ tooltipDirective.show();
+ tick(0); // Tick for the show delay (default is 0)
+ expect(tooltipDirective._tooltipInstance!._visibility).toBe('visible');
+
+ fixture.detectChanges();
+ expect(overlayContainerElement.textContent).toContain(initialTooltipMessage);
+
+ const newMessage = 'new tooltip message';
+ tooltipDirective.message = newMessage;
+
+ fixture.detectChanges();
+ expect(overlayContainerElement.textContent).toContain(newMessage);
+ }));
+
+ it('should allow extra classes to be set on the tooltip', fakeAsync(() => {
+ assertTooltipInstance(tooltipDirective, false);
+
+ tooltipDirective.show();
+ tick(0); // Tick for the show delay (default is 0)
+ fixture.detectChanges();
+
+ // Make sure classes aren't prematurely added
+ let tooltipElement = overlayContainerElement.querySelector('.mat-mdc-tooltip') as HTMLElement;
+ expect(tooltipElement.classList).not.toContain('custom-one',
+ 'Expected to not have the class before enabling matTooltipClass');
+ expect(tooltipElement.classList).not.toContain('custom-two',
+ 'Expected to not have the class before enabling matTooltipClass');
+
+ // Enable the classes via ngClass syntax
+ fixture.componentInstance.showTooltipClass = true;
+ fixture.detectChanges();
+
+ // Make sure classes are correctly added
+ tooltipElement = overlayContainerElement.querySelector('.mat-mdc-tooltip') as HTMLElement;
+ expect(tooltipElement.classList).toContain('custom-one',
+ 'Expected to have the class after enabling matTooltipClass');
+ expect(tooltipElement.classList).toContain('custom-two',
+ 'Expected to have the class after enabling matTooltipClass');
+ }));
+
+ it('should be removed after parent destroyed', fakeAsync(() => {
+ tooltipDirective.show();
+ tick(0); // Tick for the show delay (default is 0)
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+
+ fixture.destroy();
+ expect(overlayContainerElement.childNodes.length).toBe(0);
+ expect(overlayContainerElement.textContent).toBe('');
+ }));
+
+ it('should have an aria-described element with the tooltip message', fakeAsync(() => {
+ const dynamicTooltipsDemoFixture = TestBed.createComponent(DynamicTooltipsDemo);
+ const dynamicTooltipsComponent = dynamicTooltipsDemoFixture.componentInstance;
+
+ dynamicTooltipsComponent.tooltips = ['Tooltip One', 'Tooltip Two'];
+ dynamicTooltipsDemoFixture.detectChanges();
+ tick();
+
+ const buttons = dynamicTooltipsDemoFixture.nativeElement.querySelectorAll('button');
+ const firstButtonAria = buttons[0].getAttribute('aria-describedby');
+ expect(document.querySelector(`#${firstButtonAria}`)!.textContent).toBe('Tooltip One');
+
+ const secondButtonAria = buttons[1].getAttribute('aria-describedby');
+ expect(document.querySelector(`#${secondButtonAria}`)!.textContent).toBe('Tooltip Two');
+ }));
+
+ it('should not add an ARIA description for elements that have the same text as a' +
+ 'data-bound aria-label', fakeAsync(() => {
+ const ariaLabelFixture = TestBed.createComponent(DataBoundAriaLabelTooltip);
+ ariaLabelFixture.detectChanges();
+ tick();
+
+ const button = ariaLabelFixture.nativeElement.querySelector('button');
+ expect(button.getAttribute('aria-describedby')).toBeFalsy();
+ }));
+
+ it('should not try to dispose the tooltip when destroyed and done hiding', fakeAsync(() => {
+ tooltipDirective.show();
+ fixture.detectChanges();
+ tick(150);
+
+ const tooltipDelay = 1000;
+ tooltipDirective.hide();
+ tick(tooltipDelay); // Change the tooltip state to hidden and trigger animation start
+
+ // 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);
+
+ const spy = jasmine.createSpy('complete spy');
+ const subscription = tooltipDirective._tooltipInstance!.afterHidden()
+ .subscribe({complete: spy});
+
+ tooltipDirective.hide(0);
+ tick(0);
+ fixture.detectChanges();
+ tick(500);
+
+ expect(spy).toHaveBeenCalled();
+ subscription.unsubscribe();
+ }));
+
+ it('should consistently position before and after overlay origin in ltr and rtl dir', () => {
+ tooltipDirective.position = 'left';
+ const leftOrigin = tooltipDirective._getOrigin().main;
+ tooltipDirective.position = 'right';
+ const rightOrigin = tooltipDirective._getOrigin().main;
+
+ // Test expectations in LTR
+ tooltipDirective.position = 'before';
+ expect(tooltipDirective._getOrigin().main).toEqual(leftOrigin);
+ tooltipDirective.position = 'after';
+ expect(tooltipDirective._getOrigin().main).toEqual(rightOrigin);
+
+ // Test expectations in RTL
+ dir.value = 'rtl';
+ tooltipDirective.position = 'before';
+ expect(tooltipDirective._getOrigin().main).toEqual(leftOrigin);
+ tooltipDirective.position = 'after';
+ expect(tooltipDirective._getOrigin().main).toEqual(rightOrigin);
+ });
+
+ it('should consistently position before and after overlay position in ltr and rtl dir', () => {
+ tooltipDirective.position = 'left';
+ const leftOverlayPosition = tooltipDirective._getOverlayPosition().main;
+ tooltipDirective.position = 'right';
+ const rightOverlayPosition = tooltipDirective._getOverlayPosition().main;
+
+ // Test expectations in LTR
+ tooltipDirective.position = 'before';
+ expect(tooltipDirective._getOverlayPosition().main).toEqual(leftOverlayPosition);
+ tooltipDirective.position = 'after';
+ expect(tooltipDirective._getOverlayPosition().main).toEqual(rightOverlayPosition);
+
+ // Test expectations in RTL
+ dir.value = 'rtl';
+ tooltipDirective.position = 'before';
+ expect(tooltipDirective._getOverlayPosition().main).toEqual(leftOverlayPosition);
+ tooltipDirective.position = 'after';
+ expect(tooltipDirective._getOverlayPosition().main).toEqual(rightOverlayPosition);
+ });
+
+ it('should throw when trying to assign an invalid position', () => {
+ expect(() => {
+ fixture.componentInstance.position = 'everywhere';
+ fixture.detectChanges();
+ tooltipDirective.show();
+ }).toThrowError('Tooltip position "everywhere" is invalid.');
+ });
+
+ it('should pass the layout direction to the tooltip', fakeAsync(() => {
+ dir.value = 'rtl';
+
+ tooltipDirective.show();
+ tick(0);
+ fixture.detectChanges();
+
+ const tooltipWrapper =
+ overlayContainerElement.querySelector('.cdk-overlay-connected-position-bounding-box')!;
+
+ expect(tooltipWrapper).toBeTruthy('Expected tooltip to be shown.');
+ expect(tooltipWrapper.getAttribute('dir')).toBe('rtl', 'Expected tooltip to be in RTL mode.');
+ }));
+
+ it('should keep the overlay direction in sync with the trigger direction', fakeAsync(() => {
+ dir.value = 'rtl';
+ tooltipDirective.show();
+ tick(0);
+ fixture.detectChanges();
+ tick(500);
+
+ let tooltipWrapper =
+ overlayContainerElement.querySelector('.cdk-overlay-connected-position-bounding-box')!;
+ expect(tooltipWrapper.getAttribute('dir')).toBe('rtl', 'Expected tooltip to be in RTL.');
+
+ tooltipDirective.hide(0);
+ tick(0);
+ fixture.detectChanges();
+ tick(500);
+
+ dir.value = 'ltr';
+ tooltipDirective.show();
+ tick(0);
+ fixture.detectChanges();
+ tick(500);
+
+ tooltipWrapper =
+ overlayContainerElement.querySelector('.cdk-overlay-connected-position-bounding-box')!;
+ expect(tooltipWrapper.getAttribute('dir')).toBe('ltr', 'Expected tooltip to be in LTR.');
+ }));
+
+ it('should be able to set the tooltip message as a number', fakeAsync(() => {
+ fixture.componentInstance.message = 100;
+ fixture.detectChanges();
+
+ expect(tooltipDirective.message).toBe('100');
+ }));
+
+ it('should hide when clicking away', fakeAsync(() => {
+ tooltipDirective.show();
+ tick(0);
+ fixture.detectChanges();
+ tick(500);
+
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+ expect(overlayContainerElement.textContent).toContain(initialTooltipMessage);
+
+ document.body.click();
+ tick(0);
+ fixture.detectChanges();
+ tick(500);
+ fixture.detectChanges();
+
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+ expect(overlayContainerElement.textContent).toBe('');
+ }));
+
+ it('should hide when clicking away with an auxilliary button', fakeAsync(() => {
+ tooltipDirective.show();
+ tick(0);
+ fixture.detectChanges();
+ tick(500);
+
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+ expect(overlayContainerElement.textContent).toContain(initialTooltipMessage);
+
+ dispatchFakeEvent(document.body, 'auxclick');
+ tick(0);
+ fixture.detectChanges();
+ tick(500);
+ fixture.detectChanges();
+
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+ expect(overlayContainerElement.textContent).toBe('');
+ }));
+
+ it('should not hide immediately if a click fires while animating', fakeAsync(() => {
+ tooltipDirective.show();
+ tick(0);
+ fixture.detectChanges();
+
+ document.body.click();
+ fixture.detectChanges();
+ tick(500);
+
+ expect(overlayContainerElement.textContent).toContain(initialTooltipMessage);
+ }));
+
+ it('should not throw when pressing ESCAPE', fakeAsync(() => {
+ expect(() => {
+ dispatchKeyboardEvent(buttonElement, 'keydown', ESCAPE);
+ fixture.detectChanges();
+ }).not.toThrow();
+
+ // Flush due to the additional tick that is necessary for the FocusMonitor.
+ flush();
+ }));
+
+ it('should preventDefault when pressing ESCAPE', fakeAsync(() => {
+ tooltipDirective.show();
+ tick(0);
+ fixture.detectChanges();
+
+ const event = dispatchKeyboardEvent(buttonElement, 'keydown', ESCAPE);
+ fixture.detectChanges();
+ flush();
+
+ expect(event.defaultPrevented).toBe(true);
+ }));
+
+ it('should not preventDefault when pressing ESCAPE with a modifier', fakeAsync(() => {
+ tooltipDirective.show();
+ tick(0);
+ fixture.detectChanges();
+
+ const event = createKeyboardEvent('keydown', ESCAPE, undefined, {alt: true});
+ dispatchEvent(buttonElement, event);
+ fixture.detectChanges();
+ flush();
+
+ expect(event.defaultPrevented).toBe(false);
+ }));
+
+ it('should not show the tooltip on progammatic focus', fakeAsync(() => {
+ patchElementFocus(buttonElement);
+ assertTooltipInstance(tooltipDirective, false);
+
+ focusMonitor.focusVia(buttonElement, 'program');
+ tick(0);
+ fixture.detectChanges();
+ tick(500);
+
+ expect(overlayContainerElement.querySelector('.mat-mdc-tooltip')).toBeNull();
+ }));
+
+ it('should not show the tooltip on mouse focus', fakeAsync(() => {
+ patchElementFocus(buttonElement);
+ assertTooltipInstance(tooltipDirective, false);
+
+ focusMonitor.focusVia(buttonElement, 'mouse');
+ tick(0);
+ fixture.detectChanges();
+ tick(500);
+
+ expect(overlayContainerElement.querySelector('.mat-mdc-tooltip')).toBeNull();
+ }));
+
+ it('should not show the tooltip on touch focus', fakeAsync(() => {
+ patchElementFocus(buttonElement);
+ assertTooltipInstance(tooltipDirective, false);
+
+ focusMonitor.focusVia(buttonElement, 'touch');
+ tick(0);
+ fixture.detectChanges();
+ tick(500);
+
+ expect(overlayContainerElement.querySelector('.mat-mdc-tooltip')).toBeNull();
+ }));
+
+ it('should not hide the tooltip when calling `show` twice in a row', fakeAsync(() => {
+ tooltipDirective.show();
+ tick(0);
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+ fixture.detectChanges();
+ tick(500);
+
+ const overlayRef = tooltipDirective._overlayRef!;
+
+ spyOn(overlayRef, 'detach').and.callThrough();
+
+ tooltipDirective.show();
+ tick(0);
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+ fixture.detectChanges();
+ tick(500);
+
+ expect(overlayRef.detach).not.toHaveBeenCalled();
+ }));
+
+ });
+
+ describe('fallback positions', () => {
+ let fixture: ComponentFixture;
+ let tooltip: MatTooltip;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BasicTooltipDemo);
+ fixture.detectChanges();
+ tooltip = fixture.debugElement.query(By.css('button'))!.injector.get(MatTooltip);
+ });
+
+ it('should set a fallback origin position by inverting the main origin position', () => {
+ tooltip.position = 'left';
+ expect(tooltip._getOrigin().main.originX).toBe('start');
+ expect(tooltip._getOrigin().fallback.originX).toBe('end');
+
+ tooltip.position = 'right';
+ expect(tooltip._getOrigin().main.originX).toBe('end');
+ expect(tooltip._getOrigin().fallback.originX).toBe('start');
+
+ tooltip.position = 'above';
+ expect(tooltip._getOrigin().main.originY).toBe('top');
+ expect(tooltip._getOrigin().fallback.originY).toBe('bottom');
+
+ tooltip.position = 'below';
+ expect(tooltip._getOrigin().main.originY).toBe('bottom');
+ expect(tooltip._getOrigin().fallback.originY).toBe('top');
+ });
+
+ it('should set a fallback overlay position by inverting the main overlay position', () => {
+ tooltip.position = 'left';
+ expect(tooltip._getOverlayPosition().main.overlayX).toBe('end');
+ expect(tooltip._getOverlayPosition().fallback.overlayX).toBe('start');
+
+ tooltip.position = 'right';
+ expect(tooltip._getOverlayPosition().main.overlayX).toBe('start');
+ expect(tooltip._getOverlayPosition().fallback.overlayX).toBe('end');
+
+ tooltip.position = 'above';
+ expect(tooltip._getOverlayPosition().main.overlayY).toBe('bottom');
+ expect(tooltip._getOverlayPosition().fallback.overlayY).toBe('top');
+
+ tooltip.position = 'below';
+ expect(tooltip._getOverlayPosition().main.overlayY).toBe('top');
+ expect(tooltip._getOverlayPosition().fallback.overlayY).toBe('bottom');
+ });
+ });
+
+ describe('scrollable usage', () => {
+ let fixture: ComponentFixture;
+ let buttonDebugElement: DebugElement;
+ let tooltipDirective: MatTooltip;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ScrollableTooltipDemo);
+ fixture.detectChanges();
+ buttonDebugElement = fixture.debugElement.query(By.css('button'))!;
+ tooltipDirective = buttonDebugElement.injector.get(MatTooltip);
+ });
+
+ it('should hide tooltip if clipped after changing positions', fakeAsync(() => {
+ assertTooltipInstance(tooltipDirective, false);
+
+ // Show the tooltip and tick for the show delay (default is 0)
+ tooltipDirective.show();
+ fixture.detectChanges();
+ tick(0);
+
+ // Expect that the tooltip is displayed
+ expect(tooltipDirective._isTooltipVisible())
+ .toBe(true, 'Expected tooltip to be initially visible');
+
+ // Scroll the page but tick just before the default throttle should update.
+ fixture.componentInstance.scrollDown();
+ tick(SCROLL_THROTTLE_MS - 1);
+ expect(tooltipDirective._isTooltipVisible())
+ .toBe(true, 'Expected tooltip to be visible when scrolling, before throttle limit');
+
+ // Finish ticking to the throttle's limit and check that the scroll event notified the
+ // tooltip and it was hidden.
+ tick(100);
+ fixture.detectChanges();
+ expect(tooltipDirective._isTooltipVisible())
+ .toBe(false, 'Expected tooltip hidden when scrolled out of view, after throttle limit');
+ }));
+
+ it('should execute the `hide` call, after scrolling away, inside the NgZone', fakeAsync(() => {
+ const inZoneSpy = jasmine.createSpy('in zone spy');
+
+ tooltipDirective.show();
+ fixture.detectChanges();
+ tick(0);
+
+ spyOn(tooltipDirective._tooltipInstance!, 'hide').and.callFake(() => {
+ inZoneSpy(NgZone.isInAngularZone());
+ });
+
+ fixture.componentInstance.scrollDown();
+ tick(100);
+ fixture.detectChanges();
+
+ expect(inZoneSpy).toHaveBeenCalled();
+ expect(inZoneSpy).toHaveBeenCalledWith(true);
+ }));
+
+ });
+
+ describe('with OnPush', () => {
+ let fixture: ComponentFixture;
+ let buttonDebugElement: DebugElement;
+ let buttonElement: HTMLButtonElement;
+ let tooltipDirective: MatTooltip;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OnPushTooltipDemo);
+ fixture.detectChanges();
+ buttonDebugElement = fixture.debugElement.query(By.css('button'))!;
+ buttonElement = buttonDebugElement.nativeElement;
+ tooltipDirective = buttonDebugElement.injector.get(MatTooltip);
+ });
+
+ it('should show and hide the tooltip', fakeAsync(() => {
+ assertTooltipInstance(tooltipDirective, false);
+
+ tooltipDirective.show();
+ tick(0); // Tick for the show delay (default is 0)
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+
+ fixture.detectChanges();
+
+ // wait until animation has finished
+ tick(500);
+
+ // 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)');
+
+ // After hide called, a timeout delay is created that will to hide the tooltip.
+ const tooltipDelay = 1000;
+ tooltipDirective.hide(tooltipDelay);
+ expect(tooltipDirective._isTooltipVisible()).toBe(true);
+
+ // After the tooltip delay elapses, expect that the tooltip is not visible.
+ tick(tooltipDelay);
+ fixture.detectChanges();
+ expect(tooltipDirective._isTooltipVisible()).toBe(false);
+
+ // On animation complete, should expect that the tooltip has been detached.
+ flushMicrotasks();
+ assertTooltipInstance(tooltipDirective, false);
+ }));
+
+ it('should have rendered the tooltip text on init', fakeAsync(() => {
+ // We don't bind mouse events on mobile devices.
+ if (platform.IOS || platform.ANDROID) {
+ return;
+ }
+
+ dispatchFakeEvent(buttonElement, 'mouseenter');
+ fixture.detectChanges();
+ tick(0);
+
+ const tooltipElement =
+ overlayContainerElement.querySelector('.mat-mdc-tooltip') as HTMLElement;
+ expect(tooltipElement.textContent).toContain('initial tooltip message');
+ }));
+ });
+
+ describe('touch gestures', () => {
+ beforeEach(() => {
+ platform.ANDROID = true;
+ });
+
+ it('should have a delay when showing on touchstart', fakeAsync(() => {
+ const fixture = TestBed.createComponent(BasicTooltipDemo);
+ fixture.detectChanges();
+ const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
+
+ dispatchFakeEvent(button, 'touchstart');
+ fixture.detectChanges();
+ tick(250); // Halfway through the delay.
+
+ assertTooltipInstance(fixture.componentInstance.tooltip, false);
+
+ tick(250); // Finish the delay.
+ fixture.detectChanges();
+ tick(500); // Finish the animation.
+
+ assertTooltipInstance(fixture.componentInstance.tooltip, true);
+ }));
+
+ it('should be able to disable opening on touch', fakeAsync(() => {
+ const fixture = TestBed.createComponent(BasicTooltipDemo);
+ fixture.componentInstance.touchGestures = 'off';
+ fixture.detectChanges();
+ const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
+
+ dispatchFakeEvent(button, 'touchstart');
+ fixture.detectChanges();
+ tick(500); // Finish the delay.
+ fixture.detectChanges();
+ tick(500); // Finish the animation.
+
+ assertTooltipInstance(fixture.componentInstance.tooltip, false);
+ }));
+
+ it('should not prevent the default action on touchstart', () => {
+ const fixture = TestBed.createComponent(BasicTooltipDemo);
+ fixture.detectChanges();
+ const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
+ const event = dispatchFakeEvent(button, 'touchstart');
+ fixture.detectChanges();
+
+ expect(event.defaultPrevented).toBe(false);
+ });
+
+ it('should close on touchend with a delay', fakeAsync(() => {
+ const fixture = TestBed.createComponent(BasicTooltipDemo);
+ fixture.detectChanges();
+ const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
+
+ dispatchFakeEvent(button, 'touchstart');
+ fixture.detectChanges();
+ tick(500); // Finish the open delay.
+ fixture.detectChanges();
+ tick(500); // Finish the animation.
+ assertTooltipInstance(fixture.componentInstance.tooltip, true);
+
+ dispatchFakeEvent(button, 'touchend');
+ fixture.detectChanges();
+ tick(1000); // 2/3 through the delay
+ assertTooltipInstance(fixture.componentInstance.tooltip, true);
+
+ tick(500); // Finish the delay.
+ fixture.detectChanges();
+ tick(500); // Finish the exit animation.
+
+ assertTooltipInstance(fixture.componentInstance.tooltip, false);
+ }));
+
+ it('should close on touchcancel with a delay', fakeAsync(() => {
+ const fixture = TestBed.createComponent(BasicTooltipDemo);
+ fixture.detectChanges();
+ const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
+
+ dispatchFakeEvent(button, 'touchstart');
+ fixture.detectChanges();
+ tick(500); // Finish the open delay.
+ fixture.detectChanges();
+ tick(500); // Finish the animation.
+ assertTooltipInstance(fixture.componentInstance.tooltip, true);
+
+ dispatchFakeEvent(button, 'touchcancel');
+ fixture.detectChanges();
+ tick(1000); // 2/3 through the delay
+ assertTooltipInstance(fixture.componentInstance.tooltip, true);
+
+ tick(500); // Finish the delay.
+ fixture.detectChanges();
+ tick(500); // Finish the exit animation.
+
+ assertTooltipInstance(fixture.componentInstance.tooltip, false);
+ }));
+
+ it('should disable native touch interactions', () => {
+ const fixture = TestBed.createComponent(BasicTooltipDemo);
+ fixture.detectChanges();
+
+ const styles = fixture.nativeElement.querySelector('button').style;
+ expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none');
+ });
+
+ it('should allow native touch interactions if touch gestures are turned off', () => {
+ const fixture = TestBed.createComponent(BasicTooltipDemo);
+ fixture.componentInstance.touchGestures = 'off';
+ fixture.detectChanges();
+
+ const styles = fixture.nativeElement.querySelector('button').style;
+ expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy();
+ });
+
+ it('should allow text selection on inputs when gestures are set to auto', () => {
+ const fixture = TestBed.createComponent(TooltipOnTextFields);
+ fixture.detectChanges();
+
+ const inputStyle = fixture.componentInstance.input.nativeElement.style;
+ const textareaStyle = fixture.componentInstance.textarea.nativeElement.style;
+
+ expect(inputStyle.userSelect).toBeFalsy();
+ expect(inputStyle.webkitUserSelect).toBeFalsy();
+ expect((inputStyle as any).msUserSelect).toBeFalsy();
+ expect((inputStyle as any).MozUserSelect).toBeFalsy();
+
+ expect(textareaStyle.userSelect).toBeFalsy();
+ expect(textareaStyle.webkitUserSelect).toBeFalsy();
+ expect((textareaStyle as any).msUserSelect).toBeFalsy();
+ expect((textareaStyle as any).MozUserSelect).toBeFalsy();
+ });
+
+ it('should disable text selection on inputs when gestures are set to on', () => {
+ const fixture = TestBed.createComponent(TooltipOnTextFields);
+ fixture.componentInstance.touchGestures = 'on';
+ fixture.detectChanges();
+
+ const inputStyle = fixture.componentInstance.input.nativeElement.style;
+ const inputUserSelect = inputStyle.userSelect || inputStyle.webkitUserSelect ||
+ (inputStyle as any).msUserSelect || (inputStyle as any).MozUserSelect;
+ const textareaStyle = fixture.componentInstance.textarea.nativeElement.style;
+ const textareaUserSelect = textareaStyle.userSelect || textareaStyle.webkitUserSelect ||
+ (textareaStyle as any).msUserSelect ||
+ (textareaStyle as any).MozUserSelect;
+
+ expect(inputUserSelect).toBe('none');
+ expect(textareaUserSelect).toBe('none');
+ });
+
+ it('should allow native dragging on draggable elements when gestures are set to auto', () => {
+ const fixture = TestBed.createComponent(TooltipOnDraggableElement);
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.button.nativeElement.style.webkitUserDrag).toBeFalsy();
+ });
+
+ it('should disable native dragging on draggable elements when gestures are set to on', () => {
+ const fixture = TestBed.createComponent(TooltipOnDraggableElement);
+ fixture.componentInstance.touchGestures = 'on';
+ fixture.detectChanges();
+
+ const styles = fixture.componentInstance.button.nativeElement.style;
+
+ if ('webkitUserDrag' in styles) {
+ expect(styles.webkitUserDrag).toBe('none');
+ }
+ });
+
+ it('should not open on `mouseenter` on iOS', () => {
+ platform.IOS = true;
+ platform.ANDROID = false;
+
+ const fixture = TestBed.createComponent(BasicTooltipDemo);
+
+ fixture.detectChanges();
+ dispatchMouseEvent(fixture.componentInstance.button.nativeElement, 'mouseenter');
+ fixture.detectChanges();
+
+ assertTooltipInstance(fixture.componentInstance.tooltip, false);
+ });
+
+ it('should not open on `mouseenter` on Android', () => {
+ platform.ANDROID = true;
+ platform.IOS = false;
+
+ const fixture = TestBed.createComponent(BasicTooltipDemo);
+
+ fixture.detectChanges();
+ dispatchMouseEvent(fixture.componentInstance.button.nativeElement, 'mouseenter');
+ fixture.detectChanges();
+
+ assertTooltipInstance(fixture.componentInstance.tooltip, false);
+ });
+ });
+
+ describe('mouse wheel handling', () => {
+ it('should close when a wheel event causes the cursor to leave the trigger', fakeAsync(() => {
+ // We don't bind wheel events on mobile devices.
+ if (platform.IOS || platform.ANDROID) {
+ return;
+ }
+
+ const fixture = TestBed.createComponent(BasicTooltipDemo);
+ fixture.detectChanges();
+ const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
+
+ dispatchFakeEvent(button, 'mouseenter');
+ fixture.detectChanges();
+ tick(500); // Finish the open delay.
+ fixture.detectChanges();
+ tick(500); // Finish the animation.
+ assertTooltipInstance(fixture.componentInstance.tooltip, true);
+
+ // Simulate the pointer at the bottom/right of the page.
+ const wheelEvent = createFakeEvent('wheel');
+ Object.defineProperties(wheelEvent, {
+ clientX: {get: () => window.innerWidth},
+ clientY: {get: () => window.innerHeight}
+ });
+
+ dispatchEvent(button, wheelEvent);
+ fixture.detectChanges();
+ tick(1500); // Finish the delay.
+ fixture.detectChanges();
+ tick(500); // Finish the exit animation.
+
+ assertTooltipInstance(fixture.componentInstance.tooltip, false);
+ }));
+
+ it('should not close if the cursor is over the trigger after a wheel event', fakeAsync(() => {
+ // We don't bind wheel events on mobile devices.
+ if (platform.IOS || platform.ANDROID) {
+ return;
+ }
+
+ const fixture = TestBed.createComponent(BasicTooltipDemo);
+ fixture.detectChanges();
+ const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
+
+ dispatchFakeEvent(button, 'mouseenter');
+ fixture.detectChanges();
+ tick(500); // Finish the open delay.
+ fixture.detectChanges();
+ tick(500); // Finish the animation.
+ assertTooltipInstance(fixture.componentInstance.tooltip, true);
+
+ // Simulate the pointer over the trigger.
+ const triggerRect = button.getBoundingClientRect();
+ const wheelEvent = createFakeEvent('wheel');
+ Object.defineProperties(wheelEvent, {
+ clientX: {get: () => triggerRect.left + 1},
+ clientY: {get: () => triggerRect.top + 1}
+ });
+
+ dispatchEvent(button, wheelEvent);
+ fixture.detectChanges();
+ tick(1500); // Finish the delay.
+ fixture.detectChanges();
+ tick(500); // Finish the exit animation.
+
+ assertTooltipInstance(fixture.componentInstance.tooltip, true);
+ }));
+ });
+
+});
+
+@Component({
+ selector: 'app',
+ template: `
+ `
+})
+class BasicTooltipDemo {
+ position: string = 'below';
+ message: any = initialTooltipMessage;
+ showButton: boolean = true;
+ showTooltipClass = false;
+ touchGestures: TooltipTouchGestures = 'auto';
+ @ViewChild(MatTooltip) tooltip: MatTooltip;
+ @ViewChild('button') button: ElementRef;
+}
+
+@Component({
+ selector: 'app',
+ template: `
+
+
+
`
+})
+class ScrollableTooltipDemo {
+ position: string = 'below';
+ message: string = initialTooltipMessage;
+ showButton: boolean = true;
+
+ @ViewChild(CdkScrollable) scrollingContainer: CdkScrollable;
+
+ scrollDown() {
+ const scrollingContainerEl = this.scrollingContainer.getElementRef().nativeElement;
+ scrollingContainerEl.scrollTop = 250;
+
+ // Emit a scroll event from the scrolling element in our component.
+ // This event should be picked up by the scrollable directive and notify.
+ // The notification should be picked up by the service.
+ dispatchFakeEvent(scrollingContainerEl, 'scroll');
+ }
+}
+
+@Component({
+ selector: 'app',
+ template: `
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+class OnPushTooltipDemo {
+ position: string = 'below';
+ message: string = initialTooltipMessage;
+}
+
+
+@Component({
+ selector: 'app',
+ template: `
+ `,
+})
+class DynamicTooltipsDemo {
+ tooltips: string[] = [];
+}
+
+@Component({
+ template: ``,
+})
+class DataBoundAriaLabelTooltip {
+ message = 'Hello there';
+}
+
+
+@Component({
+ template: `
+
+
+
+ `,
+})
+class TooltipOnTextFields {
+ @ViewChild('input') input: ElementRef;
+ @ViewChild('textarea') textarea: ElementRef;
+ touchGestures: TooltipTouchGestures = 'auto';
+}
+
+@Component({
+ template: `
+
+ `,
+})
+class TooltipOnDraggableElement {
+ @ViewChild('button') button: ElementRef;
+ touchGestures: TooltipTouchGestures = 'auto';
+}
+
+@Component({
+ selector: 'app',
+ template: ``
+})
+class TooltipDemoWithoutPositionBinding {
+ message: any = initialTooltipMessage;
+ @ViewChild(MatTooltip) tooltip: MatTooltip;
+ @ViewChild('button') button: ElementRef;
+}
+
+/** Asserts whether a tooltip directive has a tooltip instance. */
+function assertTooltipInstance(tooltip: MatTooltip, shouldExist: boolean): void {
+ // Note that we have to cast this to a boolean, because Jasmine will go into an infinite loop
+ // if it tries to stringify the `_tooltipInstance` when an assertion fails. The infinite loop
+ // happens due to the `_tooltipInstance` having a circular structure.
+ expect(!!tooltip._tooltipInstance).toBe(shouldExist);
+}
diff --git a/src/material-experimental/mdc-tooltip/tooltip.ts b/src/material-experimental/mdc-tooltip/tooltip.ts
new file mode 100644
index 000000000000..10d9c145e05b
--- /dev/null
+++ b/src/material-experimental/mdc-tooltip/tooltip.ts
@@ -0,0 +1,116 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * 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 {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ Directive,
+ ElementRef,
+ Inject,
+ NgZone,
+ Optional,
+ ViewContainerRef,
+ ViewEncapsulation,
+} from '@angular/core';
+import {DOCUMENT} from '@angular/common';
+import {Platform} from '@angular/cdk/platform';
+import {AriaDescriber, FocusMonitor} from '@angular/cdk/a11y';
+import {Directionality} from '@angular/cdk/bidi';
+import {ConnectedPosition, Overlay, ScrollDispatcher} from '@angular/cdk/overlay';
+import {
+ MatTooltipDefaultOptions,
+ MAT_TOOLTIP_DEFAULT_OPTIONS,
+ MAT_TOOLTIP_SCROLL_STRATEGY,
+ _MatTooltipBase,
+ _TooltipComponentBase,
+} from '@angular/material/tooltip';
+import {numbers} from '@material/tooltip';
+import {matTooltipAnimations} from './tooltip-animations';
+
+/**
+ * Directive that attaches a material design tooltip to the host element. Animates the showing and
+ * hiding of a tooltip provided position (defaults to below the element).
+ *
+ * https://material.io/design/components/tooltips.html
+ */
+@Directive({
+ selector: '[matTooltip]',
+ exportAs: 'matTooltip',
+ host: {
+ 'class': 'mat-mdc-tooltip-trigger'
+ }
+})
+export class MatTooltip extends _MatTooltipBase {
+ protected readonly _tooltipComponent = TooltipComponent;
+ protected readonly _transformOriginSelector = '.mat-mdc-tooltip';
+
+ constructor(
+ overlay: Overlay,
+ elementRef: ElementRef,
+ scrollDispatcher: ScrollDispatcher,
+ viewContainerRef: ViewContainerRef,
+ ngZone: NgZone,
+ platform: Platform,
+ ariaDescriber: AriaDescriber,
+ focusMonitor: FocusMonitor,
+ @Inject(MAT_TOOLTIP_SCROLL_STRATEGY) scrollStrategy: any,
+ @Optional() dir: Directionality,
+ @Optional() @Inject(MAT_TOOLTIP_DEFAULT_OPTIONS) defaultOptions: MatTooltipDefaultOptions,
+
+ /** @breaking-change 11.0.0 _document argument to become required. */
+ @Inject(DOCUMENT) _document: any) {
+
+ super(overlay, elementRef, scrollDispatcher, viewContainerRef, ngZone, platform, ariaDescriber,
+ focusMonitor, scrollStrategy, dir, defaultOptions, _document);
+ this._viewportMargin = numbers.MIN_VIEWPORT_TOOLTIP_THRESHOLD;
+ }
+
+ protected _addOffset(position: ConnectedPosition): ConnectedPosition {
+ const offset = numbers.UNBOUNDED_ANCHOR_GAP;
+ const isLtr = !this._dir || this._dir.value == 'ltr';
+
+ if (position.originY === 'top') {
+ position.offsetY = -offset;
+ } else if (position.originY === 'bottom') {
+ position.offsetY = offset;
+ } else if (position.originX === 'start') {
+ position.offsetX = isLtr ? -offset : offset;
+ } else if (position.originX === 'end') {
+ position.offsetX = isLtr ? offset : -offset;
+ }
+
+ return position;
+ }
+}
+
+/**
+ * Internal component that wraps the tooltip's content.
+ * @docs-private
+ */
+@Component({
+ selector: 'mat-tooltip-component',
+ templateUrl: 'tooltip.html',
+ 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',
+ '(body:click)': 'this._handleBodyInteraction()',
+ '(body:auxclick)': 'this._handleBodyInteraction()',
+ 'aria-hidden': 'true',
+ }
+})
+export class TooltipComponent extends _TooltipComponentBase {
+ constructor(changeDetectorRef: ChangeDetectorRef) {
+ super(changeDetectorRef);
+ }
+}
diff --git a/src/material-experimental/mdc_require_config.js b/src/material-experimental/mdc_require_config.js
index f86b99b8cef0..ab6d9e12b0bb 100644
--- a/src/material-experimental/mdc_require_config.js
+++ b/src/material-experimental/mdc_require_config.js
@@ -33,6 +33,7 @@ require.config({
'@material/tab-scroller': '/base/npm/node_modules/@material/tab-scroller/dist/mdc.tabScroller',
'@material/data-table': '/base/npm/node_modules/@material/data-table/dist/mdc.dataTable',
'@material/textfield': '/base/npm/node_modules/@material/textfield/dist/mdc.textfield',
+ '@material/tooltip': '/base/npm/node_modules/@material/tooltip/dist/mdc.tooltip',
'@material/top-app-bar': '/base/npm/node_modules/@material/top-app-bar/dist/mdc.topAppBar',
}
});
diff --git a/src/material/tooltip/tooltip.ts b/src/material/tooltip/tooltip.ts
index 54c26086bbe3..64c1ac6dbe2c 100644
--- a/src/material/tooltip/tooltip.ts
+++ b/src/material/tooltip/tooltip.ts
@@ -12,6 +12,7 @@ import {BooleanInput, coerceBooleanProperty, NumberInput} from '@angular/cdk/coe
import {ESCAPE, hasModifierKey} from '@angular/cdk/keycodes';
import {BreakpointObserver, Breakpoints, BreakpointState} from '@angular/cdk/layout';
import {
+ ConnectedPosition,
FlexibleConnectedPositionStrategy,
HorizontalConnectionPos,
OriginConnectionPosition,
@@ -22,7 +23,7 @@ import {
VerticalConnectionPos,
} from '@angular/cdk/overlay';
import {Platform, normalizePassiveListenerOptions} from '@angular/cdk/platform';
-import {ComponentPortal} from '@angular/cdk/portal';
+import {ComponentPortal, ComponentType} from '@angular/cdk/portal';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
import {
ChangeDetectionStrategy,
@@ -123,30 +124,23 @@ export function MAT_TOOLTIP_DEFAULT_OPTIONS_FACTORY(): MatTooltipDefaultOptions
};
}
-/**
- * Directive that attaches a material design tooltip to the host element. Animates the showing and
- * hiding of a tooltip provided position (defaults to below the element).
- *
- * https://material.io/design/components/tooltips.html
- */
-@Directive({
- selector: '[matTooltip]',
- exportAs: 'matTooltip',
- host: {
- 'class': 'mat-tooltip-trigger'
- }
-})
-export class MatTooltip implements OnDestroy, AfterViewInit {
+
+@Directive()
+export abstract class _MatTooltipBase implements OnDestroy,
+ AfterViewInit {
_overlayRef: OverlayRef | null;
- _tooltipInstance: TooltipComponent | null;
+ _tooltipInstance: T | null;
- private _portal: ComponentPortal;
+ private _portal: ComponentPortal;
private _position: TooltipPosition = 'below';
private _disabled: boolean = false;
private _tooltipClass: string|string[]|Set|{[key: string]: any};
private _scrollStrategy: () => ScrollStrategy;
private _viewInitialized = false;
private _pointerExitEventsInitialized = false;
+ protected abstract readonly _tooltipComponent: ComponentType;
+ protected abstract readonly _transformOriginSelector: string;
+ protected _viewportMargin = 8;
/** Allows the user to define the position of the tooltip relative to the parent element */
@Input('matTooltipPosition')
@@ -267,10 +261,9 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
private _platform: Platform,
private _ariaDescriber: AriaDescriber,
private _focusMonitor: FocusMonitor,
- @Inject(MAT_TOOLTIP_SCROLL_STRATEGY) scrollStrategy: any,
- @Optional() private _dir: Directionality,
- @Optional() @Inject(MAT_TOOLTIP_DEFAULT_OPTIONS)
- private _defaultOptions: MatTooltipDefaultOptions,
+ scrollStrategy: any,
+ protected _dir: Directionality,
+ private _defaultOptions: MatTooltipDefaultOptions,
/** @breaking-change 11.0.0 _document argument to become required. */
@Inject(DOCUMENT) _document: any) {
@@ -345,7 +338,8 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
const overlayRef = this._createOverlay();
this._detach();
- this._portal = this._portal || new ComponentPortal(TooltipComponent, this._viewContainerRef);
+ this._portal = this._portal ||
+ new ComponentPortal(this._tooltipComponent, this._viewContainerRef);
this._tooltipInstance = overlayRef.attach(this._portal).instance;
this._tooltipInstance.afterHidden()
.pipe(takeUntil(this._destroyed))
@@ -396,9 +390,9 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
// Create connected position strategy that listens for scroll events to reposition.
const strategy = this._overlay.position()
.flexibleConnectedTo(this._elementRef)
- .withTransformOriginOn('.mat-tooltip')
+ .withTransformOriginOn(this._transformOriginSelector)
.withFlexibleDimensions(false)
- .withViewportMargin(8)
+ .withViewportMargin(this._viewportMargin)
.withScrollableContainers(scrollableAncestors);
strategy.positionChanges.pipe(takeUntil(this._destroyed)).subscribe(change => {
@@ -444,11 +438,16 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
const overlay = this._getOverlayPosition();
position.withPositions([
- {...origin.main, ...overlay.main},
- {...origin.fallback, ...overlay.fallback}
+ this._addOffset({...origin.main, ...overlay.main}),
+ this._addOffset({...origin.fallback, ...overlay.fallback})
]);
}
+ /** Adds the configured offset to a position. Used as a hook for child classes. */
+ protected _addOffset(position: ConnectedPosition): ConnectedPosition {
+ return position;
+ }
+
/**
* Returns the origin position and a fallback position based on the user's position preference.
* The fallback position is the inverse of the origin (e.g. `'below' -> 'above'`).
@@ -682,26 +681,45 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
}
/**
- * Internal component that wraps the tooltip's content.
- * @docs-private
+ * Directive that attaches a material design tooltip to the host element. Animates the showing and
+ * hiding of a tooltip provided position (defaults to below the element).
+ *
+ * https://material.io/design/components/tooltips.html
*/
-@Component({
- selector: 'mat-tooltip-component',
- templateUrl: 'tooltip.html',
- styleUrls: ['tooltip.css'],
- encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
- animations: [matTooltipAnimations.tooltipState],
+@Directive({
+ selector: '[matTooltip]',
+ exportAs: 'matTooltip',
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',
- '(body:click)': 'this._handleBodyInteraction()',
- '(body:auxclick)': 'this._handleBodyInteraction()',
- 'aria-hidden': 'true',
+ 'class': 'mat-tooltip-trigger'
}
})
-export class TooltipComponent implements OnDestroy {
+export class MatTooltip extends _MatTooltipBase {
+ protected readonly _tooltipComponent = TooltipComponent;
+ protected readonly _transformOriginSelector = '.mat-tooltip';
+
+ constructor(
+ overlay: Overlay,
+ elementRef: ElementRef,
+ scrollDispatcher: ScrollDispatcher,
+ viewContainerRef: ViewContainerRef,
+ ngZone: NgZone,
+ platform: Platform,
+ ariaDescriber: AriaDescriber,
+ focusMonitor: FocusMonitor,
+ @Inject(MAT_TOOLTIP_SCROLL_STRATEGY) scrollStrategy: any,
+ @Optional() dir: Directionality,
+ @Optional() @Inject(MAT_TOOLTIP_DEFAULT_OPTIONS) defaultOptions: MatTooltipDefaultOptions,
+
+ /** @breaking-change 11.0.0 _document argument to become required. */
+ @Inject(DOCUMENT) _document: any) {
+
+ super(overlay, elementRef, scrollDispatcher, viewContainerRef, ngZone, platform, ariaDescriber,
+ focusMonitor, scrollStrategy, dir, defaultOptions, _document);
+ }
+}
+
+@Directive()
+export abstract class _TooltipComponentBase implements OnDestroy {
/** Message to display in the tooltip */
message: string;
@@ -723,12 +741,7 @@ export class TooltipComponent implements OnDestroy {
/** Subject for notifying that the tooltip has been hidden from the view */
private readonly _onHide: Subject = new Subject();
- /** Stream that emits whether the user has a handset-sized display. */
- _isHandset: Observable = this._breakpointObserver.observe(Breakpoints.Handset);
-
- constructor(
- private _changeDetectorRef: ChangeDetectorRef,
- private _breakpointObserver: BreakpointObserver) {}
+ constructor(private _changeDetectorRef: ChangeDetectorRef) {}
/**
* Shows the tooltip with an animation originating from the provided origin
@@ -824,3 +837,34 @@ export class TooltipComponent implements OnDestroy {
this._changeDetectorRef.markForCheck();
}
}
+
+/**
+ * Internal component that wraps the tooltip's content.
+ * @docs-private
+ */
+@Component({
+ selector: 'mat-tooltip-component',
+ templateUrl: 'tooltip.html',
+ 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',
+ '(body:click)': 'this._handleBodyInteraction()',
+ '(body:auxclick)': 'this._handleBodyInteraction()',
+ 'aria-hidden': 'true',
+ }
+})
+export class TooltipComponent extends _TooltipComponentBase {
+ /** Stream that emits whether the user has a handset-sized display. */
+ _isHandset: Observable = this._breakpointObserver.observe(Breakpoints.Handset);
+
+ constructor(
+ changeDetectorRef: ChangeDetectorRef,
+ private _breakpointObserver: BreakpointObserver) {
+ super(changeDetectorRef);
+ }
+}
diff --git a/tools/public_api_guard/material/tooltip.d.ts b/tools/public_api_guard/material/tooltip.d.ts
index b1750c34a8a2..89ef9d0e7784 100644
--- a/tools/public_api_guard/material/tooltip.d.ts
+++ b/tools/public_api_guard/material/tooltip.d.ts
@@ -1,22 +1,10 @@
-export declare function getMatTooltipInvalidPositionError(position: string): Error;
-
-export declare const MAT_TOOLTIP_DEFAULT_OPTIONS: InjectionToken;
-
-export declare function MAT_TOOLTIP_DEFAULT_OPTIONS_FACTORY(): MatTooltipDefaultOptions;
-
-export declare const MAT_TOOLTIP_SCROLL_STRATEGY: InjectionToken<() => ScrollStrategy>;
-
-export declare function MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY(overlay: Overlay): () => ScrollStrategy;
-
-export declare const MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY_PROVIDER: {
- provide: InjectionToken<() => ScrollStrategy>;
- deps: (typeof Overlay)[];
- useFactory: typeof MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY;
-};
-
-export declare class MatTooltip implements OnDestroy, AfterViewInit {
+export declare abstract class _MatTooltipBase implements OnDestroy, AfterViewInit {
+ protected _dir: Directionality;
_overlayRef: OverlayRef | null;
- _tooltipInstance: TooltipComponent | null;
+ protected abstract readonly _tooltipComponent: ComponentType;
+ _tooltipInstance: T | null;
+ protected abstract readonly _transformOriginSelector: string;
+ protected _viewportMargin: number;
get disabled(): boolean;
set disabled(value: boolean);
hideDelay: number;
@@ -34,6 +22,7 @@ export declare class MatTooltip implements OnDestroy, AfterViewInit {
touchGestures: TooltipTouchGestures;
constructor(_overlay: Overlay, _elementRef: ElementRef, _scrollDispatcher: ScrollDispatcher, _viewContainerRef: ViewContainerRef, _ngZone: NgZone, _platform: Platform, _ariaDescriber: AriaDescriber, _focusMonitor: FocusMonitor, scrollStrategy: any, _dir: Directionality, _defaultOptions: MatTooltipDefaultOptions,
_document: any);
+ protected _addOffset(position: ConnectedPosition): ConnectedPosition;
_getOrigin(): {
main: OriginConnectionPosition;
fallback: OriginConnectionPosition;
@@ -51,7 +40,54 @@ export declare class MatTooltip implements OnDestroy, AfterViewInit {
static ngAcceptInputType_disabled: BooleanInput;
static ngAcceptInputType_hideDelay: NumberInput;
static ngAcceptInputType_showDelay: NumberInput;
- static ɵdir: i0.ɵɵDirectiveDefWithMeta;
+ static ɵdir: i0.ɵɵDirectiveDefWithMeta<_MatTooltipBase, never, never, { "position": "matTooltipPosition"; "disabled": "matTooltipDisabled"; "showDelay": "matTooltipShowDelay"; "hideDelay": "matTooltipHideDelay"; "touchGestures": "matTooltipTouchGestures"; "message": "matTooltip"; "tooltipClass": "matTooltipClass"; }, {}, never>;
+ static ɵfac: i0.ɵɵFactoryDef<_MatTooltipBase, never>;
+}
+
+export declare abstract class _TooltipComponentBase implements OnDestroy {
+ _hideTimeoutId: number | null;
+ _showTimeoutId: number | null;
+ _visibility: TooltipVisibility;
+ message: string;
+ tooltipClass: string | string[] | Set | {
+ [key: string]: any;
+ };
+ constructor(_changeDetectorRef: ChangeDetectorRef);
+ _animationDone(event: AnimationEvent): void;
+ _animationStart(): void;
+ _handleBodyInteraction(): void;
+ _markForCheck(): void;
+ afterHidden(): Observable;
+ hide(delay: number): void;
+ isVisible(): boolean;
+ ngOnDestroy(): void;
+ show(delay: number): void;
+ static ɵdir: i0.ɵɵDirectiveDefWithMeta<_TooltipComponentBase, never, never, {}, {}, never>;
+ static ɵfac: i0.ɵɵFactoryDef<_TooltipComponentBase, never>;
+}
+
+export declare function getMatTooltipInvalidPositionError(position: string): Error;
+
+export declare const MAT_TOOLTIP_DEFAULT_OPTIONS: InjectionToken;
+
+export declare function MAT_TOOLTIP_DEFAULT_OPTIONS_FACTORY(): MatTooltipDefaultOptions;
+
+export declare const MAT_TOOLTIP_SCROLL_STRATEGY: InjectionToken<() => ScrollStrategy>;
+
+export declare function MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY(overlay: Overlay): () => ScrollStrategy;
+
+export declare const MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY_PROVIDER: {
+ provide: InjectionToken<() => ScrollStrategy>;
+ deps: (typeof Overlay)[];
+ useFactory: typeof MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY;
+};
+
+export declare class MatTooltip extends _MatTooltipBase {
+ protected readonly _tooltipComponent: typeof TooltipComponent;
+ protected readonly _transformOriginSelector = ".mat-tooltip";
+ constructor(overlay: Overlay, elementRef: ElementRef, scrollDispatcher: ScrollDispatcher, viewContainerRef: ViewContainerRef, ngZone: NgZone, platform: Platform, ariaDescriber: AriaDescriber, focusMonitor: FocusMonitor, scrollStrategy: any, dir: Directionality, defaultOptions: MatTooltipDefaultOptions,
+ _document: any);
+ static ɵdir: i0.ɵɵDirectiveDefWithMeta;
static ɵfac: i0.ɵɵFactoryDef;
}
@@ -76,25 +112,9 @@ export declare const SCROLL_THROTTLE_MS = 20;
export declare const TOOLTIP_PANEL_CLASS = "mat-tooltip-panel";
-export declare class TooltipComponent implements OnDestroy {
- _hideTimeoutId: number | null;
+export declare class TooltipComponent extends _TooltipComponentBase {
_isHandset: Observable;
- _showTimeoutId: number | null;
- _visibility: TooltipVisibility;
- message: string;
- tooltipClass: string | string[] | Set | {
- [key: string]: any;
- };
- constructor(_changeDetectorRef: ChangeDetectorRef, _breakpointObserver: BreakpointObserver);
- _animationDone(event: AnimationEvent): void;
- _animationStart(): void;
- _handleBodyInteraction(): void;
- _markForCheck(): void;
- afterHidden(): Observable;
- hide(delay: number): void;
- isVisible(): boolean;
- ngOnDestroy(): void;
- show(delay: number): void;
+ constructor(changeDetectorRef: ChangeDetectorRef, _breakpointObserver: BreakpointObserver);
static ɵcmp: i0.ɵɵComponentDefWithMeta;
static ɵfac: i0.ɵɵFactoryDef;
}
diff --git a/tools/system-config-tmpl.js b/tools/system-config-tmpl.js
index ca531ffe7962..920745ced6e0 100644
--- a/tools/system-config-tmpl.js
+++ b/tools/system-config-tmpl.js
@@ -66,6 +66,7 @@ var pathMapping = {
'@material/tab-indicator': 'node:@material/tab-indicator/dist/mdc.tabIndicator.js',
'@material/tab-scroller': 'node:@material/tab-scroller/dist/mdc.tabScroller.js',
'@material/textfield': 'node:@material/textfield/dist/mdc.textfield.js',
+ '@material/tooltip': 'node:@material/tooltip/dist/mdc.tooltip.js',
'@material/top-app-bar': 'node:@material/top-app-bar/dist/mdc.topAppBar.js'
};