Which @angular/* package(s) are the source of the bug?
zone.js
Is this a regression?
Yes. Introduced in zone.js 0.14.7 (June 2024) by #55796 / 85c1719. Bisected: 0.13.3, 0.14.0, 0.14.4, 0.14.6 all behave correctly; 0.14.7 → 0.16.2 do not.
Description
copyEventListenerOptions in packages/zone.js/lib/common/events.ts copies the caller's options object with {...options} before forwarding it to native addEventListener. Object spread copies only own enumerable data properties. The native call, by contrast, reads dictionary members via WebIDL — a plain [[Get]] per member, which honours accessors and ignores enumerability. The copy is therefore lossy in a way the native call is not.
signal was already special-cased after it broke WalletConnect (see #54142). passive, capture, and once were not, and are silently dropped in the same circumstances.
Observed in Chromium against zone.js 0.15.1:
| option supplied as an accessor |
without zone.js |
with zone.js |
capture: true |
capture phase (eventPhase === 1) |
bubbling phase (3) |
once: true |
fires once |
fires every time |
passive getter (feature-detection probe) |
getter invoked |
never invoked |
| same options as a plain object literal |
honoured |
honoured |
Nothing throws in any case.
The passive row is the one with ecosystem-wide fallout: it defeats the MDN feature test for passive-listener support, where the getter being read is the signal. That snippet is used verbatim by Highcharts (See: https://github.com/highcharts/highcharts/blob/69edf8952a04b9872b6510fbdacd7dff8b03970b/ts/Core/Globals.ts#L201). Making every listener they register non-passive. Including ones they explicitly intended to be passive.
Please provide a link to a minimal reproduction of the bug
Standalone HTML, no framework or build:
<script src="https://unpkg.com/zone.js@0.15.1/bundles/zone.umd.js"></script>
<script>
let supported = false;
window.addEventListener('probe', function () {},
Object.defineProperty({}, 'passive', { get: () => { supported = true; return false; } }));
console.log('passive detected:', supported);
const inner = document.createElement('button');
document.body.append(inner);
document.body.addEventListener('click', e => console.log('phase:', e.eventPhase),
Object.defineProperty({}, 'capture', { get: () => true }));
inner.click();
</script>
Please provide the exception or error you saw
None — silent loss of options.
Please provide the environment you discovered this bug in (run `ng version`)
Reproduced against published zone.js 0.13.3 → 0.16.2 in headless Chromium.
Anything else?
The copy itself was the correct fix for #54142 (frozen/readonly options). The defect is that the copy is lossy, not that it exists — reverting to mutation would re-introduce #54142.
A fix is ready and will be sent as a PR that references this issue.
Which @angular/* package(s) are the source of the bug?
zone.js
Is this a regression?
Yes. Introduced in zone.js 0.14.7 (June 2024) by #55796 /
85c1719. Bisected: 0.13.3, 0.14.0, 0.14.4, 0.14.6 all behave correctly; 0.14.7 → 0.16.2 do not.Description
copyEventListenerOptionsinpackages/zone.js/lib/common/events.tscopies the caller's options object with{...options}before forwarding it to nativeaddEventListener. Object spread copies only own enumerable data properties. The native call, by contrast, reads dictionary members via WebIDL — a plain[[Get]]per member, which honours accessors and ignores enumerability. The copy is therefore lossy in a way the native call is not.signalwas already special-cased after it broke WalletConnect (see #54142).passive,capture, andoncewere not, and are silently dropped in the same circumstances.Observed in Chromium against zone.js 0.15.1:
capture: trueeventPhase === 1)once: truepassivegetter (feature-detection probe)Nothing throws in any case.
The
passiverow is the one with ecosystem-wide fallout: it defeats the MDN feature test for passive-listener support, where the getter being read is the signal. That snippet is used verbatim by Highcharts (See: https://github.com/highcharts/highcharts/blob/69edf8952a04b9872b6510fbdacd7dff8b03970b/ts/Core/Globals.ts#L201). Making every listener they register non-passive. Including ones they explicitly intended to be passive.Please provide a link to a minimal reproduction of the bug
Standalone HTML, no framework or build:
Please provide the exception or error you saw
None — silent loss of options.
Please provide the environment you discovered this bug in (run `ng version`)
Reproduced against published zone.js 0.13.3 → 0.16.2 in headless Chromium.
Anything else?
The copy itself was the correct fix for #54142 (frozen/readonly options). The defect is that the copy is lossy, not that it exists — reverting to mutation would re-introduce #54142.
A fix is ready and will be sent as a PR that references this issue.