mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Default behavior for input click (radio / checkbox).
This wasn't 100% intuitive to me. At the start of the event, the input is immediately toggled. But at any point during dispatching, the default behavior can be suppressed. So the state of the input's check during dispatching captures the "intent" of the click. But it's possible for one listener to see that input.checked == true even though, by the end of dispatching, input.checked == false because some other listener called preventDefault(). To support this, we need to capture the "current" state so that, if we need to rollback, we can. For radio buttons, this "current" state includes capturing the currently checked radio (if any).
This commit is contained in:
@@ -28,6 +28,7 @@ const Page = @import("Page.zig");
|
|||||||
const Node = @import("webapi/Node.zig");
|
const Node = @import("webapi/Node.zig");
|
||||||
const Event = @import("webapi/Event.zig");
|
const Event = @import("webapi/Event.zig");
|
||||||
const EventTarget = @import("webapi/EventTarget.zig");
|
const EventTarget = @import("webapi/EventTarget.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
@@ -254,20 +255,27 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
|||||||
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void {
|
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void {
|
||||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||||
|
|
||||||
|
const page = self.page;
|
||||||
|
const activation_state = ActivationState.create(event, target, page);
|
||||||
|
|
||||||
// Defer runs even on early return - ensures event phase is reset
|
// Defer runs even on early return - ensures event phase is reset
|
||||||
// and default actions execute (unless prevented)
|
// and default actions execute (unless prevented)
|
||||||
defer {
|
defer {
|
||||||
event._event_phase = .none;
|
event._event_phase = .none;
|
||||||
|
// Handle checkbox/radio activation rollback or commit
|
||||||
|
if (activation_state) |state| {
|
||||||
|
state.restore(event, page);
|
||||||
|
}
|
||||||
|
|
||||||
// Execute default action if not prevented
|
// Execute default action if not prevented
|
||||||
if (event._prevent_default) {
|
if (event._prevent_default) {
|
||||||
// can't return in a defer (╯°□°)╯︵ ┻━┻
|
// can't return in a defer (╯°□°)╯︵ ┻━┻
|
||||||
} else if (event._type_string.eqlSlice("click")) {
|
} else if (event._type_string.eql(comptime .wrap("click"))) {
|
||||||
self.page.handleClick(target) catch |err| {
|
page.handleClick(target) catch |err| {
|
||||||
log.warn(.event, "page.click", .{ .err = err });
|
log.warn(.event, "page.click", .{ .err = err });
|
||||||
};
|
};
|
||||||
} else if (event._type_string.eqlSlice("keydown")) {
|
} else if (event._type_string.eql(comptime .wrap("keydown"))) {
|
||||||
self.page.handleKeydown(target, event) catch |err| {
|
page.handleKeydown(target, event) catch |err| {
|
||||||
log.warn(.event, "page.keydown", .{ .err = err });
|
log.warn(.event, "page.keydown", .{ .err = err });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -302,7 +310,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
|||||||
// Even though the window isn't part of the DOM, events always propagate
|
// Even though the window isn't part of the DOM, events always propagate
|
||||||
// through it in the capture phase (unless we stopped at a shadow boundary)
|
// through it in the capture phase (unless we stopped at a shadow boundary)
|
||||||
if (path_len < path_buffer.len) {
|
if (path_len < path_buffer.len) {
|
||||||
path_buffer[path_len] = self.page.window.asEventTarget();
|
path_buffer[path_len] = page.window.asEventTarget();
|
||||||
path_len += 1;
|
path_len += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,7 +496,6 @@ fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?j
|
|||||||
const handler_type = global_event_handlers.fromEventType(event._type_string.str()) orelse return null;
|
const handler_type = global_event_handlers.fromEventType(event._type_string.str()) orelse return null;
|
||||||
|
|
||||||
// Look up the inline handler for this target
|
// Look up the inline handler for this target
|
||||||
const Element = @import("webapi/Element.zig");
|
|
||||||
const element = switch (target._type) {
|
const element = switch (target._type) {
|
||||||
.node => |n| n.is(Element) orelse return null,
|
.node => |n| n.is(Element) orelse return null,
|
||||||
else => return null,
|
else => return null,
|
||||||
@@ -612,3 +619,144 @@ fn isAncestorOrSelf(ancestor: *Node, node: *Node) bool {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles the default action for clicking on input checked/radio. Maybe this
|
||||||
|
// could be generalized if needed, but I'm not sure. This wasn't obvious to me
|
||||||
|
// but when an input is clicked, it's important to think about both the intent
|
||||||
|
// and the actual result. Imagine you have an unchecked checkbox. When clicked,
|
||||||
|
// the checkbox immediately becomes checked, and event handlers see this "checked"
|
||||||
|
// intent. But a listener can preventDefault() in which case the check we did at
|
||||||
|
// the start will be undone.
|
||||||
|
// This is a bit more complicated for radio buttons, as the checking/unchecking
|
||||||
|
// and the rollback can impact a different radio input. So if you "check" a radio
|
||||||
|
// the intent is that it becomes checked and whatever was checked before becomes
|
||||||
|
// unchecked, so that if you have to rollback (because of a preventDefault())
|
||||||
|
// then both inputs have to revert to their original values.
|
||||||
|
const ActivationState = struct {
|
||||||
|
old_checked: bool,
|
||||||
|
input: *Element.Html.Input,
|
||||||
|
previously_checked_radio: ?*Input,
|
||||||
|
|
||||||
|
const Input = Element.Html.Input;
|
||||||
|
|
||||||
|
fn create(event: *const Event, target: *Node, page: *Page) ? ActivationState {
|
||||||
|
if (event._type_string.eql(comptime .wrap("click")) == false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = target.is(Element.Html.Input) orelse return null;
|
||||||
|
if (input._input_type != .checkbox and input._input_type != .radio) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const old_checked = input._checked;
|
||||||
|
var previously_checked_radio: ?*Element.Html.Input = null;
|
||||||
|
|
||||||
|
// For radio buttons, find the currently checked radio in the group
|
||||||
|
if (input._input_type == .radio and !old_checked) {
|
||||||
|
previously_checked_radio = try findCheckedRadioInGroup(input, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle checkbox or check radio (which unchecks others in group)
|
||||||
|
const new_checked = if (input._input_type == .checkbox) !old_checked else true;
|
||||||
|
try input.setChecked(new_checked, page);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.input = input,
|
||||||
|
.old_checked = old_checked,
|
||||||
|
.previously_checked_radio = previously_checked_radio,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore(self: *const ActivationState, event: *const Event, page: *Page) void {
|
||||||
|
const input = self.input;
|
||||||
|
if (event._prevent_default) {
|
||||||
|
// Rollback: restore previous state
|
||||||
|
input._checked = self.old_checked;
|
||||||
|
input._checked_dirty = true;
|
||||||
|
if (self.previously_checked_radio) |prev_radio| {
|
||||||
|
prev_radio._checked = true;
|
||||||
|
prev_radio._checked_dirty = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit: fire input and change events only if state actually changed
|
||||||
|
// For checkboxes, state always changes. For radios, only if was unchecked.
|
||||||
|
const state_changed = (input._input_type == .checkbox) or !self.old_checked;
|
||||||
|
if (state_changed) {
|
||||||
|
fireEvent(page, input, "input") catch |err| {
|
||||||
|
log.warn(.event, "input event", .{ .err = err });
|
||||||
|
};
|
||||||
|
fireEvent(page, input, "change") catch |err| {
|
||||||
|
log.warn(.event, "change event", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findCheckedRadioInGroup(input: *Input, page: *Page) !?*Input {
|
||||||
|
const elem = input.asElement();
|
||||||
|
|
||||||
|
const name = elem.getAttributeSafe(comptime .wrap("name")) orelse return null;
|
||||||
|
if (name.len == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = input.getForm(page);
|
||||||
|
|
||||||
|
// Walk from the root of the tree containing this element
|
||||||
|
// This handles both document-attached and orphaned elements
|
||||||
|
const root = elem.asNode().getRootNode(null);
|
||||||
|
|
||||||
|
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||||
|
var walker = TreeWalker.Full.init(root, .{});
|
||||||
|
|
||||||
|
while (walker.next()) |node| {
|
||||||
|
const other_element = node.is(Element) orelse continue;
|
||||||
|
const other_input = other_element.is(Input) orelse continue;
|
||||||
|
|
||||||
|
if (other_input._input_type != .radio) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the input we're checking from
|
||||||
|
if (other_input == input) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const other_name = other_element.getAttributeSafe(comptime .wrap("name")) orelse continue;
|
||||||
|
if (!std.mem.eql(u8, name, other_name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if same form context
|
||||||
|
const other_form = other_input.getForm(page);
|
||||||
|
if (form) |f| {
|
||||||
|
const of = other_form orelse continue;
|
||||||
|
if (f != of) {
|
||||||
|
continue; // Different forms
|
||||||
|
}
|
||||||
|
} else if (other_form != null) {
|
||||||
|
continue; // form is null but other has a form
|
||||||
|
}
|
||||||
|
|
||||||
|
if (other_input._checked) {
|
||||||
|
return other_input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire input or change event
|
||||||
|
fn fireEvent(page: *Page, input: *Input, comptime typ: []const u8) !void {
|
||||||
|
const event = try Event.initTrusted(comptime .wrap(typ), .{
|
||||||
|
.bubbles = true,
|
||||||
|
.cancelable = false,
|
||||||
|
}, page);
|
||||||
|
defer if (!event._v8_handoff) event.deinit(false);
|
||||||
|
|
||||||
|
const target = input.asElement().asEventTarget();
|
||||||
|
try page._event_manager.dispatch(target, event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
283
src/browser/tests/element/html/input_click.html
Normal file
283
src/browser/tests/element/html/input_click.html
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<!-- Checkbox click tests -->
|
||||||
|
<input id="checkbox1" type="checkbox">
|
||||||
|
<input id="checkbox2" type="checkbox" checked>
|
||||||
|
<input id="checkbox_disabled" type="checkbox" disabled>
|
||||||
|
|
||||||
|
<!-- Radio click tests -->
|
||||||
|
<input id="radio1" type="radio" name="clickgroup" checked>
|
||||||
|
<input id="radio2" type="radio" name="clickgroup">
|
||||||
|
<input id="radio3" type="radio" name="clickgroup">
|
||||||
|
<input id="radio_disabled" type="radio" name="clickgroup" disabled>
|
||||||
|
|
||||||
|
<script id="checkbox_click_toggles">
|
||||||
|
{
|
||||||
|
const cb = $('#checkbox1');
|
||||||
|
testing.expectEqual(false, cb.checked);
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
testing.expectEqual(true, cb.checked);
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
testing.expectEqual(false, cb.checked);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="checkbox_click_preventDefault_reverts">
|
||||||
|
{
|
||||||
|
const cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
testing.expectEqual(false, cb.checked);
|
||||||
|
|
||||||
|
cb.addEventListener('click', (e) => {
|
||||||
|
testing.expectEqual(true, cb.checked, 'checkbox should be checked during click handler');
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
testing.expectEqual(false, cb.checked, 'checkbox should revert after preventDefault');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="checkbox_click_events_order">
|
||||||
|
{
|
||||||
|
const cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
document.body.appendChild(cb);
|
||||||
|
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
cb.addEventListener('click', () => events.push('click'));
|
||||||
|
cb.addEventListener('input', () => events.push('input'));
|
||||||
|
cb.addEventListener('change', () => events.push('change'));
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
|
||||||
|
testing.expectEqual(3, events.length);
|
||||||
|
testing.expectEqual('click', events[0]);
|
||||||
|
testing.expectEqual('input', events[1]);
|
||||||
|
testing.expectEqual('change', events[2]);
|
||||||
|
|
||||||
|
document.body.removeChild(cb);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="checkbox_click_preventDefault_no_input_change">
|
||||||
|
{
|
||||||
|
const cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
document.body.appendChild(cb);
|
||||||
|
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
cb.addEventListener('click', (e) => {
|
||||||
|
events.push('click');
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
cb.addEventListener('input', () => events.push('input'));
|
||||||
|
cb.addEventListener('change', () => events.push('change'));
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
|
||||||
|
testing.expectEqual(1, events.length, 'only click event should fire');
|
||||||
|
testing.expectEqual('click', events[0]);
|
||||||
|
|
||||||
|
document.body.removeChild(cb);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="checkbox_click_state_visible_in_handler">
|
||||||
|
{
|
||||||
|
const cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
cb.checked = true;
|
||||||
|
|
||||||
|
cb.addEventListener('click', (e) => {
|
||||||
|
testing.expectEqual(false, cb.checked, 'should see toggled state in handler');
|
||||||
|
e.preventDefault();
|
||||||
|
testing.expectEqual(false, cb.checked, 'should still be toggled after preventDefault in handler');
|
||||||
|
});
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
testing.expectEqual(true, cb.checked, 'should revert to original state after handler completes');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="radio_click_checks_clicked">
|
||||||
|
{
|
||||||
|
const r1 = $('#radio1');
|
||||||
|
const r2 = $('#radio2');
|
||||||
|
|
||||||
|
testing.expectEqual(true, r1.checked);
|
||||||
|
testing.expectEqual(false, r2.checked);
|
||||||
|
|
||||||
|
r2.click();
|
||||||
|
testing.expectEqual(false, r1.checked);
|
||||||
|
testing.expectEqual(true, r2.checked);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="radio_click_preventDefault_reverts">
|
||||||
|
{
|
||||||
|
const r1 = document.createElement('input');
|
||||||
|
r1.type = 'radio';
|
||||||
|
r1.name = 'testgroup';
|
||||||
|
r1.checked = true;
|
||||||
|
|
||||||
|
const r2 = document.createElement('input');
|
||||||
|
r2.type = 'radio';
|
||||||
|
r2.name = 'testgroup';
|
||||||
|
|
||||||
|
document.body.appendChild(r1);
|
||||||
|
document.body.appendChild(r2);
|
||||||
|
|
||||||
|
r2.addEventListener('click', (e) => {
|
||||||
|
testing.expectEqual(false, r1.checked, 'r1 should be unchecked during click handler');
|
||||||
|
testing.expectEqual(true, r2.checked, 'r2 should be checked during click handler');
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
r2.click();
|
||||||
|
|
||||||
|
testing.expectEqual(true, r1.checked, 'r1 should be restored after preventDefault');
|
||||||
|
testing.expectEqual(false, r2.checked, 'r2 should revert after preventDefault');
|
||||||
|
|
||||||
|
document.body.removeChild(r1);
|
||||||
|
document.body.removeChild(r2);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="radio_click_events_order">
|
||||||
|
{
|
||||||
|
const r = document.createElement('input');
|
||||||
|
r.type = 'radio';
|
||||||
|
r.name = 'eventtest';
|
||||||
|
document.body.appendChild(r);
|
||||||
|
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
r.addEventListener('click', () => events.push('click'));
|
||||||
|
r.addEventListener('input', () => events.push('input'));
|
||||||
|
r.addEventListener('change', () => events.push('change'));
|
||||||
|
|
||||||
|
r.click();
|
||||||
|
|
||||||
|
testing.expectEqual(3, events.length);
|
||||||
|
testing.expectEqual('click', events[0]);
|
||||||
|
testing.expectEqual('input', events[1]);
|
||||||
|
testing.expectEqual('change', events[2]);
|
||||||
|
|
||||||
|
document.body.removeChild(r);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="radio_click_already_checked_no_events">
|
||||||
|
{
|
||||||
|
const r = document.createElement('input');
|
||||||
|
r.type = 'radio';
|
||||||
|
r.name = 'alreadytest';
|
||||||
|
r.checked = true;
|
||||||
|
document.body.appendChild(r);
|
||||||
|
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
r.addEventListener('click', () => events.push('click'));
|
||||||
|
r.addEventListener('input', () => events.push('input'));
|
||||||
|
r.addEventListener('change', () => events.push('change'));
|
||||||
|
|
||||||
|
r.click();
|
||||||
|
|
||||||
|
testing.expectEqual(1, events.length, 'only click event should fire for already-checked radio');
|
||||||
|
testing.expectEqual('click', events[0]);
|
||||||
|
|
||||||
|
document.body.removeChild(r);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="disabled_checkbox_no_click">
|
||||||
|
{
|
||||||
|
const cb = $('#checkbox_disabled');
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
cb.addEventListener('click', () => events.push('click'));
|
||||||
|
cb.addEventListener('input', () => events.push('input'));
|
||||||
|
cb.addEventListener('change', () => events.push('change'));
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
|
||||||
|
testing.expectEqual(0, events.length, 'disabled checkbox should not fire any events');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="disabled_radio_no_click">
|
||||||
|
{
|
||||||
|
const r = $('#radio_disabled');
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
r.addEventListener('click', () => events.push('click'));
|
||||||
|
r.addEventListener('input', () => events.push('input'));
|
||||||
|
r.addEventListener('change', () => events.push('change'));
|
||||||
|
|
||||||
|
r.click();
|
||||||
|
|
||||||
|
testing.expectEqual(0, events.length, 'disabled radio should not fire any events');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="input_and_change_are_trusted">
|
||||||
|
{
|
||||||
|
const cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
document.body.appendChild(cb);
|
||||||
|
|
||||||
|
let inputEvent = null;
|
||||||
|
let changeEvent = null;
|
||||||
|
|
||||||
|
cb.addEventListener('input', (e) => inputEvent = e);
|
||||||
|
cb.addEventListener('change', (e) => changeEvent = e);
|
||||||
|
|
||||||
|
cb.click();
|
||||||
|
|
||||||
|
testing.expectEqual(true, inputEvent.isTrusted, 'input event should be trusted');
|
||||||
|
testing.expectEqual(true, inputEvent.bubbles, 'input event should bubble');
|
||||||
|
testing.expectEqual(false, inputEvent.cancelable, 'input event should not be cancelable');
|
||||||
|
|
||||||
|
testing.expectEqual(true, changeEvent.isTrusted, 'change event should be trusted');
|
||||||
|
testing.expectEqual(true, changeEvent.bubbles, 'change event should bubble');
|
||||||
|
testing.expectEqual(false, changeEvent.cancelable, 'change event should not be cancelable');
|
||||||
|
|
||||||
|
document.body.removeChild(cb);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="multiple_radios_click_sequence">
|
||||||
|
{
|
||||||
|
const r1 = $('#radio1');
|
||||||
|
const r2 = $('#radio2');
|
||||||
|
const r3 = $('#radio3');
|
||||||
|
|
||||||
|
// Reset to known state
|
||||||
|
r1.checked = true;
|
||||||
|
|
||||||
|
testing.expectEqual(true, r1.checked);
|
||||||
|
testing.expectEqual(false, r2.checked);
|
||||||
|
testing.expectEqual(false, r3.checked);
|
||||||
|
|
||||||
|
r2.click();
|
||||||
|
testing.expectEqual(false, r1.checked);
|
||||||
|
testing.expectEqual(true, r2.checked);
|
||||||
|
testing.expectEqual(false, r3.checked);
|
||||||
|
|
||||||
|
r3.click();
|
||||||
|
testing.expectEqual(false, r1.checked);
|
||||||
|
testing.expectEqual(false, r2.checked);
|
||||||
|
testing.expectEqual(true, r3.checked);
|
||||||
|
|
||||||
|
r1.click();
|
||||||
|
testing.expectEqual(true, r1.checked);
|
||||||
|
testing.expectEqual(false, r2.checked);
|
||||||
|
testing.expectEqual(false, r3.checked);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -496,13 +496,22 @@ fn uncheckRadioGroup(self: *Input, page: *Page) !void {
|
|||||||
|
|
||||||
const my_form = self.getForm(page);
|
const my_form = self.getForm(page);
|
||||||
|
|
||||||
|
// Walk from the root of the tree containing this element
|
||||||
|
// This handles both document-attached and orphaned elements
|
||||||
|
const root = element.asNode().getRootNode(null);
|
||||||
|
|
||||||
const TreeWalker = @import("../../TreeWalker.zig");
|
const TreeWalker = @import("../../TreeWalker.zig");
|
||||||
var walker = TreeWalker.Full.init(page.document.asNode(), .{});
|
var walker = TreeWalker.Full.init(root, .{});
|
||||||
|
|
||||||
while (walker.next()) |node| {
|
while (walker.next()) |node| {
|
||||||
const other_element = node.is(Element) orelse continue;
|
const other_element = node.is(Element) orelse continue;
|
||||||
const other_input = other_element.is(Input) orelse continue;
|
const other_input = other_element.is(Input) orelse continue;
|
||||||
|
|
||||||
|
// Skip self
|
||||||
|
if (other_input == self) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (other_input._input_type != .radio) {
|
if (other_input._input_type != .radio) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -651,5 +660,6 @@ pub const Build = struct {
|
|||||||
const testing = @import("../../../../testing.zig");
|
const testing = @import("../../../../testing.zig");
|
||||||
test "WebApi: HTML.Input" {
|
test "WebApi: HTML.Input" {
|
||||||
try testing.htmlRunner("element/html/input.html", .{});
|
try testing.htmlRunner("element/html/input.html", .{});
|
||||||
|
try testing.htmlRunner("element/html/input_click.html", .{});
|
||||||
try testing.htmlRunner("element/html/input_radio.html", .{});
|
try testing.htmlRunner("element/html/input_radio.html", .{});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1255,7 +1255,6 @@ pub const Transfer = struct {
|
|||||||
client.endTransfer(self);
|
client.endTransfer(self);
|
||||||
}
|
}
|
||||||
self.deinit();
|
self.deinit();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn terminate(self: *Transfer) void {
|
pub fn terminate(self: *Transfer) void {
|
||||||
|
|||||||
Reference in New Issue
Block a user