From 0565dca1f7cbfb862ebf66c390e4b281a67ca6c1 Mon Sep 17 00:00:00 2001 From: Abdelrahman Date: Fri, 13 Feb 2026 18:23:47 +0200 Subject: [PATCH] fix: implement ref counting for mount with same target (#17695) ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [ ] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` Fixes #17694 Not really sure how to add a failing test here. The code was generated by Codex 5.3, but I cleaned it up and manually reviewed it myself. Not sure if this is the best approach to solving the issue, though. It does add some overhead with the extra maps. --------- Co-authored-by: Rich Harris Co-authored-by: Rich Harris --- .changeset/upset-spiders-study.md | 5 ++ packages/svelte/src/internal/client/render.js | 53 ++++++++++++------- 2 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 .changeset/upset-spiders-study.md diff --git a/.changeset/upset-spiders-study.md b/.changeset/upset-spiders-study.md new file mode 100644 index 0000000000..3f90828038 --- /dev/null +++ b/.changeset/upset-spiders-study.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve delegated event handlers as long as one or more root components are using them diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index c09f5fdd05..0d5bc6cb49 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -149,8 +149,8 @@ export function hydrate(component, options) { } } -/** @type {Map} */ -const document_listeners = new Map(); +/** @type {Map>} */ +const listeners = new Map(); /** * @template {Record} Exports @@ -177,17 +177,25 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro // Add the event listener to both the container and the document. // The container listener ensures we catch events from within in case // the outer content stops propagation of the event. - target.addEventListener(event_name, handle_event_propagation, { passive }); + // + // The document listener ensures we catch events that originate from elements that were + // manually moved outside of the container (e.g. via manual portals). + for (const node of [target, document]) { + var counts = listeners.get(node); + + if (counts === undefined) { + counts = new Map(); + listeners.set(node, counts); + } - var n = document_listeners.get(event_name); + var count = counts.get(event_name); - if (n === undefined) { - // The document listener ensures we catch events that originate from elements that were - // manually moved outside of the container (e.g. via manual portals). - document.addEventListener(event_name, handle_event_propagation, { passive }); - document_listeners.set(event_name, 1); - } else { - document_listeners.set(event_name, n + 1); + if (count === undefined) { + node.addEventListener(event_name, handle_event_propagation, { passive }); + counts.set(event_name, 1); + } else { + counts.set(event_name, count + 1); + } } } }; @@ -245,15 +253,20 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro return () => { for (var event_name of registered_events) { - target.removeEventListener(event_name, handle_event_propagation); - - var n = /** @type {number} */ (document_listeners.get(event_name)); - - if (--n === 0) { - document.removeEventListener(event_name, handle_event_propagation); - document_listeners.delete(event_name); - } else { - document_listeners.set(event_name, n); + for (const node of [target, document]) { + var counts = /** @type {Map} */ (listeners.get(node)); + var count = /** @type {number} */ (counts.get(event_name)); + + if (--count == 0) { + node.removeEventListener(event_name, handle_event_propagation); + counts.delete(event_name); + + if (counts.size === 0) { + listeners.delete(node); + } + } else { + counts.set(event_name, count); + } } }