Skip to content

Commit e0c91a4

Browse files
committed
fix(material/tooltip): add opt-in for better touch device detection
Currently the touch device detection in the tooltip is based purely on the `Platform` provider which isn't able to detect some newer iOS devices properly. These changes add an additional check through a media query. The check is currently opt-in, because it can cause false positives in tests that run in headless browsers. Fixes #32503. Fixes #25287.
1 parent ef53e73 commit e0c91a4

File tree

2 files changed

+29
-6
lines changed

2 files changed

+29
-6
lines changed

goldens/material/tooltip/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
9292

9393
// @public
9494
export interface MatTooltipDefaultOptions {
95+
detectHoverCapability?: boolean;
9596
disableTooltipInteractivity?: boolean;
9697
hideDelay: number;
9798
position?: TooltipPosition;

src/material/tooltip/tooltip.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
VerticalConnectionPos,
5353
} from '@angular/cdk/overlay';
5454
import {ComponentPortal} from '@angular/cdk/portal';
55+
import {MediaMatcher} from '@angular/cdk/layout';
5556
import {Observable, Subject} from 'rxjs';
5657
import {_animationsDisabled} from '../core';
5758

@@ -146,6 +147,14 @@ export interface MatTooltipDefaultOptions {
146147
* `tooltipClass` is defined directly on the tooltip element, as it will override the default.
147148
*/
148149
tooltipClass?: string | string[];
150+
151+
/**
152+
* Whether the tooltip should use a media query to detect if the device is able to hover.
153+
* Note that this may affect tests that run in a headless browser which reports that it's
154+
* unable to hover. In such cases you may need to include an additional timeout, because
155+
* the tooltip will fall back to treating the device as a touch screen.
156+
*/
157+
detectHoverCapability?: boolean;
149158
}
150159

151160
/**
@@ -190,6 +199,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
190199
protected _dir = inject(Directionality);
191200
private _injector = inject(Injector);
192201
private _viewContainerRef = inject(ViewContainerRef);
202+
private _mediaMatcher = inject(MediaMatcher);
193203
private _animationsDisabled = _animationsDisabled();
194204
private _defaultOptions = inject<MatTooltipDefaultOptions>(MAT_TOOLTIP_DEFAULT_OPTIONS, {
195205
optional: true,
@@ -784,7 +794,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
784794

785795
// The mouse events shouldn't be bound on mobile devices, because they can prevent the
786796
// first tap from firing its click event or can cause the tooltip to open for clicks.
787-
if (this._platformSupportsMouseEvents()) {
797+
if (!this._isTouchPlatform()) {
788798
this._passiveListeners.push([
789799
'mouseenter',
790800
event => {
@@ -801,8 +811,9 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
801811

802812
this._passiveListeners.push([
803813
'touchstart',
804-
event => {
805-
const touch = (event as TouchEvent).targetTouches?.[0];
814+
e => {
815+
const event = e as TouchEvent & {_isHarnessEvent?: boolean};
816+
const touch = event.targetTouches?.[0];
806817
const origin = touch ? {x: touch.clientX, y: touch.clientY} : undefined;
807818
// Note that it's important that we don't `preventDefault` here,
808819
// because it can prevent click events from firing on the element.
@@ -830,7 +841,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
830841
this._pointerExitEventsInitialized = true;
831842

832843
const exitListeners: (readonly [string, EventListenerOrEventListenerObject])[] = [];
833-
if (this._platformSupportsMouseEvents()) {
844+
if (!this._isTouchPlatform()) {
834845
exitListeners.push(
835846
[
836847
'mouseleave',
@@ -865,8 +876,19 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
865876
});
866877
}
867878

868-
private _platformSupportsMouseEvents() {
869-
return !this._platform.IOS && !this._platform.ANDROID;
879+
private _isTouchPlatform(): boolean {
880+
if (this._platform.IOS || this._platform.ANDROID) {
881+
// If we detected iOS or Android, it's definitely supported.
882+
return true;
883+
} else if (!this._platform.isBrowser) {
884+
// If it's not a browser, it's definitely not supported.
885+
return false;
886+
}
887+
888+
return (
889+
!!this._defaultOptions?.detectHoverCapability &&
890+
this._mediaMatcher.matchMedia('(any-hover: none)').matches
891+
);
870892
}
871893

872894
/** Listener for the `wheel` event on the element. */

0 commit comments

Comments
 (0)