@@ -26,6 +26,24 @@ import {
2626interface EventTaskData extends TaskData {
2727 // use global callback or not
2828 readonly useG ?: boolean ;
29+ taskData ?: any ;
30+ removeAbortListener ?: VoidFunction | null ;
31+ }
32+
33+ /** @internal **/
34+ interface InternalTaskData {
35+ // This is used internally to avoid duplicating event listeners on
36+ // the same target when the event name is the same, such as when
37+ // `addEventListener` is called multiple times on the `document`
38+ // for the `keydown` event.
39+ isExisting ?: boolean ;
40+ // `target` is the actual event target on which `addEventListener`
41+ // is being called.
42+ target ?: any ;
43+ eventName ?: string ;
44+ capture ?: boolean ;
45+ // Not changing the type to avoid any regressions.
46+ options ?: any ; // boolean | AddEventListenerOptions
2947}
3048
3149let passiveSupported = false ;
@@ -247,7 +265,7 @@ export function patchEventTarget(
247265
248266 // a shared global taskData to pass data for scheduleEventTask
249267 // so we do not need to create a new object just for pass some data
250- const taskData : any = { } ;
268+ const taskData : InternalTaskData = { } ;
251269
252270 const nativeAddEventListener = ( proto [ zoneSymbolAddEventListener ] = proto [ ADD_EVENT_LISTENER ] ) ;
253271 const nativeRemoveEventListener = ( proto [ zoneSymbol ( REMOVE_EVENT_LISTENER ) ] =
@@ -386,6 +404,30 @@ export function patchEventTarget(
386404 const unpatchedEvents : string [ ] = ( Zone as any ) [ zoneSymbol ( 'UNPATCHED_EVENTS' ) ] ;
387405 const passiveEvents : string [ ] = _global [ zoneSymbol ( 'PASSIVE_EVENTS' ) ] ;
388406
407+ function copyEventListenerOptions ( options : any ) {
408+ if ( typeof options === 'object' && options !== null ) {
409+ // We need to destructure the target `options` object since it may
410+ // be frozen or sealed (possibly provided implicitly by a third-party
411+ // library), or its properties may be readonly.
412+ const newOptions : any = { ...options } ;
413+ // The `signal` option was recently introduced, which caused regressions in
414+ // third-party scenarios where `AbortController` was directly provided to
415+ // `addEventListener` as options. For instance, in cases like
416+ // `document.addEventListener('keydown', callback, abortControllerInstance)`,
417+ // which is valid because `AbortController` includes a `signal` getter, spreading
418+ // `{...options}` wouldn't copy the `signal`. Additionally, using `Object.create`
419+ // isn't feasible since `AbortController` is a built-in object type, and attempting
420+ // to create a new object directly with it as the prototype might result in
421+ // unexpected behavior.
422+ if ( options . signal ) {
423+ newOptions . signal = options . signal ;
424+ }
425+ return newOptions ;
426+ }
427+
428+ return options ;
429+ }
430+
389431 const makeAddListener = function (
390432 nativeListener : any ,
391433 addSource : string ,
@@ -426,7 +468,7 @@ export function patchEventTarget(
426468
427469 const passive =
428470 passiveSupported && ! ! passiveEvents && passiveEvents . indexOf ( eventName ) !== - 1 ;
429- const options = buildEventListenerOptions ( arguments [ 2 ] , passive ) ;
471+ const options = copyEventListenerOptions ( buildEventListenerOptions ( arguments [ 2 ] , passive ) ) ;
430472 const signal : AbortSignal | undefined = options ?. signal ;
431473 if ( signal ?. aborted ) {
432474 // the signal is an aborted one, just return without attaching the event listener.
@@ -484,13 +526,18 @@ export function patchEventTarget(
484526 addSource +
485527 ( eventNameToString ? eventNameToString ( eventName ) : eventName ) ;
486528 }
487- // do not create a new object as task.data to pass those things
488- // just use the global shared one
529+
530+ // In the code below, `options` should no longer be reassigned; instead, it
531+ // should only be mutated. This is because we pass that object to the native
532+ // `addEventListener`.
533+ // It's generally recommended to use the same object reference for options.
534+ // This ensures consistency and avoids potential issues.
489535 taskData . options = options ;
536+
490537 if ( once ) {
491- // if addEventListener with once options , we don't pass it to
492- // native addEventListener, instead we keep the once setting
493- // and handle ourselves.
538+ // When using ` addEventListener` with the ` once` option , we don't pass
539+ // the `once` option directly to the native `addEventListener` method.
540+ // Instead, we keep the `once` setting and handle it ourselves.
494541 taskData . options . once = false ;
495542 }
496543 taskData . target = target ;
@@ -502,15 +549,20 @@ export function patchEventTarget(
502549
503550 // keep taskData into data to allow onScheduleEventTask to access the task information
504551 if ( data ) {
505- ( data as any ) . taskData = taskData ;
552+ data . taskData = taskData ;
506553 }
507554
508555 if ( signal ) {
509- // if addEventListener with signal options , we don't pass it to
510- // native addEventListener, instead we keep the signal setting
511- // and handle ourselves.
556+ // When using ` addEventListener` with the ` signal` option , we don't pass
557+ // the `signal` option directly to the native `addEventListener` method.
558+ // Instead, we keep the `signal` setting and handle it ourselves.
512559 taskData . options . signal = undefined ;
513560 }
561+
562+ // The `scheduleEventTask` function will ultimately call `customScheduleGlobal`,
563+ // which in turn calls the native `addEventListener`. This is why `taskData.options`
564+ // is updated before scheduling the task, as `customScheduleGlobal` uses
565+ // `taskData.options` to pass it to the native `addEventListener`.
514566 const task : any = zone . scheduleEventTask (
515567 source ,
516568 delegate ,
@@ -532,7 +584,7 @@ export function patchEventTarget(
532584 // `task` object even after it goes out of scope, preventing `task` from being garbage
533585 // collected.
534586 if ( data ) {
535- ( data as any ) . removeAbortListener = ( ) => signal . removeEventListener ( 'abort' , onAbort ) ;
587+ data . removeAbortListener = ( ) => signal . removeEventListener ( 'abort' , onAbort ) ;
536588 }
537589 }
538590
@@ -542,13 +594,13 @@ export function patchEventTarget(
542594
543595 // need to clear up taskData because it is a global object
544596 if ( data ) {
545- ( data as any ) . taskData = null ;
597+ data . taskData = null ;
546598 }
547599
548600 // have to save those information to task in case
549601 // application may call task.zone.cancelTask() directly
550602 if ( once ) {
551- options . once = true ;
603+ taskData . options . once = true ;
552604 }
553605 if ( ! ( ! passiveSupported && typeof task . options === 'boolean' ) ) {
554606 // if not support passive, and we pass an option object
@@ -644,7 +696,7 @@ export function patchEventTarget(
644696
645697 // Note that `removeAllListeners` would ultimately call `removeEventListener`,
646698 // so we're safe to remove the abort listener only once here.
647- const taskData = existingTask . data as any ;
699+ const taskData = existingTask . data as EventTaskData ;
648700 if ( taskData ?. removeAbortListener ) {
649701 taskData . removeAbortListener ( ) ;
650702 taskData . removeAbortListener = null ;
0 commit comments