@@ -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
@@ -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.
@@ -811,11 +822,13 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
811822 clearTimeout ( this . _touchstartTimeout ) ;
812823 }
813824
814- const DEFAULT_LONGPRESS_DELAY = 500 ;
825+ // Harnesses can run in headless browsers that might be picked up as touch
826+ // devices, because they can't hover. If we detect a harness, skip the delay.
827+ const defaultLongpressDelay = event . _isHarnessEvent ? 0 : 500 ;
815828 this . _touchstartTimeout = setTimeout ( ( ) => {
816829 this . _touchstartTimeout = null ;
817830 this . show ( undefined , origin ) ;
818- } , this . _defaultOptions ?. touchLongPressShowDelay ?? DEFAULT_LONGPRESS_DELAY ) ;
831+ } , this . _defaultOptions ?. touchLongPressShowDelay ?? defaultLongpressDelay ) ;
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,19 @@ 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 (
891+ ! ! this . _defaultOptions ?. detectHoverCapability &&
892+ this . _mediaMatcher . matchMedia ( '(any-hover: none)' ) . matches
893+ ) ;
870894 }
871895
872896 /** Listener for the `wheel` event on the element. */
0 commit comments