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;