@@ -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.
@@ -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