use crate::{BaseDocument, ElementData, Node as BlitzDomNode, local_name};
use accesskit::{Node as AccessKitNode, NodeId, Role, Tree, TreeId, TreeUpdate};
impl BaseDocument {
pub fn build_accessibility_tree(&self) -> TreeUpdate {
let mut nodes = std::collections::HashMap::new();
let mut window = AccessKitNode::new(Role::Window);
self.visit(|node_id, node| {
let parent = node
.parent
.and_then(|parent_id| nodes.get_mut(&parent_id))
.map(|(_, parent)| parent)
.unwrap_or(&mut window);
let (id, builder) = self.build_accessibility_node(node, parent);
nodes.insert(node_id, (id, builder));
});
let mut nodes: Vec<_> = nodes
.into_iter()
.map(|(_, (id, node))| (id, node))
.collect();
nodes.push((NodeId(u64::MAX), window));
let tree = Tree::new(NodeId(u64::MAX));
TreeUpdate {
tree_id: TreeId::ROOT,
nodes,
tree: Some(tree),
focus: NodeId(self.focus_node_id.map(|id| id.as_u64()).unwrap_or(u64::MAX)),
}
}
fn build_accessibility_node(
&self,
node: &BlitzDomNode,
parent: &mut AccessKitNode,
) -> (NodeId, AccessKitNode) {
let id = NodeId(node.id.as_u64());
let mut builder = AccessKitNode::default();
if node.parent.is_none() {
builder.set_role(Role::Window)
} else if let Some(element_data) = node.element_data() {
let name = element_data.name.local.to_string();
let role_attr = element_data.attr(local_name!("role"));
let role = role_attr
.and_then(role_from_name)
.or_else(|| role_from_element_data(element_data))
.unwrap_or(Role::Unknown);
builder.set_role(role);
builder.set_html_tag(name);
} else if node.is_text_node() {
builder.set_role(Role::TextRun);
builder.set_value(node.text_content());
parent.push_labelled_by(id)
}
parent.push_child(id);
(id, builder)
}
}
fn role_from_name(name: &str) -> Option<Role> {
match name {
"alert" => Some(Role::Alert),
"alertdialog" => Some(Role::AlertDialog),
"button" => Some(Role::Button),
"checkbox" => Some(Role::CheckBox),
"dialog" => Some(Role::Dialog),
"gridcell" => Some(Role::GridCell),
"link" => Some(Role::Link),
"log" => Some(Role::Log),
"marquee" => Some(Role::Marquee),
"menuitem" => Some(Role::MenuItem),
"menuitemcheckbox" => Some(Role::MenuItemCheckBox),
"menuitemradio" => Some(Role::MenuItemRadio),
"option" => Some(Role::ListBoxOption),
"progressbar" => Some(Role::ProgressIndicator),
"radio" => Some(Role::RadioButton),
"scrollbar" => Some(Role::ScrollBar),
"slider" => Some(Role::Slider),
"spinbutton" => Some(Role::SpinButton),
"status" => Some(Role::Status),
"tab" => Some(Role::Tab),
"tabpanel" => Some(Role::TabPanel),
"textbox" => Some(Role::TextInput),
"timer" => Some(Role::Timer),
"tooltip" => Some(Role::Tooltip),
"treeitem" => Some(Role::TreeItem),
"combobox" => Some(Role::ComboBox),
"grid" => Some(Role::Grid),
"listbox" => Some(Role::ListBox),
"menu" => Some(Role::Menu),
"menubar" => Some(Role::MenuBar),
"radiogroup" => Some(Role::RadioGroup),
"tablist" => Some(Role::TabList),
"tree" => Some(Role::Tree),
"treegrid" => Some(Role::TreeGrid),
"article" => Some(Role::Article),
"columnheader" => Some(Role::ColumnHeader),
"definition" => Some(Role::Definition),
"document" => Some(Role::Document),
"group" => Some(Role::Group),
"heading" => Some(Role::Heading),
"img" => Some(Role::Image),
"list" => Some(Role::List),
"listitem" => Some(Role::ListItem),
"math" => Some(Role::Math),
"note" => Some(Role::Note),
"region" => Some(Role::Region),
"row" => Some(Role::Row),
"rowgroup" => Some(Role::RowGroup),
"rowheader" => Some(Role::RowHeader),
"toolbar" => Some(Role::Toolbar),
"application" => Some(Role::Application),
"banner" => Some(Role::Banner),
"complementary" => Some(Role::Complementary),
"contentinfo" => Some(Role::ContentInfo),
"form" => Some(Role::Form),
"main" => Some(Role::Main),
"navigation" => Some(Role::Navigation),
"search" => Some(Role::Search),
_ => None,
}
}
fn role_from_element_data(element_data: &ElementData) -> Option<Role> {
match &*element_data.name.local {
"article" => Some(Role::Article),
"aside" => Some(Role::Complementary),
"footer" => Some(Role::Footer),
"header" => Some(Role::Header),
"main" => Some(Role::Main),
"nav" => Some(Role::Navigation),
"search" => Some(Role::Search),
"section" => Some(Role::Section),
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => Some(Role::Heading),
"p" => Some(Role::Paragraph),
"blockquote" => Some(Role::Blockquote),
"figure" => Some(Role::Figure),
"figcaption" | "caption" => Some(Role::Caption),
"hr" => Some(Role::Splitter),
"ul" | "ol" | "menu" => Some(Role::List),
"li" => Some(Role::ListItem),
"dl" => Some(Role::DescriptionList),
"dt" => Some(Role::Term),
"dd" => Some(Role::Definition),
"dialog" => Some(Role::Dialog),
"fieldset" => Some(Role::Group),
"form" => Some(Role::Form),
"div" => Some(Role::GenericContainer),
"table" => Some(Role::Table),
"thead" | "tbody" | "tfoot" => Some(Role::RowGroup),
"tr" => Some(Role::Row),
"td" => Some(Role::Cell),
"th" => match element_data.attr(local_name!("scope")) {
Some("row") | Some("rowgroup") => Some(Role::RowHeader),
_ => Some(Role::ColumnHeader),
},
"a" => match element_data.attr(local_name!("href")) {
Some(_) => Some(Role::Link),
None => Some(Role::GenericContainer),
},
"button" => Some(Role::Button),
"label" => Some(Role::Label),
"legend" => Some(Role::Label),
"select" => match element_data.attr(local_name!("multiple")) {
Some(_) => Some(Role::ListBox),
None => Some(Role::ComboBox),
},
"option" => Some(Role::ListBoxOption),
"textarea" => Some(Role::MultilineTextInput),
"progress" => Some(Role::ProgressIndicator),
"meter" => Some(Role::Meter),
"output" => Some(Role::Status),
"summary" => Some(Role::DisclosureTriangle),
"code" => Some(Role::Code),
"em" => Some(Role::Emphasis),
"strong" => Some(Role::Strong),
"mark" => Some(Role::Mark),
"time" => Some(Role::Time),
"img" => Some(Role::Image),
"iframe" => Some(Role::Iframe),
"input" => {
let ty = element_data.attr(local_name!("type")).unwrap_or("text");
match ty {
"button" | "submit" | "reset" => Some(Role::Button),
"checkbox" => Some(Role::CheckBox),
"color" => Some(Role::ColorWell),
"date" => Some(Role::DateInput),
"datetime-local" => Some(Role::DateTimeInput),
"email" => Some(Role::EmailInput),
"number" => Some(Role::NumberInput),
"password" => Some(Role::PasswordInput),
"radio" => Some(Role::RadioButton),
"range" => Some(Role::Slider),
"search" => Some(Role::SearchInput),
"tel" => Some(Role::PhoneNumberInput),
"time" => Some(Role::TimeInput),
_ => Some(Role::TextInput),
}
}
_ => None,
}
}