mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 01:36:15 -04:00
webapi: retarget relatedTarget and reset shadow-tree targets pre-dispatch
Fixes 5 failing tests across WPT /dom/events/relatedTarget.window.html (0/6 -> 3/6) and /dom/events/shadow-relatedTarget.html (0/2 -> 2/2). The remaining 3 relatedTarget.window.html subtests also fail in Chrome stable (verified on wpt.fyi), so they are out of scope. Per the DOM dispatch algorithm: - The event's relatedTarget is retargeted against the dispatch target when dispatch starts (Event gains relatedTargetPtr() to reach the MouseEvent/FocusEvent storage generically), so listeners and post-dispatch reads observe e.g. the shadow host instead of a node inside a shadow tree. - The "clear targets" decision (reset target and relatedTarget to null when they would expose nodes whose root is a shadow root) is now computed when the event path is built — on the pre-dispatch tree — instead of after dispatch, so listeners moving nodes out of the shadow tree during dispatch can't defeat it, and it now also applies to relatedTarget, not just target. Coverage: /dom/events/relatedTarget.window.html 0/6 -> 3/6 (Chrome parity), /dom/events/shadow-relatedTarget.html 0/2 -> 2/2. No regressions across /dom/events. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Karl Seguin
parent
64b4276cfa
commit
f72cdf33cf
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user