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>
pull/18606/merge
subotac 1 week ago committed by GitHub
parent a224dd5867
commit c0987c09f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: clean up removed capture event handlers from spread attributes

@ -315,7 +315,8 @@ function set_attributes(
var is_option_element = element.nodeName === OPTION_TAG; var is_option_element = element.nodeName === OPTION_TAG;
for (var key in prev) { 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; next[key] = null;
} }
} }

@ -3,16 +3,31 @@ import { test } from '../../test';
export default test({ export default test({
test({ assert, target }) { test({ assert, target }) {
const [change, increment] = target.querySelectorAll('button'); const [change, remove, increment] = target.querySelectorAll('button');
increment.click(); increment.click();
flushSync(); flushSync();
assert.htmlEqual(target.innerHTML, '<button>change handlers</button><button>1 / 1</button>'); assert.htmlEqual(
target.innerHTML,
'<button>change handlers</button><button>remove capture handler</button><button>1 / 1</button>'
);
change.click(); change.click();
flushSync(); flushSync();
increment.click(); increment.click();
flushSync(); flushSync();
assert.htmlEqual(target.innerHTML, '<button>change handlers</button><button>3 / 3</button>'); assert.htmlEqual(
target.innerHTML,
'<button>change handlers</button><button>remove capture handler</button><button>3 / 3</button>'
);
remove.click();
flushSync();
increment.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
'<button>change handlers</button><button>remove capture handler</button><button>5 / 3</button>'
);
} }
}); });

@ -24,4 +24,7 @@
> >
change handlers change handlers
</button> </button>
<button onclick={() => (attrs = { onclick: attrs.onclick, onclickcapture: undefined })}>
remove capture handler
</button>
<button {...attrs}>{delegated} / {non_delegated}</button> <button {...attrs}>{delegated} / {non_delegated}</button>

Loading…
Cancel
Save