interactive: properly concatenate text content for accessible names

This fixes a bug where only the first text node was being returned, causing fragmented text nodes (e.g. <span>Sub</span><span>mit</span>) to be missing their trailing text.
This commit is contained in:
Adrià Arrufat
2026-03-11 09:57:08 +09:00
parent d1ee0442ea
commit a6ccc72d15

View File

@@ -157,7 +157,7 @@ pub fn collectInteractiveElements(
.node = node, .node = node,
.tag_name = el.getTagNameLower(), .tag_name = el.getTagNameLower(),
.role = getRole(el), .role = getRole(el),
.name = getAccessibleName(el), .name = try getAccessibleName(el, arena),
.interactivity_type = itype, .interactivity_type = itype,
.listener_types = listener_types, .listener_types = listener_types,
.disabled = isDisabled(el), .disabled = isDisabled(el),
@@ -296,7 +296,7 @@ fn getRole(el: *Element) ?[]const u8 {
}; };
} }
fn getAccessibleName(el: *Element) ?[]const u8 { fn getAccessibleName(el: *Element, arena: Allocator) !?[]const u8 {
// aria-label // aria-label
if (el.getAttributeSafe(comptime .wrap("aria-label"))) |v| { if (el.getAttributeSafe(comptime .wrap("aria-label"))) |v| {
if (v.len > 0) return v; if (v.len > 0) return v;
@@ -325,11 +325,14 @@ fn getAccessibleName(el: *Element) ?[]const u8 {
} }
// Text content (first non-empty text node, trimmed) // Text content (first non-empty text node, trimmed)
return getTextContent(el.asNode()); return try getTextContent(el.asNode(), arena);
} }
fn getTextContent(node: *Node) ?[]const u8 { fn getTextContent(node: *Node, arena: Allocator) !?[]const u8 {
var tw = TreeWalker.FullExcludeSelf.init(node, .{}); var tw: TreeWalker.FullExcludeSelf = .init(node, .{});
var chunks: std.ArrayList([]const u8) = .empty;
while (tw.next()) |child| { while (tw.next()) |child| {
// Skip text inside script/style elements. // Skip text inside script/style elements.
if (child.is(Element)) |el| { if (child.is(Element)) |el| {
@@ -344,13 +347,18 @@ fn getTextContent(node: *Node) ?[]const u8 {
if (child.is(Node.CData)) |cdata| { if (child.is(Node.CData)) |cdata| {
if (cdata.is(Node.CData.Text)) |text| { if (cdata.is(Node.CData.Text)) |text| {
const content = std.mem.trim(u8, text.getWholeText(), &std.ascii.whitespace); const content = std.mem.trim(u8, text.getWholeText(), &std.ascii.whitespace);
if (content.len > 0) return content; if (content.len > 0) {
try chunks.append(arena, content);
}
} }
} }
} }
return null;
}
if (chunks.items.len == 0) return null;
if (chunks.items.len == 1) return chunks.items[0];
return try std.mem.join(arena, " ", chunks.items);
}
fn isDisabled(el: *Element) bool { fn isDisabled(el: *Element) bool {
if (el.getAttributeSafe(comptime .wrap("disabled")) != null) return true; if (el.getAttributeSafe(comptime .wrap("disabled")) != null) return true;
return isDisabledByFieldset(el); return isDisabledByFieldset(el);