@@ -26,6 +26,24 @@ import {
26
26
interface EventTaskData extends TaskData {
27
27
// use global callback or not
28
28
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
29
47
}
30
48
31
49
let passiveSupported = false ;
@@ -247,7 +265,7 @@ export function patchEventTarget(
247
265
248
266
// a shared global taskData to pass data for scheduleEventTask
249
267
// so we do not need to create a new object just for pass some data
250
- const taskData : any = { } ;
268
+ const taskData : InternalTaskData = { } ;
251
269
252
270
const nativeAddEventListener = ( proto [ zoneSymbolAddEventListener ] = proto [ ADD_EVENT_LISTENER ] ) ;
253
271
const nativeRemoveEventListener = ( proto [ zoneSymbol ( REMOVE_EVENT_LISTENER ) ] =
@@ -386,6 +404,30 @@ export function patchEventTarget(
386
404
const unpatchedEvents : string [ ] = ( Zone as any ) [ zoneSymbol ( 'UNPATCHED_EVENTS' ) ] ;
387
405
const passiveEvents : string [ ] = _global [ zoneSymbol ( 'PASSIVE_EVENTS' ) ] ;
388
406
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
+
389
431
const makeAddListener = function (
390
432
nativeListener : any ,
391
433
addSource : string ,
@@ -426,7 +468,7 @@ export function patchEventTarget(
426
468
427
469
const passive =
428
470
passiveSupported && ! ! passiveEvents && passiveEvents . indexOf ( eventName ) !== - 1 ;
429
- const options = buildEventListenerOptions ( arguments [ 2 ] , passive ) ;
471
+ const options = copyEventListenerOptions ( buildEventListenerOptions ( arguments [ 2 ] , passive ) ) ;
430
472
const signal : AbortSignal | undefined = options ?. signal ;
431
473
if ( signal ?. aborted ) {
432
474
// the signal is an aborted one, just return without attaching the event listener.
@@ -484,13 +526,18 @@ export function patchEventTarget(
484
526
addSource +
485
527
( eventNameToString ? eventNameToString ( eventName ) : eventName ) ;
486
528
}
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.
489
535
taskData . options = options ;
536
+
490
537
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.
494
541
taskData . options . once = false ;
495
542
}
496
543
taskData . target = target ;
@@ -502,15 +549,20 @@ export function patchEventTarget(
502
549
503
550
// keep taskData into data to allow onScheduleEventTask to access the task information
504
551
if ( data ) {
505
- ( data as any ) . taskData = taskData ;
552
+ data . taskData = taskData ;
506
553
}
507
554
508
555
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.
512
559
taskData . options . signal = undefined ;
513
560
}
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`.
514
566
const task : any = zone . scheduleEventTask (
515
567
source ,
516
568
delegate ,
@@ -532,7 +584,7 @@ export function patchEventTarget(
532
584
// `task` object even after it goes out of scope, preventing `task` from being garbage
533
585
// collected.
534
586
if ( data ) {
535
- ( data as any ) . removeAbortListener = ( ) => signal . removeEventListener ( 'abort' , onAbort ) ;
587
+ data . removeAbortListener = ( ) => signal . removeEventListener ( 'abort' , onAbort ) ;
536
588
}
537
589
}
538
590
@@ -542,13 +594,13 @@ export function patchEventTarget(
542
594
543
595
// need to clear up taskData because it is a global object
544
596
if ( data ) {
545
- ( data as any ) . taskData = null ;
597
+ data . taskData = null ;
546
598
}
547
599
548
600
// have to save those information to task in case
549
601
// application may call task.zone.cancelTask() directly
550
602
if ( once ) {
551
- options . once = true ;
603
+ taskData . options . once = true ;
552
604
}
553
605
if ( ! ( ! passiveSupported && typeof task . options === 'boolean' ) ) {
554
606
// if not support passive, and we pass an option object
@@ -644,7 +696,7 @@ export function patchEventTarget(
644
696
645
697
// Note that `removeAllListeners` would ultimately call `removeEventListener`,
646
698
// 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 ;
648
700
if ( taskData ?. removeAbortListener ) {
649
701
taskData . removeAbortListener ( ) ;
650
702
taskData . removeAbortListener = null ;
0 commit comments