@@ -52,6 +52,7 @@ import {
5252 VerticalConnectionPos ,
5353} from '@angular/cdk/overlay' ;
5454import { ComponentPortal } from '@angular/cdk/portal' ;
55+ import { MediaMatcher } from '@angular/cdk/layout' ;
5556import { Observable , Subject } from 'rxjs' ;
5657import { _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