From c0987c09f816b5d7dcb5564d19c849f7e28b9e7a Mon Sep 17 00:00:00 2001 From: subotac <73706465+subotac@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:50:35 +0300 Subject: [PATCH] fix: remove stale capture listeners from spread attributes (#18618) Fixes #18608. When `set_attributes` normalized removed spread attributes, it also copied its internal `$$` listener bookkeeping keys into `next` with a `null` value. This cleared the stored callback before event cleanup, so removing a capture handler passed `null` to `removeEventListener` and left the listener attached. Skip internal `$$` keys during missing-attribute normalization. The updated runtime-runes sample verifies replacing and then removing a spread capture handler in both client and hydration modes. --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/happy-spoons-talk.md | 5 +++++ .../client/dom/elements/attributes.js | 3 ++- .../event-attribute-spread-update/_config.js | 21 ++++++++++++++++--- .../event-attribute-spread-update/main.svelte | 3 +++ 4 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 .changeset/happy-spoons-talk.md diff --git a/.changeset/happy-spoons-talk.md b/.changeset/happy-spoons-talk.md new file mode 100644 index 0000000000..cf165320db --- /dev/null +++ b/.changeset/happy-spoons-talk.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: clean up removed capture event handlers from spread attributes diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 589685b5f2..8a25be7acd 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -315,7 +315,8 @@ function set_attributes( var is_option_element = element.nodeName === OPTION_TAG; for (var key in prev) { - if (!(key in next)) { + // don't null our internal $$onX listeners + if (!(key in next) && key[0] + key[1] !== '$$') { next[key] = null; } } diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js index af03eed4c9..5fbd37c768 100644 --- a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js @@ -3,16 +3,31 @@ import { test } from '../../test'; export default test({ test({ assert, target }) { - const [change, increment] = target.querySelectorAll('button'); + const [change, remove, increment] = target.querySelectorAll('button'); increment.click(); flushSync(); - assert.htmlEqual(target.innerHTML, ''); + assert.htmlEqual( + target.innerHTML, + '' + ); change.click(); flushSync(); increment.click(); flushSync(); - assert.htmlEqual(target.innerHTML, ''); + assert.htmlEqual( + target.innerHTML, + '' + ); + + remove.click(); + flushSync(); + increment.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + '' + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte index 32d4b242cc..ee049e469c 100644 --- a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte @@ -24,4 +24,7 @@ > change handlers +