From 9d809499a5c10b1714260d19e06fc921ebefdfd1 Mon Sep 17 00:00:00 2001 From: egrs Date: Thu, 19 Feb 2026 09:20:45 +0100 Subject: [PATCH] fix input value defaults, color normalization, and event propagation resets - checkbox/radio getValue() returns "on" when no value attribute set - color input sanitization normalizes hex to lowercase per spec - initial input value is sanitized per input type during element creation - initEvent resets both stop_propagation and stop_immediate_propagation - dispatchNode resets propagation flags after dispatch per DOM spec step 12 --- src/browser/EventManager.zig | 11 ++++---- src/browser/webapi/Event.zig | 2 ++ src/browser/webapi/element/html/Input.zig | 32 ++++++++++++++++++++--- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 389cb08b..640d6bf5 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -262,6 +262,8 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: // and default actions execute (unless prevented) defer { event._event_phase = .none; + event._stop_propagation = false; + event._stop_immediate_propagation = false; // Handle checkbox/radio activation rollback or commit if (activation_state) |state| { state.restore(event, page); @@ -322,19 +324,18 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: var i: usize = path_len; while (i > 1) { i -= 1; + if (event._stop_propagation) return; const current_target = path[i]; if (self.lookup.get(.{ .event_target = @intFromPtr(current_target), .type_string = event._type_string, })) |list| { try self.dispatchPhase(list, current_target, event, was_handled, true); - if (event._stop_propagation) { - return; - } } } // Phase 2: At target + if (event._stop_propagation) return; event._event_phase = .at_target; const target_et = target.asEventTarget(); @@ -375,14 +376,12 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: if (event._bubbles) { event._event_phase = .bubbling_phase; for (path[1..]) |current_target| { + if (event._stop_propagation) break; if (self.lookup.get(.{ .type_string = event._type_string, .event_target = @intFromPtr(current_target), })) |list| { try self.dispatchPhase(list, current_target, event, was_handled, false); - if (event._stop_propagation) { - break; - } } } } diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 569e5f70..1ab895f5 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -129,6 +129,8 @@ pub fn initEvent( self._bubbles = bubbles orelse false; self._cancelable = cancelable orelse false; self._stop_propagation = false; + self._stop_immediate_propagation = false; + self._prevent_default = false; } pub fn deinit(self: *Event, shutdown: bool) void { diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig index 7ab4db6b..dfa107f9 100644 --- a/src/browser/webapi/element/html/Input.zig +++ b/src/browser/webapi/element/html/Input.zig @@ -125,7 +125,10 @@ pub fn setType(self: *Input, typ: []const u8, page: *Page) !void { } pub fn getValue(self: *const Input) []const u8 { - return self._value orelse self._default_value orelse ""; + return self._value orelse self._default_value orelse switch (self._input_type) { + .checkbox, .radio => "on", + else => "", + }; } pub fn setValue(self: *Input, value: []const u8, page: *Page) !void { @@ -474,10 +477,19 @@ fn sanitizeValue(self: *Input, value: []const u8, page: *Page) ![]const u8 { }, .color => { if (value.len == 7 and value[0] == '#') { + var needs_lower = false; for (value[1..]) |c| { if (!std.ascii.isHex(c)) return "#000000"; + if (c >= 'A' and c <= 'F') needs_lower = true; } - return value; + if (!needs_lower) return value; + // Normalize to lowercase per spec + const result = try page.call_arena.alloc(u8, 7); + result[0] = '#'; + for (value[1..], 0..) |c, j| { + result[j + 1] = std.ascii.toLower(c); + } + return result; } return "#000000"; }, @@ -581,8 +593,6 @@ pub const Build = struct { self._default_value = element.getAttributeSafe(comptime .wrap("value")); self._default_checked = element.getAttributeSafe(comptime .wrap("checked")) != null; - // Current state starts equal to default - self._value = self._default_value; self._checked = self._default_checked; self._input_type = if (element.getAttributeSafe(comptime .wrap("type"))) |type_attr| @@ -590,6 +600,20 @@ pub const Build = struct { else .text; + // Current value starts equal to default, but sanitized per input type. + // sanitizeValue allocates temporaries from call_arena, so we must + // persist any new buffer into page.arena for the value to survive. + if (self._default_value) |dv| { + const sanitized = try self.sanitizeValue(dv, page); + if (sanitized.ptr == dv.ptr and sanitized.len == dv.len) { + self._value = self._default_value; + } else { + self._value = try page.arena.dupe(u8, sanitized); + } + } else { + self._value = null; + } + // If this is a checked radio button, uncheck others in its group if (self._checked and self._input_type == .radio) { try self.uncheckRadioGroup(page);