Skip to content

Commit 15d6308

Browse files
authored
fix: catch delegated events from elements moved outside the container (#10060)
fixes #9777
1 parent 8a85059 commit 15d6308

File tree

4 files changed

+59
-0
lines changed

4 files changed

+59
-0
lines changed

.changeset/dirty-bats-punch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: handle delegated events of elements moved outside the container

packages/svelte/src/internal/client/render.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,6 +1280,10 @@ function handle_event_propagation(root_element, event) {
12801280
const handled_at = event.__root;
12811281
if (handled_at) {
12821282
const at_idx = path.indexOf(handled_at);
1283+
if (at_idx !== -1 && root_element === document) {
1284+
// This is the fallback document listener but the event was already handled -> ignore
1285+
return;
1286+
}
12831287
if (at_idx < path.indexOf(root_element)) {
12841288
path_idx = at_idx;
12851289
}
@@ -2778,13 +2782,17 @@ export function mount(component, options) {
27782782
set_current_hydration_fragment(previous_hydration_fragment);
27792783
}
27802784
const bound_event_listener = handle_event_propagation.bind(null, container);
2785+
const bound_document_event_listener = handle_event_propagation.bind(null, document);
27812786

27822787
/** @param {Array<string>} events */
27832788
const event_handle = (events) => {
27842789
for (let i = 0; i < events.length; i++) {
27852790
const event_name = events[i];
27862791
if (!registered_events.has(event_name)) {
27872792
registered_events.add(event_name);
2793+
// Add the event listener to both the container and the document.
2794+
// The container listener ensures we catch events from within in case
2795+
// the outer content stops propagation of the event.
27882796
container.addEventListener(
27892797
event_name,
27902798
bound_event_listener,
@@ -2794,6 +2802,17 @@ export function mount(component, options) {
27942802
}
27952803
: undefined
27962804
);
2805+
// The document listener ensures we catch events that originate from elements that were
2806+
// manually moved outside of the container (e.g. via manual portals).
2807+
document.addEventListener(
2808+
event_name,
2809+
bound_document_event_listener,
2810+
PassiveDelegatedEvents.includes(event_name)
2811+
? {
2812+
passive: true
2813+
}
2814+
: undefined
2815+
);
27972816
}
27982817
}
27992818
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { test } from '../../test';
2+
3+
// Tests that event delegation still works when the element with the event listener is moved outside the container
4+
export default test({
5+
async test({ assert, target }) {
6+
const btn1 = target.parentElement?.querySelector('button');
7+
const btn2 = target.querySelector('button');
8+
9+
btn1?.click();
10+
await Promise.resolve();
11+
assert.htmlEqual(
12+
target.parentElement?.innerHTML ?? '',
13+
'<main><div><button>clicks: 1</button></div></main><button>clicks: 1</button>'
14+
);
15+
16+
btn2?.click();
17+
await Promise.resolve();
18+
assert.htmlEqual(
19+
target.parentElement?.innerHTML ?? '',
20+
'<main><div><button>clicks: 2</button></div></main><button>clicks: 2</button>'
21+
);
22+
}
23+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
let count = $state(0);
3+
let el;
4+
$effect(() => {
5+
document.getElementsByTagName('body')[0].appendChild(el);
6+
})
7+
</script>
8+
9+
<div>
10+
<button bind:this={el} onclick={() => count++}>clicks: {count}</button>
11+
<button onclick={() => count++}>clicks: {count}</button>
12+
</div>

0 commit comments

Comments
 (0)