Skip to content

Commit 96ff533

Browse files
authored
Remove hidden nodes from the acccessibility tree (#823)
* Remove hidden nodes from the acccessibility tree This is as per https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion. It also facilitates testing: in cases where there is only one non-hidden element of a particular role, one can just query for that role without having to specify the query more narrowly. * Add tests * Move tests to blitz-tests * Make function into method of BlitzDomNode * Fix test * Remove redundant tests * Use HtmlDocument to simplify test setup * Remove extra check for hidden attribute -- it's not required * Also exclude from accessibility tree nodes descended from hidden nodes, as per the spec
1 parent a50cb89 commit 96ff533

6 files changed

Lines changed: 202 additions & 2 deletions

File tree

‎Cargo.lock‎

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ smol_str = "0.3"
188188
bitflags = "2.8.0"
189189
bytemuck = "1"
190190
rayon = "1"
191+
test-that = "0.5.2"
191192
thread_local = "1"
192193

193194
[profile.profile]

‎packages/blitz-dom/src/accessibility.rs‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
use crate::{BaseDocument, ElementData, Node as BlitzDomNode, local_name};
22
use accesskit::{Node as AccessKitNode, NodeId, Role, Tree, TreeId, TreeUpdate};
3+
use style::properties::longhands::visibility;
34

45
impl BaseDocument {
56
pub fn build_accessibility_tree(&self) -> TreeUpdate {
67
let mut nodes = std::collections::HashMap::new();
78
let mut window = AccessKitNode::new(Role::Window);
9+
let mut hidden_nodes = std::collections::HashSet::new();
810

911
self.visit(|node_id, node| {
12+
if node.is_hidden_from_accessibility_tree()
13+
|| node
14+
.parent
15+
.map(|p| hidden_nodes.contains(&p))
16+
.unwrap_or(false)
17+
{
18+
hidden_nodes.insert(node_id);
19+
return;
20+
}
1021
let parent = node
1122
.parent
1223
.and_then(|parent_id| nodes.get_mut(&parent_id))
@@ -55,6 +66,11 @@ impl BaseDocument {
5566

5667
builder.set_role(role);
5768
builder.set_html_tag(name);
69+
70+
// https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
71+
if element_data.attr(local_name!("aria-hidden")) == Some("true") {
72+
builder.set_hidden();
73+
}
5874
} else if node.is_text_node() {
5975
builder.set_role(Role::TextRun);
6076
builder.set_value(node.text_content());
@@ -67,6 +83,21 @@ impl BaseDocument {
6783
}
6884
}
6985

86+
impl BlitzDomNode {
87+
// https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
88+
fn is_hidden_from_accessibility_tree(&self) -> bool {
89+
self.try_stylo_element_data()
90+
.as_ref()
91+
.and_then(|s| s.get())
92+
.map(|s| {
93+
s.styles.is_display_none()
94+
|| s.styles.primary().clone_visibility()
95+
== visibility::computed_value::T::Hidden
96+
})
97+
.unwrap_or(false)
98+
}
99+
}
100+
70101
fn role_from_name(name: &str) -> Option<Role> {
71102
match name {
72103
"alert" => Some(Role::Alert),

‎tests/blitz-tests/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ accesskit = { workspace = true }
3131
markup5ever = { workspace = true }
3232
keyboard-types = { workspace = true }
3333
taffy = { workspace = true }
34+
test-that = { workspace = true }
3435
usvg = { workspace = true }
3536

3637
[lib]
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
use accesskit::{Node as AccessKitNode, Role};
2+
use blitz_dom::DocumentConfig;
3+
use blitz_html::{HtmlDocument, HtmlProvider};
4+
use blitz_traits::shell::{ColorScheme, Viewport};
5+
use std::sync::Arc;
6+
use test_that::prelude::*;
7+
8+
#[test]
9+
fn includes_ordinary_div_as_node() -> TestResult<()> {
10+
let mut document =
11+
HtmlDocument::from_html("<html><div></div></html>", default_document_config());
12+
document.resolve(0.0);
13+
14+
let tree_update = document.build_accessibility_tree();
15+
16+
verify_that!(
17+
tree_update.nodes,
18+
contains((
19+
anything(),
20+
matches_pattern!(AccessKitNode {
21+
role(): eq(Role::GenericContainer),
22+
is_hidden(): eq(false),
23+
})
24+
))
25+
)
26+
}
27+
28+
#[test]
29+
fn excludes_div_with_hidden_attribute() -> TestResult<()> {
30+
let mut document =
31+
HtmlDocument::from_html("<html><div hidden></div></html>", default_document_config());
32+
document.resolve(0.0);
33+
34+
let tree_update = document.build_accessibility_tree();
35+
36+
verify_that!(
37+
tree_update.nodes,
38+
not(contains((
39+
anything(),
40+
matches_pattern!(AccessKitNode {
41+
role(): eq(Role::GenericContainer)
42+
})
43+
)))
44+
)
45+
}
46+
47+
#[test]
48+
fn excludes_div_with_display_none() -> TestResult<()> {
49+
let mut document = HtmlDocument::from_html(
50+
r#"<html><div style="display: none;"></div></html>"#,
51+
default_document_config(),
52+
);
53+
document.resolve(0.0);
54+
55+
let tree_update = document.build_accessibility_tree();
56+
57+
verify_that!(
58+
tree_update.nodes,
59+
not(contains((
60+
anything(),
61+
matches_pattern!(AccessKitNode {
62+
role(): eq(Role::GenericContainer)
63+
})
64+
)))
65+
)
66+
}
67+
68+
#[test]
69+
fn excludes_div_with_visibility_hidden() -> TestResult<()> {
70+
let mut document = HtmlDocument::from_html(
71+
r#"<html><div style="visibility: hidden;"></div></html>"#,
72+
default_document_config(),
73+
);
74+
document.resolve(0.0);
75+
76+
let tree_update = document.build_accessibility_tree();
77+
78+
verify_that!(
79+
tree_update.nodes,
80+
not(contains((
81+
anything(),
82+
matches_pattern!(AccessKitNode {
83+
role(): eq(Role::GenericContainer)
84+
})
85+
)))
86+
)
87+
}
88+
89+
#[test]
90+
fn sets_hidden_flag_on_element_with_aria_hidden_attribute() -> TestResult<()> {
91+
let mut document = HtmlDocument::from_html(
92+
r#"<html><div aria-hidden="true"></div></html>"#,
93+
default_document_config(),
94+
);
95+
document.resolve(0.0);
96+
97+
let tree_update = document.build_accessibility_tree();
98+
99+
verify_that!(
100+
tree_update.nodes,
101+
contains((
102+
anything(),
103+
matches_pattern!(AccessKitNode {
104+
role(): eq(Role::GenericContainer),
105+
is_hidden(): eq(true),
106+
})
107+
))
108+
)
109+
}
110+
111+
#[test]
112+
fn excludes_child_element_of_hidden_element() -> TestResult<()> {
113+
let mut document = HtmlDocument::from_html(
114+
r#"<html>
115+
<head/>
116+
<body>
117+
<div hidden="true">
118+
<button/>
119+
</div>
120+
</body>
121+
</html>"#,
122+
default_document_config(),
123+
);
124+
document.resolve(0.0);
125+
126+
let tree_update = document.build_accessibility_tree();
127+
128+
verify_that!(
129+
tree_update.nodes,
130+
not(contains((
131+
anything(),
132+
matches_pattern!(AccessKitNode {
133+
role(): eq(Role::Button),
134+
})
135+
)))
136+
)
137+
}
138+
139+
fn default_document_config() -> DocumentConfig {
140+
DocumentConfig {
141+
viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
142+
html_parser_provider: Some(Arc::new(HtmlProvider) as _),
143+
..Default::default()
144+
}
145+
}

‎tests/blitz-tests/tests/accessibility_roles.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ fn a_semantic_page_has_no_unknown_elements() {
192192
<footer>End</footer>
193193
</body></html>"##;
194194

195-
// <html>, <head> and <body> have no roles of their own. Everything else in
195+
// <html> and <body> have no roles of their own. Everything else in
196196
// this document should map to something an assistive technology can use.
197-
assert_eq!(unknown_tags(html), vec!["body", "head", "html"]);
197+
assert_eq!(unknown_tags(html), vec!["body", "html"]);
198198
}

0 commit comments

Comments
 (0)