diff --git a/.changeset/chilly-kings-join.md b/.changeset/chilly-kings-join.md new file mode 100644 index 000000000000..b53d7e6cb693 --- /dev/null +++ b/.changeset/chilly-kings-join.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent event delegation logic conflicting between svelte instances diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 041698eb9d20..e598a789497f 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -11,8 +11,11 @@ import { set_active_reaction } from '../../runtime.js'; import { without_reactive_context } from './bindings/shared.js'; -import { can_delegate_event } from '../../../../utils.js'; +/** + * Used on elements, as a map of event type -> event handler, + * and on events themselves to track which element handled an event + */ export const event_symbol = Symbol('events'); /** @type {Set} */ @@ -177,8 +180,8 @@ export function handle_event_propagation(event) { last_propagated_event = event; // composedPath contains list of nodes the event has propagated through. - // 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 + // We check `event_symbol` to skip all nodes below it in case this is a + // parent of the `event_symbol` node, which indicates that there's nested // mounted apps. In this case we don't want to trigger events multiple times. var path_idx = 0; @@ -186,7 +189,7 @@ export function handle_event_propagation(event) { // without it the variable will be DCE'd and things will // fail mysteriously in Firefox // @ts-expect-error is added below - var handled_at = last_propagated_event === event && event.__root; + var handled_at = last_propagated_event === event && event[event_symbol]; if (handled_at) { var at_idx = path.indexOf(handled_at); @@ -198,7 +201,7 @@ export function handle_event_propagation(event) { // -> ignore, but set handle_at to document/window so that we're resetting the event // chain in case someone manually dispatches the same event object again. // @ts-expect-error - event.__root = handler_element; + event[event_symbol] = handler_element; return; } @@ -298,7 +301,7 @@ export function handle_event_propagation(event) { } } finally { // @ts-expect-error is used above - event.__root = handler_element; + event[event_symbol] = handler_element; // @ts-ignore remove proxy on currentTarget delete event.currentTarget; set_active_reaction(previous_reaction); diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 0d5bc6cb4928..76a73852d524 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -161,48 +161,6 @@ const listeners = new Map(); function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) { init_operations(); - /** @type {Set} */ - var registered_events = new Set(); - - /** @param {Array} events */ - var event_handle = (events) => { - for (var i = 0; i < events.length; i++) { - var event_name = events[i]; - - if (registered_events.has(event_name)) continue; - registered_events.add(event_name); - - var passive = is_passive_event(event_name); - - // 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. - // - // 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 count = counts.get(event_name); - - if (count === undefined) { - node.addEventListener(event_name, handle_event_propagation, { passive }); - counts.set(event_name, 1); - } else { - counts.set(event_name, count + 1); - } - } - } - }; - - event_handle(array_from(all_registered_events)); - root_event_handles.add(event_handle); - /** @type {Exports} */ // @ts-expect-error will be defined because the render effect runs synchronously var component = undefined; @@ -251,6 +209,49 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro } ); + // Setup event delegation _after_ component is mounted - if an error would happen during mount, it would otherwise not be cleaned up + /** @type {Set} */ + var registered_events = new Set(); + + /** @param {Array} events */ + var event_handle = (events) => { + for (var i = 0; i < events.length; i++) { + var event_name = events[i]; + + if (registered_events.has(event_name)) continue; + registered_events.add(event_name); + + var passive = is_passive_event(event_name); + + // 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. + // + // 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 count = counts.get(event_name); + + if (count === undefined) { + node.addEventListener(event_name, handle_event_propagation, { passive }); + counts.set(event_name, 1); + } else { + counts.set(event_name, count + 1); + } + } + } + }; + + event_handle(array_from(all_registered_events)); + root_event_handles.add(event_handle); + return () => { for (var event_name of registered_events) { for (const node of [target, document]) {