Skip to main content

blitz_dom/
accessibility.rs

1use crate::{BaseDocument, 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
48            // <https://www.w3.org/TR/html-aam-1.0/>
49            let role = match &*name {
50                // Document structure
51                "article" => Role::Article,
52                "aside" => Role::Complementary,
53                "footer" => Role::Footer,
54                "header" => Role::Header,
55                "main" => Role::Main,
56                "nav" => Role::Navigation,
57                "search" => Role::Search,
58                "section" => Role::Section,
59                "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => Role::Heading,
60                "p" => Role::Paragraph,
61                "blockquote" => Role::Blockquote,
62                "figure" => Role::Figure,
63                "figcaption" | "caption" => Role::Caption,
64                "hr" => Role::Splitter,
65
66                // Grouping
67                "ul" | "ol" | "menu" => Role::List,
68                "li" => Role::ListItem,
69                "dl" => Role::DescriptionList,
70                "dt" => Role::Term,
71                "dd" => Role::Definition,
72                "dialog" => Role::Dialog,
73                "fieldset" => Role::Group,
74                "form" => Role::Form,
75                "div" => Role::GenericContainer,
76
77                // Tables
78                "table" => Role::Table,
79                "thead" | "tbody" | "tfoot" => Role::RowGroup,
80                "tr" => Role::Row,
81                "td" => Role::Cell,
82                "th" => match element_data.attr(local_name!("scope")) {
83                    Some("row") | Some("rowgroup") => Role::RowHeader,
84                    _ => Role::ColumnHeader,
85                },
86
87                // Interactive
88                // An <a> is only a link when it has an href.
89                "a" => match element_data.attr(local_name!("href")) {
90                    Some(_) => Role::Link,
91                    None => Role::GenericContainer,
92                },
93                "button" => Role::Button,
94                "label" => Role::Label,
95                "legend" => Role::Label,
96                "select" => match element_data.attr(local_name!("multiple")) {
97                    Some(_) => Role::ListBox,
98                    None => Role::ComboBox,
99                },
100                "option" => Role::ListBoxOption,
101                "textarea" => Role::MultilineTextInput,
102                "progress" => Role::ProgressIndicator,
103                "meter" => Role::Meter,
104                "output" => Role::Status,
105                "summary" => Role::DisclosureTriangle,
106
107                // Inline semantics
108                "code" => Role::Code,
109                "em" => Role::Emphasis,
110                "strong" => Role::Strong,
111                "mark" => Role::Mark,
112                "time" => Role::Time,
113                "img" => Role::Image,
114                "iframe" => Role::Iframe,
115
116                "input" => {
117                    let ty = element_data.attr(local_name!("type")).unwrap_or("text");
118                    match ty {
119                        "button" | "submit" | "reset" => Role::Button,
120                        "checkbox" => Role::CheckBox,
121                        "color" => Role::ColorWell,
122                        "date" => Role::DateInput,
123                        "datetime-local" => Role::DateTimeInput,
124                        "email" => Role::EmailInput,
125                        "number" => Role::NumberInput,
126                        "password" => Role::PasswordInput,
127                        "radio" => Role::RadioButton,
128                        "range" => Role::Slider,
129                        "search" => Role::SearchInput,
130                        "tel" => Role::PhoneNumberInput,
131                        "time" => Role::TimeInput,
132                        _ => Role::TextInput,
133                    }
134                }
135                _ => Role::Unknown,
136            };
137
138            builder.set_role(role);
139            builder.set_html_tag(name);
140        } else if node.is_text_node() {
141            builder.set_role(Role::TextRun);
142            builder.set_value(node.text_content());
143            parent.push_labelled_by(id)
144        }
145
146        parent.push_child(id);
147
148        (id, builder)
149    }
150}