@@ -145,6 +145,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
145145 private _tooltipClass : string | string [ ] | Set < string > | { [ key : string ] : any } ;
146146 private _scrollStrategy : ( ) => ScrollStrategy ;
147147 private _viewInitialized = false ;
148+ private _pointerExitEventsInitialized = false ;
148149
149150 /** Allows the user to define the position of the tooltip relative to the parent element */
150151 @Input ( 'matTooltipPosition' )
@@ -175,7 +176,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
175176 if ( this . _disabled ) {
176177 this . hide ( 0 ) ;
177178 } else {
178- this . _setupPointerEvents ( ) ;
179+ this . _setupPointerEnterEventsIfNeeded ( ) ;
179180 }
180181 }
181182
@@ -205,17 +206,15 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
205206 @Input ( 'matTooltip' )
206207 get message ( ) { return this . _message ; }
207208 set message ( value : string ) {
208- if ( this . _message ) {
209- this . _ariaDescriber . removeDescription ( this . _elementRef . nativeElement , this . _message ) ;
210- }
209+ this . _ariaDescriber . removeDescription ( this . _elementRef . nativeElement , this . _message ) ;
211210
212211 // If the message is not a string (e.g. number), convert it to a string and trim it.
213212 this . _message = value != null ? `${ value } ` . trim ( ) : '' ;
214213
215214 if ( ! this . _message && this . _isTooltipVisible ( ) ) {
216215 this . hide ( 0 ) ;
217216 } else {
218- this . _setupPointerEvents ( ) ;
217+ this . _setupPointerEnterEventsIfNeeded ( ) ;
219218 this . _updateTooltipMessage ( ) ;
220219 this . _ngZone . runOutsideAngular ( ( ) => {
221220 // The `AriaDescriber` has some functionality that avoids adding a description if it's the
@@ -241,7 +240,8 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
241240 }
242241
243242 /** Manually-bound passive event listeners. */
244- private _passiveListeners = new Map < string , EventListenerOrEventListenerObject > ( ) ;
243+ private readonly _passiveListeners :
244+ ( readonly [ string , EventListenerOrEventListenerObject ] ) [ ] = [ ] ;
245245
246246 /** Timer started at the last `touchstart` event. */
247247 private _touchstartTimeout : number ;
@@ -283,7 +283,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
283283 ngAfterViewInit ( ) {
284284 // This needs to happen after view init so the initial values for all inputs have been set.
285285 this . _viewInitialized = true ;
286- this . _setupPointerEvents ( ) ;
286+ this . _setupPointerEnterEventsIfNeeded ( ) ;
287287
288288 this . _focusMonitor . monitor ( this . _elementRef )
289289 . pipe ( takeUntil ( this . _destroyed ) )
@@ -312,10 +312,10 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
312312
313313 // Clean up the event listeners set in the constructor
314314 nativeElement . removeEventListener ( 'keydown' , this . _handleKeydown ) ;
315- this . _passiveListeners . forEach ( ( listener , event ) => {
315+ this . _passiveListeners . forEach ( ( [ event , listener ] ) => {
316316 nativeElement . removeEventListener ( event , listener , passiveListenerOptions ) ;
317317 } ) ;
318- this . _passiveListeners . clear ( ) ;
318+ this . _passiveListeners . length = 0 ;
319319
320320 this . _destroyed . next ( ) ;
321321 this . _destroyed . complete ( ) ;
@@ -549,49 +549,82 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
549549 }
550550
551551 /** Binds the pointer events to the tooltip trigger. */
552- private _setupPointerEvents ( ) {
552+ private _setupPointerEnterEventsIfNeeded ( ) {
553553 // Optimization: Defer hooking up events if there's no message or the tooltip is disabled.
554554 if ( this . _disabled || ! this . message || ! this . _viewInitialized ||
555- this . _passiveListeners . size ) {
555+ this . _passiveListeners . length ) {
556556 return ;
557557 }
558558
559559 // The mouse events shouldn't be bound on mobile devices, because they can prevent the
560560 // first tap from firing its click event or can cause the tooltip to open for clicks.
561- if ( ! this . _platform . IOS && ! this . _platform . ANDROID ) {
561+ if ( this . _platformSupportsMouseEvents ( ) ) {
562562 this . _passiveListeners
563- . set ( 'mouseenter' , ( ) => this . show ( ) )
564- . set ( 'mouseleave' , ( ) => this . hide ( ) ) ;
563+ . push ( [ 'mouseenter' , ( ) => {
564+ this . _setupPointerExitEventsIfNeeded ( ) ;
565+ this . show ( ) ;
566+ } ] ) ;
567+ } else if ( this . touchGestures !== 'off' ) {
568+ this . _disableNativeGesturesIfNecessary ( ) ;
569+
570+ this . _passiveListeners
571+ . push ( [ 'touchstart' , ( ) => {
572+ // Note that it's important that we don't `preventDefault` here,
573+ // because it can prevent click events from firing on the element.
574+ this . _setupPointerExitEventsIfNeeded ( ) ;
575+ clearTimeout ( this . _touchstartTimeout ) ;
576+ this . _touchstartTimeout = setTimeout ( ( ) => this . show ( ) , LONGPRESS_DELAY ) ;
577+ } ] ) ;
578+ }
579+
580+ this . _addListeners ( this . _passiveListeners ) ;
581+ }
582+
583+ private _setupPointerExitEventsIfNeeded ( ) {
584+ if ( this . _pointerExitEventsInitialized ) {
585+ return ;
586+ }
587+ this . _pointerExitEventsInitialized = true ;
588+
589+ const exitListeners : ( readonly [ string , EventListenerOrEventListenerObject ] ) [ ] = [ ] ;
590+ if ( this . _platformSupportsMouseEvents ( ) ) {
591+ exitListeners . push ( [ 'mouseleave' , ( ) => this . hide ( ) ] ) ;
565592 } else if ( this . touchGestures !== 'off' ) {
566593 this . _disableNativeGesturesIfNecessary ( ) ;
567594 const touchendListener = ( ) => {
568595 clearTimeout ( this . _touchstartTimeout ) ;
569596 this . hide ( this . _defaultOptions . touchendHideDelay ) ;
570597 } ;
571598
572- this . _passiveListeners
573- . set ( 'touchend' , touchendListener )
574- . set ( 'touchcancel' , touchendListener )
575- . set ( 'touchstart' , ( ) => {
576- // Note that it's important that we don't `preventDefault` here,
577- // because it can prevent click events from firing on the element.
578- clearTimeout ( this . _touchstartTimeout ) ;
579- this . _touchstartTimeout = setTimeout ( ( ) => this . show ( ) , LONGPRESS_DELAY ) ;
580- } ) ;
599+ exitListeners . push (
600+ [ 'touchend' , touchendListener ] ,
601+ [ 'touchcancel' , touchendListener ] ,
602+ ) ;
581603 }
582604
583- this . _passiveListeners . forEach ( ( listener , event ) => {
605+ this . _addListeners ( exitListeners ) ;
606+ this . _passiveListeners . push ( ...exitListeners ) ;
607+ }
608+
609+ private _addListeners (
610+ listeners : ReadonlyArray < readonly [ string , EventListenerOrEventListenerObject ] > ) {
611+ listeners . forEach ( ( [ event , listener ] ) => {
584612 this . _elementRef . nativeElement . addEventListener ( event , listener , passiveListenerOptions ) ;
585613 } ) ;
586614 }
587615
616+ private _platformSupportsMouseEvents ( ) {
617+ return ! this . _platform . IOS && ! this . _platform . ANDROID ;
618+ }
619+
588620 /** Disables the native browser gestures, based on how the tooltip has been configured. */
589621 private _disableNativeGesturesIfNecessary ( ) {
590- const element = this . _elementRef . nativeElement ;
591- const style = element . style ;
592622 const gestures = this . touchGestures ;
593623
594624 if ( gestures !== 'off' ) {
625+ const element = this . _elementRef . nativeElement ;
626+ const style = element . style ;
627+
595628 // If gestures are set to `auto`, we don't disable text selection on inputs and
596629 // textareas, because it prevents the user from typing into them on iOS Safari.
597630 if ( gestures === 'on' || ( element . nodeName !== 'INPUT' && element . nodeName !== 'TEXTAREA' ) ) {
0 commit comments