From d030ee4929813f33d6243e6d3aacf48a26abb5f7 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 25 Dec 2025 22:44:52 +0200 Subject: [PATCH] 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. --- goldens/material/tooltip/index.api.md | 1 + src/material/tooltip/tooltip.ts | 29 +++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/goldens/material/tooltip/index.api.md b/goldens/material/tooltip/index.api.md index 79aa94a2a9a2..09e1f0eaadf5 100644 --- a/goldens/material/tooltip/index.api.md +++ b/goldens/material/tooltip/index.api.md @@ -92,6 +92,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit { // @public export interface MatTooltipDefaultOptions { + detectHoverCapability?: boolean; disableTooltipInteractivity?: boolean; hideDelay: number; position?: TooltipPosition; diff --git a/src/material/tooltip/tooltip.ts b/src/material/tooltip/tooltip.ts index be6dbbcb79bc..ee15859de170 100644 --- a/src/material/tooltip/tooltip.ts +++ b/src/material/tooltip/tooltip.ts @@ -52,6 +52,7 @@ import { VerticalConnectionPos, } from '@angular/cdk/overlay'; import {ComponentPortal} from '@angular/cdk/portal'; +import {MediaMatcher} from '@angular/cdk/layout'; import {Observable, Subject} from 'rxjs'; import {_animationsDisabled} from '../core'; @@ -146,6 +147,14 @@ export interface MatTooltipDefaultOptions { * `tooltipClass` is defined directly on the tooltip element, as it will override the default. */ tooltipClass?: string | string[]; + + /** + * Whether the tooltip should use a media query to detect if the device is able to hover. + * Note that this may affect tests that run in a headless browser which reports that it's + * unable to hover. In such cases you may need to include an additional timeout, because + * the tooltip will fall back to treating the device as a touch screen. + */ + detectHoverCapability?: boolean; } /** @@ -190,6 +199,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit { protected _dir = inject(Directionality); private _injector = inject(Injector); private _viewContainerRef = inject(ViewContainerRef); + private _mediaMatcher = inject(MediaMatcher); private _animationsDisabled = _animationsDisabled(); private _defaultOptions = inject(MAT_TOOLTIP_DEFAULT_OPTIONS, { optional: true, @@ -784,7 +794,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit { // The mouse events shouldn't be bound on mobile devices, because they can prevent the // first tap from firing its click event or can cause the tooltip to open for clicks. - if (this._platformSupportsMouseEvents()) { + if (!this._isTouchPlatform()) { this._passiveListeners.push([ 'mouseenter', event => { @@ -830,7 +840,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit { this._pointerExitEventsInitialized = true; const exitListeners: (readonly [string, EventListenerOrEventListenerObject])[] = []; - if (this._platformSupportsMouseEvents()) { + if (!this._isTouchPlatform()) { exitListeners.push( [ 'mouseleave', @@ -865,8 +875,19 @@ export class MatTooltip implements OnDestroy, AfterViewInit { }); } - private _platformSupportsMouseEvents() { - return !this._platform.IOS && !this._platform.ANDROID; + private _isTouchPlatform(): boolean { + if (this._platform.IOS || this._platform.ANDROID) { + // If we detected iOS or Android, it's definitely supported. + return true; + } else if (!this._platform.isBrowser) { + // If it's not a browser, it's definitely not supported. + return false; + } + + return ( + !!this._defaultOptions?.detectHoverCapability && + this._mediaMatcher.matchMedia('(any-hover: none)').matches + ); } /** Listener for the `wheel` event on the element. */