diff --git a/goldens/public-api/core/index.api.md b/goldens/public-api/core/index.api.md index 55444de6b41..a8c38f6f029 100644 --- a/goldens/public-api/core/index.api.md +++ b/goldens/public-api/core/index.api.md @@ -1139,6 +1139,16 @@ export function linkedSignal(options: { equal?: ValueEqualityFn>; }): WritableSignal; +// @public +export interface ListenerOptions { + // (undocumented) + capture?: boolean; + // (undocumented) + once?: boolean; + // (undocumented) + passive?: boolean; +} + // @public export const LOCALE_ID: InjectionToken; @@ -1524,7 +1534,7 @@ export abstract class Renderer2 { abstract destroy(): void; destroyNode: ((node: any) => void) | null; abstract insertBefore(parent: any, newChild: any, refChild: any, isMove?: boolean): void; - abstract listen(target: 'window' | 'document' | 'body' | any, eventName: string, callback: (event: any) => boolean | void): () => void; + abstract listen(target: 'window' | 'document' | 'body' | any, eventName: string, callback: (event: any) => boolean | void, options?: ListenerOptions): () => void; abstract nextSibling(node: any): any; abstract parentNode(node: any): any; abstract removeAttribute(el: any, name: string, namespace?: string | null): void; diff --git a/goldens/public-api/platform-browser/index.api.md b/goldens/public-api/platform-browser/index.api.md index 51fb029be84..2b6b6a09006 100644 --- a/goldens/public-api/platform-browser/index.api.md +++ b/goldens/public-api/platform-browser/index.api.md @@ -14,6 +14,7 @@ import { HttpTransferCacheOptions } from '@angular/common/http'; import * as i0 from '@angular/core'; import * as i1 from '@angular/common'; import { InjectionToken } from '@angular/core'; +import { ListenerOptions } from '@angular/core'; import { NgZone } from '@angular/core'; import { PlatformRef } from '@angular/core'; import { Predicate } from '@angular/core'; @@ -77,7 +78,7 @@ export const EVENT_MANAGER_PLUGINS: InjectionToken; // @public export class EventManager { constructor(plugins: EventManagerPlugin[], _zone: NgZone); - addEventListener(element: HTMLElement, eventName: string, handler: Function): Function; + addEventListener(element: HTMLElement, eventName: string, handler: Function, options?: ListenerOptions): Function; getZone(): NgZone; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; @@ -88,7 +89,7 @@ export class EventManager { // @public export abstract class EventManagerPlugin { constructor(_doc: any); - abstract addEventListener(element: HTMLElement, eventName: string, handler: Function): Function; + abstract addEventListener(element: HTMLElement, eventName: string, handler: Function, options?: ListenerOptions): Function; // (undocumented) manager: EventManager; abstract supports(eventName: string): boolean; diff --git a/packages/animations/browser/src/render/renderer.ts b/packages/animations/browser/src/render/renderer.ts index 81b7afcb5b5..59aade0bbb8 100644 --- a/packages/animations/browser/src/render/renderer.ts +++ b/packages/animations/browser/src/render/renderer.ts @@ -14,6 +14,7 @@ import { RendererFactory2, RendererStyleFlags2, ɵAnimationRendererType as AnimationRendererType, + type ListenerOptions, } from '@angular/core'; import type {AnimationEngine} from './animation_engine_next'; @@ -135,8 +136,13 @@ export class BaseAnimationRenderer implements Renderer2 { this.delegate.setValue(node, value); } - listen(target: any, eventName: string, callback: (event: any) => boolean | void): () => void { - return this.delegate.listen(target, eventName, callback); + listen( + target: any, + eventName: string, + callback: (event: any) => boolean | void, + options?: ListenerOptions, + ): () => void { + return this.delegate.listen(target, eventName, callback, options); } protected disableAnimations(element: any, value: boolean) { @@ -173,6 +179,7 @@ export class AnimationRenderer extends BaseAnimationRenderer implements Renderer target: 'window' | 'document' | 'body' | any, eventName: string, callback: (event: any) => any, + options?: ListenerOptions, ): () => void { if (eventName.charAt(0) == ANIMATION_PREFIX) { const element = resolveElementFromTarget(target); @@ -188,7 +195,7 @@ export class AnimationRenderer extends BaseAnimationRenderer implements Renderer this.factory.scheduleListenerCallback(countId, callback, event); }); } - return this.delegate.listen(target, eventName, callback); + return this.delegate.listen(target, eventName, callback, options); } } diff --git a/packages/common/src/dom_adapter.ts b/packages/common/src/dom_adapter.ts index d8d9ad285ec..8df0c109afa 100644 --- a/packages/common/src/dom_adapter.ts +++ b/packages/common/src/dom_adapter.ts @@ -41,7 +41,7 @@ export abstract class DomAdapter { abstract isShadowRoot(node: any): boolean; // Used by KeyEventsPlugin - abstract onAndCancel(el: any, evt: any, listener: any): Function; + abstract onAndCancel(el: any, evt: any, listener: any, options?: any): Function; // Used by PlatformLocation and ServerEventManagerPlugin abstract getGlobalEventTarget(doc: Document, target: string): any; diff --git a/packages/core/src/render.ts b/packages/core/src/render.ts index 8d9b50a9fa0..1e52f89feac 100644 --- a/packages/core/src/render.ts +++ b/packages/core/src/render.ts @@ -7,5 +7,5 @@ */ // Public API for render -export {Renderer2, RendererFactory2} from './render/api'; +export {Renderer2, RendererFactory2, ListenerOptions} from './render/api'; export {RendererStyleFlags2, RendererType2} from './render/api_flags'; diff --git a/packages/core/src/render/api.ts b/packages/core/src/render/api.ts index 578774b7250..7c5a7ed27f1 100644 --- a/packages/core/src/render/api.ts +++ b/packages/core/src/render/api.ts @@ -217,12 +217,14 @@ export abstract class Renderer2 { * DOM element. * @param eventName The event to listen for. * @param callback A handler function to invoke when the event occurs. + * @param options Options that configure how the event listener is bound. * @returns An "unlisten" function for disposing of this handler. */ abstract listen( target: 'window' | 'document' | 'body' | any, eventName: string, callback: (event: any) => boolean | void, + options?: ListenerOptions, ): () => void; /** @@ -252,3 +254,13 @@ export const enum AnimationRendererType { Regular = 0, Delegated = 1, } + +/** + * Options that can be used to configure an event listener. + * @publicApi + */ +export interface ListenerOptions { + capture?: boolean; + once?: boolean; + passive?: boolean; +} diff --git a/packages/core/src/render3/interfaces/renderer.ts b/packages/core/src/render3/interfaces/renderer.ts index 5273993f0f8..a716de5f269 100644 --- a/packages/core/src/render3/interfaces/renderer.ts +++ b/packages/core/src/render3/interfaces/renderer.ts @@ -7,6 +7,7 @@ */ import {RendererStyleFlags2, RendererType2} from '../../render/api_flags'; +import type {ListenerOptions} from '../../render/api'; import {TrustedHTML, TrustedScript, TrustedScriptURL} from '../../util/security/trusted_type_defs'; import {RComment, RElement, RNode, RText} from './renderer_dom'; @@ -68,6 +69,7 @@ export interface Renderer { target: GlobalTargetName | RNode, eventName: string, callback: (event: any) => boolean | void, + options?: ListenerOptions, ): () => void; } diff --git a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json index f64ab6e4d4f..2d3a75f60a2 100644 --- a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json @@ -164,6 +164,9 @@ { "name": "DomAdapter" }, + { + "name": "DomEventsPlugin" + }, { "name": "DomRendererFactory2" }, @@ -263,6 +266,9 @@ { "name": "InputFlags" }, + { + "name": "KeyEventsPlugin" + }, { "name": "LContainerFlags" }, diff --git a/packages/core/test/bundling/hydration/bundle.golden_symbols.json b/packages/core/test/bundling/hydration/bundle.golden_symbols.json index a87abb1d3dd..dfab160290c 100644 --- a/packages/core/test/bundling/hydration/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hydration/bundle.golden_symbols.json @@ -104,6 +104,9 @@ { "name": "DomAdapter" }, + { + "name": "DomEventsPlugin" + }, { "name": "DomRendererFactory2" }, @@ -212,6 +215,9 @@ { "name": "InputFlags" }, + { + "name": "KeyEventsPlugin" + }, { "name": "LContainerFlags" }, diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index 726dd0c3db0..b19bd31db6a 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -176,6 +176,9 @@ { "name": "DomAdapter" }, + { + "name": "DomEventsPlugin" + }, { "name": "DomRendererFactory2" }, @@ -290,6 +293,9 @@ { "name": "ItemComponent" }, + { + "name": "KeyEventsPlugin" + }, { "name": "LContainerFlags" }, diff --git a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json index d454dd92ce7..cc2717c2419 100644 --- a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json +++ b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json @@ -95,6 +95,9 @@ { "name": "DomAdapter" }, + { + "name": "DomEventsPlugin" + }, { "name": "DomRendererFactory2" }, @@ -176,6 +179,9 @@ { "name": "InputFlags" }, + { + "name": "KeyEventsPlugin" + }, { "name": "LContainerFlags" }, diff --git a/packages/core/test/render3/imported_renderer2.ts b/packages/core/test/render3/imported_renderer2.ts index 22bbd24bcca..dc4f9ac5dd1 100644 --- a/packages/core/test/render3/imported_renderer2.ts +++ b/packages/core/test/render3/imported_renderer2.ts @@ -7,7 +7,7 @@ */ import {PLATFORM_BROWSER_ID, PLATFORM_SERVER_ID} from '@angular/common/src/platform_id'; -import {NgZone, RendererFactory2, RendererType2} from '@angular/core'; +import {type ListenerOptions, NgZone, RendererFactory2, RendererType2} from '@angular/core'; import {NoopNgZone} from '@angular/core/src/zone/ng_zone'; import {EventManager, ɵDomRendererFactory2, ɵSharedStylesHost} from '@angular/platform-browser'; import {EventManagerPlugin} from '@angular/platform-browser/src/dom/events/event_manager'; @@ -21,13 +21,23 @@ export class SimpleDomEventsPlugin extends EventManagerPlugin { return true; } - override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { + override addEventListener( + element: HTMLElement, + eventName: string, + handler: Function, + options?: ListenerOptions, + ): Function { let callback: EventListener = handler as EventListener; - element.addEventListener(eventName, callback, false); - return () => this.removeEventListener(element, eventName, callback); + element.addEventListener(eventName, callback, options); + return () => this.removeEventListener(element, eventName, callback, options); } - removeEventListener(target: any, eventName: string, callback: Function): void { + removeEventListener( + target: any, + eventName: string, + callback: Function, + options?: ListenerOptions, + ): void { return target.removeEventListener.apply(target, [eventName, callback, false]); } } diff --git a/packages/platform-browser/animations/async/src/async_animation_renderer.ts b/packages/platform-browser/animations/async/src/async_animation_renderer.ts index e9693eacece..d65b7a5b715 100644 --- a/packages/platform-browser/animations/async/src/async_animation_renderer.ts +++ b/packages/platform-browser/animations/async/src/async_animation_renderer.ts @@ -24,8 +24,8 @@ import { ɵChangeDetectionScheduler as ChangeDetectionScheduler, ɵNotificationSource as NotificationSource, ɵRuntimeError as RuntimeError, - Injector, InjectionToken, + type ListenerOptions, } from '@angular/core'; import {ɵRuntimeErrorCode as RuntimeErrorCode} from '@angular/platform-browser'; @@ -278,13 +278,20 @@ export class DynamicDelegationRenderer implements Renderer2 { this.delegate.setValue(node, value); } - listen(target: any, eventName: string, callback: (event: any) => boolean | void): () => void { + listen( + target: any, + eventName: string, + callback: (event: any) => boolean | void, + options?: ListenerOptions, + ): () => void { // We need to keep track of animation events registred by the default renderer // So we can also register them against the animation renderer if (this.shouldReplay(eventName)) { - this.replay!.push((renderer: Renderer2) => renderer.listen(target, eventName, callback)); + this.replay!.push((renderer: Renderer2) => + renderer.listen(target, eventName, callback, options), + ); } - return this.delegate.listen(target, eventName, callback); + return this.delegate.listen(target, eventName, callback, options); } private shouldReplay(propOrEventName: string): boolean { diff --git a/packages/platform-browser/src/browser/browser_adapter.ts b/packages/platform-browser/src/browser/browser_adapter.ts index a7327c04fbb..93f376f8492 100644 --- a/packages/platform-browser/src/browser/browser_adapter.ts +++ b/packages/platform-browser/src/browser/browser_adapter.ts @@ -19,16 +19,15 @@ import {GenericBrowserDomAdapter} from './generic_browser_adapter'; * @security Tread carefully! Interacting with the DOM directly is dangerous and * can introduce XSS risks. */ -/* tslint:disable:requireParameterType no-console */ export class BrowserDomAdapter extends GenericBrowserDomAdapter { static makeCurrent() { setRootDomAdapter(new BrowserDomAdapter()); } - override onAndCancel(el: Node, evt: any, listener: any): Function { - el.addEventListener(evt, listener); + override onAndCancel(el: Node, evt: any, listener: any, options: any): Function { + el.addEventListener(evt, listener, options); return () => { - el.removeEventListener(evt, listener); + el.removeEventListener(evt, listener, options); }; } override dispatchEvent(el: Node, evt: any) { diff --git a/packages/platform-browser/src/dom/dom_renderer.ts b/packages/platform-browser/src/dom/dom_renderer.ts index 63891746c3f..7a696e1c3a0 100644 --- a/packages/platform-browser/src/dom/dom_renderer.ts +++ b/packages/platform-browser/src/dom/dom_renderer.ts @@ -22,6 +22,7 @@ import { RendererType2, ViewEncapsulation, ɵRuntimeError as RuntimeError, + type ListenerOptions, } from '@angular/core'; import {RuntimeErrorCode} from '../errors'; @@ -342,6 +343,7 @@ class DefaultDomRenderer2 implements Renderer2 { target: 'window' | 'document' | 'body' | any, event: string, callback: (event: any) => boolean, + options?: ListenerOptions, ): () => void { (typeof ngDevMode === 'undefined' || ngDevMode) && this.throwOnSyntheticProps && @@ -357,6 +359,7 @@ class DefaultDomRenderer2 implements Renderer2 { target, event, this.decoratePreventDefault(callback), + options, ) as VoidFunction; } diff --git a/packages/platform-browser/src/dom/events/dom_events.ts b/packages/platform-browser/src/dom/events/dom_events.ts index 17ff6131905..d9f72d08ce0 100644 --- a/packages/platform-browser/src/dom/events/dom_events.ts +++ b/packages/platform-browser/src/dom/events/dom_events.ts @@ -7,7 +7,7 @@ */ import {DOCUMENT} from '@angular/common'; -import {Inject, Injectable} from '@angular/core'; +import {Inject, Injectable, type ListenerOptions} from '@angular/core'; import {EventManagerPlugin} from './event_manager'; @@ -23,12 +23,22 @@ export class DomEventsPlugin extends EventManagerPlugin { return true; } - override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { - element.addEventListener(eventName, handler as EventListener, false); - return () => this.removeEventListener(element, eventName, handler as EventListener); + override addEventListener( + element: HTMLElement, + eventName: string, + handler: Function, + options?: ListenerOptions, + ): Function { + element.addEventListener(eventName, handler as EventListener, options); + return () => this.removeEventListener(element, eventName, handler as EventListener, options); } - removeEventListener(target: any, eventName: string, callback: Function): void { - return target.removeEventListener(eventName, callback as EventListener); + removeEventListener( + target: any, + eventName: string, + callback: Function, + options?: ListenerOptions, + ): void { + return target.removeEventListener(eventName, callback as EventListener, options); } } diff --git a/packages/platform-browser/src/dom/events/event_manager.ts b/packages/platform-browser/src/dom/events/event_manager.ts index 2e540ffd9e1..e76d04745da 100644 --- a/packages/platform-browser/src/dom/events/event_manager.ts +++ b/packages/platform-browser/src/dom/events/event_manager.ts @@ -12,6 +12,7 @@ import { InjectionToken, NgZone, ɵRuntimeError as RuntimeError, + type ListenerOptions, } from '@angular/core'; import {RuntimeErrorCode} from '../../errors'; @@ -56,11 +57,17 @@ export class EventManager { * @param eventName The name of the event to listen for. * @param handler A function to call when the notification occurs. Receives the * event object as an argument. + * @param options Options that configure how the event listener is bound. * @returns A callback function that can be used to remove the handler. */ - addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { + addEventListener( + element: HTMLElement, + eventName: string, + handler: Function, + options?: ListenerOptions, + ): Function { const plugin = this._findPluginFor(eventName); - return plugin.addEventListener(element, eventName, handler); + return plugin.addEventListener(element, eventName, handler, options); } /** @@ -115,5 +122,10 @@ export abstract class EventManagerPlugin { /** * Implement the behaviour for the supported events */ - abstract addEventListener(element: HTMLElement, eventName: string, handler: Function): Function; + abstract addEventListener( + element: HTMLElement, + eventName: string, + handler: Function, + options?: ListenerOptions, + ): Function; } diff --git a/packages/platform-browser/src/dom/events/key_events.ts b/packages/platform-browser/src/dom/events/key_events.ts index a74bb040c04..fe3822f0c21 100644 --- a/packages/platform-browser/src/dom/events/key_events.ts +++ b/packages/platform-browser/src/dom/events/key_events.ts @@ -7,7 +7,7 @@ */ import {DOCUMENT, ɵgetDOM as getDOM} from '@angular/common'; -import {Inject, Injectable, NgZone} from '@angular/core'; +import {Inject, Injectable, type ListenerOptions, NgZone} from '@angular/core'; import {EventManagerPlugin} from './event_manager'; @@ -74,7 +74,12 @@ export class KeyEventsPlugin extends EventManagerPlugin { * event object as an argument. * @returns The key event that was registered. */ - override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { + override addEventListener( + element: HTMLElement, + eventName: string, + handler: Function, + options?: ListenerOptions, + ): Function { const parsedEvent = KeyEventsPlugin.parseEventName(eventName)!; const outsideHandler = KeyEventsPlugin.eventCallback( @@ -84,7 +89,7 @@ export class KeyEventsPlugin extends EventManagerPlugin { ); return this.manager.getZone().runOutsideAngular(() => { - return getDOM().onAndCancel(element, parsedEvent['domEventName'], outsideHandler); + return getDOM().onAndCancel(element, parsedEvent['domEventName'], outsideHandler, options); }); } diff --git a/packages/platform-server/src/server_events.ts b/packages/platform-server/src/server_events.ts index 95ed9a8a1f6..500672aeb77 100644 --- a/packages/platform-server/src/server_events.ts +++ b/packages/platform-server/src/server_events.ts @@ -7,7 +7,7 @@ */ import {DOCUMENT, ɵgetDOM as getDOM} from '@angular/common'; -import {Inject, Injectable} from '@angular/core'; +import {Inject, Injectable, type ListenerOptions} from '@angular/core'; import {EventManagerPlugin} from '@angular/platform-browser'; @Injectable() @@ -21,7 +21,12 @@ export class ServerEventManagerPlugin extends EventManagerPlugin { return true; } - override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { - return getDOM().onAndCancel(element, eventName, handler); + override addEventListener( + element: HTMLElement, + eventName: string, + handler: Function, + options?: ListenerOptions, + ): Function { + return getDOM().onAndCancel(element, eventName, handler, options); } }