webapi: support onclick property handler on Window and Document

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 <noreply@anthropic.com>
This commit is contained in:
Francis Bouvier
2026-07-10 20:16:55 +02:00
committed by Karl Seguin
parent 86f9059f3f
commit a0418aed5c
2 changed files with 33 additions and 0 deletions

View File

@@ -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, .{});

View File

@@ -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, .{});