From 86f9059f3f40815f955d617eacda69a581aeaa01 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Fri, 10 Jul 2026 20:10:52 +0200 Subject: [PATCH] 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 --- src/browser/EventManager.zig | 60 +++++++++++++++++++ .../webapi/net/XMLHttpRequestEventTarget.zig | 15 +++++ 2 files changed, 75 insertions(+) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 4802d9fe7..0ac74e2fa 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -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, }; diff --git a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig index cfa258fc0..275586e19 100644 --- a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig +++ b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig @@ -16,6 +16,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +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; }