Skip to content

table: Expose DataTable's structure and cells to assistive technology - #3667

Merged
huacnlee merged 1 commit into
longbridge:mainfrom
Elitelord:data-table-a11y-roles
Oct 24, 2026
Merged

huacnlee merged 1 commit into
longbridge:mainfrom
Elitelord:data-table-a11y-roles

Conversation

@Elitelord

@Elitelord Elitelord commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Part of #3647 (items 1–3: container, header cells, body cells).

Description

DataTable only gave each row Role::Row. A screen reader found a flat list of unnamed rows: no table, no column headers, and no cell content, because GPUI only exposes text that is a Text with an id. This PR exposes the table's structure.

  • Container: table-inner is a Grid, or a Table when neither rows nor cells are selectable. It sets aria_row_count(rows_count + 1) and aria_column_count(columns_count). The table is virtualized, so the counts cover every row, not just the rendered ones.
  • Header: the header is a Row at aria_row_index(0). Each leaf header is a ColumnHeader named after its column (aria_label(column.name)), with its aria_column_index. The "Sort by …" button stays inside it.
  • Body: rows carry aria_row_index(row_ix + 1). Each cell is a Cell with its aria_column_index. In cell selection mode it is a GridCell with aria_selected, which AccessKit's Windows adapter reports through SelectionItemPattern.
  • Cell names: TableDelegate::cell_label lets a delegate name its cells. It defaults to None, so existing delegates are unaffected. It is only called while window.is_a11y_active() (or under test-support), the same gate InputState uses for its value. That way a delegate's formatting costs nothing when no assistive technology is connected.
  • Story: the DataTable story implements cell_label with the text it already draws (format_cell).

Public API

gpui-component

  • TableDelegate::cell_label(&self, row_ix: usize, col_ix: usize, cx: &App) -> Option<SharedString>: the text a screen reader reads for a cell. Defaults to None.

Screenshot

UI Automation tree of cargo run -p gpui-component-story -- datatable (Windows 11, read with UIA's control view):

Before After
38 elements, 18 named. The table is 14 × DataItem "" plus 4 "Sort by" buttons. 342 elements, 314 named.
DataGrid ""                              (localized: "data grid")
  DataItem ""                            (header row)
    DataItem "ID"                        (localized: "column header")
    DataItem "Market"
    DataItem "Name"
    DataItem "Symbol"
      Button "Sort by Symbol"
    …
  DataItem ""                            (row)
    DataItem "0"
    DataItem "US"
    DataItem "Unity Software Inc."
    DataItem "U.US"
    …

How to Test

  • cargo test -p gpui-kit --features test-support,component --test integration: 419 passed. Two tests are new:
    • table_exposes_its_header_and_cells_to_assistive_technology: a Grid container, ColumnHeaders named "Name"/"Status", and a cell with the delegate's label.
    • selectable_cells_report_their_selection: GridCells report selected.
    • Both fail without the state.rs change.
  • cargo run -p gpui-component-story -- datatable with Accessibility Insights for Windows or Narrator: the tree above.
  • cargo clippy -p gpui-component -p gpui-component-story --all-targets -- -D warnings and cargo fmt --check are clean.

Not in this PR

Open questions

  • Grid vs Table: I made the container a Grid whenever rows or cells are selectable (the default), and a Table otherwise. Happy to use one role for both.
  • Row indices: they count the header as row 0, the way Chromium does. Tell me if you'd prefer body rows to start at 0.
  • cell_label naming: fine as is, or would you prefer another name (e.g. accessible_cell_text)?

I've only tested on Windows (UI Automation); I haven't tried VoiceOver on macOS.

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate.
  • Passed cargo run for story tests related to the changes.
  • Tested macOS, Windows and Linux platforms performance (if the change is platform-specific)

🤖 Generated with Claude Code

DataTable only gave each row `Role::Row`, so a screen reader found a flat
list of unnamed rows: no table, no column headers, and no cell content,
because GPUI exposes no plain text to the accessibility tree.

- The table is a `Grid` (a `Table` when nothing is selectable), with row and
  column counts covering every row, not just the rendered ones.
- The header is a row of `ColumnHeader`s named after their columns.
- Body cells are `Cell`s, or `GridCell`s that report their selection in cell
  selection mode, with their column index. Rows carry their row index; the
  header row is row 0.
- `TableDelegate::cell_label` lets a delegate name its cells. It defaults to
  `None` and is only called while assistive technology is active.

The DataTable story names its cells with the text it already draws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Elitelord
Elitelord marked this pull request as ready for review October 24, 2026 04:20
@huacnlee
huacnlee merged commit 0119807 into longbridge:main Oct 24, 2026
18 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.

2 participants