From a0418aed5c80d3548a5f03295121dc5b669c7ab7 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Fri, 10 Jul 2026 20:16:55 +0200 Subject: [PATCH] webapi: support onclick property handler on Window and Document MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the failing test in WPT /dom/events/handler-count.html?window and ?document (1/2 -> 2/2 each): setting `window.onclick` or `document.onclick` did nothing, because neither interface exposed the accessor — the assignment created an inert expando and the handler never fired for clicks bubbling up from the page. Both now store the handler in the frame's attribute-listener map (the same mechanism as element and ShadowRoot property handlers), which the dispatch propagation path already consults for any event target. Non-callable values clear the handler per [LegacyTreatNonObjectAsNull]. Coverage: /dom/events/handler-count.html?window 1/2 -> 2/2, /dom/events/handler-count.html?document 1/2 -> 2/2. No regressions across /dom/events. Co-Authored-By: Claude Fable 5 --- src/browser/webapi/Document.zig | 17 +++++++++++++++++ src/browser/webapi/Window.zig | 16 ++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index e78b1ea60..281328f25 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -21,6 +21,7 @@ const lp = @import("lightpanda"); const js = @import("../js/js.zig"); const Frame = @import("../Frame.zig"); +const Window = @import("Window.zig"); const URL = @import("../URL.zig"); const idna = @import("../../sys/idna.zig"); const public_suffix_list = @import("../../data/public_suffix_list.zig"); @@ -89,6 +90,21 @@ pub fn setOnSelectionChange(self: *Document, listener: ?js.Function) !void { } } +// Stored in the frame's attribute-listener map (like element and ShadowRoot +// property handlers), which the dispatch propagation path consults for any +// event target. +pub fn getOnClick(self: *Document, frame: *Frame) ?js.Function.Global { + return frame._event_target_attr_listeners.get(.{ .target = self.asEventTarget(), .handler = .onclick }); +} + +pub fn setOnClick(self: *Document, setter: ?Window.FunctionSetter, frame: *Frame) !void { + if (Window.getFunctionFromSetter(setter)) |cb| { + try frame._event_target_attr_listeners.put(frame.arena, .{ .target = self.asEventTarget(), .handler = .onclick }, cb); + } else { + _ = frame._event_target_attr_listeners.remove(.{ .target = self.asEventTarget(), .handler = .onclick }); + } +} + pub const Type = union(enum) { generic, html: *HTMLDocument, @@ -1265,6 +1281,7 @@ pub const JsApi = struct { } pub const onselectionchange = bridge.accessor(Document.getOnSelectionChange, Document.setOnSelectionChange, .{}); + pub const onclick = bridge.accessor(Document.getOnClick, Document.setOnClick, .{}); pub const URL = bridge.accessor(Document.getURL, null, .{}); pub const location = bridge.accessor(Document.getLocation, Document.setLocation, .{}); pub const documentURI = bridge.accessor(Document.getURL, null, .{}); diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index e374d0168..b83f10ee5 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -425,6 +425,21 @@ pub fn setOnScroll(self: *Window, setter: ?FunctionSetter) void { self._on_scroll = getFunctionFromSetter(setter); } +// Stored in the frame's attribute-listener map (like element and ShadowRoot +// property handlers), which the dispatch propagation path consults for any +// event target. +pub fn getOnClick(self: *Window, frame: *Frame) ?js.Function.Global { + return frame._event_target_attr_listeners.get(.{ .target = self.asEventTarget(), .handler = .onclick }); +} + +pub fn setOnClick(self: *Window, setter: ?FunctionSetter, frame: *Frame) !void { + if (getFunctionFromSetter(setter)) |cb| { + try frame._event_target_attr_listeners.put(frame.arena, .{ .target = self.asEventTarget(), .handler = .onclick }, cb); + } else { + _ = frame._event_target_attr_listeners.remove(.{ .target = self.asEventTarget(), .handler = .onclick }); + } +} + // The "window-reflecting body element event handler set" (HTML spec): these // event handlers of body and frameset elements are aliases for the Window's. // Returns the Window storage slot for the given content attribute name, or @@ -1159,6 +1174,7 @@ pub const JsApi = struct { pub const onfocus = bridge.accessor(Window.getOnFocus, Window.setOnFocus, .{}); pub const onresize = bridge.accessor(Window.getOnResize, Window.setOnResize, .{}); pub const onscroll = bridge.accessor(Window.getOnScroll, Window.setOnScroll, .{}); + pub const onclick = bridge.accessor(Window.getOnClick, Window.setOnClick, .{}); pub const onmessage = bridge.accessor(Window.getOnMessage, Window.setOnMessage, .{}); pub const onrejectionhandled = bridge.accessor(Window.getOnRejectionHandled, Window.setOnRejectionHandled, .{}); pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});