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
This commit is contained in:
egrs
2026-02-19 09:20:45 +01:00
parent b8196cd06e
commit 9d809499a5
3 changed files with 35 additions and 10 deletions

View File

@@ -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;
}
}
}
}

View File

@@ -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 {

View File

@@ -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);