webapi: window.event shadow-tree rule; property handlers on direct dispatch

Fixes the 3 failing tests in WPT /dom/events/event-global.html
(5/8 -> 8/8):

- "window.event is undefined if the target is in a shadow tree" (and
  the window.onerror variant): per the DOM invoke algorithm, the
  window's current event is only set while invoking listeners whose
  invocation target is not in a shadow tree. window.event was set once
  for the whole dispatch; it is now set per invocation target (in
  dispatchPhase and around inline handler calls), left undefined for
  targets whose root is a shadow root.
- "window.event is set to the current event, which is the event passed
  to dispatch (2)": script-dispatched events on non-node targets only
  ran addEventListener listeners. Property event handlers now fire too:
  XMLHttpRequestEventTarget resolves its on* fields by event type, and
  the Window's handler fields are consulted both when an event
  propagates to the window (getInlineHandler) and when one is
  dispatched directly on it.

Coverage: /dom/events/event-global.html 5/8 -> 8/8. No regressions
across /dom/events.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Francis Bouvier
2026-07-10 20:10:52 +02:00
committed by Karl Seguin
parent 6df58c2318
commit 86f9059f3f
2 changed files with 75 additions and 0 deletions

View File

@@ -111,10 +111,30 @@ pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, co
switch (target._type) {
.node => |node| try self.dispatchNode(node, event, opts),
// Property event handlers (e.g. xhr.onload, window.onerror) fire for
// script-dispatched events too, not only for the internal dispatch
// paths which pass them explicitly.
.xhr => |xhr| try self.dispatchDirect(target, event, xhr.inlineHandler(event._type_string), .{ .context = "dispatch" }),
.window => |w| try self.dispatchDirect(target, event, windowInlineHandler(w, event._type_string), .{ .context = "dispatch" }),
else => try self.dispatchDirect(target, event, null, .{ .context = "dispatch" }),
}
}
// Resolves the Window's property event handler for the given event type.
fn windowInlineHandler(window: *@import("webapi/Window.zig"), typ: lp.String) ?js.Function.Global {
const global_event_handlers = @import("webapi/global_event_handlers.zig");
const handler_type = global_event_handlers.fromEventType(typ.str()) orelse return null;
return switch (handler_type) {
.onerror => window._on_error,
.onload => window._on_load,
.onblur => window._on_blur,
.onfocus => window._on_focus,
.onresize => window._on_resize,
.onscroll => window._on_scroll,
else => null,
};
}
// There are a lot of events that can be attached via addEventListener or as
// a property, like the XHR events, or window.onload. You might think that the
// property is just a shortcut for calling addEventListener, but they are distinct.
@@ -284,6 +304,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
was_handled = true;
event._current_target = target_et;
const prev_current_event = window._current_event;
window._current_event = currentEventForTarget(target_et, event);
defer window._current_event = prev_current_event;
// Inline handlers (e.g. onclick property) follow the same "report,
// don't propagate" rule as addEventListener listeners — see Listener.run.
var caught: js.TryCatch.Caught = undefined;
@@ -331,6 +355,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
was_handled = true;
event._current_target = current_target;
const prev_current_event = window._current_event;
window._current_event = currentEventForTarget(current_target, event);
defer window._current_event = prev_current_event;
const original_target = event._target;
if (event._needs_retargeting) {
event._target = getAdjustedTarget(original_target, current_target);
@@ -360,6 +388,22 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
}
}
// Per spec ("invocation target in shadow tree"), window.event is left
// undefined while invoking listeners whose target lives in a shadow tree.
fn currentEventForTarget(target: *EventTarget, event: *Event) ?*Event {
const ShadowRoot = @import("webapi/ShadowRoot.zig");
switch (target._type) {
.node => |n| {
const root = n.getRootNode(.{});
if (root.is(ShadowRoot) != null) {
return null;
}
},
else => {},
}
return event;
}
const DispatchPhaseOpts = struct {
capture_only: ?bool = null,
apply_ignore: bool = false,
@@ -376,6 +420,11 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
const frame = self.frame;
const base = &self.base;
const window = frame.window;
const prev_current_event = window._current_event;
window._current_event = currentEventForTarget(current_target, event);
defer window._current_event = prev_current_event;
// Track dispatch depth for deferred removal
base.dispatch_depth += 1;
defer {
@@ -477,6 +526,17 @@ fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?j
// Look up the inline handler for this target
const html_element = switch (target._type) {
.node => |n| n.is(Element.Html) orelse return null,
// The Window stores its event handlers in dedicated fields; an event
// propagating to the window must fire them too.
.window => |w| return switch (handler_type) {
.onerror => w._on_error,
.onload => w._on_load,
.onblur => w._on_blur,
.onfocus => w._on_focus,
.onresize => w._on_resize,
.onscroll => w._on_scroll,
else => null,
},
else => return null,
};

View File

@@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const EventTarget = @import("../EventTarget.zig");
@@ -83,6 +84,20 @@ pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchT
);
}
// Resolves the property event handler for the given event type, so that a
// script-dispatched event (target.dispatchEvent) fires it like the internal
// dispatch path does.
pub fn inlineHandler(self: *const XMLHttpRequestEventTarget, typ: lp.String) ?js.Function.Global {
if (typ.eql(comptime .wrap("abort"))) return self._on_abort;
if (typ.eql(comptime .wrap("error"))) return self._on_error;
if (typ.eql(comptime .wrap("load"))) return self._on_load;
if (typ.eql(comptime .wrap("loadend"))) return self._on_load_end;
if (typ.eql(comptime .wrap("loadstart"))) return self._on_load_start;
if (typ.eql(comptime .wrap("progress"))) return self._on_progress;
if (typ.eql(comptime .wrap("timeout"))) return self._on_timeout;
return null;
}
pub fn getOnAbort(self: *const XMLHttpRequestEventTarget) ?js.Function.Global {
return self._on_abort;
}