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.id == 0 {
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            // TODO match more roles
49            let role = match &*name {
50                "button" => Role::Button,
51                "div" => Role::GenericContainer,
52                "header" => Role::Header,
53                "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => Role::Heading,
54                "p" => Role::Paragraph,
55                "section" => Role::Section,
56                "input" => {
57                    let ty = element_data.attr(local_name!("type")).unwrap_or("text");
58                    match ty {
59                        "number" => Role::NumberInput,
60                        "checkbox" => Role::CheckBox,
61                        _ => Role::TextInput,
62                    }
63                }
64                _ => Role::Unknown,
65            };
66
67            builder.set_role(role);
68            builder.set_html_tag(name);
69        } else if node.is_text_node() {
70            builder.set_role(Role::TextRun);
71            builder.set_value(node.text_content());
72            parent.push_labelled_by(id)
73        }
74
75        parent.push_child(id);
76
77        (id, builder)
78    }
79}