diff --git a/goldens/material/chips/index.api.md b/goldens/material/chips/index.api.md index 0cc3258191bf..627c1889451f 100644 --- a/goldens/material/chips/index.api.md +++ b/goldens/material/chips/index.api.md @@ -435,7 +435,7 @@ export class MatChipRemove extends MatChipAction { } // @public -export class MatChipRow extends MatChip implements AfterViewInit { +export class MatChipRow extends MatChip implements AfterViewInit, OnDestroy { constructor(...args: unknown[]); // (undocumented) protected basicChipAttrName: string; @@ -464,6 +464,8 @@ export class MatChipRow extends MatChip implements AfterViewInit { // (undocumented) ngAfterViewInit(): void; // (undocumented) + ngOnDestroy(): void; + // (undocumented) static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; diff --git a/src/material/chips/chip-row.ts b/src/material/chips/chip-row.ts index c3d9453aad81..0b545be2a364 100644 --- a/src/material/chips/chip-row.ts +++ b/src/material/chips/chip-row.ts @@ -14,10 +14,13 @@ import { ContentChild, EventEmitter, Input, + OnDestroy, Output, + Renderer2, ViewChild, ViewEncapsulation, afterNextRender, + inject, } from '@angular/core'; import {takeUntil} from 'rxjs/operators'; import {MatChip, MatChipEvent} from './chip'; @@ -72,8 +75,10 @@ export interface MatChipEditedEvent extends MatChipEvent { changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatChipAction, MatChipEditInput], }) -export class MatChipRow extends MatChip implements AfterViewInit { +export class MatChipRow extends MatChip implements AfterViewInit, OnDestroy { protected override basicChipAttrName = 'mat-basic-chip-row'; + private _renderer = inject(Renderer2); + private _cleanupMousedown: (() => void) | undefined; /** * The editing action has to be triggered in a timeout. While we're waiting on it, a blur @@ -123,12 +128,16 @@ export class MatChipRow extends MatChip implements AfterViewInit { super.ngAfterViewInit(); // Sets _alreadyFocused (ahead of click) when chip already has focus. - this._ngZone.runOutsideAngular(() => { - this._elementRef.nativeElement.addEventListener( - 'mousedown', - () => (this._alreadyFocused = this._hasFocus()), - ); - }); + this._cleanupMousedown = this._ngZone.runOutsideAngular(() => + this._renderer.listen(this._elementRef.nativeElement, 'mousedown', () => { + this._alreadyFocused = this._hasFocus(); + }), + ); + } + + override ngOnDestroy(): void { + super.ngOnDestroy(); + this._cleanupMousedown?.(); } protected _hasLeadingActionIcon() { diff --git a/src/material/sidenav/drawer.ts b/src/material/sidenav/drawer.ts index 9fd9b4705cf0..118da3c6940b 100644 --- a/src/material/sidenav/drawer.ts +++ b/src/material/sidenav/drawer.ts @@ -44,7 +44,7 @@ import { DOCUMENT, signal, } from '@angular/core'; -import {fromEvent, merge, Observable, Subject} from 'rxjs'; +import {merge, Observable, Subject} from 'rxjs'; import {debounceTime, filter, map, mapTo, startWith, take, takeUntil} from 'rxjs/operators'; import {_animationsDisabled} from '../core'; @@ -350,27 +350,23 @@ export class MatDrawer implements AfterViewInit, OnDestroy { * time a key is pressed. Instead we re-enter the zone only if the `ESC` key is pressed * and we don't have close disabled. */ - this._ngZone.runOutsideAngular(() => { + this._eventCleanups = this._ngZone.runOutsideAngular(() => { + const renderer = this._renderer; const element = this._elementRef.nativeElement; - (fromEvent(element, 'keydown') as Observable) - .pipe( - filter(event => { - return event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event); - }), - takeUntil(this._destroyed), - ) - .subscribe(event => - this._ngZone.run(() => { - this.close(); - event.stopPropagation(); - event.preventDefault(); - }), - ); - this._eventCleanups = [ - this._renderer.listen(element, 'transitionrun', this._handleTransitionEvent), - this._renderer.listen(element, 'transitionend', this._handleTransitionEvent), - this._renderer.listen(element, 'transitioncancel', this._handleTransitionEvent), + return [ + renderer.listen(element, 'keydown', (event: KeyboardEvent) => { + if (event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event)) { + this._ngZone.run(() => { + this.close(); + event.stopPropagation(); + event.preventDefault(); + }); + } + }), + renderer.listen(element, 'transitionrun', this._handleTransitionEvent), + renderer.listen(element, 'transitionend', this._handleTransitionEvent), + renderer.listen(element, 'transitioncancel', this._handleTransitionEvent), ]; }); diff --git a/src/material/tooltip/tooltip.ts b/src/material/tooltip/tooltip.ts index ee15859de170..cda69cd18575 100644 --- a/src/material/tooltip/tooltip.ts +++ b/src/material/tooltip/tooltip.ts @@ -31,9 +31,10 @@ import { afterNextRender, Injector, DOCUMENT, + Renderer2, } from '@angular/core'; import {NgClass} from '@angular/common'; -import {normalizePassiveListenerOptions, Platform} from '@angular/cdk/platform'; +import {Platform} from '@angular/cdk/platform'; import {AriaDescriber, FocusMonitor} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import { @@ -167,7 +168,7 @@ export const TOOLTIP_PANEL_CLASS = 'mat-mdc-tooltip-panel'; const PANEL_CLASS = 'tooltip-panel'; /** Options used to bind passive event listeners. */ -const passiveListenerOptions = normalizePassiveListenerOptions({passive: true}); +const passiveListenerOptions = {passive: true}; // These constants were taken from MDC's `numbers` object. We can't import them from MDC, // because they have some top-level references to `window` which break during SSR. @@ -200,6 +201,8 @@ export class MatTooltip implements OnDestroy, AfterViewInit { private _injector = inject(Injector); private _viewContainerRef = inject(ViewContainerRef); private _mediaMatcher = inject(MediaMatcher); + private _document = inject(DOCUMENT); + private _renderer = inject(Renderer2); private _animationsDisabled = _animationsDisabled(); private _defaultOptions = inject(MAT_TOOLTIP_DEFAULT_OPTIONS, { optional: true, @@ -363,8 +366,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit { } /** Manually-bound passive event listeners. */ - private readonly _passiveListeners: (readonly [string, EventListenerOrEventListenerObject])[] = - []; + private readonly _eventCleanups: (() => void)[] = []; /** Timer started at the last `touchstart` event. */ private _touchstartTimeout: null | ReturnType = null; @@ -438,17 +440,11 @@ export class MatTooltip implements OnDestroy, AfterViewInit { this._tooltipInstance = null; } - // Clean up the event listeners set in the constructor - this._passiveListeners.forEach(([event, listener]) => { - nativeElement.removeEventListener(event, listener, passiveListenerOptions); - }); - this._passiveListeners.length = 0; - + this._eventCleanups.forEach(cleanup => cleanup()); + this._eventCleanups.length = 0; this._destroyed.next(); this._destroyed.complete(); - this._isDestroyed = true; - this._ariaDescriber.removeDescription(nativeElement, this.message, 'tooltip'); this._focusMonitor.stopMonitoring(nativeElement); } @@ -783,54 +779,40 @@ export class MatTooltip implements OnDestroy, AfterViewInit { /** Binds the pointer events to the tooltip trigger. */ private _setupPointerEnterEventsIfNeeded() { // Optimization: Defer hooking up events if there's no message or the tooltip is disabled. - if ( - this._disabled || - !this.message || - !this._viewInitialized || - this._passiveListeners.length - ) { + if (this._disabled || !this.message || !this._viewInitialized || this._eventCleanups.length) { return; } // The mouse events shouldn't be bound on mobile devices, because they can prevent the // first tap from firing its click event or can cause the tooltip to open for clicks. if (!this._isTouchPlatform()) { - this._passiveListeners.push([ - 'mouseenter', - event => { - this._setupPointerExitEventsIfNeeded(); - let point = undefined; - if ((event as MouseEvent).x !== undefined && (event as MouseEvent).y !== undefined) { - point = event as MouseEvent; - } - this.show(undefined, point); - }, - ]); + this._addListener('mouseenter', (event: MouseEvent) => { + this._setupPointerExitEventsIfNeeded(); + let point = undefined; + if (event.x !== undefined && event.y !== undefined) { + point = event; + } + this.show(undefined, point); + }); } else if (this.touchGestures !== 'off') { this._disableNativeGesturesIfNecessary(); + this._addListener('touchstart', (event: TouchEvent) => { + const touch = event.targetTouches?.[0]; + const origin = touch ? {x: touch.clientX, y: touch.clientY} : undefined; + // Note that it's important that we don't `preventDefault` here, + // because it can prevent click events from firing on the element. + this._setupPointerExitEventsIfNeeded(); + if (this._touchstartTimeout) { + clearTimeout(this._touchstartTimeout); + } - this._passiveListeners.push([ - 'touchstart', - event => { - const touch = (event as TouchEvent).targetTouches?.[0]; - const origin = touch ? {x: touch.clientX, y: touch.clientY} : undefined; - // Note that it's important that we don't `preventDefault` here, - // because it can prevent click events from firing on the element. - this._setupPointerExitEventsIfNeeded(); - if (this._touchstartTimeout) { - clearTimeout(this._touchstartTimeout); - } - - const DEFAULT_LONGPRESS_DELAY = 500; - this._touchstartTimeout = setTimeout(() => { - this._touchstartTimeout = null; - this.show(undefined, origin); - }, this._defaultOptions?.touchLongPressShowDelay ?? DEFAULT_LONGPRESS_DELAY); - }, - ]); + const DEFAULT_LONGPRESS_DELAY = 500; + this._touchstartTimeout = setTimeout(() => { + this._touchstartTimeout = null; + this.show(undefined, origin); + }, this._defaultOptions?.touchLongPressShowDelay ?? DEFAULT_LONGPRESS_DELAY); + }); } - - this._addListeners(this._passiveListeners); } private _setupPointerExitEventsIfNeeded() { @@ -839,20 +821,28 @@ export class MatTooltip implements OnDestroy, AfterViewInit { } this._pointerExitEventsInitialized = true; - const exitListeners: (readonly [string, EventListenerOrEventListenerObject])[] = []; if (!this._isTouchPlatform()) { - exitListeners.push( - [ - 'mouseleave', - event => { - const newTarget = (event as MouseEvent).relatedTarget as Node | null; - if (!newTarget || !this._overlayRef?.overlayElement.contains(newTarget)) { - this.hide(); - } - }, - ], - ['wheel', event => this._wheelListener(event as WheelEvent)], - ); + this._addListener('mouseleave', (event: MouseEvent) => { + const newTarget = event.relatedTarget as Node | null; + if (!newTarget || !this._overlayRef?.overlayElement.contains(newTarget)) { + this.hide(); + } + }); + + this._addListener('wheel', (event: WheelEvent) => { + if (this._isTooltipVisible()) { + const elementUnderPointer = this._document.elementFromPoint(event.clientX, event.clientY); + const element = this._elementRef.nativeElement; + + // On non-touch devices we depend on the `mouseleave` event to close the tooltip, but it + // won't fire if the user scrolls away using the wheel without moving their cursor. We + // work around it by finding the element under the user's cursor and closing the tooltip + // if it's not the trigger. + if (elementUnderPointer !== element && !element.contains(elementUnderPointer)) { + this.hide(); + } + } + }); } else if (this.touchGestures !== 'off') { this._disableNativeGesturesIfNecessary(); const touchendListener = () => { @@ -862,17 +852,15 @@ export class MatTooltip implements OnDestroy, AfterViewInit { this.hide(this._defaultOptions?.touchendHideDelay); }; - exitListeners.push(['touchend', touchendListener], ['touchcancel', touchendListener]); + this._addListener('touchend', touchendListener); + this._addListener('touchcancel', touchendListener); } - - this._addListeners(exitListeners); - this._passiveListeners.push(...exitListeners); } - private _addListeners(listeners: (readonly [string, EventListenerOrEventListenerObject])[]) { - listeners.forEach(([event, listener]) => { - this._elementRef.nativeElement.addEventListener(event, listener, passiveListenerOptions); - }); + private _addListener(name: string, listener: (event: T) => void) { + this._eventCleanups.push( + this._renderer.listen(this._elementRef.nativeElement, name, listener, passiveListenerOptions), + ); } private _isTouchPlatform(): boolean { @@ -890,24 +878,6 @@ export class MatTooltip implements OnDestroy, AfterViewInit { ); } - /** Listener for the `wheel` event on the element. */ - private _wheelListener(event: WheelEvent) { - if (this._isTooltipVisible()) { - const elementUnderPointer = this._injector - .get(DOCUMENT) - .elementFromPoint(event.clientX, event.clientY); - const element = this._elementRef.nativeElement; - - // On non-touch devices we depend on the `mouseleave` event to close the tooltip, but it - // won't fire if the user scrolls away using the wheel without moving their cursor. We - // work around it by finding the element under the user's cursor and closing the tooltip - // if it's not the trigger. - if (elementUnderPointer !== element && !element.contains(elementUnderPointer)) { - this.hide(); - } - } - } - /** Disables the native browser gestures, based on how the tooltip has been configured. */ private _disableNativeGesturesIfNecessary() { const gestures = this.touchGestures;