diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 7cffc2d79..5087ae527 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -166,6 +166,15 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts const et = target.asEventTarget(); event._target = et; event._dispatch_target = et; // Store original target for composedPath() + + // Retarget the relatedTarget against the dispatch target up front + // (DOM dispatch step 4); listeners observe the retargeted value and + // it survives the dispatch. + if (event.relatedTargetPtr()) |related_ptr| { + if (related_ptr.*) |related| { + related_ptr.* = getAdjustedTarget(related, et); + } + } } const frame = self.frame; @@ -195,6 +204,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts var path_len: usize = 0; var node_path_len: usize = 0; var path_buffer: [128]*EventTarget = undefined; + var clear_targets = false; // Defer runs even on early return - ensures event phase is reset // and default actions execute (unless prevented) @@ -203,7 +213,14 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts event._current_target = null; event._stop_propagation = false; event._stop_immediate_propagation = false; - if (event._needs_retargeting and node_path_len > 0) { + if (clear_targets) { + // Don't leak nodes living in a shadow tree: reset the targets + // (decided on the pre-dispatch tree, see below). + event._target = null; + if (event.relatedTargetPtr()) |related_ptr| { + related_ptr.* = null; + } + } else if (event._needs_retargeting and node_path_len > 0) { const adjusted = getAdjustedTarget(event._dispatch_target, path_buffer[node_path_len - 1]); event._target = if (rootIsShadowRoot(adjusted)) null else adjusted; } @@ -274,6 +291,26 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts } } + // DOM dispatch: decide up front — on the pre-dispatch tree, so listener + // mutations can't affect it — whether target and relatedTarget must be + // reset after dispatch because they would expose nodes inside a shadow + // tree. + if (node_path_len > 0) { + const last = path_buffer[node_path_len - 1]; + if (event._needs_retargeting) { + if (rootIsShadowRoot(getAdjustedTarget(event._dispatch_target, last))) { + clear_targets = true; + } + } + if (event.relatedTargetPtr()) |related_ptr| { + if (related_ptr.*) |related| { + if (rootIsShadowRoot(getAdjustedTarget(related, last))) { + clear_targets = true; + } + } + } + } + const path = path_buffer[0..path_len]; // Phase 1: Capturing phase (root → target, excluding target) diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 298370fc7..a0323c86f 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -169,6 +169,19 @@ pub fn as(self: *Event, comptime T: type) *T { return self.is(T).?; } +// Storage of the subtype's relatedTarget, for event types that have one. +// Used by dispatch for retargeting and shadow-tree resets. +pub fn relatedTargetPtr(self: *Event) ?*?*EventTarget { + switch (self._type) { + .ui_event => |ui| switch (ui._type) { + .mouse_event => |me| return &me._related_target, + .focus_event => |fe| return &fe._related_target, + else => return null, + }, + else => return null, + } +} + pub fn is(self: *Event, comptime T: type) ?*T { switch (self._type) { .generic => return if (T == Event) self else null,