1use crate::{BaseDocument, ElementData, Node as BlitzDomNode, local_name};
2use accesskit::{Node as AccessKitNode, NodeId, Role, Tree, TreeId, TreeUpdate};
3
4impl BaseDocument {
5 pub fn build_accessibility_tree(&self) -> TreeUpdate {
6 let mut nodes = std::collections::HashMap::new();
7 let mut window = AccessKitNode::new(Role::Window);
8
9 self.visit(|node_id, node| {
10 let parent = node
11 .parent
12 .and_then(|parent_id| nodes.get_mut(&parent_id))
13 .map(|(_, parent)| parent)
14 .unwrap_or(&mut window);
15 let (id, builder) = self.build_accessibility_node(node, parent);
16
17 nodes.insert(node_id, (id, builder));
18 });
19
20 let mut nodes: Vec<_> = nodes
21 .into_iter()
22 .map(|(_, (id, node))| (id, node))
23 .collect();
24 nodes.push((NodeId(u64::MAX), window));
25
26 let tree = Tree::new(NodeId(u64::MAX));
27 TreeUpdate {
28 tree_id: TreeId::ROOT,
29 nodes,
30 tree: Some(tree),
31 focus: NodeId(self.focus_node_id.map(|id| id.as_u64()).unwrap_or(u64::MAX)),
32 }
33 }
34
35 fn build_accessibility_node(
36 &self,
37 node: &BlitzDomNode,
38 parent: &mut AccessKitNode,
39 ) -> (NodeId, AccessKitNode) {
40 let id = NodeId(node.id.as_u64());
41
42 let mut builder = AccessKitNode::default();
43 if node.parent.is_none() {
44 builder.set_role(Role::Window)
45 } else if let Some(element_data) = node.element_data() {
46 let name = element_data.name.local.to_string();
47 let role_attr = element_data.attr(local_name!("role"));
48
49 let role = role_attr
52 .and_then(role_from_name)
53 .or_else(|| role_from_element_data(element_data))
54 .unwrap_or(Role::Unknown);
55
56 builder.set_role(role);
57 builder.set_html_tag(name);
58 } else if node.is_text_node() {
59 builder.set_role(Role::TextRun);
60 builder.set_value(node.text_content());
61 parent.push_labelled_by(id)
62 }
63
64 parent.push_child(id);
65
66 (id, builder)
67 }
68}
69
70fn role_from_name(name: &str) -> Option<Role> {
71 match name {
72 "alert" => Some(Role::Alert),
73 "alertdialog" => Some(Role::AlertDialog),
74 "button" => Some(Role::Button),
75 "checkbox" => Some(Role::CheckBox),
76 "dialog" => Some(Role::Dialog),
77 "gridcell" => Some(Role::GridCell),
78 "link" => Some(Role::Link),
79 "log" => Some(Role::Log),
80 "marquee" => Some(Role::Marquee),
81 "menuitem" => Some(Role::MenuItem),
82 "menuitemcheckbox" => Some(Role::MenuItemCheckBox),
83 "menuitemradio" => Some(Role::MenuItemRadio),
84 "option" => Some(Role::ListBoxOption),
85 "progressbar" => Some(Role::ProgressIndicator),
86 "radio" => Some(Role::RadioButton),
87 "scrollbar" => Some(Role::ScrollBar),
88 "slider" => Some(Role::Slider),
89 "spinbutton" => Some(Role::SpinButton),
90 "status" => Some(Role::Status),
91 "tab" => Some(Role::Tab),
92 "tabpanel" => Some(Role::TabPanel),
93 "textbox" => Some(Role::TextInput),
94 "timer" => Some(Role::Timer),
95 "tooltip" => Some(Role::Tooltip),
96 "treeitem" => Some(Role::TreeItem),
97 "combobox" => Some(Role::ComboBox),
98 "grid" => Some(Role::Grid),
99 "listbox" => Some(Role::ListBox),
100 "menu" => Some(Role::Menu),
101 "menubar" => Some(Role::MenuBar),
102 "radiogroup" => Some(Role::RadioGroup),
103 "tablist" => Some(Role::TabList),
104 "tree" => Some(Role::Tree),
105 "treegrid" => Some(Role::TreeGrid),
106 "article" => Some(Role::Article),
107 "columnheader" => Some(Role::ColumnHeader),
108 "definition" => Some(Role::Definition),
109 "document" => Some(Role::Document),
110 "group" => Some(Role::Group),
111 "heading" => Some(Role::Heading),
112 "img" => Some(Role::Image),
113 "list" => Some(Role::List),
114 "listitem" => Some(Role::ListItem),
115 "math" => Some(Role::Math),
116 "note" => Some(Role::Note),
117 "region" => Some(Role::Region),
118 "row" => Some(Role::Row),
119 "rowgroup" => Some(Role::RowGroup),
120 "rowheader" => Some(Role::RowHeader),
121 "toolbar" => Some(Role::Toolbar),
122 "application" => Some(Role::Application),
123 "banner" => Some(Role::Banner),
124 "complementary" => Some(Role::Complementary),
125 "contentinfo" => Some(Role::ContentInfo),
126 "form" => Some(Role::Form),
127 "main" => Some(Role::Main),
128 "navigation" => Some(Role::Navigation),
129 "search" => Some(Role::Search),
130 _ => None,
131 }
132}
133
134fn role_from_element_data(element_data: &ElementData) -> Option<Role> {
135 match &*element_data.name.local {
137 "article" => Some(Role::Article),
139 "aside" => Some(Role::Complementary),
140 "footer" => Some(Role::Footer),
141 "header" => Some(Role::Header),
142 "main" => Some(Role::Main),
143 "nav" => Some(Role::Navigation),
144 "search" => Some(Role::Search),
145 "section" => Some(Role::Section),
146 "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => Some(Role::Heading),
147 "p" => Some(Role::Paragraph),
148 "blockquote" => Some(Role::Blockquote),
149 "figure" => Some(Role::Figure),
150 "figcaption" | "caption" => Some(Role::Caption),
151 "hr" => Some(Role::Splitter),
152
153 "ul" | "ol" | "menu" => Some(Role::List),
155 "li" => Some(Role::ListItem),
156 "dl" => Some(Role::DescriptionList),
157 "dt" => Some(Role::Term),
158 "dd" => Some(Role::Definition),
159 "dialog" => Some(Role::Dialog),
160 "fieldset" => Some(Role::Group),
161 "form" => Some(Role::Form),
162 "div" => Some(Role::GenericContainer),
163
164 "table" => Some(Role::Table),
166 "thead" | "tbody" | "tfoot" => Some(Role::RowGroup),
167 "tr" => Some(Role::Row),
168 "td" => Some(Role::Cell),
169 "th" => match element_data.attr(local_name!("scope")) {
170 Some("row") | Some("rowgroup") => Some(Role::RowHeader),
171 _ => Some(Role::ColumnHeader),
172 },
173
174 "a" => match element_data.attr(local_name!("href")) {
177 Some(_) => Some(Role::Link),
178 None => Some(Role::GenericContainer),
179 },
180 "button" => Some(Role::Button),
181 "label" => Some(Role::Label),
182 "legend" => Some(Role::Label),
183 "select" => match element_data.attr(local_name!("multiple")) {
184 Some(_) => Some(Role::ListBox),
185 None => Some(Role::ComboBox),
186 },
187 "option" => Some(Role::ListBoxOption),
188 "textarea" => Some(Role::MultilineTextInput),
189 "progress" => Some(Role::ProgressIndicator),
190 "meter" => Some(Role::Meter),
191 "output" => Some(Role::Status),
192 "summary" => Some(Role::DisclosureTriangle),
193
194 "code" => Some(Role::Code),
196 "em" => Some(Role::Emphasis),
197 "strong" => Some(Role::Strong),
198 "mark" => Some(Role::Mark),
199 "time" => Some(Role::Time),
200 "img" => Some(Role::Image),
201 "iframe" => Some(Role::Iframe),
202
203 "input" => {
204 let ty = element_data.attr(local_name!("type")).unwrap_or("text");
205 match ty {
206 "button" | "submit" | "reset" => Some(Role::Button),
207 "checkbox" => Some(Role::CheckBox),
208 "color" => Some(Role::ColorWell),
209 "date" => Some(Role::DateInput),
210 "datetime-local" => Some(Role::DateTimeInput),
211 "email" => Some(Role::EmailInput),
212 "number" => Some(Role::NumberInput),
213 "password" => Some(Role::PasswordInput),
214 "radio" => Some(Role::RadioButton),
215 "range" => Some(Role::Slider),
216 "search" => Some(Role::SearchInput),
217 "tel" => Some(Role::PhoneNumberInput),
218 "time" => Some(Role::TimeInput),
219 _ => Some(Role::TextInput),
220 }
221 }
222 _ => None,
223 }
224}