Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AccessKit integration #78

Merged
merged 27 commits into from
Jul 9, 2024
Merged

AccessKit integration #78

merged 27 commits into from
Jul 9, 2024

Conversation

matthunz
Copy link
Contributor

@matthunz matthunz commented Jun 20, 2024

Starts work on AccessKit integration (#14)

  • Connect accesskit-winit adapter
  • Add Dom::visit to traverse the dom top-down
  • Build initial AccessKit tree
  • Send tree changes from Dioxus updates to adapter

I think it's best to wait for more interactivity and focus system design before processing AccessKit events with the DOM 🤔 totally open to trying to add that here though

@matthunz matthunz marked this pull request as draft June 20, 2024 21:38
@matthunz matthunz marked this pull request as ready for review June 21, 2024 20:22
@nicoburns
Copy link
Collaborator

@matthunz Have you tried actually using this with a screenreader?

Comment on lines 369 to 375
#[cfg(feature = "accesskit")]
adapter: accesskit_winit::Adapter::with_event_loop_proxy(&window, proxy.clone()),
#[cfg(feature = "accesskit")]
node_ids: Default::default(),
#[cfg(feature = "accesskit")]
id_to_node_ids: Default::default(),
#[cfg(feature = "accesskit")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps these fields ought to be grouped together in a struct?

Copy link
Contributor Author

@matthunz matthunz Jun 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure, I like this a lot, those cfg flags are super ugly

pub struct AccessibilityState {
/// Adapter to connect to the [`EventLoop`](`winit::event_loop::EventLoop`).
adapter: accesskit_winit::Adapter,
/// Next ID to assign an an [`accesskit::Node`].
next_id: u64,
}

#[cfg(feature = "accesskit")]
node_ids: std::collections::HashMap<accesskit::NodeId, usize>,

#[cfg(feature = "accesskit")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the "accesskit id" ought to be a field on the node itself? Every node will have one, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense to me but would we need to move accesskit to Blitz core for that? For now I removed the field until we have support for accessibility events (that doesn't seem clear to me to implement yet unless you have any ideas 🤔 )

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but would we need to move accesskit to Blitz core for that?

I don't think so? Node IDs are just usize atm, and are accessible from outside of the crate. We'll might want to switch to a newtype at some point, but I'm sure we'll make conversions to plain types available.

#[cfg(feature = "accesskit")]
id_to_node_ids: std::collections::HashMap<usize, accesskit::NodeId>,

#[cfg(feature = "accesskit")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all of these new fields ought to have comments documenting what they are/do.

let name = element_data.name.local.to_string();
let role = match &*name {
"button" => accesskit::Role::Button,
"div" | "section" => accesskit::Role::Group,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AccessKit docs suggest that div ought to be Role::GenericContainer (https://docs.rs/accesskit/latest/accesskit/enum.Role.html#variant.GenericContainer)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like there's a bunch that could easily be plugged in here (e.g. h1, h2 should have Heading role). Happy to defer that until later, but could we at least get a TODO in here?

Copy link
Contributor Author

@matthunz matthunz Jun 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tips 👍 I added those and some others I could think of here so far

// TODO match more roles
let role = match &*name {
"button" => Role::Button,
"div" => Role::GenericContainer,
"header" => Role::Header,
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => Role::Heading,
"p" => Role::Paragraph,
"section" => Role::Section,
"input" => {
let ty = element_data.attr(local_name!("type")).unwrap_or("text");
match ty {
"number" => Role::NumberInput,
_ => Role::TextInput,
}
}
_ => Role::Unknown,
};

@@ -308,6 +398,47 @@ impl<'a, Doc: DocumentLike> View<'a, Doc> {
self.waker = None;
self.renderer.suspend();
}

#[cfg(feature = "accesskit")]
fn build_accessibility_tree(&mut self) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this function (and probably build_node above too) into their own accesskit module?

@@ -87,6 +92,8 @@ pub struct Document {
pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,

pub(crate) hover_node_id: Option<usize>,

pub changed: HashSet<usize>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a HashSet rather than a Vec? It never seems to be randomly queried, only ever iterated over?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My plan was to eventually have this be used for the fine-grained accessibility tree updates but I don't see clear path for that yet 🤔 we could totally switch this to a bool for now if we want to stick with the coarse updates for now.

Personally I think we can get away with coarse-grained for a little while before apps in Blitz start getting overly complex

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, HashSet for deduplicating insertions. Makes sense.

Comment on lines 103 to 107
// TODO send fine grained accessibility tree updates.
let changed = mem::take(&mut self.renderer.dom.as_mut().changed);
if !changed.is_empty() {
self.build_accessibility_tree()
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like it wouldn't be too hard to send a TreeUpdate with just changed nodes here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right this is how we should handle this, I was just having some trouble getting the hierarchy working. The issue I had was creating IDs for the accesskit elements so maybe re-using the Blitz element IDs here like you mentioned could help?

node_builder.set_name(node.text_content());
}

let id = accesskit::NodeId(self.next_id);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh... we get to choose the AccessKit ids... Is there any reason not to use the dom's NodeId directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are able to map 1-1 from nodes (and text nodes) I'd say we should totally do this 👀

Comment on lines +343 to +345
accesskit_winit::WindowEvent::ActionRequested(_req) => {
// TODO
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might it be worth at least implementing "click" here (which of course is the only event we support generally. I think the code could just be copied from the regular click handler (skipping the bit which determines which node to generate the event for as we should be given a node id directly)

Cargo.toml Outdated
euclid = { version = "0.22", features = ["serde"] }
# mozbuild = "0.1.0"

[dev-dependencies]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for moving all of these deps from "dev-dependencies" to "dependencies"?

@matthunz
Copy link
Contributor Author

@matthunz Have you tried actually using this with a screenreader?

Yeah it looks to be reading the elements OK so far, I posted this video in the Dioxus discord a little while back if you missed it

dioxus2.mp4

};

node_builder.set_role(role);
node_builder.set_name(name);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this scope, the variable name seem to always contain the "HTML element type" which is not something the user should see. You could call NodeBuilder::set_html_tag (although we currently don't expose this property to assistive technologies) but you should not use it for the AccessKit node name.

@DataTriny
Copy link

Hello @matthunz, AccessKit dev here.

I've tried a modified version of the accessibility example to add more kind of nodes and my screen reader is already able to pick them up.
One thing you will have to tackle early on is the labelled_by relationship. I tried an h1 and a button and on both cases the text that my screen reader attaches to the node is the HTML element node. I assume that the actual textual content is a child node of the element, so you will have to put the text node's ID in the labelled_by list property on the parent container.

@matthunz
Copy link
Contributor Author

matthunz commented Jul 8, 2024

Hey @DataTriny thanks for your insight!

I switched NodeBuilder::set_name to NodeBuilder::set_html_tag in 9ae0fb7 . If there is a plan to send this to the platform eventually I'd say we could keep it here until then 🤔

I also tried added some basic labelled_by relationships in a882185 . I'm hoping concatenating child text node ids into the parent elements labelled_by field should give the right result.

I'd also really like to connect to aria-labelled-by at some point 👀

Copy link
Collaborator

@nicoburns nicoburns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to accept this as an initial implementation and iterate :)

@nicoburns nicoburns merged commit 57f9f5b into DioxusLabs:main Jul 9, 2024
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants