Skip to content

Commit b632f54

Browse files
authored
Merge pull request w-ahmad#397 from w-ahmad/w-ahmad-accessibility-support
feat: add UI Automation / accessibility support
2 parents 0e0bb9d + db44a5d commit b632f54

27 files changed

Lines changed: 1509 additions & 14 deletions

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,4 +360,7 @@ MigrationBackup/
360360
.ionide/
361361

362362
# Fody - auto-generated XML schema
363-
FodyWeavers.xsd
363+
FodyWeavers.xsd
364+
365+
# DocFx - auto-generated api eference documents
366+
/docs/api

docs/docs/accessibility.md

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Accessibility
2+
3+
`TableView` exposes a complete UI Automation (UIA) tree so that screen readers, keyboard-only users, and accessibility testing tools can navigate and interact with the control. Custom `AutomationPeer` classes are registered for every interactive element: the table itself, rows, cells, column headers, and row headers.
4+
5+
## When to use it
6+
7+
You do not need to opt in. Accessibility support is always active. This page explains what is exposed, how to improve automation names for custom content, and what limitations apply.
8+
9+
## Supported UIA patterns
10+
11+
| Element | UIA patterns |
12+
|---|---|
13+
| [`TableView`](xref:WinUI.TableView.TableView) | `GridPattern`, `TablePattern`, `SelectionPattern` (inherited from `ListView`) |
14+
| [`TableViewRow`](xref:WinUI.TableView.TableViewRow) | `SelectionItemPattern` (inherited), `ExpandCollapsePattern` when `RowDetailsVisibilityMode` is `VisibleWhenExpanded` |
15+
| [`TableViewCell`](xref:WinUI.TableView.TableViewCell) | `GridItemPattern`, `TableItemPattern`, `SelectionItemPattern` |
16+
| [`TableViewColumnHeader`](xref:WinUI.TableView.TableViewColumnHeader) | `InvokePattern` when [`CanSort`](xref:WinUI.TableView.TableViewColumn.CanSort) is `true` |
17+
| `TableViewRowHeader` | Structural header element |
18+
19+
### GridPattern / TablePattern (TableView)
20+
21+
`TableView` implements `IGridProvider` and `ITableProvider`:
22+
23+
- **`RowCount`** — number of data rows currently loaded
24+
- **`ColumnCount`** — number of visible columns
25+
- **`GetItem(row, column)`** — returns the automation element for the specified cell (only for realized, on-screen cells)
26+
- **`GetColumnHeaders()`** — returns automation elements for all visible column headers
27+
- **`RowOrColumnMajor`** — always `RowMajor`
28+
29+
### GridItemPattern / TableItemPattern (cells)
30+
31+
Each `TableViewCell` exposes `IGridItemProvider` and `ITableItemProvider`:
32+
33+
- **`Row`** — zero-based row index
34+
- **`Column`** — zero-based column index
35+
- **`RowSpan` / `ColumnSpan`** — always 1
36+
- **`ContainingGrid`** — the owning `TableView`
37+
- **`GetColumnHeaderItems()`** — the column header for this cell
38+
39+
### SelectionItemPattern (cells and rows)
40+
41+
- `IsSelected` reflects the current selection state
42+
- `Select()`, `AddToSelection()`, and `RemoveFromSelection()` manipulate the selection programmatically
43+
- `SelectionContainer` returns the owning `TableView`
44+
- Row `SelectionItemPattern` is provided automatically by the base `ListView` infrastructure
45+
46+
### InvokePattern (column headers)
47+
48+
When a column is sortable ([`CanSort`](xref:WinUI.TableView.TableViewColumn.CanSort) is `true`), its header exposes `IInvokeProvider`. Invoking the header cycles the sort direction: ascending → descending → unsorted.
49+
50+
### ExpandCollapsePattern (row details)
51+
52+
When [`RowDetailsVisibilityMode`](xref:WinUI.TableView.TableView.RowDetailsVisibilityMode) is `VisibleWhenExpanded`, each row exposes `IExpandCollapseProvider` so automation clients can expand or collapse the details pane programmatically.
53+
54+
## Automation names
55+
56+
### TableView
57+
58+
Uses [`AutomationProperties.Name`](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.automation.automationproperties.name) if set; otherwise inherits from the base `ListView` peer.
59+
60+
```xml
61+
<tv:TableView AutomationProperties.Name="Products table" />
62+
```
63+
64+
### Column headers
65+
66+
Format: `"{headerText}"`, with an optional sort-state suffix and filter suffix when applicable.
67+
68+
| State | Automation name example |
69+
|---|---|
70+
| Unsorted, unfiltered | `"Name"` |
71+
| Sorted ascending | `"Name, Sort Ascending"` |
72+
| Sorted descending | `"Name, Sort Descending"` |
73+
| Sorted and filtered | `"Name, Sort Descending, Filtered"` |
74+
75+
### Rows
76+
77+
Format: `"Row {N}"` (1-based index). The help text contains a summary of all cells in the row, for example `"Name: Waheed Ahmad, Age: 30, City: London"`.
78+
79+
### Cells
80+
81+
Format: `"{columnHeader}, Row {N}, {cellValue}"`.
82+
83+
Examples:
84+
85+
- `"Name, Row 3, Waheed Ahmad"`
86+
- `"Salary, Row 7, 95000"`
87+
88+
Override the name for a specific column by setting `AutomationProperties.Name` through a `CellStyle`:
89+
90+
```xml
91+
<tv:TableViewTextColumn Header="Status">
92+
<tv:TableViewTextColumn.CellStyle>
93+
<Style TargetType="tv:TableViewCell">
94+
<Setter Property="AutomationProperties.Name"
95+
Value="{Binding StatusDescription}" />
96+
</Style>
97+
</tv:TableViewTextColumn.CellStyle>
98+
</tv:TableViewTextColumn>
99+
```
100+
101+
### Row headers
102+
103+
Uses `AutomationProperties.Name` if set on the `TableViewRowHeader`, otherwise derives the name from the header's content, or falls back to `"Row {N}"`.
104+
105+
## Keyboard navigation
106+
107+
| Key | Action |
108+
|---|---|
109+
| Arrow keys | Move the current cell or row |
110+
| **Tab** / **Shift+Tab** | Move to the next / previous cell |
111+
| **Enter** | Move to the next row cell, or commit an edit |
112+
| **Space** | Select or deselect the current cell or row |
113+
| **F2** | Begin editing the current cell |
114+
| **Escape** | Cancel editing and return focus to the current cell |
115+
| **Home** / **End** | Move to the first / last column in the row |
116+
| **Ctrl+Home** / **Ctrl+End** | Move to the first / last row |
117+
| **Page Up** / **Page Down** | Move by one page |
118+
| **Ctrl+A** | Select all |
119+
| **Ctrl+Shift+A** | Deselect all |
120+
| **Ctrl+C** | Copy to clipboard |
121+
122+
## Screen reader behavior
123+
124+
When navigating with a screen reader such as Narrator:
125+
126+
1. Moving to the **TableView** announces it as a table with its row and column count.
127+
2. Moving between **rows** announces the row number and a summary of its content.
128+
3. Moving between **cells** announces the column header, row number, and current value.
129+
4. **Column headers** announce their header text, current sort state, and filter state.
130+
5. **Editing**: pressing **F2** (or double-tapping) enters edit mode. Focus moves to the editing control (for example, a `TextBox`), which announces itself independently with its own value. Pressing **Escape** returns focus to the cell, keeping Narrator on the correct cell.
131+
6. **Selection changes** are announced through `SelectionItemPattern.IsSelected`.
132+
133+
## Custom automation names for template columns
134+
135+
`TableViewTemplateColumn` cells display developer-defined content. The cell automation name defaults to `"{columnHeader}, Row {N}"` unless overridden. Set `AutomationProperties.Name` directly on the root element of the template:
136+
137+
```xml
138+
<tv:TableViewTemplateColumn Header="Actions">
139+
<tv:TableViewTemplateColumn.CellTemplate>
140+
<DataTemplate>
141+
<Button Content="Edit"
142+
AutomationProperties.Name="Edit row" />
143+
</DataTemplate>
144+
</tv:TableViewTemplateColumn.CellTemplate>
145+
</tv:TableViewTemplateColumn>
146+
```
147+
148+
## Custom column header names
149+
150+
To override the automation name of a column header (for example, to provide a longer description for an abbreviated header):
151+
152+
```xml
153+
<tv:TableViewTextColumn Header="DOB">
154+
<tv:TableViewTextColumn.HeaderStyle>
155+
<Style TargetType="tv:TableViewColumnHeader">
156+
<Setter Property="AutomationProperties.Name"
157+
Value="Date of Birth" />
158+
</Style>
159+
</tv:TableViewTextColumn.HeaderStyle>
160+
</tv:TableViewTextColumn>
161+
```
162+
163+
## Notes and limitations
164+
165+
- **Virtualized cells**: `IGridProvider.GetItem(row, column)` returns `null` for rows that are not currently in the visual tree. Scroll the target row into view before calling `GetItem`.
166+
- **IValueProvider on cells**: `IValueProvider` is not implemented on the cell peer. Instead, the editing element (for example, a `TextBox`) exposes `IValueProvider` while the cell is in edit mode.
167+
- **Custom column types**: cells in `TableViewTemplateColumn` show developer-defined content. Accessible names for those cells default to the column header and row index unless the template explicitly sets `AutomationProperties.Name`.
168+
- **Non-Windows platforms**: automation peers are compiled and run on all platforms (WinUI 3 and Uno Platform). However, not all Uno Platform targets expose a full UIA tree to assistive technologies. On WebAssembly (WASM), the platform relies on ARIA attributes managed by Uno. On Desktop Skia targets, accessibility support depends on the native platform's accessibility APIs.
169+
170+
## Related articles
171+
172+
- [Selection](selection.md)
173+
- [Editing](editing.md)
174+
- [Sorting](sorting.md)
175+
- [Filtering](filtering.md)
176+
- [Row details](row-details.md)
177+
- [Row headers](row-headers.md)
178+
- [Styling rows, cells, and headers](styling.md)
179+

docs/docs/datagrid-feature-comparison.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ WinUI.TableView is actively maintained and continues to add new features based o
6767
| Alternating rows |||| |
6868
| Conditional styling |||| TableView supports conditional cell styling. |
6969
| Cell and row context flyouts |||| TableView has built-in support for row and cell context flyouts. |
70-
| Accessibility / Narrator | Not verified || | |
70+
| Accessibility / Narrator | Not verified || | |
7171
| Validation |||| WPF has strong cell and row validation support. TableView editing events may help. |
7272
| Row virtualization |||| |
7373

docs/docs/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
href: performance.md
6161
- name: Native AOT Compatibility
6262
href: aot-compatibility.md
63+
- name: Accessibility
64+
href: accessibility.md
6365
- name: Migration
6466
items:
6567
- name: Migrating from WPF DataGrid
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
using Microsoft.UI.Xaml.Automation;
2+
using Microsoft.UI.Xaml.Automation.Peers;
3+
using Microsoft.UI.Xaml.Automation.Provider;
4+
using System.Collections.Generic;
5+
6+
namespace WinUI.TableView.AutomationPeers;
7+
8+
/// <summary>
9+
/// Exposes <see cref="TableView"/> to UI Automation, implementing the Grid and Table patterns
10+
/// so automation clients can navigate the row/column structure of the control.
11+
/// </summary>
12+
public partial class TableViewAutomationPeer : ListViewAutomationPeer, IGridProvider, ITableProvider
13+
{
14+
private readonly TableView _owner;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="TableViewAutomationPeer"/> class.
18+
/// </summary>
19+
/// <param name="owner">The <see cref="TableView"/> that is associated with this peer.</param>
20+
public TableViewAutomationPeer(TableView owner) : base(owner)
21+
{
22+
_owner = owner;
23+
}
24+
25+
/// <inheritdoc/>
26+
protected override string GetClassNameCore()
27+
{
28+
return nameof(TableView);
29+
}
30+
31+
/// <inheritdoc/>
32+
protected override AutomationControlType GetAutomationControlTypeCore()
33+
{
34+
return AutomationControlType.DataGrid;
35+
}
36+
37+
/// <inheritdoc/>
38+
protected override string GetLocalizedControlTypeCore()
39+
{
40+
return "table view";
41+
}
42+
43+
/// <inheritdoc/>
44+
protected override object GetPatternCore(PatternInterface patternInterface)
45+
{
46+
return patternInterface switch
47+
{
48+
PatternInterface.Grid => this,
49+
PatternInterface.Table => this,
50+
_ => base.GetPatternCore(patternInterface)
51+
};
52+
}
53+
54+
/// <summary>
55+
/// Gets the total number of rows in the grid, equal to the number of items in the <see cref="TableView"/>.
56+
/// </summary>
57+
public int RowCount => _owner.Items.Count;
58+
59+
/// <summary>
60+
/// Gets the total number of visible columns in the grid.
61+
/// </summary>
62+
public int ColumnCount => _owner.Columns.VisibleColumns.Count;
63+
64+
/// <summary>
65+
/// Returns the automation peer for the cell at the specified row and column index.
66+
/// Returns <see langword="null"/> if the cell is not realized (virtualized) or the indices are out of range.
67+
/// </summary>
68+
/// <param name="row">Zero-based row index.</param>
69+
/// <param name="column">Zero-based column index.</param>
70+
public IRawElementProviderSimple? GetItem(int row, int column)
71+
{
72+
if (row < 0 || row >= RowCount || column < 0 || column >= ColumnCount)
73+
{
74+
return null;
75+
}
76+
77+
var slot = new TableViewCellSlot(row, column);
78+
var cell = _owner.GetCellFromSlot(slot);
79+
if (cell is null)
80+
{
81+
return null;
82+
}
83+
84+
var peer = CreatePeerForElement(cell);
85+
return peer is null ? null : ProviderFromPeer(peer);
86+
}
87+
88+
/// <summary>
89+
/// Returns automation peers for all row header elements.
90+
/// </summary>
91+
public IRawElementProviderSimple[] GetRowHeaders()
92+
{
93+
var providers = new List<IRawElementProviderSimple>();
94+
95+
foreach (var row in _owner.Rows)
96+
{
97+
var rowHeader = row.RowPresenter?.RowHeader;
98+
if (rowHeader is null)
99+
{
100+
continue;
101+
}
102+
103+
var peer = CreatePeerForElement(rowHeader);
104+
if (peer is null)
105+
{
106+
continue;
107+
}
108+
109+
var provider = ProviderFromPeer(peer);
110+
if (provider is not null)
111+
{
112+
providers.Add(provider);
113+
}
114+
}
115+
116+
return [.. providers];
117+
}
118+
119+
/// <summary>
120+
/// Returns automation peers for all visible column header elements.
121+
/// </summary>
122+
public IRawElementProviderSimple[] GetColumnHeaders()
123+
{
124+
var providers = new List<IRawElementProviderSimple>();
125+
126+
foreach (var column in _owner.Columns.VisibleColumns)
127+
{
128+
if (column.HeaderControl is { } headerControl)
129+
{
130+
var peer = CreatePeerForElement(headerControl);
131+
if (peer is not null)
132+
{
133+
var provider = ProviderFromPeer(peer);
134+
if (provider is not null)
135+
{
136+
providers.Add(provider);
137+
}
138+
}
139+
}
140+
}
141+
142+
return [.. providers];
143+
}
144+
145+
/// <summary>
146+
/// Gets the primary axis of traversal for the table. TableView is row-major.
147+
/// </summary>
148+
public RowOrColumnMajor RowOrColumnMajor => RowOrColumnMajor.RowMajor;
149+
}

0 commit comments

Comments
 (0)