mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Merge pull request #1537 from lightpanda-io/input_click
Default behavior for input click (radio / checkbox).
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