Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chilly-kings-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: prevent event delegation logic conflicting between svelte instances
15 changes: 9 additions & 6 deletions packages/svelte/src/internal/client/dom/elements/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>} */
Expand Down Expand Up @@ -177,16 +180,16 @@ 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;

// the `last_propagated_event === event` check is redundant, but
// 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);
Expand All @@ -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;
}

Expand Down Expand Up @@ -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);
Expand Down
85 changes: 43 additions & 42 deletions packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,48 +161,6 @@ const listeners = new Map();
function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) {
init_operations();

/** @type {Set<string>} */
var registered_events = new Set();

/** @param {Array<string>} 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;
Expand Down Expand Up @@ -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<string>} */
Comment thread
Rich-Harris marked this conversation as resolved.
var registered_events = new Set();

/** @param {Array<string>} 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]) {
Expand Down
Loading