Skip to content

Commit 68a4eaf

Browse files
author
Roy Lin
committed
feat: resolve native accessibility relationships
1 parent 60b0cd1 commit 68a4eaf

25 files changed

Lines changed: 1524 additions & 206 deletions

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ winui = []
109109
winui-native = [
110110
"winui",
111111
"dep:windows",
112+
"dep:windows-collections",
112113
"dep:windows-core",
113114
"dep:winui3",
114115
]
@@ -207,6 +208,7 @@ windows = { version = "0.62.0", optional = true, features = [
207208
"Win32_UI_Shell",
208209
"Win32_UI_WindowsAndMessaging",
209210
] }
211+
windows-collections = { version = "0.3.2", optional = true }
210212
windows-core = { version = "0.62.0", optional = true }
211213
winui3 = { package = "winio-winui3", version = "0.4.2", optional = true, features = [
212214
"UI_Composition",

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ object graph at the host boundary.
3434
and WAI-ARIA live-region updates through native assistive-technology APIs,
3535
independent visible and accessible labels with native `aria-label`
3636
projection, native descriptive accessibility metadata with field-level
37-
capability reporting, validated ARIA state tokens with field-level native
38-
capability reporting and exact AppKit/GTK4 projection where supported,
37+
capability reporting, resolvable accessibility ID-reference relationships
38+
with exact GTK4 and supported AppKit/WinUI projection, validated ARIA state
39+
tokens with field-level native capability reporting and exact AppKit/GTK4
40+
projection where supported,
3941
decimal/percent/date/time formatting, versioned
4042
capability reporting, shared native event-source state machines, and
4143
accessibility conformance checks. Versioned
@@ -54,7 +56,7 @@ object graph at the host boundary.
5456
IME/dead-key conformance, measured overlay collision/arrow conformance and
5557
scroll locking, remaining native focus conformance, broader message catalogs,
5658
currency/unit parsing and formatting, date-range/time-zone formatting,
57-
complete native accessibility relationship and remaining state/role-edge
59+
remaining AppKit/WinUI accessibility relationship and state/role-edge
5860
projection,
5961
and real-platform assistive-technology fixtures are still in progress.
6062
- `.rsx` component source modules with imports, local Rust types, hook

docs/architecture.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,15 @@ rerender updates, and when native change events arrive.
271271
ARIA relationship attributes including `aria-labelledby`, `aria-describedby`,
272272
`aria-details`, `aria-controls`, `aria-owns`, `aria-flowto`,
273273
`aria-errormessage`, and `aria-activedescendant` project into structured
274-
native accessibility relationship hints and stay available in metadata.
274+
native accessibility relationship hints and stay available in metadata. A
275+
shared native registry resolves their ID-reference lists against mounted node
276+
`id` metadata, including forward references, id changes, removal, and
277+
duplicate-id ambiguity. GTK4 projects all eight relationships through
278+
`GtkAccessible`. WinUI projects described-by, controls, and flow-to lists plus
279+
a single labelled-by target through UI Automation. AppKit projects a single
280+
static-text labelled-by target as its title UI element. Remaining
281+
backend/field combinations and non-accessible wrappers retain portable
282+
semantics and report field-level capability issues.
275283
ARIA description and value attributes including `aria-description`,
276284
`aria-roledescription`, `aria-keyshortcuts`, and `aria-valuetext` project into
277285
structured native accessibility description hints and stay available in metadata.

docs/react-aria-native.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,17 @@ The first shared interaction milestone is available in the portable runtime:
331331
generic value-text overrides remain portable. Logical AppKit collection/tab
332332
items, GTK4 menu-model items, and the WinUI window wrapper report portable
333333
coverage instead of silently dropping unsupported native properties.
334+
- Accessibility relationship ID references resolve to mounted host nodes through
335+
one shared registry. Forward references bind when their target mounts; target
336+
id changes and removal rebind or clear dependents; duplicate ids stay
337+
unresolved instead of selecting an arbitrary node. GTK4 projects
338+
`aria-labelledby`, `aria-describedby`, `aria-details`, `aria-controls`,
339+
`aria-owns`, `aria-flowto`, `aria-errormessage`, and
340+
`aria-activedescendant`. WinUI projects complete description, control, and
341+
flow-to lists plus a single label target. AppKit projects a single resolved
342+
static-text label through its title UI element. Every relationship field has
343+
an independent capability entry, including explicit portable exceptions for
344+
logical GTK4 menu items and the WinUI window wrapper.
334345
- Accessibility state fields remain independent through native props, setter
335346
diffs, recording, accessibility-tree projection, and capability audits.
336347
AppKit projects `aria-hidden` and `aria-modal` through `NSAccessibility`.
@@ -348,11 +359,11 @@ The first shared interaction milestone is available in the portable runtime:
348359
- Accessibility conformance validates names, focus uniqueness, selection and
349360
checked states, exclusive-container selection, relationships, and duplicate
350361
node identity. Invalid `aria-autocomplete`, `aria-current`, `aria-haspopup`,
351-
`aria-pressed`, `aria-live`, and `aria-relevant` values are reported as
352-
errors rather than silently changing state or announcement behavior. The
353-
same semantic tree assertions run against AppKit, GTK4, and WinUI planning
354-
adapters. Mounted selection is the source of truth for accessible
355-
selected/checked state.
362+
`aria-pressed`, `aria-live`, and `aria-relevant` values, plus a
363+
multi-reference `aria-activedescendant`, are reported as errors rather than
364+
silently changing state or announcement behavior. The same semantic tree
365+
assertions run against AppKit, GTK4, and WinUI planning adapters. Mounted
366+
selection is the source of truth for accessible selected/checked state.
356367
- AppKit, GTK4, and WinUI use the same press and keyboard state machines. Their
357368
view-backed widgets emit pointer press/re-entry/cancellation, hover, focus,
358369
key, modality, modifier, repeat, and local-position data through one portable
@@ -387,6 +398,7 @@ existence of a platform object:
387398
| Live regions | Stable keyed updates implement WAI-ARIA `polite`/`assertive`, `atomic`, `relevant`, and ancestor `busy` semantics in the shared runtime. Implicit alert/status/log/output policies and nested boundaries are honored. AppKit, GTK4, and WinUI deliver the resulting typed message through their native assistive-technology APIs; headless and protocol hosts retain the same ordered command. |
388399
| Accessible names | `aria-label` is independent from visible native text and is projected through AppKit accessibility labels, GTK4 accessible label properties, and WinUI UI Automation names. Headless and protocol output retain the same computed name. The capability manifest conservatively reports portable-only coverage for AppKit logical list/tree and tab items, GTK4 `gio::MenuItem`, and the WinUI window wrapper. |
389400
| Accessible descriptions | Description, role-description, shortcut, and value-text metadata have independent executable capabilities. GTK4 projects all four. AppKit projects description, role description, and value text; WinUI projects description and shortcuts. Unsupported field/backend combinations and non-accessible logical wrappers remain available to headless/protocol consumers and produce explicit portable capability issues. |
401+
| Accessible relationships | A shared registry resolves ID-reference lists across forward mounting, id changes, removal, and duplicate-id ambiguity. GTK4 projects all eight relationship fields. WinUI projects description, controls, and flow-to lists plus single-target labels; AppKit projects single static-text labels. Remaining backend/field combinations and non-accessible wrappers retain portable semantics and produce field-level capability issues. |
390402
| Accessible states | Hidden, autocomplete, multiline, current, popup, pressed, live-region, busy, and modal semantics have independent executable capabilities. AppKit projects hidden and modal; GTK4 projects hidden, autocomplete, multiline, popup presence, pressed, busy, and modal. Live-region policy is evaluated by the shared runtime and delivered through each backend's native announcement channel. WinUI direct-state gaps, `aria-current`, the GTK4 popup subtype, and logical wrapper exceptions remain portable and auditable instead of being silently dropped. |
391403
| Native menu activation | AppKit and GTK4 menu items emit terminal press only because their menu models do not expose a mounted generic view event source. |
392404
| Hover and typed modality | View-backed widgets; explicit exceptions are reported for AppKit non-view wrappers/items, GTK4 menu items, and the WinUI window wrapper. |
@@ -472,7 +484,7 @@ props:
472484
| P1 | Collections and selection | Complete IME/dead-key typeahead conformance and add real-platform fixtures for layout-aware page navigation. |
473485
| P1 | NumberField interaction | Add real-platform assistive-technology fixtures for localized labels and focused announcements. Group/input/button anatomy, localized stepper labels and role descriptions, focused native value announcements, minimum-anchored button and continuous press-and-hold stepping, Arrow/Page/Home/End keyboard semantics, focused vertical-wheel stepping with horizontal/zoom rejection and `isWheelDisabled`, mouse input-focus preservation, decimal-noise cleanup, boundary disabling, cancellation/re-entry, and native-default suppression are implemented. |
474486
| P1 | Internationalization | Expand message formatting beyond NumberField, and add currency/unit parsing and formatting plus date ranges/time zones. Reusable decimal/percent parsing and formatting, partial-input validation, locale-aware filtering, localized NumberField model/display conversion, and a 34-locale NumberField message catalog now build on inherited locale/direction. |
475-
| P1 | Accessibility conformance | Complete native relationship projection, add exact native mappings for remaining state gaps such as `aria-current` and direct AppKit/WinUI autocomplete, multiline, popup, pressed, and busy semantics where platform APIs permit, close the documented logical-item/window name, description, state, and announcement exceptions, fill the remaining AppKit shortcut and WinUI role-description/value-text gaps, and add real assistive-technology fixtures. Independent `aria-label`, descriptive metadata, and state-field capability reporting, exact AppKit/GTK4 state projection where supported, validated ARIA state tokens, the typed announcement channel, WAI-ARIA live-region diff policy, and focused NumberField value announcements are implemented. |
487+
| P1 | Accessibility conformance | Complete multi-label projection on AppKit and WinUI; add AppKit description/details/control/ownership/flow/error/active-descendant mappings and WinUI details/ownership/error/active-descendant mappings where platform APIs permit; add exact native mappings for remaining state gaps such as `aria-current` and direct AppKit/WinUI autocomplete, multiline, popup, pressed, and busy semantics; close the documented logical-item/window name, description, relationship, state, and announcement exceptions; fill the remaining AppKit shortcut and WinUI role-description/value-text gaps; and add real assistive-technology fixtures. Shared lifecycle-aware relationship resolution, all GTK4 relationship mappings, supported AppKit/WinUI relationship mappings, independent `aria-label`, descriptive metadata, state-field capabilities, exact AppKit/GTK4 state projection where supported, validated ARIA tokens, the typed announcement channel, WAI-ARIA live-region diff policy, and focused NumberField value announcements are implemented. |
476488
| P2 | Overlays | Complete measured boundary-driven collision and arrow projection, native scroll locking, configurable outside-interaction filters, multi-window layer coordination, and real-platform positioning conformance fixtures. |
477489
| P2 | Capability enforcement | Turn reported capability gaps into adapter policy and conformance gates where portable fallback is not sufficient. |
478490
| P2 | Environment style variants | Add native environment and ancestry evaluators for responsive/container, theme, group, peer, and structural selector variants. These remain preserved in the style IR but inactive at runtime today. |

src/accessibility.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ use serde::{Deserialize, Serialize};
44

55
mod conformance;
66
mod live_region;
7+
#[cfg(any(
8+
test,
9+
all(target_os = "macos", feature = "appkit-native"),
10+
all(target_os = "linux", feature = "gtk4-native"),
11+
all(target_os = "windows", feature = "winui-native")
12+
))]
13+
pub(crate) mod relationship_registry;
714
pub use conformance::{
815
AccessibilityConformanceIssue, AccessibilityConformanceReport, AccessibilityIssueCode,
916
AccessibilityIssueSeverity,

src/accessibility/conformance.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub enum AccessibilityIssueCode {
2323
InvalidMultipleState,
2424
MultipleSelectedItems,
2525
EmptyRelationship,
26+
InvalidActiveDescendant,
2627
InvalidAutocomplete,
2728
InvalidCurrent,
2829
InvalidHasPopup,
@@ -159,6 +160,19 @@ fn validate_node<'a>(
159160
});
160161
}
161162
}
163+
if node
164+
.relationships
165+
.active_descendant
166+
.as_deref()
167+
.is_some_and(|value| value.split_whitespace().count() > 1)
168+
{
169+
push_error(
170+
issues,
171+
node,
172+
AccessibilityIssueCode::InvalidActiveDescendant,
173+
"aria-activedescendant must reference exactly one element".to_string(),
174+
);
175+
}
162176
if node
163177
.state
164178
.autocomplete

src/accessibility/conformance_tests.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,18 @@ fn conformance_rejects_invalid_accessibility_state_tokens() {
120120
assert!(report.issues.iter().any(|issue| issue.code == code));
121121
}
122122
}
123+
124+
#[test]
125+
fn conformance_rejects_multiple_active_descendant_references() {
126+
let mut list = node(AccessibilityRole::ListBox);
127+
list.label = Some("Results".to_string());
128+
list.relationships.active_descendant = Some("first second".to_string());
129+
130+
let report = AccessibilityConformanceReport::validate(&list);
131+
132+
assert!(!report.is_conformant());
133+
assert!(report
134+
.issues
135+
.iter()
136+
.any(|issue| issue.code == AccessibilityIssueCode::InvalidActiveDescendant));
137+
}

0 commit comments

Comments
 (0)