sass-references/angular-material/material/tooltip/tooltip.spec.ts

1792 lines
62 KiB
TypeScript

import {FocusMonitor} from '@angular/cdk/a11y';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {ESCAPE} from '@angular/cdk/keycodes';
import {CdkScrollable, OverlayContainer, OverlayModule} from '@angular/cdk/overlay';
import {Platform} from '@angular/cdk/platform';
import {
createFakeEvent,
createKeyboardEvent,
createMouseEvent,
dispatchEvent,
dispatchFakeEvent,
dispatchKeyboardEvent,
dispatchMouseEvent,
patchElementFocus,
} from '@angular/cdk/testing/private';
import {
ChangeDetectionStrategy,
Component,
DebugElement,
ElementRef,
ViewChild,
} from '@angular/core';
import {
ComponentFixture,
TestBed,
fakeAsync,
flush,
inject,
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,
MatTooltip,
MatTooltipModule,
SCROLL_THROTTLE_MS,
TooltipPosition,
TooltipTouchGestures,
} from './index';
const initialTooltipMessage = 'initial tooltip message';
describe('MatTooltip', () => {
let overlayContainerElement: HTMLElement;
let dir: {value: Direction; change: Subject<Direction>};
let platform: Platform;
let focusMonitor: FocusMonitor;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
MatTooltipModule,
OverlayModule,
BasicTooltipDemo,
ScrollableTooltipDemo,
OnPushTooltipDemo,
DynamicTooltipsDemo,
TooltipOnTextFields,
TooltipOnDraggableElement,
DataBoundAriaLabelTooltip,
],
providers: [
{
provide: Directionality,
useFactory: () => {
return (dir = {value: 'ltr', change: new Subject()});
},
},
],
});
inject(
[OverlayContainer, FocusMonitor, Platform],
(oc: OverlayContainer, fm: FocusMonitor, pl: Platform) => {
overlayContainerElement = oc.getContainerElement();
focusMonitor = fm;
platform = pl;
},
)();
}));
describe('basic usage', () => {
let fixture: ComponentFixture<BasicTooltipDemo>;
let buttonDebugElement: DebugElement;
let buttonElement: HTMLButtonElement;
let tooltipDirective: MatTooltip;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(BasicTooltipDemo);
fixture.detectChanges();
tick();
buttonDebugElement = fixture.debugElement.query(By.css('button'))!;
buttonElement = buttonDebugElement.nativeElement;
tooltipDirective = buttonDebugElement.injector.get<MatTooltip>(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
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.classList).toContain('mat-mdc-tooltip-show');
expect(overlayContainerElement.textContent).toContain(initialTooltipMessage);
// 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);
// 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.
finishCurrentTooltipAnimation(overlayContainerElement, false);
assertTooltipInstance(tooltipDirective, false);
flush();
}));
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();
finishCurrentTooltipAnimation(overlayContainerElement, true);
tooltipDirective._overlayRef!.detach();
tick(0);
fixture.detectChanges();
expect(tooltipDirective._isTooltipVisible()).toBe(false);
assertTooltipInstance(tooltipDirective, false);
tooltipDirective.show();
tick(0);
finishCurrentTooltipAnimation(overlayContainerElement, true);
expect(tooltipDirective._isTooltipVisible()).toBe(true);
flush();
}));
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, BasicTooltipDemo],
providers: [
{
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
useValue: {showDelay: 1337, hideDelay: 7331},
},
],
});
fixture = TestBed.createComponent(BasicTooltipDemo);
fixture.detectChanges();
tooltipDirective = fixture.debugElement
.query(By.css('button'))!
.injector.get<MatTooltip>(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);
flush();
}));
it('should be able to override the default position', fakeAsync(() => {
TestBed.resetTestingModule().configureTestingModule({
imports: [MatTooltipModule, OverlayModule],
declarations: [TooltipDemoWithoutPositionBinding],
providers: [
{
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
useValue: {position: 'right'},
},
],
});
const newFixture = TestBed.createComponent(TooltipDemoWithoutPositionBinding);
newFixture.detectChanges();
tooltipDirective = newFixture.debugElement
.query(By.css('button'))!
.injector.get<MatTooltip>(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 be able to define a default (global) tooltip class', fakeAsync(() => {
TestBed.resetTestingModule().configureTestingModule({
declarations: [TooltipDemoWithoutTooltipClassBinding],
imports: [MatTooltipModule, OverlayModule],
providers: [
{
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
useValue: {tooltipClass: 'my-default-tooltip-class'},
},
],
});
const fixture = TestBed.createComponent(TooltipDemoWithoutTooltipClassBinding);
fixture.detectChanges();
tooltipDirective = fixture.componentInstance.tooltip;
tooltipDirective.show();
fixture.detectChanges();
tick();
const overlayRef = tooltipDirective._overlayRef!;
const tooltipElement = overlayRef.overlayElement.querySelector(
'.mat-mdc-tooltip',
) as HTMLElement;
expect(tooltipDirective.tooltipClass).toBe('my-default-tooltip-class');
expect(tooltipElement.classList).toContain('my-default-tooltip-class');
}));
it('should be able to provide tooltip class over the custom default one', fakeAsync(() => {
TestBed.resetTestingModule().configureTestingModule({
declarations: [TooltipDemoWithTooltipClassBinding],
imports: [MatTooltipModule, OverlayModule],
providers: [
{
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
useValue: {tooltipClass: 'my-default-tooltip-class'},
},
],
});
const fixture = TestBed.createComponent(TooltipDemoWithTooltipClassBinding);
fixture.detectChanges();
tooltipDirective = fixture.componentInstance.tooltip;
tooltipDirective.show();
fixture.detectChanges();
tick();
const overlayRef = tooltipDirective._overlayRef!;
const tooltipElement = overlayRef.overlayElement.querySelector(
'.mat-mdc-tooltip',
) as HTMLElement;
expect(tooltipDirective.tooltipClass).not.toBe('my-default-tooltip-class');
expect(tooltipElement.classList).not.toContain('my-default-tooltip-class');
expect(tooltipElement.classList).toContain('fixed-tooltip-class');
}));
it('should position on the bottom-left by default', fakeAsync(() => {
// We don't bind mouse events on mobile devices.
if (platform.IOS || platform.ANDROID) {
return;
}
TestBed.resetTestingModule().configureTestingModule({
imports: [MatTooltipModule, OverlayModule],
declarations: [WideTooltipDemo],
});
const wideFixture = TestBed.createComponent(WideTooltipDemo);
wideFixture.detectChanges();
tooltipDirective = wideFixture.debugElement
.query(By.css('button'))!
.injector.get<MatTooltip>(MatTooltip);
const button: HTMLButtonElement = wideFixture.nativeElement.querySelector('button');
const triggerRect = button.getBoundingClientRect();
dispatchMouseEvent(button, 'mouseenter', triggerRect.right - 100, triggerRect.top + 100);
wideFixture.detectChanges();
tick();
expect(tooltipDirective._isTooltipVisible()).toBe(true);
expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBeLessThan(
triggerRect.right - 250,
);
expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBeGreaterThanOrEqual(
triggerRect.bottom,
);
}));
it('should be able to override the default positionAtOrigin', async () => {
// We don't bind mouse events on mobile devices.
if (platform.IOS || platform.ANDROID) {
return;
}
TestBed.resetTestingModule().configureTestingModule({
imports: [MatTooltipModule, OverlayModule],
declarations: [WideTooltipDemo],
providers: [
{
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
useValue: {positionAtOrigin: true},
},
],
});
const wideFixture = TestBed.createComponent(WideTooltipDemo);
wideFixture.detectChanges();
tooltipDirective = wideFixture.debugElement
.query(By.css('button'))!
.injector.get<MatTooltip>(MatTooltip);
const button: HTMLButtonElement = wideFixture.nativeElement.querySelector('button');
const triggerRect = button.getBoundingClientRect();
dispatchMouseEvent(button, 'mouseenter', triggerRect.right - 100, triggerRect.top + 100);
wideFixture.detectChanges();
await new Promise<void>(resolve => setTimeout(resolve));
expect(tooltipDirective._isTooltipVisible()).toBe(true);
const actualOffsetLeft = tooltipDirective._overlayRef!.overlayElement.offsetLeft;
const expectedOffsetLeft = triggerRect.right - 100 - 20;
expect(actualOffsetLeft).toBeLessThanOrEqual(expectedOffsetLeft + 1);
expect(actualOffsetLeft).toBeGreaterThanOrEqual(expectedOffsetLeft - 1);
expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBe(triggerRect.top + 100);
});
it('should be able to disable tooltip interactivity', fakeAsync(() => {
TestBed.resetTestingModule().configureTestingModule({
imports: [MatTooltipModule, OverlayModule, NoopAnimationsModule],
declarations: [TooltipDemoWithoutPositionBinding],
providers: [
{
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
useValue: {disableTooltipInteractivity: true},
},
],
});
const newFixture = TestBed.createComponent(TooltipDemoWithoutPositionBinding);
newFixture.detectChanges();
tooltipDirective = newFixture.debugElement
.query(By.css('button'))!
.injector.get<MatTooltip>(MatTooltip);
tooltipDirective.show();
newFixture.detectChanges();
tick();
expect(tooltipDirective._overlayRef?.overlayElement.classList).toContain(
'mat-mdc-tooltip-panel-non-interactive',
);
}));
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)
.withContext('Expected the overlay panel element to have the tooltip panel class set.')
.toContain('mat-mdc-tooltip-panel');
}));
it('should not show if disabled', fakeAsync(() => {
// Test that disabling the tooltip will not set the tooltip visible
tooltipDirective.disabled = true;
fixture.changeDetectorRef.markForCheck();
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;
fixture.changeDetectorRef.markForCheck();
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.changeDetectorRef.markForCheck();
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);
spyOn(tooltipDirective._overlayRef!, 'updatePosition').and.callThrough();
tooltipDirective.position = 'above';
fixture.detectChanges();
tick();
assertTooltipInstance(tooltipDirective, true);
expect(tooltipDirective._overlayRef!.updatePosition).toHaveBeenCalled();
}));
it('should update the tooltip position when the directionality changes', fakeAsync(() => {
tooltipDirective.position = 'right';
tooltipDirective.show();
tick();
assertTooltipInstance(tooltipDirective, true);
const spy = spyOn(tooltipDirective as any, '_updatePosition').and.callThrough();
dir.change.next('rtl');
assertTooltipInstance(tooltipDirective, true);
expect(spy).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();
finishCurrentTooltipAnimation(overlayContainerElement, false);
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!.isVisible()).toBe(true);
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.changeDetectorRef.markForCheck();
fixture.detectChanges();
// Make sure classes are correctly added
tooltipElement = overlayContainerElement.querySelector('.mat-mdc-tooltip') as HTMLElement;
expect(tooltipElement.classList)
.withContext('Expected to have the class after enabling matTooltipClass')
.toContain('custom-one');
expect(tooltipElement.classList)
.withContext('Expected to have the class after enabling matTooltipClass')
.toContain('custom-two');
}));
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('');
flush();
}));
it('should have an aria-describedby 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 toggle aria-describedby depending on whether the tooltip is disabled', fakeAsync(() => {
expect(buttonElement.getAttribute('aria-describedby')).toBeTruthy();
fixture.componentInstance.tooltipDisabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick();
expect(buttonElement.hasAttribute('aria-describedby')).toBe(false);
fixture.componentInstance.tooltipDisabled = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick();
expect(buttonElement.getAttribute('aria-describedby')).toBeTruthy();
}));
it('should not try to dispose the tooltip when destroyed and done hiding', fakeAsync(() => {
tooltipDirective.show();
fixture.detectChanges();
finishCurrentTooltipAnimation(overlayContainerElement, true);
const tooltipDelay = 1000;
tooltipDirective.hide();
tick(tooltipDelay); // Change the tooltip state to hidden and trigger animation start
fixture.componentInstance.showButton = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
}));
it('should complete the afterHidden stream when tooltip is destroyed', fakeAsync(() => {
tooltipDirective.show();
fixture.detectChanges();
finishCurrentTooltipAnimation(overlayContainerElement, true);
const spy = jasmine.createSpy('complete spy');
const subscription = tooltipDirective
._tooltipInstance!.afterHidden()
.subscribe({complete: spy});
tooltipDirective.hide(0);
tick(0);
fixture.detectChanges();
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.changeDetectorRef.markForCheck();
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).withContext('Expected tooltip to be shown.').toBeTruthy();
expect(tooltipWrapper.getAttribute('dir'))
.withContext('Expected tooltip to be in RTL mode.')
.toBe('rtl');
}));
it('should keep the overlay direction in sync with the trigger direction', fakeAsync(() => {
dir.value = 'rtl';
tooltipDirective.show();
tick(0);
fixture.detectChanges();
finishCurrentTooltipAnimation(overlayContainerElement, true);
let tooltipWrapper = overlayContainerElement.querySelector(
'.cdk-overlay-connected-position-bounding-box',
)!;
expect(tooltipWrapper.getAttribute('dir'))
.withContext('Expected tooltip to be in RTL.')
.toBe('rtl');
tooltipDirective.hide(0);
tick(0);
fixture.detectChanges();
finishCurrentTooltipAnimation(overlayContainerElement, false);
dir.value = 'ltr';
tooltipDirective.show();
tick(0);
fixture.detectChanges();
finishCurrentTooltipAnimation(overlayContainerElement, true);
tooltipWrapper = overlayContainerElement.querySelector(
'.cdk-overlay-connected-position-bounding-box',
)!;
expect(tooltipWrapper.getAttribute('dir'))
.withContext('Expected tooltip to be in LTR.')
.toBe('ltr');
flush();
}));
it('should be able to set the tooltip message as a number', fakeAsync(() => {
fixture.componentInstance.message = 100;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(tooltipDirective.message).toBe('100');
}));
it('should hide when clicking away', fakeAsync(() => {
tooltipDirective.show();
tick(0);
fixture.detectChanges();
finishCurrentTooltipAnimation(overlayContainerElement, true);
expect(tooltipDirective._isTooltipVisible()).toBe(true);
expect(overlayContainerElement.textContent).toContain(initialTooltipMessage);
document.body.click();
tick(0);
fixture.detectChanges();
finishCurrentTooltipAnimation(overlayContainerElement, false);
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();
finishCurrentTooltipAnimation(overlayContainerElement, true);
expect(tooltipDirective._isTooltipVisible()).toBe(true);
expect(overlayContainerElement.textContent).toContain(initialTooltipMessage);
dispatchFakeEvent(document.body, 'auxclick');
tick(0);
fixture.detectChanges();
finishCurrentTooltipAnimation(overlayContainerElement, false);
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);
finishCurrentTooltipAnimation(overlayContainerElement, true);
expect(overlayContainerElement.textContent).toContain(initialTooltipMessage);
flush();
}));
it('should hide when pressing escape', fakeAsync(() => {
tooltipDirective.show();
tick(0);
fixture.detectChanges();
tick(500);
expect(tooltipDirective._isTooltipVisible()).toBe(true);
expect(overlayContainerElement.textContent).toContain(initialTooltipMessage);
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
tick(0);
fixture.detectChanges();
tick(500);
fixture.detectChanges();
finishCurrentTooltipAnimation(overlayContainerElement, false);
expect(tooltipDirective._isTooltipVisible()).toBe(false);
expect(overlayContainerElement.textContent).toBe('');
flush();
}));
it('should not throw when pressing ESCAPE', fakeAsync(() => {
expect(() => {
dispatchKeyboardEvent(document.body, '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(document.body, '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(document.body, event);
fixture.detectChanges();
flush();
expect(event.defaultPrevented).toBe(false);
}));
it('should not show the tooltip on programmatic 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();
finishCurrentTooltipAnimation(overlayContainerElement, true);
const overlayRef = tooltipDirective._overlayRef!;
spyOn(overlayRef, 'detach').and.callThrough();
tooltipDirective.show();
tick(0);
expect(tooltipDirective._isTooltipVisible()).toBe(true);
fixture.detectChanges();
finishCurrentTooltipAnimation(overlayContainerElement, true);
expect(overlayRef.detach).not.toHaveBeenCalled();
flush();
}));
it('should set a class on the overlay panel that reflects the position', fakeAsync(() => {
// Move the element so that the primary position is always used.
buttonElement.style.position = 'fixed';
buttonElement.style.top = buttonElement.style.left = '200px';
fixture.componentInstance.message = 'hi';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
setPositionAndShow('below');
const classList = tooltipDirective._overlayRef!.overlayElement.classList;
expect(classList).toContain('mat-mdc-tooltip-panel-below');
setPositionAndShow('above');
expect(classList).not.toContain('mat-mdc-tooltip-panel-below');
expect(classList).toContain('mat-mdc-tooltip-panel-above');
setPositionAndShow('left');
expect(classList).not.toContain('mat-mdc-tooltip-panel-above');
expect(classList).toContain('mat-mdc-tooltip-panel-left');
setPositionAndShow('right');
expect(classList).not.toContain('mat-mdc-tooltip-panel-left');
expect(classList).toContain('mat-mdc-tooltip-panel-right');
function setPositionAndShow(position: TooltipPosition) {
tooltipDirective.hide(0);
fixture.detectChanges();
tick(0);
tooltipDirective.position = position;
tooltipDirective.show(0);
fixture.detectChanges();
tick(0);
fixture.detectChanges();
tick(500);
}
}));
it('should account for RTL when setting the tooltip position class', fakeAsync(() => {
// Move the element so that the primary position is always used.
buttonElement.style.position = 'fixed';
buttonElement.style.top = buttonElement.style.left = '200px';
fixture.componentInstance.message = 'hi';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
dir.value = 'ltr';
tooltipDirective.position = 'after';
fixture.changeDetectorRef.markForCheck();
tooltipDirective.show(0);
fixture.detectChanges();
tick(0);
fixture.detectChanges();
tick(500);
const classList = tooltipDirective._overlayRef!.overlayElement.classList;
expect(classList).not.toContain('mat-mdc-tooltip-panel-after');
expect(classList).not.toContain('mat-mdc-tooltip-panel-before');
expect(classList).not.toContain('mat-mdc-tooltip-panel-left');
expect(classList).toContain('mat-mdc-tooltip-panel-right');
tooltipDirective.hide(0);
fixture.detectChanges();
tick(0);
dir.value = 'rtl';
tooltipDirective.show(0);
fixture.detectChanges();
tick(0);
fixture.detectChanges();
tick(500);
expect(classList).not.toContain('mat-mdc-tooltip-panel-after');
expect(classList).not.toContain('mat-mdc-tooltip-panel-before');
expect(classList).not.toContain('mat-mdc-tooltip-panel-right');
expect(classList).toContain('mat-mdc-tooltip-panel-left');
}));
it('should clear the show timeout on destroy', fakeAsync(() => {
assertTooltipInstance(tooltipDirective, false);
tooltipDirective.show(1000);
fixture.detectChanges();
// Note that we aren't asserting anything, but `fakeAsync` will
// throw if we have any timers by the end of the test.
fixture.destroy();
flush();
}));
it('should clear the hide timeout on destroy', fakeAsync(() => {
assertTooltipInstance(tooltipDirective, false);
tooltipDirective.show();
tick(0);
fixture.detectChanges();
tick(500);
tooltipDirective.hide(1000);
fixture.detectChanges();
// Note that we aren't asserting anything, but `fakeAsync` will
// throw if we have any timers by the end of the test.
fixture.destroy();
flush();
}));
it('should set the multiline class on tooltips with messages that overflow', fakeAsync(() => {
fixture.componentInstance.message =
'This is a very long message that should cause the' +
'tooltip message body to overflow onto a new line.';
tooltipDirective.show();
fixture.detectChanges();
tick();
// Need to detect changes again to wait for the multiline class to be applied.
fixture.detectChanges();
const tooltipElement = overlayContainerElement.querySelector(
'.mat-mdc-tooltip',
) as HTMLElement;
expect(tooltipElement.classList).toContain('mdc-tooltip--multiline');
expect(tooltipDirective._tooltipInstance?._isMultiline).toBeTrue();
}));
it('should hide on mouseleave on the trigger', fakeAsync(() => {
// We don't bind mouse events on mobile devices.
if (platform.IOS || platform.ANDROID) {
return;
}
dispatchMouseEvent(fixture.componentInstance.button.nativeElement, 'mouseenter');
fixture.detectChanges();
tick(0);
expect(tooltipDirective._isTooltipVisible()).toBe(true);
dispatchMouseEvent(fixture.componentInstance.button.nativeElement, 'mouseleave');
fixture.detectChanges();
tick(0);
expect(tooltipDirective._isTooltipVisible()).toBe(false);
}));
it('should not hide on mouseleave if the pointer goes from the trigger to the tooltip', fakeAsync(() => {
// We don't bind mouse events on mobile devices.
if (platform.IOS || platform.ANDROID) {
return;
}
dispatchMouseEvent(fixture.componentInstance.button.nativeElement, 'mouseenter');
fixture.detectChanges();
tick(0);
expect(tooltipDirective._isTooltipVisible()).toBe(true);
const tooltipElement = overlayContainerElement.querySelector(
'.mat-mdc-tooltip',
) as HTMLElement;
const event = createMouseEvent('mouseleave');
Object.defineProperty(event, 'relatedTarget', {value: tooltipElement});
dispatchEvent(fixture.componentInstance.button.nativeElement, event);
fixture.detectChanges();
tick(0);
expect(tooltipDirective._isTooltipVisible()).toBe(true);
}));
it('should hide on mouseleave on the tooltip', fakeAsync(() => {
// We don't bind mouse events on mobile devices.
if (platform.IOS || platform.ANDROID) {
return;
}
dispatchMouseEvent(fixture.componentInstance.button.nativeElement, 'mouseenter');
fixture.detectChanges();
tick(0);
expect(tooltipDirective._isTooltipVisible()).toBe(true);
const tooltipElement = overlayContainerElement.querySelector(
'.mat-mdc-tooltip',
) as HTMLElement;
dispatchMouseEvent(tooltipElement, 'mouseleave');
fixture.detectChanges();
tick(0);
expect(tooltipDirective._isTooltipVisible()).toBe(false);
}));
it('should not hide on mouseleave if the pointer goes from the tooltip to the trigger', fakeAsync(() => {
// We don't bind mouse events on mobile devices.
if (platform.IOS || platform.ANDROID) {
return;
}
dispatchMouseEvent(fixture.componentInstance.button.nativeElement, 'mouseenter');
fixture.detectChanges();
tick(0);
expect(tooltipDirective._isTooltipVisible()).toBe(true);
const tooltipElement = overlayContainerElement.querySelector(
'.mat-mdc-tooltip',
) as HTMLElement;
const event = createMouseEvent('mouseleave');
Object.defineProperty(event, 'relatedTarget', {
value: fixture.componentInstance.button.nativeElement,
});
dispatchEvent(tooltipElement, event);
fixture.detectChanges();
tick(0);
expect(tooltipDirective._isTooltipVisible()).toBe(true);
}));
});
describe('fallback positions', () => {
let fixture: ComponentFixture<BasicTooltipDemo>;
let tooltip: MatTooltip;
beforeEach(() => {
fixture = TestBed.createComponent(BasicTooltipDemo);
fixture.detectChanges();
tooltip = fixture.debugElement.query(By.css('button'))!.injector.get<MatTooltip>(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<ScrollableTooltipDemo>;
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>(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 that the tooltip is displayed
expect(tooltipDirective._isTooltipVisible())
.withContext('Expected tooltip to be initially visible')
.toBe(true);
// Scroll the page but tick just before the default throttle should update.
fixture.componentInstance.scrollDown();
tick(SCROLL_THROTTLE_MS - 1);
expect(tooltipDirective._isTooltipVisible())
.withContext('Expected tooltip to be visible when scrolling, before throttle limit')
.toBe(true);
// 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())
.withContext('Expected tooltip hidden when scrolled out of view, after throttle limit')
.toBe(false);
}));
});
describe('with OnPush', () => {
let fixture: ComponentFixture<OnPushTooltipDemo>;
let buttonDebugElement: DebugElement;
let buttonElement: HTMLButtonElement;
let tooltipDirective: MatTooltip;
beforeEach(() => {
fixture = TestBed.createComponent(OnPushTooltipDemo);
fixture.detectChanges();
buttonDebugElement = fixture.debugElement.query(By.css('button'))!;
buttonElement = <HTMLButtonElement>buttonDebugElement.nativeElement;
tooltipDirective = buttonDebugElement.injector.get<MatTooltip>(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
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.classList).toContain('mat-mdc-tooltip-show');
// 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.
finishCurrentTooltipAnimation(overlayContainerElement, false);
assertTooltipInstance(tooltipDirective, false);
flush();
}));
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(500); // Finish the delay.
fixture.detectChanges();
finishCurrentTooltipAnimation(overlayContainerElement, true); // Finish the animation.
assertTooltipInstance(fixture.componentInstance.tooltip, true);
flush();
}));
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();
finishCurrentTooltipAnimation(overlayContainerElement, true); // 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();
finishCurrentTooltipAnimation(overlayContainerElement, false); // Finish the exit animation.
assertTooltipInstance(fixture.componentInstance.tooltip, false);
flush();
}));
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();
finishCurrentTooltipAnimation(overlayContainerElement, true); // 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();
finishCurrentTooltipAnimation(overlayContainerElement, false); // Finish the exit animation.
assertTooltipInstance(fixture.componentInstance.tooltip, false);
flush();
}));
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();
finishCurrentTooltipAnimation(overlayContainerElement, true);
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();
finishCurrentTooltipAnimation(overlayContainerElement, false);
assertTooltipInstance(fixture.componentInstance.tooltip, false);
flush();
}));
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();
finishCurrentTooltipAnimation(overlayContainerElement, true);
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();
finishCurrentTooltipAnimation(overlayContainerElement, false);
assertTooltipInstance(fixture.componentInstance.tooltip, true);
flush();
}));
});
});
@Component({
selector: 'app',
template: `
@if (showButton) {
<button #button
[matTooltip]="message"
[matTooltipPosition]="position"
[matTooltipClass]="{'custom-one': showTooltipClass, 'custom-two': showTooltipClass}"
[matTooltipTouchGestures]="touchGestures"
[matTooltipDisabled]="tooltipDisabled">Button</button>
}`,
standalone: true,
imports: [MatTooltipModule, OverlayModule],
})
class BasicTooltipDemo {
position = 'below';
message: any = initialTooltipMessage;
showButton = true;
showTooltipClass = false;
tooltipDisabled = false;
touchGestures: TooltipTouchGestures = 'auto';
@ViewChild(MatTooltip) tooltip: MatTooltip;
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
}
@Component({
selector: 'app',
template: `
<div cdkScrollable style="padding: 100px; margin: 300px;
height: 200px; width: 200px; overflow: auto;">
@if (showButton) {
<button style="margin-bottom: 600px"
[matTooltip]="message"
[matTooltipPosition]="position">Button</button>
}
</div>`,
standalone: true,
imports: [MatTooltipModule, OverlayModule],
})
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: `
<button [matTooltip]="message"
[matTooltipPosition]="position">
Button
</button>`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatTooltipModule, OverlayModule],
})
class OnPushTooltipDemo {
position: string = 'below';
message: string = initialTooltipMessage;
}
@Component({
selector: 'app',
template: `
@for (tooltip of tooltips; track tooltip) {
<button [matTooltip]="tooltip">Button {{tooltip}}</button>
}
`,
standalone: true,
imports: [MatTooltipModule, OverlayModule],
})
class DynamicTooltipsDemo {
tooltips: string[] = [];
}
@Component({
template: `<button [matTooltip]="message" [attr.aria-label]="message">Click me</button>`,
standalone: true,
imports: [MatTooltipModule, OverlayModule],
})
class DataBoundAriaLabelTooltip {
message = 'Hello there';
}
@Component({
template: `
<input
#input
matTooltip="Something"
[matTooltipTouchGestures]="touchGestures">
<textarea
#textarea
matTooltip="Another thing"
[matTooltipTouchGestures]="touchGestures"></textarea>
`,
standalone: true,
imports: [MatTooltipModule, OverlayModule],
})
class TooltipOnTextFields {
@ViewChild('input') input: ElementRef<HTMLInputElement>;
@ViewChild('textarea') textarea: ElementRef<HTMLTextAreaElement>;
touchGestures: TooltipTouchGestures = 'auto';
}
@Component({
template: `
<button
#button
draggable="true"
matTooltip="Drag me"
[matTooltipTouchGestures]="touchGestures"></button>
`,
standalone: true,
imports: [MatTooltipModule, OverlayModule],
})
class TooltipOnDraggableElement {
@ViewChild('button') button: ElementRef;
touchGestures: TooltipTouchGestures = 'auto';
}
@Component({
selector: 'app',
template: `<button #button [matTooltip]="message">Button</button>`,
standalone: false,
})
class TooltipDemoWithoutPositionBinding {
message: any = initialTooltipMessage;
@ViewChild(MatTooltip) tooltip: MatTooltip;
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
}
@Component({
selector: 'app',
template: `<button #button [matTooltip]="message">Button</button>`,
standalone: false,
})
class TooltipDemoWithoutTooltipClassBinding {
message = initialTooltipMessage;
@ViewChild(MatTooltip) tooltip: MatTooltip;
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
}
@Component({
selector: 'app',
template: `
<button #button matTooltipClass="fixed-tooltip-class" [matTooltip]="message">Button</button>
`,
standalone: false,
})
class TooltipDemoWithTooltipClassBinding {
message: any = initialTooltipMessage;
@ViewChild(MatTooltip) tooltip: MatTooltip;
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
}
@Component({
selector: 'app',
styles: `button { width: 500px; height: 500px; }`,
template: `<button #button [matTooltip]="message">Button</button>`,
standalone: false,
})
class WideTooltipDemo {
message = 'Test';
@ViewChild(MatTooltip) tooltip: MatTooltip;
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
}
/** 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);
}
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);
}