-
-
Notifications
You must be signed in to change notification settings - Fork 119
Expand file tree
/
Copy pathAccessibleNameCalculator.cs
More file actions
130 lines (113 loc) · 3.39 KB
/
AccessibleNameCalculator.cs
File metadata and controls
130 lines (113 loc) · 3.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
using AngleSharp.Dom;
namespace Bunit.Roles;
internal static class AccessibleNameCalculator
{
internal static string? GetAccessibleName(IElement element, INodeList rootNodes)
{
// 1. aria-labelledby — resolve each referenced ID, join with spaces
var labelledBy = element.GetAttribute("aria-labelledby");
if (!string.IsNullOrEmpty(labelledBy))
{
var ids = labelledBy.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var parts = new List<string>();
foreach (var id in ids)
{
var referenced = rootNodes.TryQuerySelector($"#{id}");
if (referenced is not null)
{
var text = referenced.TextContent.Trim();
if (!string.IsNullOrEmpty(text))
parts.Add(text);
}
}
if (parts.Count > 0)
return string.Join(" ", parts);
}
// 2. aria-label attribute
var ariaLabel = element.GetAttribute("aria-label");
if (!string.IsNullOrEmpty(ariaLabel))
return ariaLabel.Trim();
// 3. For IMG, AREA, INPUT[type=image]: alt attribute
var isImageInput = element.NodeName == "INPUT" &&
string.Equals(element.GetAttribute("type"), "image", StringComparison.OrdinalIgnoreCase);
if (element.NodeName is "IMG" or "AREA" || isImageInput)
{
var alt = element.GetAttribute("alt");
if (!string.IsNullOrEmpty(alt))
return alt.Trim();
}
// 4. For form controls: associated <label>
if (element.NodeName is "INPUT" or "SELECT" or "TEXTAREA" or "METER" or "OUTPUT" or "PROGRESS")
{
var labelName = GetLabelName(element, rootNodes);
if (labelName is not null)
return labelName;
}
// 5. Text content (for buttons, links, headings, etc.)
var textContent = element.TextContent.Trim();
if (!string.IsNullOrEmpty(textContent))
return textContent;
// 6. title attribute (fallback)
var title = element.GetAttribute("title");
if (!string.IsNullOrEmpty(title))
return title.Trim();
// 7. placeholder attribute (fallback for inputs)
if (element.NodeName is "INPUT" or "TEXTAREA")
{
var placeholder = element.GetAttribute("placeholder");
if (!string.IsNullOrEmpty(placeholder))
return placeholder.Trim();
}
return null;
}
private static string? GetLabelName(IElement element, INodeList rootNodes)
{
// Check for label via "for" attribute matching element's id
var id = element.GetAttribute("id");
if (!string.IsNullOrEmpty(id))
{
var labels = rootNodes.TryQuerySelectorAll($"label[for='{id}']");
foreach (var label in labels)
{
var text = label.TextContent.Trim();
if (!string.IsNullOrEmpty(text))
return text;
}
}
// Check for wrapping <label>
var parent = element.ParentElement;
while (parent is not null)
{
if (parent.NodeName == "LABEL")
{
var text = GetTextContentExcluding(parent, element).Trim();
if (!string.IsNullOrEmpty(text))
return text;
}
parent = parent.ParentElement;
}
return null;
}
private static string GetTextContentExcluding(IElement container, IElement excluded)
{
var parts = new List<string>();
CollectTextNodes(container, excluded, parts);
return string.Join("", parts);
}
private static void CollectTextNodes(INode node, IElement excluded, List<string> parts)
{
foreach (var child in node.ChildNodes)
{
if (child == excluded)
continue;
if (child.NodeType == NodeType.Text)
{
parts.Add(child.TextContent);
}
else if (child.NodeType == NodeType.Element)
{
CollectTextNodes(child, excluded, parts);
}
}
}
}