From f9a638fad1c718ea9c9e6b82d2e3a87e2e98f30a Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 13 Dec 2024 19:33:07 +0100 Subject: [PATCH] refactor(multiple): use renderer for manual event bindings Switches our manual calls into `addEventListener` to use the renderer instead so that tooling (e.g. the tracing service) gets notified about them. Note that these changes only include the events that don't pass event options. The remaining events will be changed in a follow-up, because they require us to update to the latest `next` version of Angular. --- src/cdk/dialog/dialog-container.ts | 10 ++-- src/cdk/drag-drop/drag-drop.ts | 4 +- src/cdk/drag-drop/drag-ref.ts | 11 ++-- src/cdk/drag-drop/preview-ref.ts | 11 ++-- .../private/shared-resize-observer.ts | 15 +++-- .../overlay-keyboard-dispatcher.spec.ts | 8 ++- .../overlay-keyboard-dispatcher.ts | 28 ++++----- .../overlay/fullscreen-overlay-container.ts | 40 +++++-------- src/cdk/overlay/overlay-ref.ts | 29 ++++++--- src/cdk/overlay/overlay.ts | 3 + src/cdk/scrolling/viewport-ruler.ts | 27 +++------ src/cdk/text-field/autosize.ts | 12 ++-- .../autocomplete/autocomplete-trigger.ts | 60 +++++++++---------- src/material/button/button-base.ts | 12 +++- src/material/expansion/expansion-panel.ts | 14 +++-- .../form-field/directives/line-ripple.ts | 12 +++- src/material/input/input.ts | 50 +++++++--------- src/material/list/selection-list.ts | 12 ++-- src/material/progress-bar/progress-bar.ts | 11 +++- src/material/radio/radio.ts | 14 +++-- src/material/sidenav/drawer.ts | 10 ++-- src/material/slider/slider-input.ts | 19 +++--- src/material/slider/slider-thumb.ts | 29 ++++----- src/material/timepicker/timepicker-input.ts | 11 +++- tools/public_api_guard/cdk/drag-drop.md | 3 +- tools/public_api_guard/cdk/overlay.md | 3 +- 26 files changed, 249 insertions(+), 209 deletions(-) diff --git a/src/cdk/dialog/dialog-container.ts b/src/cdk/dialog/dialog-container.ts index a75c4a8fc6f2..7f426063e700 100644 --- a/src/cdk/dialog/dialog-container.ts +++ b/src/cdk/dialog/dialog-container.ts @@ -33,6 +33,7 @@ import { Injector, NgZone, OnDestroy, + Renderer2, ViewChild, ViewEncapsulation, afterNextRender, @@ -79,6 +80,7 @@ export class CdkDialogContainer protected _ngZone = inject(NgZone); private _overlayRef = inject(OverlayRef); private _focusMonitor = inject(FocusMonitor); + private _renderer = inject(Renderer2); private _platform = inject(Platform); protected _document = inject(DOCUMENT, {optional: true})!; @@ -223,13 +225,13 @@ export class CdkDialogContainer // The tabindex attribute should be removed to avoid navigating to that element again this._ngZone.runOutsideAngular(() => { const callback = () => { - element.removeEventListener('blur', callback); - element.removeEventListener('mousedown', callback); + deregisterBlur(); + deregisterMousedown(); element.removeAttribute('tabindex'); }; - element.addEventListener('blur', callback); - element.addEventListener('mousedown', callback); + const deregisterBlur = this._renderer.listen(element, 'blur', callback); + const deregisterMousedown = this._renderer.listen(element, 'mousedown', callback); }); } element.focus(options); diff --git a/src/cdk/drag-drop/drag-drop.ts b/src/cdk/drag-drop/drag-drop.ts index 590f68731378..f3b43d3d5d3c 100644 --- a/src/cdk/drag-drop/drag-drop.ts +++ b/src/cdk/drag-drop/drag-drop.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Injectable, NgZone, ElementRef, inject} from '@angular/core'; +import {Injectable, NgZone, ElementRef, inject, RendererFactory2} from '@angular/core'; import {DOCUMENT} from '@angular/common'; import {ViewportRuler} from '@angular/cdk/scrolling'; import {DragRef, DragRefConfig} from './drag-ref'; @@ -28,6 +28,7 @@ export class DragDrop { private _ngZone = inject(NgZone); private _viewportRuler = inject(ViewportRuler); private _dragDropRegistry = inject(DragDropRegistry); + private _renderer = inject(RendererFactory2).createRenderer(null, null); constructor(...args: unknown[]); constructor() {} @@ -48,6 +49,7 @@ export class DragDrop { this._ngZone, this._viewportRuler, this._dragDropRegistry, + this._renderer, ); } diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index ea70c698a4d0..0d20f2aa38e6 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -19,6 +19,7 @@ import { ElementRef, EmbeddedViewRef, NgZone, + Renderer2, TemplateRef, ViewContainerRef, signal, @@ -378,6 +379,7 @@ export class DragRef { private _ngZone: NgZone, private _viewportRuler: ViewportRuler, private _dragDropRegistry: DragDropRegistry, + private _renderer: Renderer2, ) { this.withRootElement(element).withParent(_config.parentDragRef || null); this._parentPositions = new ParentPositionTracker(_document); @@ -853,6 +855,7 @@ export class DragRef { this._pickupPositionOnPage, this._initialTransform, this._config.zIndex || 1000, + this._renderer, ); this._preview.attach(this._getPreviewInsertionPoint(parent, shadowRoot)); @@ -1106,24 +1109,24 @@ export class DragRef { return this._ngZone.runOutsideAngular(() => { return new Promise(resolve => { - const handler = ((event: TransitionEvent) => { + const handler = (event: TransitionEvent) => { if ( !event || (this._preview && _getEventTarget(event) === this._preview.element && event.propertyName === 'transform') ) { - this._preview?.removeEventListener('transitionend', handler); + cleanupListener(); resolve(); clearTimeout(timeout); } - }) as EventListenerOrEventListenerObject; + }; // If a transition is short enough, the browser might not fire the `transitionend` event. // Since we know how long it's supposed to take, add a timeout with a 50% buffer that'll // fire if the transition hasn't completed when it was supposed to. const timeout = setTimeout(handler as Function, duration * 1.5); - this._preview!.addEventListener('transitionend', handler); + const cleanupListener = this._preview!.addEventListener('transitionend', handler); }); }); } diff --git a/src/cdk/drag-drop/preview-ref.ts b/src/cdk/drag-drop/preview-ref.ts index 497655515f55..d1ddbfbddf16 100644 --- a/src/cdk/drag-drop/preview-ref.ts +++ b/src/cdk/drag-drop/preview-ref.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {EmbeddedViewRef, TemplateRef, ViewContainerRef} from '@angular/core'; +import {EmbeddedViewRef, Renderer2, TemplateRef, ViewContainerRef} from '@angular/core'; import {Direction} from '@angular/cdk/bidi'; import { extendStyles, @@ -56,6 +56,7 @@ export class PreviewRef { }, private _initialTransform: string | null, private _zIndex: number, + private _renderer: Renderer2, ) {} attach(parent: HTMLElement): void { @@ -91,12 +92,8 @@ export class PreviewRef { return getTransformTransitionDurationInMs(this._preview); } - addEventListener(name: string, handler: EventListenerOrEventListenerObject) { - this._preview.addEventListener(name, handler); - } - - removeEventListener(name: string, handler: EventListenerOrEventListenerObject) { - this._preview.removeEventListener(name, handler); + addEventListener(name: string, handler: (event: any) => void): () => void { + return this._renderer.listen(this._preview, name, handler); } private _createPreview(): HTMLElement { diff --git a/src/cdk/observers/private/shared-resize-observer.ts b/src/cdk/observers/private/shared-resize-observer.ts index a0cbd76c1cf6..6b3016ff4748 100644 --- a/src/cdk/observers/private/shared-resize-observer.ts +++ b/src/cdk/observers/private/shared-resize-observer.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ -import {inject, Injectable, NgZone, OnDestroy} from '@angular/core'; +import {inject, Injectable, NgZone, OnDestroy, RendererFactory2} from '@angular/core'; import {Observable, Subject} from 'rxjs'; import {filter, shareReplay, takeUntil} from 'rxjs/operators'; @@ -98,6 +98,8 @@ class SingleBoxSharedResizeObserver { providedIn: 'root', }) export class SharedResizeObserver implements OnDestroy { + private _cleanupErrorListener: (() => void) | undefined; + /** Map of box type to shared resize observer. */ private _observers = new Map(); @@ -107,7 +109,12 @@ export class SharedResizeObserver implements OnDestroy { constructor() { if (typeof ResizeObserver !== 'undefined' && (typeof ngDevMode === 'undefined' || ngDevMode)) { this._ngZone.runOutsideAngular(() => { - window.addEventListener('error', loopLimitExceededErrorHandler); + const renderer = inject(RendererFactory2).createRenderer(null, null); + this._cleanupErrorListener = renderer.listen( + 'window', + 'error', + loopLimitExceededErrorHandler, + ); }); } } @@ -117,9 +124,7 @@ export class SharedResizeObserver implements OnDestroy { observer.destroy(); } this._observers.clear(); - if (typeof ResizeObserver !== 'undefined' && (typeof ngDevMode === 'undefined' || ngDevMode)) { - window.removeEventListener('error', loopLimitExceededErrorHandler); - } + this._cleanupErrorListener?.(); } /** diff --git a/src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.spec.ts b/src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.spec.ts index 7a103af0b6b6..a15c226ca71b 100644 --- a/src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.spec.ts +++ b/src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.spec.ts @@ -138,15 +138,17 @@ describe('OverlayKeyboardDispatcher', () => { it('should dispose of the global keyboard event handler correctly', () => { const overlayRef = overlay.create(); const body = document.body; - spyOn(body, 'addEventListener'); spyOn(body, 'removeEventListener'); keyboardDispatcher.add(overlayRef); - expect(body.addEventListener).toHaveBeenCalledWith('keydown', jasmine.any(Function)); + expect(body.addEventListener).toHaveBeenCalledWith('keydown', jasmine.any(Function), false); overlayRef.dispose(); - expect(body.removeEventListener).toHaveBeenCalledWith('keydown', jasmine.any(Function)); + expect(document.body.removeEventListener).toHaveBeenCalledWith( + 'keydown', + jasmine.any(Function), + ); }); it('should skip overlays that do not have keydown event subscriptions', () => { diff --git a/src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.ts b/src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.ts index 4d7e5979fde2..82d8a9bb5604 100644 --- a/src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.ts +++ b/src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Injectable, NgZone, inject} from '@angular/core'; +import {Injectable, NgZone, RendererFactory2, inject} from '@angular/core'; import {BaseOverlayDispatcher} from './base-overlay-dispatcher'; import type {OverlayRef} from '../overlay-ref'; @@ -17,7 +17,9 @@ import type {OverlayRef} from '../overlay-ref'; */ @Injectable({providedIn: 'root'}) export class OverlayKeyboardDispatcher extends BaseOverlayDispatcher { - private _ngZone = inject(NgZone, {optional: true}); + private _ngZone = inject(NgZone); + private _renderer = inject(RendererFactory2).createRenderer(null, null); + private _cleanupKeydown: (() => void) | undefined; /** Add a new overlay to the list of attached overlay refs. */ override add(overlayRef: OverlayRef): void { @@ -25,14 +27,10 @@ export class OverlayKeyboardDispatcher extends BaseOverlayDispatcher { // Lazily start dispatcher once first overlay is added if (!this._isAttached) { - /** @breaking-change 14.0.0 _ngZone will be required. */ - if (this._ngZone) { - this._ngZone.runOutsideAngular(() => - this._document.body.addEventListener('keydown', this._keydownListener), - ); - } else { - this._document.body.addEventListener('keydown', this._keydownListener); - } + this._ngZone.runOutsideAngular(() => { + this._cleanupKeydown = this._renderer.listen('body', 'keydown', this._keydownListener); + }); + this._isAttached = true; } } @@ -40,7 +38,7 @@ export class OverlayKeyboardDispatcher extends BaseOverlayDispatcher { /** Detaches the global keyboard event listener. */ protected detach() { if (this._isAttached) { - this._document.body.removeEventListener('keydown', this._keydownListener); + this._cleanupKeydown?.(); this._isAttached = false; } } @@ -57,13 +55,7 @@ export class OverlayKeyboardDispatcher extends BaseOverlayDispatcher { // because we don't want overlays that don't handle keyboard events to block the ones below // them that do. if (overlays[i]._keydownEvents.observers.length > 0) { - const keydownEvents = overlays[i]._keydownEvents; - /** @breaking-change 14.0.0 _ngZone will be required. */ - if (this._ngZone) { - this._ngZone.run(() => keydownEvents.next(event)); - } else { - keydownEvents.next(event); - } + this._ngZone.run(() => overlays[i]._keydownEvents.next(event)); break; } } diff --git a/src/cdk/overlay/fullscreen-overlay-container.ts b/src/cdk/overlay/fullscreen-overlay-container.ts index ed4dbf5fbf87..301e1a1eca29 100644 --- a/src/cdk/overlay/fullscreen-overlay-container.ts +++ b/src/cdk/overlay/fullscreen-overlay-container.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Injectable, OnDestroy} from '@angular/core'; +import {inject, Injectable, OnDestroy, RendererFactory2} from '@angular/core'; import {OverlayContainer} from './overlay-container'; /** @@ -18,8 +18,9 @@ import {OverlayContainer} from './overlay-container'; */ @Injectable({providedIn: 'root'}) export class FullscreenOverlayContainer extends OverlayContainer implements OnDestroy { + private _renderer = inject(RendererFactory2).createRenderer(null, null); private _fullScreenEventName: string | undefined; - private _fullScreenListener: () => void; + private _cleanupFullScreenListener: (() => void) | undefined; constructor(...args: unknown[]); @@ -29,38 +30,27 @@ export class FullscreenOverlayContainer extends OverlayContainer implements OnDe override ngOnDestroy() { super.ngOnDestroy(); - - if (this._fullScreenEventName && this._fullScreenListener) { - this._document.removeEventListener(this._fullScreenEventName, this._fullScreenListener); - } + this._cleanupFullScreenListener?.(); } protected override _createContainer(): void { + const eventName = this._getEventName(); super._createContainer(); this._adjustParentForFullscreenChange(); - this._addFullscreenChangeListener(() => this._adjustParentForFullscreenChange()); - } - private _adjustParentForFullscreenChange(): void { - if (!this._containerElement) { - return; + if (eventName) { + this._cleanupFullScreenListener?.(); + this._cleanupFullScreenListener = this._renderer.listen('document', eventName, () => { + this._adjustParentForFullscreenChange(); + }); } - - const fullscreenElement = this.getFullscreenElement(); - const parent = fullscreenElement || this._document.body; - parent.appendChild(this._containerElement); } - private _addFullscreenChangeListener(fn: () => void) { - const eventName = this._getEventName(); - - if (eventName) { - if (this._fullScreenListener) { - this._document.removeEventListener(eventName, this._fullScreenListener); - } - - this._document.addEventListener(eventName, fn); - this._fullScreenListener = fn; + private _adjustParentForFullscreenChange(): void { + if (this._containerElement) { + const fullscreenElement = this.getFullscreenElement(); + const parent = fullscreenElement || this._document.body; + parent.appendChild(this._containerElement); } } diff --git a/src/cdk/overlay/overlay-ref.ts b/src/cdk/overlay/overlay-ref.ts index d1b4e4f5cbf5..7eca1b8f182f 100644 --- a/src/cdk/overlay/overlay-ref.ts +++ b/src/cdk/overlay/overlay-ref.ts @@ -14,6 +14,7 @@ import { EmbeddedViewRef, EnvironmentInjector, NgZone, + Renderer2, afterNextRender, afterRender, untracked, @@ -46,10 +47,8 @@ export class OverlayRef implements PortalOutlet { private _positionStrategy: PositionStrategy | undefined; private _scrollStrategy: ScrollStrategy | undefined; private _locationChanges: SubscriptionLike = Subscription.EMPTY; - private _backdropClickHandler = (event: MouseEvent) => this._backdropClick.next(event); - private _backdropTransitionendHandler = (event: TransitionEvent) => { - this._disposeBackdrop(event.target as HTMLElement | null); - }; + private _cleanupBackdropClick: (() => void) | undefined; + private _cleanupBackdropTransitionEnd: (() => void) | undefined; /** * Reference to the parent of the `_host` at the time it was detached. Used to restore @@ -82,6 +81,7 @@ export class OverlayRef implements PortalOutlet { private _outsideClickDispatcher: OverlayOutsideClickDispatcher, private _animationsDisabled = false, private _injector: EnvironmentInjector, + private _renderer: Renderer2, ) { if (_config.scrollStrategy) { this._scrollStrategy = _config.scrollStrategy; @@ -449,7 +449,12 @@ export class OverlayRef implements PortalOutlet { // Forward backdrop clicks such that the consumer of the overlay can perform whatever // action desired when such a click occurs (usually closing the overlay). - this._backdropElement.addEventListener('click', this._backdropClickHandler); + this._cleanupBackdropClick?.(); + this._cleanupBackdropClick = this._renderer.listen( + this._backdropElement, + 'click', + (event: MouseEvent) => this._backdropClick.next(event), + ); // Add class to fade-in the backdrop after one frame. if (!this._animationsDisabled && typeof requestAnimationFrame !== 'undefined') { @@ -494,7 +499,14 @@ export class OverlayRef implements PortalOutlet { backdropToDetach.classList.remove('cdk-overlay-backdrop-showing'); this._ngZone.runOutsideAngular(() => { - backdropToDetach!.addEventListener('transitionend', this._backdropTransitionendHandler); + this._cleanupBackdropTransitionEnd?.(); + this._cleanupBackdropTransitionEnd = this._renderer.listen( + backdropToDetach, + 'transitionend', + (event: TransitionEvent) => { + this._disposeBackdrop(event.target as HTMLElement | null); + }, + ); }); // If the backdrop doesn't have a transition, the `transitionend` event won't fire. @@ -565,9 +577,10 @@ export class OverlayRef implements PortalOutlet { /** Removes a backdrop element from the DOM. */ private _disposeBackdrop(backdrop: HTMLElement | null) { + this._cleanupBackdropClick?.(); + this._cleanupBackdropTransitionEnd?.(); + if (backdrop) { - backdrop.removeEventListener('click', this._backdropClickHandler); - backdrop.removeEventListener('transitionend', this._backdropTransitionendHandler); backdrop.remove(); // It is possible that a new portal has been attached to this overlay since we started diff --git a/src/cdk/overlay/overlay.ts b/src/cdk/overlay/overlay.ts index b7cb242a5574..5598d296d0d2 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -17,6 +17,7 @@ import { ANIMATION_MODULE_TYPE, EnvironmentInjector, inject, + RendererFactory2, } from '@angular/core'; import {_IdGenerator} from '@angular/cdk/a11y'; import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; @@ -50,6 +51,7 @@ export class Overlay { private _outsideClickDispatcher = inject(OverlayOutsideClickDispatcher); private _animationsModuleType = inject(ANIMATION_MODULE_TYPE, {optional: true}); private _idGenerator = inject(_IdGenerator); + private _renderer = inject(RendererFactory2).createRenderer(null, null); private _appRef: ApplicationRef; private _styleLoader = inject(_CdkPrivateStyleLoader); @@ -86,6 +88,7 @@ export class Overlay { this._outsideClickDispatcher, this._animationsModuleType === 'NoopAnimations', this._injector.get(EnvironmentInjector), + this._renderer, ); } diff --git a/src/cdk/scrolling/viewport-ruler.ts b/src/cdk/scrolling/viewport-ruler.ts index c0070df1ee26..a053c8736f4f 100644 --- a/src/cdk/scrolling/viewport-ruler.ts +++ b/src/cdk/scrolling/viewport-ruler.ts @@ -7,7 +7,7 @@ */ import {Platform} from '@angular/cdk/platform'; -import {Injectable, NgZone, OnDestroy, inject} from '@angular/core'; +import {Injectable, NgZone, OnDestroy, RendererFactory2, inject} from '@angular/core'; import {Observable, Subject} from 'rxjs'; import {auditTime} from 'rxjs/operators'; import {DOCUMENT} from '@angular/common'; @@ -28,6 +28,7 @@ export interface ViewportScrollPosition { @Injectable({providedIn: 'root'}) export class ViewportRuler implements OnDestroy { private _platform = inject(Platform); + private _listeners: (() => void)[] | undefined; /** Cached viewport dimensions. */ private _viewportSize: {width: number; height: number} | null; @@ -35,11 +36,6 @@ export class ViewportRuler implements OnDestroy { /** Stream of viewport change events. */ private readonly _change = new Subject(); - /** Event listener that will be used to handle the viewport change events. */ - private _changeListener = (event: Event) => { - this._change.next(event); - }; - /** Used to reference correct document/window */ protected _document = inject(DOCUMENT, {optional: true})!; @@ -47,15 +43,15 @@ export class ViewportRuler implements OnDestroy { constructor() { const ngZone = inject(NgZone); + const renderer = inject(RendererFactory2).createRenderer(null, null); ngZone.runOutsideAngular(() => { if (this._platform.isBrowser) { - const window = this._getWindow(); - - // Note that bind the events ourselves, rather than going through something like RxJS's - // `fromEvent` so that we can ensure that they're bound outside of the NgZone. - window.addEventListener('resize', this._changeListener); - window.addEventListener('orientationchange', this._changeListener); + const changeListener = (event: Event) => this._change.next(event); + this._listeners = [ + renderer.listen('window', 'resize', changeListener), + renderer.listen('window', 'orientationchange', changeListener), + ]; } // Clear the cached position so that the viewport is re-measured next time it is required. @@ -65,12 +61,7 @@ export class ViewportRuler implements OnDestroy { } ngOnDestroy() { - if (this._platform.isBrowser) { - const window = this._getWindow(); - window.removeEventListener('resize', this._changeListener); - window.removeEventListener('orientationchange', this._changeListener); - } - + this._listeners?.forEach(cleanup => cleanup()); this._change.complete(); } diff --git a/src/cdk/text-field/autosize.ts b/src/cdk/text-field/autosize.ts index 98ee6616ee97..deb7d4ca057e 100644 --- a/src/cdk/text-field/autosize.ts +++ b/src/cdk/text-field/autosize.ts @@ -17,6 +17,7 @@ import { NgZone, booleanAttribute, inject, + Renderer2, } from '@angular/core'; import {DOCUMENT} from '@angular/common'; import {Platform} from '@angular/cdk/platform'; @@ -41,11 +42,13 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { private _elementRef = inject>(ElementRef); private _platform = inject(Platform); private _ngZone = inject(NgZone); + private _renderer = inject(Renderer2); /** Keep track of the previous textarea value to avoid resizing when the value hasn't changed. */ private _previousValue?: string; private _initialHeight: string | undefined; private readonly _destroyed = new Subject(); + private _listenerCleanups: (() => void)[] | undefined; private _minRows: number; private _maxRows: number; @@ -162,8 +165,10 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { .pipe(auditTime(16), takeUntil(this._destroyed)) .subscribe(() => this.resizeToFitContent(true)); - this._textareaElement.addEventListener('focus', this._handleFocusEvent); - this._textareaElement.addEventListener('blur', this._handleFocusEvent); + this._listenerCleanups = [ + this._renderer.listen(this._textareaElement, 'focus', this._handleFocusEvent), + this._renderer.listen(this._textareaElement, 'blur', this._handleFocusEvent), + ]; }); this._isViewInited = true; @@ -172,8 +177,7 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { } ngOnDestroy() { - this._textareaElement.removeEventListener('focus', this._handleFocusEvent); - this._textareaElement.removeEventListener('blur', this._handleFocusEvent); + this._listenerCleanups?.forEach(cleanup => cleanup()); this._destroyed.next(); this._destroyed.complete(); } diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index 159c036ce973..03c43eed53c2 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -34,6 +34,7 @@ import { NgZone, OnChanges, OnDestroy, + Renderer2, SimpleChanges, ViewContainerRef, afterNextRender, @@ -49,7 +50,7 @@ import { _getOptionScrollPosition, } from '@angular/material/core'; import {MAT_FORM_FIELD, MatFormField} from '@angular/material/form-field'; -import {Observable, Subject, Subscription, defer, fromEvent, merge, of as observableOf} from 'rxjs'; +import {Observable, Subject, Subscription, defer, merge, of as observableOf} from 'rxjs'; import {delay, filter, map, startWith, switchMap, take, tap} from 'rxjs/operators'; import { MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, @@ -130,6 +131,7 @@ export const MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY_PROVIDER = { export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewInit, OnChanges, OnDestroy { + private _injector = inject(Injector); private _element = inject>(ElementRef); private _overlay = inject(Overlay); private _viewContainerRef = inject(ViewContainerRef); @@ -139,6 +141,8 @@ export class MatAutocompleteTrigger private _formField = inject(MAT_FORM_FIELD, {optional: true, host: true}); private _document = inject(DOCUMENT); private _viewportRuler = inject(ViewportRuler); + private _scrollStrategy = inject(MAT_AUTOCOMPLETE_SCROLL_STRATEGY); + private _renderer = inject(Renderer2); private _defaults = inject( MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, {optional: true}, @@ -147,9 +151,10 @@ export class MatAutocompleteTrigger private _overlayRef: OverlayRef | null; private _portal: TemplatePortal; private _componentDestroyed = false; - private _scrollStrategy = inject(MAT_AUTOCOMPLETE_SCROLL_STRATEGY); + private _initialized = new Subject(); private _keydownSubscription: Subscription | null; private _outsideClickSubscription: Subscription | null; + private _cleanupWindowBlur: (() => void) | undefined; /** Old value of the native input. Used to work around issues with the `input` event on IE. */ private _previousValue: string | number | null; @@ -244,10 +249,6 @@ export class MatAutocompleteTrigger @Input({alias: 'matAutocompleteDisabled', transform: booleanAttribute}) autocompleteDisabled: boolean; - private _initialized = new Subject(); - - private _injector = inject(Injector); - constructor(...args: unknown[]); constructor() {} @@ -257,12 +258,7 @@ export class MatAutocompleteTrigger ngAfterViewInit() { this._initialized.next(); this._initialized.complete(); - - const window = this._getWindow(); - - if (typeof window !== 'undefined') { - this._zone.runOutsideAngular(() => window.addEventListener('blur', this._windowBlurHandler)); - } + this._cleanupWindowBlur = this._renderer.listen('window', 'blur', this._windowBlurHandler); } ngOnChanges(changes: SimpleChanges) { @@ -276,12 +272,7 @@ export class MatAutocompleteTrigger } ngOnDestroy() { - const window = this._getWindow(); - - if (typeof window !== 'undefined') { - window.removeEventListener('blur', this._windowBlurHandler); - } - + this._cleanupWindowBlur?.(); this._handsetLandscapeSubscription.unsubscribe(); this._viewportSubscription.unsubscribe(); this._componentDestroyed = true; @@ -408,12 +399,8 @@ export class MatAutocompleteTrigger /** Stream of clicks outside of the autocomplete panel. */ private _getOutsideClickStream(): Observable { - return merge( - fromEvent(this._document, 'click') as Observable, - fromEvent(this._document, 'auxclick') as Observable, - fromEvent(this._document, 'touchend') as Observable, - ).pipe( - filter(event => { + return new Observable(observer => { + const listener = (event: MouseEvent | TouchEvent) => { // If we're in the Shadow DOM, the event target will be the shadow root, so we have to // fall back to check the first element in the path of the click event. const clickTarget = _getEventTarget(event)!; @@ -422,7 +409,7 @@ export class MatAutocompleteTrigger : null; const customOrigin = this.connectedTo ? this.connectedTo.elementRef.nativeElement : null; - return ( + if ( this._overlayAttached && clickTarget !== this._element.nativeElement && // Normally focus moves inside `mousedown` so this condition will almost always be @@ -434,9 +421,21 @@ export class MatAutocompleteTrigger (!customOrigin || !customOrigin.contains(clickTarget)) && !!this._overlayRef && !this._overlayRef.overlayElement.contains(clickTarget) - ); - }), - ); + ) { + observer.next(event); + } + }; + + const cleanups = [ + this._renderer.listen('document', 'click', listener), + this._renderer.listen('document', 'auxclick', listener), + this._renderer.listen('document', 'touchend', listener), + ]; + + return () => { + cleanups.forEach(current => current()); + }; + }); } // Implemented as part of ControlValueAccessor. @@ -996,11 +995,6 @@ export class MatAutocompleteTrigger return !element.readOnly && !element.disabled && !this.autocompleteDisabled; } - /** Use defaultView of injected document if available or fallback to global window reference */ - private _getWindow(): Window { - return this._document?.defaultView || window; - } - /** Scrolls to a particular option in the list. */ private _scrollToOption(index: number): void { // Given that we are not actually focusing active options, we must manually adjust scroll diff --git a/src/material/button/button-base.ts b/src/material/button/button-base.ts index 8d4881954085..18d61f8b0bba 100644 --- a/src/material/button/button-base.ts +++ b/src/material/button/button-base.ts @@ -21,6 +21,7 @@ import { numberAttribute, OnDestroy, OnInit, + Renderer2, } from '@angular/core'; import {_StructuralStylesLoader, MatRippleLoader, ThemePalette} from '@angular/material/core'; import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; @@ -242,6 +243,9 @@ export const MAT_ANCHOR_HOST = { */ @Directive() export class MatAnchorBase extends MatButtonBase implements OnInit, OnDestroy { + private _renderer = inject(Renderer2); + private _cleanupClick: () => void; + @Input({ transform: (value: unknown) => { return value == null ? undefined : numberAttribute(value); @@ -251,13 +255,17 @@ export class MatAnchorBase extends MatButtonBase implements OnInit, OnDestroy { ngOnInit(): void { this._ngZone.runOutsideAngular(() => { - this._elementRef.nativeElement.addEventListener('click', this._haltDisabledEvents); + this._cleanupClick = this._renderer.listen( + this._elementRef.nativeElement, + 'click', + this._haltDisabledEvents, + ); }); } override ngOnDestroy(): void { super.ngOnDestroy(); - this._elementRef.nativeElement.removeEventListener('click', this._haltDisabledEvents); + this._cleanupClick?.(); } _haltDisabledEvents = (event: Event): void => { diff --git a/src/material/expansion/expansion-panel.ts b/src/material/expansion/expansion-panel.ts index de2bfec1182d..04dfdf0e0b02 100644 --- a/src/material/expansion/expansion-panel.ts +++ b/src/material/expansion/expansion-panel.ts @@ -31,6 +31,7 @@ import { ANIMATION_MODULE_TYPE, inject, NgZone, + Renderer2, } from '@angular/core'; import {_IdGenerator} from '@angular/cdk/a11y'; import {Subject} from 'rxjs'; @@ -98,6 +99,8 @@ export class MatExpansionPanel private _document = inject(DOCUMENT); private _ngZone = inject(NgZone); private _elementRef = inject>(ElementRef); + private _renderer = inject(Renderer2); + private _cleanupTransitionEnd: (() => void) | undefined; /** Whether the toggle indicator should be hidden. */ @Input({transform: booleanAttribute}) @@ -215,10 +218,7 @@ export class MatExpansionPanel override ngOnDestroy() { super.ngOnDestroy(); - this._bodyWrapper?.nativeElement.removeEventListener( - 'transitionend', - this._transitionEndListener, - ); + this._cleanupTransitionEnd?.(); this._inputChanges.complete(); } @@ -255,7 +255,11 @@ export class MatExpansionPanel } else { setTimeout(() => { const element = this._elementRef.nativeElement; - element.addEventListener('transitionend', this._transitionEndListener); + this._cleanupTransitionEnd = this._renderer.listen( + element, + 'transitionend', + this._transitionEndListener, + ); element.classList.add('mat-expansion-panel-animations-enabled'); }, 200); } diff --git a/src/material/form-field/directives/line-ripple.ts b/src/material/form-field/directives/line-ripple.ts index 7f5737944123..3fc842c264d3 100644 --- a/src/material/form-field/directives/line-ripple.ts +++ b/src/material/form-field/directives/line-ripple.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Directive, ElementRef, NgZone, OnDestroy, inject} from '@angular/core'; +import {Directive, ElementRef, NgZone, OnDestroy, Renderer2, inject} from '@angular/core'; /** Class added when the line ripple is active. */ const ACTIVATE_CLASS = 'mdc-line-ripple--active'; @@ -30,14 +30,20 @@ const DEACTIVATING_CLASS = 'mdc-line-ripple--deactivating'; }) export class MatFormFieldLineRipple implements OnDestroy { private _elementRef = inject>(ElementRef); + private _cleanupTransitionEnd: () => void; constructor(...args: unknown[]); constructor() { const ngZone = inject(NgZone); + const renderer = inject(Renderer2); ngZone.runOutsideAngular(() => { - this._elementRef.nativeElement.addEventListener('transitionend', this._handleTransitionEnd); + this._cleanupTransitionEnd = renderer.listen( + this._elementRef.nativeElement, + 'transitionend', + this._handleTransitionEnd, + ); }); } @@ -61,6 +67,6 @@ export class MatFormFieldLineRipple implements OnDestroy { }; ngOnDestroy() { - this._elementRef.nativeElement.removeEventListener('transitionend', this._handleTransitionEnd); + this._cleanupTransitionEnd(); } } diff --git a/src/material/input/input.ts b/src/material/input/input.ts index 3be8e80a1ebe..e0a768ebaffc 100644 --- a/src/material/input/input.ts +++ b/src/material/input/input.ts @@ -23,6 +23,7 @@ import { NgZone, OnChanges, OnDestroy, + Renderer2, WritableSignal, } from '@angular/core'; import {_IdGenerator} from '@angular/cdk/a11y'; @@ -101,6 +102,7 @@ export class MatInput private _autofillMonitor = inject(AutofillMonitor); private _ngZone = inject(NgZone); protected _formField? = inject(MAT_FORM_FIELD, {optional: true}); + private _renderer = inject(Renderer2); protected _uid = inject(_IdGenerator).getId('mat-input-'); protected _previousNativeValue: any; @@ -108,8 +110,9 @@ export class MatInput private _signalBasedValueAccessor?: {value: WritableSignal}; private _previousPlaceholder: string | null; private _errorStateTracker: _ErrorStateTracker; - private _webkitBlinkWheelListenerAttached = false; private _config = inject(MAT_INPUT_CONFIG, {optional: true}); + private _cleanupIosKeyup: (() => void) | undefined; + private _cleanupWebkitWheel: (() => void) | undefined; /** `aria-describedby` IDs assigned by the form field. */ private _formFieldDescribedBy: string[] | undefined; @@ -214,6 +217,7 @@ export class MatInput return this._type; } set type(value: string) { + const prevType = this._type; this._type = value || 'text'; this._validateType(); @@ -224,7 +228,9 @@ export class MatInput (this._elementRef.nativeElement as HTMLInputElement).type = this._type; } - this._ensureWheelDefaultBehavior(); + if (this._type !== prevType) { + this._ensureWheelDefaultBehavior(); + } } protected _type = 'text'; @@ -329,7 +335,7 @@ export class MatInput // exists on iOS, we only bother to install the listener on iOS. if (this._platform.IOS) { this._ngZone.runOutsideAngular(() => { - element.addEventListener('keyup', this._iOSKeyupListener); + this._cleanupIosKeyup = this._renderer.listen(element, 'keyup', this._iOSKeyupListener); }); } @@ -381,13 +387,8 @@ export class MatInput this._autofillMonitor.stopMonitoring(this._elementRef.nativeElement); } - if (this._platform.IOS) { - this._elementRef.nativeElement.removeEventListener('keyup', this._iOSKeyupListener); - } - - if (this._webkitBlinkWheelListenerAttached) { - this._elementRef.nativeElement.removeEventListener('wheel', this._webkitBlinkWheelListener); - } + this._cleanupIosKeyup?.(); + this._cleanupWebkitWheel?.(); } ngDoCheck() { @@ -626,27 +627,22 @@ export class MatInput /** * In blink and webkit browsers a focused number input does not increment or decrement its value - * on mouse wheel interaction unless a wheel event listener is attached to it or one of its ancestors or a passive wheel listener is attached somewhere in the DOM. - * For example: Hitting a tooltip once enables the mouse wheel input for all number inputs as long as it exists. - * In order to get reliable and intuitive behavior we apply a wheel event on our own - * thus making sure increment and decrement by mouse wheel works every time. + * on mouse wheel interaction unless a wheel event listener is attached to it or one of its + * ancestors or a passive wheel listener is attached somewhere in the DOM. For example: Hitting + * a tooltip once enables the mouse wheel input for all number inputs as long as it exists. In + * order to get reliable and intuitive behavior we apply a wheel event on our own thus making + * sure increment and decrement by mouse wheel works every time. * @docs-private */ private _ensureWheelDefaultBehavior(): void { - if ( - !this._webkitBlinkWheelListenerAttached && - this._type === 'number' && - (this._platform.BLINK || this._platform.WEBKIT) - ) { - this._ngZone.runOutsideAngular(() => { - this._elementRef.nativeElement.addEventListener('wheel', this._webkitBlinkWheelListener); - }); - this._webkitBlinkWheelListenerAttached = true; - } + this._cleanupWebkitWheel?.(); - if (this._webkitBlinkWheelListenerAttached && this._type !== 'number') { - this._elementRef.nativeElement.removeEventListener('wheel', this._webkitBlinkWheelListener); - this._webkitBlinkWheelListenerAttached = true; + if (this._type === 'number' && (this._platform.BLINK || this._platform.WEBKIT)) { + this._cleanupWebkitWheel = this._renderer.listen( + this._elementRef.nativeElement, + 'wheel', + this._webkitBlinkWheelListener, + ); } } diff --git a/src/material/list/selection-list.ts b/src/material/list/selection-list.ts index c75002c2e38c..1b21344e6ad2 100644 --- a/src/material/list/selection-list.ts +++ b/src/material/list/selection-list.ts @@ -25,6 +25,7 @@ import { OnDestroy, Output, QueryList, + Renderer2, SimpleChanges, ViewEncapsulation, forwardRef, @@ -78,9 +79,11 @@ export class MatSelectionList { _element = inject>(ElementRef); private _ngZone = inject(NgZone); + private _renderer = inject(Renderer2); private _initialized = false; private _keyManager: FocusKeyManager; + private _listenerCleanups: (() => void)[] | undefined; /** Emits when the list has been destroyed. */ private _destroyed = new Subject(); @@ -173,8 +176,10 @@ export class MatSelectionList // These events are bound outside the zone, because they don't change // any change-detected properties and they can trigger timeouts. this._ngZone.runOutsideAngular(() => { - this._element.nativeElement.addEventListener('focusin', this._handleFocusin); - this._element.nativeElement.addEventListener('focusout', this._handleFocusout); + this._listenerCleanups = [ + this._renderer.listen(this._element.nativeElement, 'focusin', this._handleFocusin), + this._renderer.listen(this._element.nativeElement, 'focusout', this._handleFocusout), + ]; }); if (this._value) { @@ -200,8 +205,7 @@ export class MatSelectionList ngOnDestroy() { this._keyManager?.destroy(); - this._element.nativeElement.removeEventListener('focusin', this._handleFocusin); - this._element.nativeElement.removeEventListener('focusout', this._handleFocusout); + this._listenerCleanups?.forEach(current => current()); this._destroyed.next(); this._destroyed.complete(); this._isDestroyed = true; diff --git a/src/material/progress-bar/progress-bar.ts b/src/material/progress-bar/progress-bar.ts index 498aeb537299..9de09605c4c1 100644 --- a/src/material/progress-bar/progress-bar.ts +++ b/src/material/progress-bar/progress-bar.ts @@ -22,6 +22,7 @@ import { inject, numberAttribute, ANIMATION_MODULE_TYPE, + Renderer2, } from '@angular/core'; import {DOCUMENT} from '@angular/common'; import {ThemePalette} from '@angular/material/core'; @@ -110,6 +111,8 @@ export class MatProgressBar implements AfterViewInit, OnDestroy { readonly _elementRef = inject>(ElementRef); private _ngZone = inject(NgZone); private _changeDetectorRef = inject(ChangeDetectorRef); + private _renderer = inject(Renderer2); + private _cleanupTransitionEnd: (() => void) | undefined; _animationMode? = inject(ANIMATION_MODULE_TYPE, {optional: true}); constructor(...args: unknown[]); @@ -203,12 +206,16 @@ export class MatProgressBar implements AfterViewInit, OnDestroy { // Run outside angular so change detection didn't get triggered on every transition end // instead only on the animation that we care about (primary value bar's transitionend) this._ngZone.runOutsideAngular(() => { - this._elementRef.nativeElement.addEventListener('transitionend', this._transitionendHandler); + this._cleanupTransitionEnd = this._renderer.listen( + this._elementRef.nativeElement, + 'transitionend', + this._transitionendHandler, + ); }); } ngOnDestroy() { - this._elementRef.nativeElement.removeEventListener('transitionend', this._transitionendHandler); + this._cleanupTransitionEnd?.(); } /** Gets the transform style that should be applied to the primary bar. */ diff --git a/src/material/radio/radio.ts b/src/material/radio/radio.ts index 97d26cb3dec5..fe8469e2e4e5 100644 --- a/src/material/radio/radio.ts +++ b/src/material/radio/radio.ts @@ -36,6 +36,7 @@ import { inject, numberAttribute, HostAttributeToken, + Renderer2, } from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import { @@ -419,7 +420,9 @@ export class MatRadioButton implements OnInit, AfterViewInit, DoCheck, OnDestroy }); private _ngZone = inject(NgZone); - private _uniqueId: string = inject(_IdGenerator).getId('mat-radio-'); + private _renderer = inject(Renderer2); + private _uniqueId = inject(_IdGenerator).getId('mat-radio-'); + private _cleanupClick: (() => void) | undefined; /** The unique ID for the radio button. */ @Input() id: string = this._uniqueId; @@ -673,13 +676,16 @@ export class MatRadioButton implements OnInit, AfterViewInit, DoCheck, OnDestroy // 1. Its logic is completely DOM-related so we can avoid some change detections. // 2. There appear to be some internal tests that break when this triggers a change detection. this._ngZone.runOutsideAngular(() => { - this._inputElement.nativeElement.addEventListener('click', this._onInputClick); + this._cleanupClick = this._renderer.listen( + this._inputElement.nativeElement, + 'click', + this._onInputClick, + ); }); } ngOnDestroy() { - // We need to null check in case the button was destroyed before `ngAfterViewInit`. - this._inputElement?.nativeElement.removeEventListener('click', this._onInputClick); + this._cleanupClick?.(); this._focusMonitor.stopMonitoring(this._elementRef); this._removeUniqueSelectionListener(); } diff --git a/src/material/sidenav/drawer.ts b/src/material/sidenav/drawer.ts index 361440a53834..0a4b58e137cd 100644 --- a/src/material/sidenav/drawer.ts +++ b/src/material/sidenav/drawer.ts @@ -42,6 +42,7 @@ import { OnDestroy, Output, QueryList, + Renderer2, ViewChild, ViewEncapsulation, } from '@angular/core'; @@ -176,6 +177,7 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy private _focusMonitor = inject(FocusMonitor); private _platform = inject(Platform); private _ngZone = inject(NgZone); + private _renderer = inject(Renderer2); private readonly _interactivityChecker = inject(InteractivityChecker); private _doc = inject(DOCUMENT, {optional: true})!; _container? = inject(MAT_DRAWER_CONTAINER, {optional: true}); @@ -401,13 +403,13 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy // The tabindex attribute should be removed to avoid navigating to that element again this._ngZone.runOutsideAngular(() => { const callback = () => { - element.removeEventListener('blur', callback); - element.removeEventListener('mousedown', callback); + cleanupBlur(); + cleanupMousedown(); element.removeAttribute('tabindex'); }; - element.addEventListener('blur', callback); - element.addEventListener('mousedown', callback); + const cleanupBlur = this._renderer.listen(element, 'blur', callback); + const cleanupMousedown = this._renderer.listen(element, 'mousedown', callback); }); } element.focus(options); diff --git a/src/material/slider/slider-input.ts b/src/material/slider/slider-input.ts index 23e76e9ef058..bbf72df06bea 100644 --- a/src/material/slider/slider-input.ts +++ b/src/material/slider/slider-input.ts @@ -19,6 +19,7 @@ import { numberAttribute, OnDestroy, Output, + Renderer2, signal, } from '@angular/core'; import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms'; @@ -87,6 +88,8 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA readonly _elementRef = inject>(ElementRef); readonly _cdr = inject(ChangeDetectorRef); protected _slider = inject<_MatSlider>(MAT_SLIDER); + private _platform = inject(Platform); + private _listenerCleanups: (() => void)[]; @Input({transform: numberAttribute}) get value(): number { @@ -275,22 +278,22 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA */ protected _isControlInitialized = false; - private _platform = inject(Platform); - constructor(...args: unknown[]); constructor() { + const renderer = inject(Renderer2); + this._ngZone.runOutsideAngular(() => { - this._hostElement.addEventListener('pointerdown', this._onPointerDown.bind(this)); - this._hostElement.addEventListener('pointermove', this._onPointerMove.bind(this)); - this._hostElement.addEventListener('pointerup', this._onPointerUp.bind(this)); + this._listenerCleanups = [ + renderer.listen(this._hostElement, 'pointerdown', this._onPointerDown.bind(this)), + renderer.listen(this._hostElement, 'pointermove', this._onPointerMove.bind(this)), + renderer.listen(this._hostElement, 'pointerup', this._onPointerUp.bind(this)), + ]; }); } ngOnDestroy(): void { - this._hostElement.removeEventListener('pointerdown', this._onPointerDown); - this._hostElement.removeEventListener('pointermove', this._onPointerMove); - this._hostElement.removeEventListener('pointerup', this._onPointerUp); + this._listenerCleanups.forEach(cleanup => cleanup()); this._destroyed.next(); this._destroyed.complete(); this.dragStart.complete(); diff --git a/src/material/slider/slider-thumb.ts b/src/material/slider/slider-thumb.ts index f5999aed1e8d..6d54c0274d95 100644 --- a/src/material/slider/slider-thumb.ts +++ b/src/material/slider/slider-thumb.ts @@ -15,6 +15,7 @@ import { Input, NgZone, OnDestroy, + Renderer2, ViewChild, ViewEncapsulation, inject, @@ -53,6 +54,8 @@ export class MatSliderVisualThumb implements _MatSliderVisualThumb, AfterViewIni readonly _cdr = inject(ChangeDetectorRef); private readonly _ngZone = inject(NgZone); private _slider = inject<_MatSlider>(MAT_SLIDER); + private _renderer = inject(Renderer2); + private _listenerCleanups: (() => void)[] | undefined; /** Whether the slider displays a numeric value label upon pressing the thumb. */ @Input() discrete: boolean; @@ -122,26 +125,20 @@ export class MatSliderVisualThumb implements _MatSliderVisualThumb, AfterViewIni // of the NgZone to prevent Angular from needlessly running change detection. this._ngZone.runOutsideAngular(() => { const input = this._sliderInputEl!; - input.addEventListener('pointermove', this._onPointerMove); - input.addEventListener('pointerdown', this._onDragStart); - input.addEventListener('pointerup', this._onDragEnd); - input.addEventListener('pointerleave', this._onMouseLeave); - input.addEventListener('focus', this._onFocus); - input.addEventListener('blur', this._onBlur); + const renderer = this._renderer; + this._listenerCleanups = [ + renderer.listen(input, 'pointermove', this._onPointerMove), + renderer.listen(input, 'pointerdown', this._onDragStart), + renderer.listen(input, 'pointerup', this._onDragEnd), + renderer.listen(input, 'pointerleave', this._onMouseLeave), + renderer.listen(input, 'focus', this._onFocus), + renderer.listen(input, 'blur', this._onBlur), + ]; }); } ngOnDestroy() { - const input = this._sliderInputEl; - - if (input) { - input.removeEventListener('pointermove', this._onPointerMove); - input.removeEventListener('pointerdown', this._onDragStart); - input.removeEventListener('pointerup', this._onDragEnd); - input.removeEventListener('pointerleave', this._onMouseLeave); - input.removeEventListener('focus', this._onFocus); - input.removeEventListener('blur', this._onBlur); - } + this._listenerCleanups?.forEach(cleanup => cleanup()); } private _onPointerMove = (event: PointerEvent): void => { diff --git a/src/material/timepicker/timepicker-input.ts b/src/material/timepicker/timepicker-input.ts index 435ec05ce715..7af16d3a29e6 100644 --- a/src/material/timepicker/timepicker-input.ts +++ b/src/material/timepicker/timepicker-input.ts @@ -20,6 +20,7 @@ import { ModelSignal, OnDestroy, OutputRefSubscription, + Renderer2, Signal, signal, } from '@angular/core'; @@ -89,6 +90,7 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O private _onChange: ((value: any) => void) | undefined; private _onTouched: (() => void) | undefined; private _validatorOnChange: (() => void) | undefined; + private _cleanupClick: () => void; private _accessorDisabled = signal(false); private _localeSubscription: Subscription; private _timepickerSubscription: OutputRefSubscription | undefined; @@ -158,6 +160,7 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O validateAdapter(this._dateAdapter, this._dateFormats); } + const renderer = inject(Renderer2); this._validator = this._getValidator(); this._respondToValueChanges(); this._respondToMinMaxChanges(); @@ -170,7 +173,11 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O // Bind the click listener manually to the overlay origin, because we want the entire // form field to be clickable, if the timepicker is used in `mat-form-field`. - this.getOverlayOrigin().nativeElement.addEventListener('click', this._handleClick); + this._cleanupClick = renderer.listen( + this.getOverlayOrigin().nativeElement, + 'click', + this._handleClick, + ); } /** @@ -236,7 +243,7 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O } ngOnDestroy(): void { - this.getOverlayOrigin().nativeElement.removeEventListener('click', this._handleClick); + this._cleanupClick(); this._timepickerSubscription?.unsubscribe(); this._localeSubscription.unsubscribe(); } diff --git a/tools/public_api_guard/cdk/drag-drop.md b/tools/public_api_guard/cdk/drag-drop.md index 0624dfeb708b..28e2c19ceba0 100644 --- a/tools/public_api_guard/cdk/drag-drop.md +++ b/tools/public_api_guard/cdk/drag-drop.md @@ -16,6 +16,7 @@ import { NumberInput } from '@angular/cdk/coercion'; import { Observable } from 'rxjs'; import { OnChanges } from '@angular/core'; import { OnDestroy } from '@angular/core'; +import { Renderer2 } from '@angular/core'; import { SimpleChanges } from '@angular/core'; import { Subject } from 'rxjs'; import { TemplateRef } from '@angular/core'; @@ -378,7 +379,7 @@ export class DragDropRegistry<_ = unknown, __ = unknown> implements OnDestroy { // @public export class DragRef { - constructor(element: ElementRef | HTMLElement, _config: DragRefConfig, _document: Document, _ngZone: NgZone, _viewportRuler: ViewportRuler, _dragDropRegistry: DragDropRegistry); + constructor(element: ElementRef | HTMLElement, _config: DragRefConfig, _document: Document, _ngZone: NgZone, _viewportRuler: ViewportRuler, _dragDropRegistry: DragDropRegistry, _renderer: Renderer2); readonly beforeStarted: Subject; constrainPosition?: (userPointerPosition: Point, dragRef: DragRef, dimensions: DOMRect, pickupPositionInElement: Point) => Point; data: T; diff --git a/tools/public_api_guard/cdk/overlay.md b/tools/public_api_guard/cdk/overlay.md index 8f1bc5ce0c76..38eb6af08fac 100644 --- a/tools/public_api_guard/cdk/overlay.md +++ b/tools/public_api_guard/cdk/overlay.md @@ -27,6 +27,7 @@ import { OnChanges } from '@angular/core'; import { OnDestroy } from '@angular/core'; import { Platform } from '@angular/cdk/platform'; import { PortalOutlet } from '@angular/cdk/portal'; +import { Renderer2 } from '@angular/core'; import { ScrollDispatcher } from '@angular/cdk/scrolling'; import { SimpleChanges } from '@angular/core'; import { Subject } from 'rxjs'; @@ -356,7 +357,7 @@ export class OverlayPositionBuilder { // @public export class OverlayRef implements PortalOutlet { - constructor(_portalOutlet: PortalOutlet, _host: HTMLElement, _pane: HTMLElement, _config: ImmutableObject, _ngZone: NgZone, _keyboardDispatcher: OverlayKeyboardDispatcher, _document: Document, _location: Location_2, _outsideClickDispatcher: OverlayOutsideClickDispatcher, _animationsDisabled: boolean | undefined, _injector: EnvironmentInjector); + constructor(_portalOutlet: PortalOutlet, _host: HTMLElement, _pane: HTMLElement, _config: ImmutableObject, _ngZone: NgZone, _keyboardDispatcher: OverlayKeyboardDispatcher, _document: Document, _location: Location_2, _outsideClickDispatcher: OverlayOutsideClickDispatcher, _animationsDisabled: boolean | undefined, _injector: EnvironmentInjector, _renderer: Renderer2); addPanelClass(classes: string | string[]): void; // (undocumented) attach(portal: ComponentPortal): ComponentRef;