Skip to content

Commit dbd4a6a

Browse files
committed
perf(tooltip): Hoop up to pointer leave events when pointer enter events fire
Rather than greedily at startup
1 parent 7c49399 commit dbd4a6a

File tree

1 file changed

+51
-22
lines changed

1 file changed

+51
-22
lines changed

src/material/tooltip/tooltip.ts

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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

@@ -215,7 +216,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
215216
if (!this._message && this._isTooltipVisible()) {
216217
this.hide(0);
217218
} else {
218-
this._setupPointerEvents();
219+
this._setupPointerEnterEventsIfNeeded();
219220
this._updateTooltipMessage();
220221
this._ngZone.runOutsideAngular(() => {
221222
// The `AriaDescriber` has some functionality that avoids adding a description if it's the
@@ -241,7 +242,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
241242
}
242243

243244
/** Manually-bound passive event listeners. */
244-
private _passiveListeners = new Map<string, EventListenerOrEventListenerObject>();
245+
private readonly _passiveListeners: Array<readonly [string, EventListenerOrEventListenerObject]> = [];
245246

246247
/** Timer started at the last `touchstart` event. */
247248
private _touchstartTimeout: number;
@@ -283,7 +284,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
283284
ngAfterViewInit() {
284285
// This needs to happen after view init so the initial values for all inputs have been set.
285286
this._viewInitialized = true;
286-
this._setupPointerEvents();
287+
this._setupPointerEnterEventsIfNeeded();
287288

288289
this._focusMonitor.monitor(this._elementRef)
289290
.pipe(takeUntil(this._destroyed))
@@ -312,10 +313,10 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
312313

313314
// Clean up the event listeners set in the constructor
314315
nativeElement.removeEventListener('keydown', this._handleKeydown);
315-
this._passiveListeners.forEach((listener, event) => {
316+
this._passiveListeners.forEach(([event, listener]) => {
316317
nativeElement.removeEventListener(event, listener, passiveListenerOptions);
317318
});
318-
this._passiveListeners.clear();
319+
this._passiveListeners.length = 0;
319320

320321
this._destroyed.next();
321322
this._destroyed.complete();
@@ -549,49 +550,77 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
549550
}
550551

551552
/** Binds the pointer events to the tooltip trigger. */
552-
private _setupPointerEvents() {
553+
private _setupPointerEnterEventsIfNeeded() {
553554
// Optimization: Defer hooking up events if there's no message or the tooltip is disabled.
554555
if (this._disabled || !this.message || !this._viewInitialized ||
555-
this._passiveListeners.size) {
556+
this._passiveListeners.length) {
556557
return;
557558
}
558559

559560
// The mouse events shouldn't be bound on mobile devices, because they can prevent the
560561
// first tap from firing its click event or can cause the tooltip to open for clicks.
561562
if (!this._platform.IOS && !this._platform.ANDROID) {
562563
this._passiveListeners
563-
.set('mouseenter', () => this.show())
564-
.set('mouseleave', () => this.hide());
564+
.push(['mouseenter', () => {
565+
this._setupPointerExitEventsIfNeeded();
566+
this.show();
567+
}]);
568+
} else if (this.touchGestures !== 'off') {
569+
this._disableNativeGesturesIfNecessary();
570+
571+
this._passiveListeners
572+
.push(['touchstart', () => {
573+
// Note that it's important that we don't `preventDefault` here,
574+
// because it can prevent click events from firing on the element.
575+
this._setupPointerExitEventsIfNeeded();
576+
clearTimeout(this._touchstartTimeout);
577+
this._touchstartTimeout = setTimeout(() => this.show(), LONGPRESS_DELAY);
578+
}]);
579+
}
580+
581+
this._hookupListeners(this._passiveListeners);
582+
}
583+
584+
private _setupPointerExitEventsIfNeeded() {
585+
if (this._pointerExitEventsInitialized) {
586+
return;
587+
}
588+
this._pointerExitEventsInitialized = true;
589+
590+
const exitListeners: Array<readonly [string, EventListenerOrEventListenerObject]> = [];
591+
if (!this._platform.IOS && !this._platform.ANDROID) {
592+
exitListeners.push(['mouseleave', () => this.hide()]);
565593
} else if (this.touchGestures !== 'off') {
566594
this._disableNativeGesturesIfNecessary();
567595
const touchendListener = () => {
568596
clearTimeout(this._touchstartTimeout);
569597
this.hide(this._defaultOptions.touchendHideDelay);
570598
};
571599

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-
});
600+
exitListeners.push(
601+
['touchend', touchendListener],
602+
['touchcancel', touchendListener],
603+
);
581604
}
582605

583-
this._passiveListeners.forEach((listener, event) => {
606+
this._hookupListeners(exitListeners);
607+
this._passiveListeners.push(...exitListeners);
608+
}
609+
610+
private _hookupListeners(listeners: ReadonlyArray<readonly [string, EventListenerOrEventListenerObject]>) {
611+
listeners.forEach(([event, listener]) => {
584612
this._elementRef.nativeElement.addEventListener(event, listener, passiveListenerOptions);
585613
});
586614
}
587615

588616
/** Disables the native browser gestures, based on how the tooltip has been configured. */
589617
private _disableNativeGesturesIfNecessary() {
590-
const element = this._elementRef.nativeElement;
591-
const style = element.style;
592618
const gestures = this.touchGestures;
593619

594620
if (gestures !== 'off') {
621+
const element = this._elementRef.nativeElement;
622+
const style = element.style;
623+
595624
// If gestures are set to `auto`, we don't disable text selection on inputs and
596625
// textareas, because it prevents the user from typing into them on iOS Safari.
597626
if (gestures === 'on' || (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA')) {

0 commit comments

Comments
 (0)