Skip to main content

blitz_dom/
accessibility.rs

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            // TODO: The roles of elements with strong native semantics cannot be overridden; see
50            // https://www.w3.org/TR/wai-aria-1.2/#host_general_conflict.
51            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    // <https://www.w3.org/TR/html-aam-1.0/>
136    match &*element_data.name.local {
137        // Document structure
138        "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        // Grouping
154        "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        // Tables
165        "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        // Interactive
175        // An <a> is only a link when it has an href.
176        "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        // Inline semantics
195        "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}