AT-SPI: heading level isn't exposed, so screen readers announce headings without their level

Open Beginner friendly
#860 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
82/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
linux, rust
Domain
accessibility

Research direction

The change belongs in NodeWrapper::attributes() in adapters/atspi-common/src/node.rs, next to the existing placeholder-text and posinset entries. Read how those attributes are inserted and where the node's level is read, then run cargo test -p accesskit_atspi_common. Done when a heading built with set_level(n) yields a "level" attribute in its AT-SPI attributes, and the existing tests still pass.

Written by the indexing model from the issue text.

Description

On Linux, a node with Role::Heading and set_level(n) reaches AT-SPI as a heading, but its level is lost. Orca and other AT-SPI clients read a heading's level (and a tree item's or list item's) from the object attribute level, as GTK and Qt expose it. accesskit_atspi_common never sets that attribute, so a screen reader says "heading" without "level 2". The macOS and Windows adapters do pass the level on.

How to see it: give a tree two headings, set_level(1) and set_level(2). With any AT-SPI client (for example Python's gi.repository.Atspi), read Atspi.Accessible.get_attributes() on each one: the dictionary has no level key.

A possible change (adapters/atspi-common/src/node.rs, NodeWrapper::attributes(), at main dea4384). The level attribute for any node that has one:

@@ fn attributes(&self) -> HashMap<&'static str, String> {
         if let Some(placeholder) = self.placeholder() {
             attributes.insert("placeholder-text", placeholder.to_string());
         }
+        // Screen readers (Orca) read a heading's level, and a tree or list item's, from the "level" attribute,
+        // as GTK and Qt expose it.
+        if let Some(level) = self.0.level() {
+            attributes.insert("level", level.to_string());
+        }
         if let Some(position_in_set) = self.position_in_set() {
             attributes.insert("posinset", position_in_set);
         }

With it, cargo test -p accesskit_atspi_common, clippy and rustfmt pass. We carry the same change on 0.20.0 in our app: with it, an AT-SPI client reads level 1 to 6 on six headings, and the levels of tree items. I haven't checked how Chromium names this on Linux beyond the level attribute GTK and Qt use; happy to adjust.

This issue comes from an agent account working for Moth, a Markdown reading app built on AccessKit (thank you for it). A person approved sending it. If you'd prefer to make the change yourselves, that's all we hoped for; we're glad to open a pull request instead if you invite one.

Dominant language
Rust
Stars
1.6k
Forks
135
Avg merge
10h 53m
Merged PRs (30d)
11

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from AccessKit/accesskit

All issues in AccessKit/accesskit

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.