Skip to content

Commit 35b5a74

Browse files
author
Roy Lin
committed
feat: project native accessibility descriptions
1 parent f15d718 commit 35b5a74

8 files changed

Lines changed: 359 additions & 12 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ object graph at the host boundary.
3333
34-locale NumberField accessibility messages, focused value announcements
3434
and WAI-ARIA live-region updates through native assistive-technology APIs,
3535
independent visible and accessible labels with native `aria-label`
36-
projection,
36+
projection, native descriptive accessibility metadata with field-level
37+
capability reporting,
3738
decimal/percent/date/time formatting, versioned
3839
capability reporting, shared native event-source state machines, and
3940
accessibility conformance checks. Versioned

docs/react-aria-native.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,15 @@ The first shared interaction milestone is available in the portable runtime:
322322
accessible name; otherwise the visible label is the fallback. AppKit applies
323323
`setAccessibilityLabel`, GTK4 updates the `GtkAccessible` label property, and
324324
WinUI calls `AutomationProperties.SetName`.
325+
- `aria-description`, `aria-roledescription`, `aria-keyshortcuts`, and
326+
`aria-valuetext` remain distinct through the same IR and setter path. GTK4
327+
projects all four through `GtkAccessible`. AppKit projects description, role
328+
description, and value text through `NSAccessibility`; its missing generic
329+
shortcut property remains portable. WinUI projects description and shortcuts
330+
through UI Automation `HelpText` and `AcceleratorKey`; role description and
331+
generic value-text overrides remain portable. Logical AppKit collection/tab
332+
items, GTK4 menu-model items, and the WinUI window wrapper report portable
333+
coverage instead of silently dropping unsupported native properties.
325334
- Native IR capabilities are versioned. Every host exposes a feature manifest
326335
with unsupported, portable, or native support levels, role-specific
327336
overrides, and auditable capability issues. Protocol render responses carry
@@ -366,6 +375,7 @@ existence of a platform object:
366375
| NumberField stepping | The shared runtime maps ArrowUp/ArrowDown/PageUp/PageDown, Home/End, focused vertical wheels, and stepper presses to model-space `Change` events. Wheel input rejects horizontal-dominant gestures and control-wheel zoom and honors `isWheelDisabled`. Built-in decrement and increment buttons expose the same next values and boundary/read-only state through native Button controls, and mouse presses preserve input focus. AppKit, GTK4, and WinUI share cancellable pointer-hold stepping with immediate mouse/pen activation, delayed touch activation, and 60-millisecond repeats; handled toolkit defaults and terminal native clicks are suppressed. Default stepper labels, the role description, and empty-value text follow a 34-locale catalog. Focused value changes emit assertive native accessibility announcements on all three backends. |
367376
| 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. |
368377
| 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. |
378+
| 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. |
369379
| 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. |
370380
| 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. |
371381
| Focus within | Portable runtime routing on AppKit, GTK4, WinUI, and headless hosts. Native blur/focus batches are linked with `relatedTarget`; direct focus callbacks remain target-only while focus-within callbacks run only when a subtree boundary is crossed. |
@@ -450,7 +460,7 @@ props:
450460
| P1 | Collections and selection | Complete IME/dead-key typeahead conformance and add real-platform fixtures for layout-aware page navigation. |
451461
| 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. |
452462
| 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. |
453-
| P1 | Accessibility conformance | Complete native relationship, state, and role-specific property projection, close the documented logical-item/window accessible-name exceptions, and add real assistive-technology fixtures. Independent `aria-label` projection, the typed announcement channel, WAI-ARIA live-region diff policy, and focused NumberField value announcements are implemented on AppKit, GTK4, and WinUI. |
463+
| P1 | Accessibility conformance | Complete native relationship and state projection, close the documented logical-item/window accessible-name and description exceptions, fill the remaining AppKit shortcut and WinUI role-description/value-text gaps, and add real assistive-technology fixtures. Independent `aria-label` and field-level descriptive metadata projection, the typed announcement channel, WAI-ARIA live-region diff policy, and focused NumberField value announcements are implemented with explicit per-backend capabilities. |
454464
| 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. |
455465
| P2 | Capability enforcement | Turn reported capability gaps into adapter policy and conformance gates where portable fallback is not sufficient. |
456466
| 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/appkit_native/update.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ impl AppKitNativeSurface {
7272
NativeWidgetSetter::SetAccessibilityLabel(value) => {
7373
set_appkit_accessibility_label(&handle.widget, value.as_deref());
7474
}
75+
NativeWidgetSetter::SetAccessibilityDescription(value) => {
76+
set_appkit_accessibility_description(&handle.widget, value);
77+
}
7578
NativeWidgetSetter::SetWindowResizable(value) => {
7679
if let AppKitOsWidget::Window(window) = &handle.widget {
7780
let mut style = window.styleMask();
@@ -606,7 +609,6 @@ impl AppKitNativeSurface {
606609
| NativeWidgetSetter::SetHtmlFormAssociation(_)
607610
| NativeWidgetSetter::SetHtmlCollection(_)
608611
| NativeWidgetSetter::SetAccessibilityRelationships(_)
609-
| NativeWidgetSetter::SetAccessibilityDescription(_)
610612
| NativeWidgetSetter::SetAccessibilityStructure(_)
611613
| NativeWidgetSetter::SetAccessibilityState(_)
612614
| NativeWidgetSetter::SetWebStyle(_)
@@ -634,3 +636,35 @@ fn set_appkit_accessibility_label(widget: &AppKitOsWidget, value: Option<&str>)
634636
}
635637
}
636638
}
639+
640+
fn set_appkit_accessibility_description(
641+
widget: &AppKitOsWidget,
642+
value: &crate::accessibility::AccessibilityDescriptionProps,
643+
) {
644+
let description = value.description.as_deref().map(ns_string);
645+
let role_description = value.role_description.as_deref().map(ns_string);
646+
let value_text = value.value_text.as_deref().map(ns_string);
647+
648+
macro_rules! apply {
649+
($target:expr) => {{
650+
let target = $target;
651+
target.setAccessibilityHelp(description.as_deref());
652+
target.setAccessibilityRoleDescription(role_description.as_deref());
653+
target.setAccessibilityValueDescription(value_text.as_deref());
654+
}};
655+
}
656+
657+
match widget {
658+
AppKitOsWidget::Window(window) => apply!(window),
659+
AppKitOsWidget::Panel(panel) => apply!(panel),
660+
AppKitOsWidget::Popover(state) => apply!(&state.popover),
661+
AppKitOsWidget::Menu(menu) => apply!(menu),
662+
AppKitOsWidget::MenuItem(item) => apply!(item),
663+
AppKitOsWidget::ComboBoxItem(_) | AppKitOsWidget::TabViewItem(_) => {}
664+
_ => {
665+
if let Some(view) = widget.as_view() {
666+
apply!(view);
667+
}
668+
}
669+
}
670+
}

src/capability.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ pub enum NativeCapabilityFeature {
2727
AnchoredOverlayPosition,
2828
AccessibilityRole,
2929
AccessibilityName,
30+
AccessibilityDescription,
31+
AccessibilityRoleDescription,
32+
AccessibilityKeyShortcuts,
33+
AccessibilityValueText,
3034
AccessibilityRelationships,
3135
AccessibilityState,
3236
AccessibilityAnnouncements,
@@ -257,6 +261,87 @@ impl NativeCapabilities {
257261
"computed names use the backend's native accessibility-name property"
258262
}),
259263
),
264+
NativeFeatureCapability::new(
265+
Feature::AccessibilityDescription,
266+
if headless { Portable } else { Native },
267+
Some(match backend {
268+
NativeBackendKind::AppKit => {
269+
"descriptions use the native NSAccessibility help property"
270+
}
271+
NativeBackendKind::Gtk4 => {
272+
"descriptions use the native GtkAccessible description property"
273+
}
274+
NativeBackendKind::WinUI => {
275+
"descriptions use the native UI Automation help-text property"
276+
}
277+
NativeBackendKind::Headless => {
278+
"headless mode retains descriptions without an OS accessibility object"
279+
}
280+
}),
281+
),
282+
NativeFeatureCapability::new(
283+
Feature::AccessibilityRoleDescription,
284+
match backend {
285+
NativeBackendKind::AppKit | NativeBackendKind::Gtk4 => Native,
286+
NativeBackendKind::WinUI | NativeBackendKind::Headless => Portable,
287+
},
288+
Some(match backend {
289+
NativeBackendKind::AppKit => {
290+
"role descriptions use the native NSAccessibility role-description property"
291+
}
292+
NativeBackendKind::Gtk4 => {
293+
"role descriptions use the native GtkAccessible role-description property"
294+
}
295+
NativeBackendKind::WinUI => {
296+
"role descriptions remain in portable accessibility output because the current WinUI binding has no exact attached-property setter"
297+
}
298+
NativeBackendKind::Headless => {
299+
"headless mode retains role descriptions without an OS accessibility object"
300+
}
301+
}),
302+
),
303+
NativeFeatureCapability::new(
304+
Feature::AccessibilityKeyShortcuts,
305+
match backend {
306+
NativeBackendKind::Gtk4 | NativeBackendKind::WinUI => Native,
307+
NativeBackendKind::AppKit | NativeBackendKind::Headless => Portable,
308+
},
309+
Some(match backend {
310+
NativeBackendKind::AppKit => {
311+
"key shortcuts remain in portable accessibility output because NSAccessibility has no equivalent setter"
312+
}
313+
NativeBackendKind::Gtk4 => {
314+
"key shortcuts use the native GtkAccessible key-shortcuts property"
315+
}
316+
NativeBackendKind::WinUI => {
317+
"key shortcuts use the native UI Automation accelerator-key property"
318+
}
319+
NativeBackendKind::Headless => {
320+
"headless mode retains key shortcuts without an OS accessibility object"
321+
}
322+
}),
323+
),
324+
NativeFeatureCapability::new(
325+
Feature::AccessibilityValueText,
326+
match backend {
327+
NativeBackendKind::AppKit | NativeBackendKind::Gtk4 => Native,
328+
NativeBackendKind::WinUI | NativeBackendKind::Headless => Portable,
329+
},
330+
Some(match backend {
331+
NativeBackendKind::AppKit => {
332+
"value text uses the native NSAccessibility value-description property"
333+
}
334+
NativeBackendKind::Gtk4 => {
335+
"value text uses the native GtkAccessible value-text property"
336+
}
337+
NativeBackendKind::WinUI => {
338+
"value text remains in portable accessibility output because WinUI has no generic attached-property override for a control pattern value"
339+
}
340+
NativeBackendKind::Headless => {
341+
"headless mode retains value text without an OS accessibility object"
342+
}
343+
}),
344+
),
260345
NativeFeatureCapability::new(
261346
Feature::AccessibilityRelationships,
262347
Portable,
@@ -399,6 +484,22 @@ impl NativeCapabilities {
399484
"AppKit logical combo-box/list and tab items retain the computed name in portable accessibility output but do not expose an independent native accessibility-label setter",
400485
),
401486
);
487+
for feature in [
488+
Feature::AccessibilityDescription,
489+
Feature::AccessibilityRoleDescription,
490+
Feature::AccessibilityKeyShortcuts,
491+
Feature::AccessibilityValueText,
492+
] {
493+
set_role_capability(
494+
&mut role_overrides,
495+
role,
496+
feature,
497+
Portable,
498+
Some(
499+
"AppKit logical combo-box/list and tab items retain descriptive accessibility metadata in portable output but do not expose an independent native accessibility-property setter",
500+
),
501+
);
502+
}
402503
}
403504
}
404505
NativeBackendKind::Gtk4 => {
@@ -411,6 +512,22 @@ impl NativeCapabilities {
411512
"GTK4 gio::MenuItem retains the computed name in portable accessibility output but has no independent GtkAccessible label property",
412513
),
413514
);
515+
for feature in [
516+
Feature::AccessibilityDescription,
517+
Feature::AccessibilityRoleDescription,
518+
Feature::AccessibilityKeyShortcuts,
519+
Feature::AccessibilityValueText,
520+
] {
521+
set_role_capability(
522+
&mut role_overrides,
523+
NativeRole::MenuItem,
524+
feature,
525+
Portable,
526+
Some(
527+
"GTK4 gio::MenuItem retains descriptive accessibility metadata in portable output but has no independent GtkAccessible property",
528+
),
529+
);
530+
}
414531
}
415532
NativeBackendKind::WinUI => {
416533
set_role_capability(
@@ -422,6 +539,22 @@ impl NativeCapabilities {
422539
"the WinUI Window wrapper retains the computed name in portable accessibility output but is not a UIElement AutomationProperties target",
423540
),
424541
);
542+
for feature in [
543+
Feature::AccessibilityDescription,
544+
Feature::AccessibilityRoleDescription,
545+
Feature::AccessibilityKeyShortcuts,
546+
Feature::AccessibilityValueText,
547+
] {
548+
set_role_capability(
549+
&mut role_overrides,
550+
NativeRole::Window,
551+
feature,
552+
Portable,
553+
Some(
554+
"the WinUI Window wrapper retains descriptive accessibility metadata in portable output but is not a UIElement AutomationProperties target",
555+
),
556+
);
557+
}
425558
}
426559
NativeBackendKind::Headless => {}
427560
}
@@ -658,6 +791,18 @@ fn requested_features(role: NativeRole, props: &NativeProps) -> Vec<NativeCapabi
658791
if props.accessibility_label.is_some() && props.accessibility_label != props.label {
659792
features.push(Feature::AccessibilityName);
660793
}
794+
if props.accessibility_description.description.is_some() {
795+
features.push(Feature::AccessibilityDescription);
796+
}
797+
if props.accessibility_description.role_description.is_some() {
798+
features.push(Feature::AccessibilityRoleDescription);
799+
}
800+
if props.accessibility_description.key_shortcuts.is_some() {
801+
features.push(Feature::AccessibilityKeyShortcuts);
802+
}
803+
if props.accessibility_description.value_text.is_some() {
804+
features.push(Feature::AccessibilityValueText);
805+
}
661806
if props.accessibility_relationships != Default::default() {
662807
features.push(Feature::AccessibilityRelationships);
663808
}

0 commit comments

Comments
 (0)