From 567cd973122851df242be06c5fff4599060d0510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Tue, 24 Mar 2026 13:13:53 +0900 Subject: [PATCH] webapi.Element: centralize disabled state logic --- src/browser/forms.zig | 36 +------------------------ src/browser/interactive.zig | 32 +--------------------- src/browser/webapi/Element.zig | 26 ++++++++++++++++++ src/browser/webapi/net/FormData.zig | 42 +---------------------------- src/cdp/AXNode.zig | 8 +++--- 5 files changed, 33 insertions(+), 111 deletions(-) diff --git a/src/browser/forms.zig b/src/browser/forms.zig index 5277c614..adb0ba2b 100644 --- a/src/browser/forms.zig +++ b/src/browser/forms.zig @@ -179,15 +179,13 @@ fn collectFormFields( page: *Page, ) ![]FormField { var fields: std.ArrayList(FormField) = .empty; - const form_node = form.asNode(); var elements = try form.getElements(page); var it = try elements.iterator(); while (it.next()) |el| { const node = el.asNode(); - const is_disabled = el.getAttributeSafe(comptime .wrap("disabled")) != null or - isDisabledByFieldset(el, form_node); + const is_disabled = el.isDisabled(); if (el.is(Element.Html.Input)) |input| { if (input._input_type == .hidden) continue; @@ -267,38 +265,6 @@ fn collectSelectOptions( return options.items; } -/// Returns true if `element` is disabled by an ancestor
, -/// stopping at the form boundary. -/// Per spec, elements inside the first child of a disabled fieldset -/// are NOT disabled by that fieldset. -fn isDisabledByFieldset(element: *Element, form_node: *Node) bool { - const element_node = element.asNode(); - var current: ?*Node = element_node._parent; - while (current) |node| { - if (node == form_node) { - return false; - } - - current = node._parent; - const el = node.is(Element) orelse continue; - - if (el.getTag() == .fieldset and el.getAttributeSafe(comptime .wrap("disabled")) != null) { - var child = el.firstElementChild(); - while (child) |c| { - if (c.getTag() == .legend) { - if (c.asNode().contains(element_node)) { - return false; - } - break; - } - child = c.nextElementSibling(); - } - return true; - } - } - return false; -} - const testing = @import("../testing.zig"); fn testForms(html: []const u8) ![]FormInfo { diff --git a/src/browser/interactive.zig b/src/browser/interactive.zig index 944205bd..75226ad1 100644 --- a/src/browser/interactive.zig +++ b/src/browser/interactive.zig @@ -162,7 +162,7 @@ pub fn collectInteractiveElements( .name = try getAccessibleName(el, arena), .interactivity_type = itype, .listener_types = listener_types, - .disabled = isDisabled(el), + .disabled = el.isDisabled(), .tab_index = html_el.getTabIndex(), .id = el.getAttributeSafe(comptime .wrap("id")), .class = el.getAttributeSafe(comptime .wrap("class")), @@ -412,36 +412,6 @@ fn getTextContent(node: *Node, arena: Allocator) !?[]const u8 { // strip out trailing space return arr.items[0 .. arr.items.len - 1]; } -fn isDisabled(el: *Element) bool { - if (el.getAttributeSafe(comptime .wrap("disabled")) != null) return true; - return isDisabledByFieldset(el); -} - -/// Check if an element is disabled by an ancestor
. -/// Per spec, elements inside the first child of a disabled fieldset -/// are NOT disabled by that fieldset. -fn isDisabledByFieldset(el: *Element) bool { - const element_node = el.asNode(); - var current: ?*Node = element_node._parent; - while (current) |node| { - current = node._parent; - const ancestor = node.is(Element) orelse continue; - - if (ancestor.getTag() == .fieldset and ancestor.getAttributeSafe(comptime .wrap("disabled")) != null) { - // Check if element is inside the first child of this fieldset - var child = ancestor.firstElementChild(); - while (child) |c| { - if (c.getTag() == .legend) { - if (c.asNode().contains(element_node)) return false; - break; - } - child = c.nextElementSibling(); - } - return true; - } - } - return false; -} fn getInputType(el: *Element) ?[]const u8 { if (el.is(Element.Html.Input)) |input| { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index e9f77e45..67c1e319 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -573,6 +573,32 @@ pub fn hasAttributeSafe(self: *const Element, name: String) bool { return attributes.hasSafe(name); } +pub fn isDisabled(self: *Element) bool { + if (self.getAttributeSafe(comptime .wrap("disabled")) != null) { + return true; + } + + const element_node = self.asNode(); + var current: ?*Node = element_node._parent; + while (current) |node| { + current = node._parent; + const ancestor = node.is(Element) orelse continue; + + if (ancestor.getTag() == .fieldset and ancestor.getAttributeSafe(comptime .wrap("disabled")) != null) { + var child = ancestor.firstElementChild(); + while (child) |c| { + if (c.getTag() == .legend) { + if (c.asNode().contains(element_node)) return false; + break; + } + child = c.nextElementSibling(); + } + return true; + } + } + return false; +} + pub fn hasAttributes(self: *const Element) bool { const attributes = self._attributes orelse return false; return attributes.isEmpty() == false; diff --git a/src/browser/webapi/net/FormData.zig b/src/browser/webapi/net/FormData.zig index 48b8d0af..69246f93 100644 --- a/src/browser/webapi/net/FormData.zig +++ b/src/browser/webapi/net/FormData.zig @@ -125,15 +125,10 @@ fn collectForm(arena: Allocator, form_: ?*Form, submitter_: ?*Element, page: *Pa var list: KeyValueList = .empty; const form = form_ orelse return list; - const form_node = form.asNode(); - var elements = try form.getElements(page); var it = try elements.iterator(); while (it.next()) |element| { - if (element.getAttributeSafe(comptime .wrap("disabled")) != null) { - continue; - } - if (isDisabledByFieldset(element, form_node)) { + if (element.isDisabled()) { continue; } @@ -202,41 +197,6 @@ fn collectForm(arena: Allocator, form_: ?*Form, submitter_: ?*Element, page: *Pa return list; } -// Returns true if `element` is disabled by an ancestor
, -// stopping the upward walk when the form node is reached. -// Per spec, elements inside the first child of a disabled fieldset -// are NOT disabled by that fieldset. -fn isDisabledByFieldset(element: *Element, form_node: *Node) bool { - const element_node = element.asNode(); - var current: ?*Node = element_node._parent; - while (current) |node| { - // Stop at the form boundary (common case optimisation) - if (node == form_node) { - return false; - } - - current = node._parent; - const el = node.is(Element) orelse continue; - - if (el.getTag() == .fieldset and el.getAttributeSafe(comptime .wrap("disabled")) != null) { - // Check if `element` is inside the first child of this fieldset - var child = el.firstElementChild(); - while (child) |c| { - if (c.getTag() == .legend) { - // Found the first legend; exempt if element is a descendant - if (c.asNode().contains(element_node)) { - return false; - } - break; - } - child = c.nextElementSibling(); - } - return true; - } - } - return false; -} - pub const JsApi = struct { pub const bridge = js.Bridge(FormData); diff --git a/src/cdp/AXNode.zig b/src/cdp/AXNode.zig index 41a8e085..7e10c562 100644 --- a/src/cdp/AXNode.zig +++ b/src/cdp/AXNode.zig @@ -295,7 +295,7 @@ pub const Writer = struct { }, .input => { const input = el.as(DOMNode.Element.Html.Input); - const is_disabled = el.hasAttributeSafe(comptime .wrap("disabled")); + const is_disabled = el.isDisabled(); switch (input._input_type) { .text, .email, .tel, .url, .search, .password, .number => { @@ -332,7 +332,7 @@ pub const Writer = struct { } }, .textarea => { - const is_disabled = el.hasAttributeSafe(comptime .wrap("disabled")); + const is_disabled = el.isDisabled(); try self.writeAXProperty(.{ .name = .invalid, .value = .{ .token = "false" } }, w); if (!is_disabled) { @@ -347,7 +347,7 @@ pub const Writer = struct { try self.writeAXProperty(.{ .name = .required, .value = .{ .boolean = el.hasAttributeSafe(comptime .wrap("required")) } }, w); }, .select => { - const is_disabled = el.hasAttributeSafe(comptime .wrap("disabled")); + const is_disabled = el.isDisabled(); try self.writeAXProperty(.{ .name = .invalid, .value = .{ .token = "false" } }, w); if (!is_disabled) { @@ -391,7 +391,7 @@ pub const Writer = struct { } }, .button => { - const is_disabled = el.hasAttributeSafe(comptime .wrap("disabled")); + const is_disabled = el.isDisabled(); try self.writeAXProperty(.{ .name = .invalid, .value = .{ .token = "false" } }, w); if (!is_disabled) { try self.writeAXProperty(.{ .name = .focusable, .value = .{ .booleanOrUndefined = true } }, w);