diff --git a/.changeset/big-eyes-carry.md b/.changeset/big-eyes-carry.md
new file mode 100644
index 000000000000..8e2b4ac7364e
--- /dev/null
+++ b/.changeset/big-eyes-carry.md
@@ -0,0 +1,5 @@
+---
+"svelte": patch
+---
+
+fix: handle event delegation correctly when having sibling event listeners
diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js
index 0658780b2fc3..a0051c6b531a 100644
--- a/packages/svelte/src/internal/client/render.js
+++ b/packages/svelte/src/internal/client/render.js
@@ -1279,11 +1279,11 @@ export function delegate(events) {
}
/**
- * @param {Node} root_element
+ * @param {Node} handler_element
* @param {Event} event
* @returns {void}
*/
-function handle_event_propagation(root_element, event) {
+function handle_event_propagation(handler_element, event) {
const event_name = event.type;
const path = event.composedPath?.() || [];
let current_target = /** @type {null | Element} */ (path[0] || event.target);
@@ -1298,22 +1298,37 @@ function handle_event_propagation(root_element, event) {
// We check __root to skip all nodes below it in case this is a
// parent of the __root node, which indicates that there's nested
// mounted apps. In this case we don't want to trigger events multiple times.
- // We're deliberately not skipping if the index is the same or higher, because
- // someone could create an event programmatically and emit it multiple times,
- // in which case we want to handle the whole propagation chain properly each time.
let path_idx = 0;
// @ts-expect-error is added below
const handled_at = event.__root;
if (handled_at) {
const at_idx = path.indexOf(handled_at);
- if (at_idx !== -1 && root_element === document) {
- // This is the fallback document listener but the event was already handled -> ignore
+ if (at_idx !== -1 && handler_element === document) {
+ // This is the fallback document listener but the event was already handled
+ // -> ignore, but set handle_at to document so that we're resetting the event
+ // chain in case someone manually dispatches the same event object again.
+ // @ts-expect-error
+ event.__root = document;
return;
}
- if (at_idx < path.indexOf(root_element)) {
- path_idx = at_idx;
+ // We're deliberately not skipping if the index is higher, because
+ // someone could create an event programmatically and emit it multiple times,
+ // in which case we want to handle the whole propagation chain properly each time.
+ // (this will only be a false negative if the event is dispatched multiple times and
+ // the fallback document listener isn't reached in between, but that's super rare)
+ const handler_idx = path.indexOf(handler_element);
+ if (handler_idx === -1) {
+ // handle_idx can theoretically be -1 (happened in some JSDOM testing scenarios with an event listener on the window object)
+ // so guard against that, too, and assume that everything was handled at this point.
+ return;
+ }
+ if (at_idx <= handler_idx) {
+ // +1 because at_idx is the element which was already handled, and there can only be one delegated event per element.
+ // Avoids on:click and onclick on the same event resulting in onclick being fired twice.
+ path_idx = at_idx + 1;
}
}
+
current_target = /** @type {Element} */ (path[path_idx] || event.target);
// Proxy currentTarget to correct target
define_property(event, 'currentTarget', {
@@ -1339,16 +1354,20 @@ function handle_event_propagation(root_element, event) {
delegated.call(current_target, event);
}
}
- if (event.cancelBubble || parent_element === root_element) {
+ if (
+ event.cancelBubble ||
+ parent_element === handler_element ||
+ current_target === handler_element
+ ) {
break;
}
current_target = parent_element;
}
// @ts-expect-error is used above
- event.__root = root_element;
+ event.__root = handler_element;
// @ts-expect-error is used above
- current_target = root_element;
+ current_target = handler_element;
}
/**
diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-4/_config.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-4/_config.js
new file mode 100644
index 000000000000..9830e746467c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-4/_config.js
@@ -0,0 +1,35 @@
+import { test } from '../../test';
+import { log } from './log.js';
+
+export default test({
+ before_test() {
+ log.length = 0;
+ },
+
+ async test({ assert, target }) {
+ const [btn1, btn2] = target.querySelectorAll('button');
+
+ btn1?.click();
+ await Promise.resolve();
+ assert.deepEqual(log, [
+ 'button main',
+ 'div main 1',
+ 'div main 2',
+ 'document main',
+ 'document sub',
+ 'window main',
+ 'window sub'
+ ]);
+
+ log.length = 0;
+ btn2?.click();
+ await Promise.resolve();
+ assert.deepEqual(log, [
+ 'button sub',
+ 'document main',
+ 'document sub',
+ 'window main',
+ 'window sub'
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-4/log.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-4/log.js
new file mode 100644
index 000000000000..d3df521f4da7
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-4/log.js
@@ -0,0 +1,2 @@
+/** @type {any[]} */
+export const log = [];
diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-4/main.svelte
new file mode 100644
index 000000000000..9fa1484907e4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-4/main.svelte
@@ -0,0 +1,13 @@
+
+
+