Skip to content

Commit 368bde1

Browse files
committed
fix(material/tooltip): 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. Fixes #32503. Fixes #25287.
1 parent ef53e73 commit 368bde1

File tree

2 files changed

+31
-10
lines changed

2 files changed

+31
-10
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class MatTooltipHarness extends ComponentHarness {
4343
// We need to dispatch both `touchstart` and a hover event, because the tooltip binds
4444
// different events depending on the device. The `changedTouches` is there in case the
4545
// element has ripples.
46-
await host.dispatchEvent('touchstart', {changedTouches: []});
46+
await host.dispatchEvent('touchstart', {changedTouches: [], _isHarnessEvent: true});
4747
await host.hover();
4848
const panel = await this._optionalPanel();
4949
await panel?.dispatchEvent('animationend', {animationName: this._showAnimationName});

src/material/tooltip/tooltip.ts

Lines changed: 30 additions & 9 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

@@ -190,6 +191,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
190191
protected _dir = inject(Directionality);
191192
private _injector = inject(Injector);
192193
private _viewContainerRef = inject(ViewContainerRef);
194+
private _mediaMatcher = inject(MediaMatcher);
193195
private _animationsDisabled = _animationsDisabled();
194196
private _defaultOptions = inject<MatTooltipDefaultOptions>(MAT_TOOLTIP_DEFAULT_OPTIONS, {
195197
optional: true,
@@ -784,7 +786,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
784786

785787
// The mouse events shouldn't be bound on mobile devices, because they can prevent the
786788
// first tap from firing its click event or can cause the tooltip to open for clicks.
787-
if (this._platformSupportsMouseEvents()) {
789+
if (!this._isTouchPlatform()) {
788790
this._passiveListeners.push([
789791
'mouseenter',
790792
event => {
@@ -801,8 +803,9 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
801803

802804
this._passiveListeners.push([
803805
'touchstart',
804-
event => {
805-
const touch = (event as TouchEvent).targetTouches?.[0];
806+
e => {
807+
const event = e as TouchEvent & {_isHarnessEvent?: boolean};
808+
const touch = event.targetTouches?.[0];
806809
const origin = touch ? {x: touch.clientX, y: touch.clientY} : undefined;
807810
// Note that it's important that we don't `preventDefault` here,
808811
// because it can prevent click events from firing on the element.
@@ -811,11 +814,21 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
811814
clearTimeout(this._touchstartTimeout);
812815
}
813816

814-
const DEFAULT_LONGPRESS_DELAY = 500;
815-
this._touchstartTimeout = setTimeout(() => {
817+
// Harnesses can run in headless browsers that might be picked up as touch
818+
// devices, because they can't hover. If we detect a harness, skip the delay.
819+
const showOnTouch = () => {
816820
this._touchstartTimeout = null;
817821
this.show(undefined, origin);
818-
}, this._defaultOptions?.touchLongPressShowDelay ?? DEFAULT_LONGPRESS_DELAY);
822+
};
823+
824+
if (event._isHarnessEvent) {
825+
showOnTouch();
826+
} else {
827+
this._touchstartTimeout = setTimeout(
828+
showOnTouch,
829+
this._defaultOptions?.touchLongPressShowDelay ?? 500,
830+
);
831+
}
819832
},
820833
]);
821834
}
@@ -830,7 +843,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
830843
this._pointerExitEventsInitialized = true;
831844

832845
const exitListeners: (readonly [string, EventListenerOrEventListenerObject])[] = [];
833-
if (this._platformSupportsMouseEvents()) {
846+
if (!this._isTouchPlatform()) {
834847
exitListeners.push(
835848
[
836849
'mouseleave',
@@ -865,8 +878,16 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
865878
});
866879
}
867880

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

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

0 commit comments

Comments
 (0)