Skip to content

feat(macos): Expose heading levels and table structure - #830

Open
xiaolai wants to merge 1 commit into
AccessKit:mainfrom
xiaolai:macos-heading-table-structure
Open

xiaolai wants to merge 1 commit into
AccessKit:mainfrom
xiaolai:macos-heading-table-structure

Conversation

@xiaolai

@xiaolai xiaolai commented Oct 18, 2026

Copy link
Copy Markdown

What

On macOS, headings and tables currently expose almost no structure:

  • Role::Heading maps to the role string "Heading", which is not a standard AX role, and the node's level is never read.
  • Tables, grids and tree grids get the right roles, but no AXRows, AXRowCount, AXColumnCount, AXIndex, cell ranges or column headers (issue Computed table information #10 covers the related computed-table case).

This PR:

  • maps Role::Heading to AXHeading (NSAccessibilityHeadingRole, macOS 26), and exposes the level as AXHeadingLevel (1-based) and as AXValue, as WebKit and Chromium do for releases before macOS 26;
  • for Table, Grid and TreeGrid: AXRows (row children plus the rows inside RowGroup children), AXRowCount, AXColumnCount and AXColumnHeaderUIElements (the ColumnHeader cells of its rows);
  • for rows: AXIndex; for cells (Cell, GridCell, ColumnHeader, RowHeader): AXRowIndexRange and AXColumnIndexRange.

Counts and indices come only from the provider (row_count, column_count, row_index, column_index, spans). When the provider gives none, AXRowCount falls back to the number of rows exposed, and every other structural attribute is withheld through isAccessibilitySelectorAllowed: rather than reported as 0.

AXColumns is not implemented (AppKit marks it optional), and neither are per-cell header links, since AccessKit has no property for them.

accessibilityAttributeValue: now returns NSObject instead of NSString, so that it can return an NSNumber for AXHeadingLevel; the existing string attributes are unchanged.

Testing

On macOS 27 (Apple silicon), with a winit host exposing a book chapter with four headings (levels 1, 2, 1, 2) and a 3 × 2 table with a header row, queried through AXUIElement:

  • before: role Heading, role description "Heading", no level; table with no AXRows or counts;
  • after: role AXHeading, description "heading", AXHeadingLevel 1, 2, 1, 2; AXRows holding exactly the 3 rows, AXRowCount 3, AXColumnCount 2, each row's AXIndex, each cell's row and column range, and AXColumnHeaderUIElements equal to the two header cells;

cargo fmt --check, cargo clippy -p accesskit_macos --all-targets -- -D warnings and cargo test -p accesskit_macos pass. The adapter has no unit tests to extend; I have not built with Rust 1.87 locally, and nothing here uses newer language or library features. VoiceOver itself has not yet been run against this build.

Headings now use the AXHeading role (NSAccessibilityHeadingRole, macOS 26)
instead of the non-standard "Heading" string, and expose their level both as
AXHeadingLevel (1-based) and as AXValue, as WebKit and Chromium do for older
releases.

Tables, grids and tree grids now expose AXRows (their rows, including rows
inside row groups), AXRowCount, AXColumnCount and
AXColumnHeaderUIElements; rows expose AXIndex; cells expose
AXRowIndexRange and AXColumnIndexRange. Counts and indices come from the
provider. When the provider gives none, AXRowCount falls back to the
number of rows exposed and every other structural attribute is withheld
through isAccessibilitySelectorAllowed: instead of being reported as 0.

This branch has not been deployed

No deployments
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.

1 participant