Merge pull request #3275 from lightpanda-io/interactive-role-tokens

interactive: read the role attribute as a token list
This commit is contained in:
Karl Seguin authored and GitHub committed 2026-08-26 13:54:29 +08:00
commit fc7b53b59c
2 files changed
+46 -11

No files matched your search

+26 -3
View File
@@ -295,7 +295,7 @@ pub fn classifyInteractivity(
}
// 2. ARIA interactive role
if (el.getAttributeSafe(comptime .wrap("role"))) |role| {
if (explicitRole(el)) |role| {
if (isInteractiveRole(role)) return .aria;
}
@@ -367,9 +367,15 @@ pub fn isContentRole(role: []const u8) bool {
return content_roles.has(lowered);
}
// ARIA `role` is a space-separated fallback list; the first token wins.
pub fn explicitRole(el: *Element) ?[]const u8 {
const attr = el.getAttributeSafe(comptime .wrap("role")) orelse return null;
var it = std.mem.tokenizeAny(u8, attr, " \t\n\r");
return it.next();
}
fn getRole(el: *Element) ?[]const u8 {
// Explicit role attribute takes precedence
if (el.getAttributeSafe(comptime .wrap("role"))) |role| return role;
if (explicitRole(el)) |role| return role;
// Implicit role from tag
return switch (el.getTag()) {
@@ -563,6 +569,23 @@ test "browser.interactive: aria role" {
try testing.expectEqual(InteractivityType.aria, elements[0].interactivity_type);
}
test "browser.interactive: aria role token list" {
const elements = try testInteractive(
\\<div role="switch checkbox">Fallback</div>
\\<div role=" button ">Padded</div>
\\<div role="">Empty</div>
\\<button role=" ">Blank</button>
);
defer testing.test_session.closeAllPages();
try testing.expectEqual(3, elements.len);
try testing.expectEqual("switch", elements[0].role.?);
try testing.expectEqual(InteractivityType.aria, elements[0].interactivity_type);
try testing.expectEqual("button", elements[1].role.?);
try testing.expectEqual(InteractivityType.aria, elements[1].interactivity_type);
try testing.expectEqual("button", elements[2].tag_name);
try testing.expectEqual("button", elements[2].role.?);
}
test "browser.interactive: contenteditable" {
const elements = try testInteractive("<div contenteditable=\"true\">Edit me</div>");
defer testing.test_session.closeAllPages();
+20 -8
View File
@@ -22,6 +22,7 @@ const lp = @import("lightpanda");
const Frame = @import("../browser/Frame.zig");
const DOMNode = @import("../browser/webapi/Node.zig");
const Label = @import("../browser/webapi/element/html/Label.zig");
const interactive = @import("../browser/interactive.zig");
const Node = @import("Node.zig");
@@ -915,13 +916,7 @@ role_attr: ?[]const u8,
pub fn fromNode(dom: *DOMNode) AXNode {
return .{
.dom = dom,
.role_attr = blk: {
if (dom.is(DOMNode.Element.Html) == null) {
break :blk null;
}
const elt = dom.as(DOMNode.Element);
break :blk elt.getAttributeSafe(comptime .wrap("role"));
},
.role_attr = if (dom.is(DOMNode.Element.Html) != null) interactive.explicitRole(dom.as(DOMNode.Element)) else null,
};
}
@@ -1429,7 +1424,6 @@ fn isIgnore(self: AXNode, frame: *Frame, cache: *DOMNode.Element.VisibilityCache
pub fn getRole(self: AXNode) ![]const u8 {
if (self.role_attr) |role_value| {
// TODO the role can have multiple comma separated values.
return role_value;
}
@@ -1790,6 +1784,24 @@ test "AXNode: Writer query filters by role" {
try testing.expectEqual("Visible", name_val);
}
test "AXNode: role attribute token list" {
var page = try testing.pageTest("cdp/accname.html", .{});
defer page.close();
const frame = page.frame().?;
const div = try frame.window._document.createElement("div", null, frame);
try Frame.parse.htmlAsChildren(frame, div.asNode(),
\\<div role="switch checkbox"></div><div role=" heading "></div><h1 role=""></h1>
);
var child = div.asNode().firstChild().?;
try testing.expectEqual("switch", try AXNode.fromNode(child).getRole());
child = child.nextSibling().?;
try testing.expectEqual("heading", try AXNode.fromNode(child).getRole());
child = child.nextSibling().?;
try testing.expectEqual("heading", try AXNode.fromNode(child).getRole());
}
test "AXNode: writer maps password input to textbox" {
var registry = Node.Registry.init(testing.allocator);
defer registry.deinit();