Skip to content

Commit 7b22da4

Browse files
committed
refactor(material/tooltip): move more event registrations to renderer
Switches over the tooltip's event registration to go through the renderer like the rest of the repo.
1 parent 008aa7a commit 7b22da4

1 file changed

Lines changed: 59 additions & 89 deletions

File tree

src/material/tooltip/tooltip.ts

Lines changed: 59 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ import {
3131
afterNextRender,
3232
Injector,
3333
DOCUMENT,
34+
Renderer2,
3435
} from '@angular/core';
3536
import {NgClass} from '@angular/common';
36-
import {normalizePassiveListenerOptions, Platform} from '@angular/cdk/platform';
37+
import {Platform} from '@angular/cdk/platform';
3738
import {AriaDescriber, FocusMonitor} from '@angular/cdk/a11y';
3839
import {Directionality} from '@angular/cdk/bidi';
3940
import {
@@ -167,7 +168,7 @@ export const TOOLTIP_PANEL_CLASS = 'mat-mdc-tooltip-panel';
167168
const PANEL_CLASS = 'tooltip-panel';
168169

169170
/** Options used to bind passive event listeners. */
170-
const passiveListenerOptions = normalizePassiveListenerOptions({passive: true});
171+
const passiveListenerOptions = {passive: true};
171172

172173
// These constants were taken from MDC's `numbers` object. We can't import them from MDC,
173174
// because they have some top-level references to `window` which break during SSR.
@@ -200,6 +201,8 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
200201
private _injector = inject(Injector);
201202
private _viewContainerRef = inject(ViewContainerRef);
202203
private _mediaMatcher = inject(MediaMatcher);
204+
private _document = inject(DOCUMENT);
205+
private _renderer = inject(Renderer2);
203206
private _animationsDisabled = _animationsDisabled();
204207
private _defaultOptions = inject<MatTooltipDefaultOptions>(MAT_TOOLTIP_DEFAULT_OPTIONS, {
205208
optional: true,
@@ -363,8 +366,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
363366
}
364367

365368
/** Manually-bound passive event listeners. */
366-
private readonly _passiveListeners: (readonly [string, EventListenerOrEventListenerObject])[] =
367-
[];
369+
private readonly _eventCleanups: (() => void)[] = [];
368370

369371
/** Timer started at the last `touchstart` event. */
370372
private _touchstartTimeout: null | ReturnType<typeof setTimeout> = null;
@@ -438,17 +440,11 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
438440
this._tooltipInstance = null;
439441
}
440442

441-
// Clean up the event listeners set in the constructor
442-
this._passiveListeners.forEach(([event, listener]) => {
443-
nativeElement.removeEventListener(event, listener, passiveListenerOptions);
444-
});
445-
this._passiveListeners.length = 0;
446-
443+
this._eventCleanups.forEach(cleanup => cleanup());
444+
this._eventCleanups.length = 0;
447445
this._destroyed.next();
448446
this._destroyed.complete();
449-
450447
this._isDestroyed = true;
451-
452448
this._ariaDescriber.removeDescription(nativeElement, this.message, 'tooltip');
453449
this._focusMonitor.stopMonitoring(nativeElement);
454450
}
@@ -783,54 +779,40 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
783779
/** Binds the pointer events to the tooltip trigger. */
784780
private _setupPointerEnterEventsIfNeeded() {
785781
// Optimization: Defer hooking up events if there's no message or the tooltip is disabled.
786-
if (
787-
this._disabled ||
788-
!this.message ||
789-
!this._viewInitialized ||
790-
this._passiveListeners.length
791-
) {
782+
if (this._disabled || !this.message || !this._viewInitialized || this._eventCleanups.length) {
792783
return;
793784
}
794785

795786
// The mouse events shouldn't be bound on mobile devices, because they can prevent the
796787
// first tap from firing its click event or can cause the tooltip to open for clicks.
797788
if (!this._isTouchPlatform()) {
798-
this._passiveListeners.push([
799-
'mouseenter',
800-
event => {
801-
this._setupPointerExitEventsIfNeeded();
802-
let point = undefined;
803-
if ((event as MouseEvent).x !== undefined && (event as MouseEvent).y !== undefined) {
804-
point = event as MouseEvent;
805-
}
806-
this.show(undefined, point);
807-
},
808-
]);
789+
this._addListener('mouseenter', (event: MouseEvent) => {
790+
this._setupPointerExitEventsIfNeeded();
791+
let point = undefined;
792+
if (event.x !== undefined && event.y !== undefined) {
793+
point = event;
794+
}
795+
this.show(undefined, point);
796+
});
809797
} else if (this.touchGestures !== 'off') {
810798
this._disableNativeGesturesIfNecessary();
799+
this._addListener('touchstart', (event: TouchEvent) => {
800+
const touch = event.targetTouches?.[0];
801+
const origin = touch ? {x: touch.clientX, y: touch.clientY} : undefined;
802+
// Note that it's important that we don't `preventDefault` here,
803+
// because it can prevent click events from firing on the element.
804+
this._setupPointerExitEventsIfNeeded();
805+
if (this._touchstartTimeout) {
806+
clearTimeout(this._touchstartTimeout);
807+
}
811808

812-
this._passiveListeners.push([
813-
'touchstart',
814-
event => {
815-
const touch = (event as TouchEvent).targetTouches?.[0];
816-
const origin = touch ? {x: touch.clientX, y: touch.clientY} : undefined;
817-
// Note that it's important that we don't `preventDefault` here,
818-
// because it can prevent click events from firing on the element.
819-
this._setupPointerExitEventsIfNeeded();
820-
if (this._touchstartTimeout) {
821-
clearTimeout(this._touchstartTimeout);
822-
}
823-
824-
const DEFAULT_LONGPRESS_DELAY = 500;
825-
this._touchstartTimeout = setTimeout(() => {
826-
this._touchstartTimeout = null;
827-
this.show(undefined, origin);
828-
}, this._defaultOptions?.touchLongPressShowDelay ?? DEFAULT_LONGPRESS_DELAY);
829-
},
830-
]);
809+
const DEFAULT_LONGPRESS_DELAY = 500;
810+
this._touchstartTimeout = setTimeout(() => {
811+
this._touchstartTimeout = null;
812+
this.show(undefined, origin);
813+
}, this._defaultOptions?.touchLongPressShowDelay ?? DEFAULT_LONGPRESS_DELAY);
814+
});
831815
}
832-
833-
this._addListeners(this._passiveListeners);
834816
}
835817

836818
private _setupPointerExitEventsIfNeeded() {
@@ -839,20 +821,28 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
839821
}
840822
this._pointerExitEventsInitialized = true;
841823

842-
const exitListeners: (readonly [string, EventListenerOrEventListenerObject])[] = [];
843824
if (!this._isTouchPlatform()) {
844-
exitListeners.push(
845-
[
846-
'mouseleave',
847-
event => {
848-
const newTarget = (event as MouseEvent).relatedTarget as Node | null;
849-
if (!newTarget || !this._overlayRef?.overlayElement.contains(newTarget)) {
850-
this.hide();
851-
}
852-
},
853-
],
854-
['wheel', event => this._wheelListener(event as WheelEvent)],
855-
);
825+
this._addListener('mouseleave', (event: MouseEvent) => {
826+
const newTarget = event.relatedTarget as Node | null;
827+
if (!newTarget || !this._overlayRef?.overlayElement.contains(newTarget)) {
828+
this.hide();
829+
}
830+
});
831+
832+
this._addListener('wheel', (event: WheelEvent) => {
833+
if (this._isTooltipVisible()) {
834+
const elementUnderPointer = this._document.elementFromPoint(event.clientX, event.clientY);
835+
const element = this._elementRef.nativeElement;
836+
837+
// On non-touch devices we depend on the `mouseleave` event to close the tooltip, but it
838+
// won't fire if the user scrolls away using the wheel without moving their cursor. We
839+
// work around it by finding the element under the user's cursor and closing the tooltip
840+
// if it's not the trigger.
841+
if (elementUnderPointer !== element && !element.contains(elementUnderPointer)) {
842+
this.hide();
843+
}
844+
}
845+
});
856846
} else if (this.touchGestures !== 'off') {
857847
this._disableNativeGesturesIfNecessary();
858848
const touchendListener = () => {
@@ -862,17 +852,15 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
862852
this.hide(this._defaultOptions?.touchendHideDelay);
863853
};
864854

865-
exitListeners.push(['touchend', touchendListener], ['touchcancel', touchendListener]);
855+
this._addListener('touchend', touchendListener);
856+
this._addListener('touchcancel', touchendListener);
866857
}
867-
868-
this._addListeners(exitListeners);
869-
this._passiveListeners.push(...exitListeners);
870858
}
871859

872-
private _addListeners(listeners: (readonly [string, EventListenerOrEventListenerObject])[]) {
873-
listeners.forEach(([event, listener]) => {
874-
this._elementRef.nativeElement.addEventListener(event, listener, passiveListenerOptions);
875-
});
860+
private _addListener<T extends Event>(name: string, listener: (event: T) => void) {
861+
this._eventCleanups.push(
862+
this._renderer.listen(this._elementRef.nativeElement, name, listener, passiveListenerOptions),
863+
);
876864
}
877865

878866
private _isTouchPlatform(): boolean {
@@ -890,24 +878,6 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
890878
);
891879
}
892880

893-
/** Listener for the `wheel` event on the element. */
894-
private _wheelListener(event: WheelEvent) {
895-
if (this._isTooltipVisible()) {
896-
const elementUnderPointer = this._injector
897-
.get(DOCUMENT)
898-
.elementFromPoint(event.clientX, event.clientY);
899-
const element = this._elementRef.nativeElement;
900-
901-
// On non-touch devices we depend on the `mouseleave` event to close the tooltip, but it
902-
// won't fire if the user scrolls away using the wheel without moving their cursor. We
903-
// work around it by finding the element under the user's cursor and closing the tooltip
904-
// if it's not the trigger.
905-
if (elementUnderPointer !== element && !element.contains(elementUnderPointer)) {
906-
this.hide();
907-
}
908-
}
909-
}
910-
911881
/** Disables the native browser gestures, based on how the tooltip has been configured. */
912882
private _disableNativeGesturesIfNecessary() {
913883
const gestures = this.touchGestures;

0 commit comments

Comments
 (0)