Skip to content

Commit 4fc2795

Browse files
author
Roy Lin
committed
feat: add localized number field announcements
1 parent cacf558 commit 4fc2795

39 files changed

Lines changed: 1401 additions & 62 deletions

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ objc2-app-kit = { version = "0.3.2", default-features = false, features = [
139139
"objc2-core-foundation",
140140
"objc2-core-graphics",
141141
"objc2-quartz-core",
142+
"NSAccessibilityConstants",
142143
"NSApplication",
143144
"NSBox",
144145
"NSButton",
@@ -177,6 +178,7 @@ objc2-foundation = { version = "0.3.2", default-features = false, features = [
177178
"std",
178179
"NSDate",
179180
"NSGeometry",
181+
"NSValue",
180182
"NSObject",
181183
"NSRunLoop",
182184
"NSString",
@@ -189,7 +191,7 @@ objc2-quartz-core = { version = "0.3.2", default-features = false, features = [
189191
], optional = true }
190192

191193
[target.'cfg(target_os = "linux")'.dependencies]
192-
gtk4_crate = { package = "gtk4", version = "0.11.4", optional = true }
194+
gtk4_crate = { package = "gtk4", version = "0.11.4", features = ["v4_14"], optional = true }
193195

194196
[target.'cfg(target_os = "windows")'.dependencies]
195197
windows = { version = "0.62.0", optional = true, features = [

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ object graph at the host boundary.
3030
model/display conversion, minimum-anchored step normalization, grouped
3131
decrement/input/increment controls, portable Arrow/Page/Home/End and
3232
focus-gated wheel stepping with native default suppression,
33+
34-locale NumberField accessibility messages, focused value announcements
34+
through native assistive-technology APIs,
3335
decimal/percent/date/time formatting, versioned
3436
capability reporting, shared native event-source state machines, and
3537
accessibility conformance checks. Versioned
@@ -46,9 +48,9 @@ object graph at the host boundary.
4648
Populating the AppKit and GTK4 manifests with real platform automation,
4749
role-edge input parity, real-platform layout-aware page-navigation fixtures,
4850
IME/dead-key conformance, measured overlay collision/arrow conformance and
49-
scroll locking, remaining native focus conformance, message catalogs,
51+
scroll locking, remaining native focus conformance, broader message catalogs,
5052
currency/unit parsing and formatting, date-range/time-zone formatting,
51-
and NumberField live announcements are still in progress.
53+
and complete native accessibility-property projection are still in progress.
5254
- `.rsx` component source modules with imports, local Rust types, hook
5355
registrations, Rust selector/reducer expressions, and a final `rsx!(...)`
5456
view.

docs/architecture.md

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -453,8 +453,8 @@ prefers the GDK Unicode scalar over its symbolic key name, and WinUI uses
453453
falling back to a stable virtual-key name. Full IME and dead-key composition
454454
remains an adapter conformance gap.
455455
`I18nManager` also resolves that inherited locale for reusable
456-
`LocaleCollator`, `LocaleNumberParser`, `LocaleNumberFormatter`, and
457-
`LocaleDateFormatter` objects.
456+
`LocaleCollator`, `LocaleNumberParser`, `LocaleNumberFormatter`,
457+
`LocaleDateFormatter`, and `LocaleMessageFormatter` objects.
458458
The public collator exposes search/sort usage, ECMA-402 sensitivity,
459459
case-first, numeric ordering, and locale-equivalent starts-with, ends-with, and
460460
contains filters. Decimal parsing exposes complete parsing, partial-input
@@ -472,6 +472,11 @@ uses the same public collator filter. Native number text fields use the cached
472472
parser and formatter for localized display, range/step normalization, and
473473
model-space change events, so interactive behavior and application-level
474474
locale rules cannot drift.
475+
`LocaleMessageFormatter` owns the small accessibility-message layer that must
476+
follow inherited locale even though no Web runtime exists. Its NumberField
477+
catalog contains the same decrease, increase, numeric-role, and empty-value
478+
messages for 34 React Aria locales. Language fallback is deterministic, with
479+
separate Portuguese regions and simplified/traditional Chinese resolution.
475480
NumberField is represented as a native group with separate decrement button,
476481
numeric text input, and increment button nodes. The semantic hook computes
477482
button action values and `canIncrement`/`canDecrement` from the same
@@ -494,6 +499,16 @@ or a step boundary stops the active timer; re-entry starts a fresh cycle.
494499
Native terminal button activation is consumed so it cannot duplicate the
495500
portable step. Read-only and disabled fields retain focus semantics but do not
496501
produce step changes.
502+
Automatic stepper labels and the numeric-input role description are projected
503+
after locale inheritance, so custom labels remain untouched while native
504+
buttons receive localized defaults. A focused marked numeric input compares
505+
its previous and current normalized accessible value after reconciliation.
506+
Changes emit an assertive `AccessibilityAnnouncement`; empty values use the
507+
effective locale's message and ASCII minus signs are normalized to U+2212.
508+
`AccessibilityAnnouncementHost` is an optional host capability. The planning
509+
host serializes it, headless records it, and the native command stack forwards
510+
it to AppKit accessibility notifications, GTK4 accessible announcements, or
511+
WinUI UI Automation notifications.
497512
AppKit keeps logical item identity separate from the collection selection
498513
target: a row button is registered as the ListBoxItem or TreeItem responder,
499514
while activation reports the selected value to the owning collection.
@@ -839,6 +854,13 @@ to execute:
839854
- `{"type": "insertChild", "parent": ..., "child": ..., "index": ...}`
840855
- `{"type": "remove", "id": ...}`
841856
- `{"type": "setRoot", "id": ...}`
857+
- `{"type": "requestFocus", "id": ...}`
858+
- `{"type": "positionOverlay", "overlay": ..., "anchor": ..., "request": ...}`
859+
- `{"type": "accessibilityAnnouncement", "announcement": {"node": ..., "message": ..., "priority": "assertive"}}`
860+
861+
Strict protocol v1 flattens the same announcement payload beside its command
862+
type (`node`, `message`, and `priority`) and validates a non-zero target plus a
863+
non-empty message during decoding.
842864

843865
The planning host's command vector is a pending delivery queue, not cumulative
844866
diagnostic history. `take_commands()` moves all commands produced since the
@@ -937,6 +959,9 @@ before native handles or recorded backend objects are replaced.
937959
- `insert_child(parent, child, index)`
938960
- `remove_widget(id)`
939961
- `set_root_widget(id)`
962+
- `request_focus(id)`
963+
- `position_overlay(overlay, anchor, request)`
964+
- `announce_accessibility(announcement)`
940965

941966
If the platform layer owns event callbacks, it also implements
942967
`NativeEventSource::take_native_events()` and queues `NativeEvent` records such
@@ -1162,8 +1187,9 @@ Feature-gated platform executor surfaces:
11621187
`autoCapitalize`, `autoCorrect`,
11631188
`virtualKeyboardPolicy`, and `spellCheck` into GTK input purpose and input
11641189
hint flags. It is a Linux-only feature so macOS and Windows builds can enable
1165-
all portable features without linking GTK. Linux builds require GTK4
1166-
development libraries and `pkg-config`.
1190+
all portable features without linking GTK. Linux builds require GTK 4.14 or
1191+
newer development libraries and `pkg-config`; 4.14 provides the native
1192+
accessible announcement API used by the typed announcement command.
11671193
GTK callbacks enqueue press, text change, focus, blur, toggle, and
11681194
selection-change events through the same `NativeEventSource` and action
11691195
routing path as AppKit. GTK widgets also attach `EventControllerKey`

docs/packaging.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ The crate does not currently generate a `.dmg` or `.pkg`.
9292

9393
Prerequisites:
9494

95-
- GTK4 development libraries.
95+
- GTK 4.14 or newer development libraries. The native accessibility
96+
announcement API is part of the required backend contract.
9697
- `pkg-config`.
9798
- A Linux package toolchain only when producing distro packages.
9899

docs/react-aria-native.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,11 @@ The first shared interaction milestone is available in the portable runtime:
260260
keyed native tree. Scoped overrides and default locale changes are preserved
261261
across rerenders, and BCP 47 language/script subtags provide deterministic
262262
RTL inference. It creates reusable, thread-safe `LocaleCollator`,
263-
`LocaleNumberParser`, `LocaleNumberFormatter`, and `LocaleDateFormatter`
264-
values from the effective node locale. Collation covers search/sort
263+
`LocaleNumberParser`, `LocaleNumberFormatter`, `LocaleDateFormatter`, and
264+
`LocaleMessageFormatter` values from the effective node locale. The message
265+
formatter includes React Aria-compatible NumberField labels, role
266+
descriptions, and empty-value text for 34 locales with deterministic
267+
language/script/region fallback. Collation covers search/sort
265268
sensitivity, case-first, numeric ordering, and locale-equivalent prefix,
266269
suffix, and substring filtering. Stable ICU4X decimal parsing covers
267270
localized signs and separators, partial-input validation, and automatic
@@ -296,6 +299,14 @@ The first shared interaction milestone is available in the portable runtime:
296299
AppKit, GTK4, and WinUI claim handled keys and wheels before the toolkit
297300
default runs and consume terminal stepper activation, preventing native
298301
controls from applying a duplicate change.
302+
Default stepper labels and the numeric-input role description are localized
303+
from the inherited locale; explicit stepper-label overrides remain
304+
authoritative. When the focused input's normalized accessible value changes,
305+
the runtime emits an assertive typed announcement. AppKit posts
306+
`NSAccessibilityAnnouncementRequestedNotification`, GTK4 calls
307+
`gtk_accessible_announce`, and WinUI raises a UI Automation notification.
308+
Empty values use the same locale catalog, and negative announcements use the
309+
screen-reader-friendly Unicode minus sign.
299310
- Native IR capabilities are versioned. Every host exposes a feature manifest
300311
with unsupported, portable, or native support levels, role-specific
301312
overrides, and auditable capability issues. Protocol render responses carry
@@ -335,7 +346,7 @@ existence of a platform object:
335346
| Complete press lifecycle | Button, disclosure summary, link, image-map area, ListBoxItem, and TreeItem on AppKit, GTK4, and WinUI; WinUI menu items also use the complete lifecycle. |
336347
| Long press | Shared AppKit, GTK4, and WinUI press sources emit start/end and recognize terminal long press after the configured threshold. `NSTimer`, GTK main-loop timeout, and `DispatcherQueueTimer` provide threshold-time delivery, and release-time evaluation is the fallback. |
337348
| Move | AppKit mouse/pen drag events, GTK4 `GestureDrag`, and WinUI mouse/touch/pen pointer capture use one incremental move state machine. All three normalize Arrow keys to a complete keyboard lifecycle and prevent the underlying native default. |
338-
| 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. Automatic localized button messages and live value announcements remain incomplete. |
349+
| 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. |
339350
| 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. |
340351
| 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. |
341352
| 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. |
@@ -415,9 +426,9 @@ props:
415426
| P1 | Event propagation | Add platform-run conformance fixtures for conditional `Stop`/`Continue` across nested native controls. |
416427
| P1 | Focus management | Add platform-run conformance fixtures for post-mount `autoFocus`, nested containment, and restoration. |
417428
| P1 | Collections and selection | Complete IME/dead-key typeahead conformance and add real-platform fixtures for layout-aware page navigation. |
418-
| P1 | NumberField interaction | Add automatic localized stepper labels and live value announcements. Group/input/button anatomy, 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. |
419-
| P1 | Internationalization | Add message formatting, currency/unit parsing and formatting, and date ranges/time zones. Reusable decimal/percent parsing and formatting, partial-input validation, locale-aware filtering, and localized NumberField model/display conversion now build on inherited locale/direction. |
420-
| P1 | Accessibility conformance | Complete OS accessibility API projection, relationships, live regions, value announcements, and role-specific native adapter coverage. |
429+
| 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. |
430+
| 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. |
431+
| P1 | Accessibility conformance | Complete OS accessibility-property projection, relationships, general live regions, and role-specific native adapter coverage. The typed announcement channel and focused NumberField value announcements are implemented on AppKit, GTK4, and WinUI. |
421432
| 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. |
422433
| P2 | Capability enforcement | Turn reported capability gaps into adapter policy and conformance gates where portable fallback is not sufficient. |
423434
| 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. |

docs/roadmap.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,9 @@ The following foundations are implemented and are not roadmap placeholders:
8080
model/display normalization, grouped native stepper controls,
8181
minimum-anchored button, Arrow/Page/Home/End, and focus-gated vertical-wheel
8282
stepping, cancellable cross-platform pointer-hold repetition, platform-default
83-
suppression, horizontal/zoom rejection, and decimal/percent/date/time
84-
formatting
83+
suppression, horizontal/zoom rejection, a 34-locale NumberField accessibility
84+
catalog, assertive focused-value announcements on AppKit/GTK4/WinUI, and
85+
decimal/percent/date/time formatting
8586
- an activation-ordered mounted overlay stack with topmost Escape/outside
8687
dismissal, modal background inertness, portaled foreground layers, and
8788
shared focus containment, autofocus, and restoration

docs/rsx.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,17 +1500,19 @@ and control-wheel zoom are ignored. Step boundaries are anchored at
15001500
/>
15011501
```
15021502

1503-
The accessible-label overrides are optional. Without them, the built-in
1504-
English labels include the field label. Applications should provide localized
1505-
overrides until automatic message catalogs are implemented.
1503+
The accessible-label overrides are optional. Without them, the built-in labels
1504+
include the field label and follow the inherited locale through the 34-locale
1505+
NumberField message catalog. Explicit overrides are never replaced.
15061506
`isWheelDisabled={true}` disables wheel changes without disabling keyboard or
15071507
button stepping. Mouse stepper presses preserve input focus. Mouse and pen
15081508
steppers fire immediately, begin repeating after 400 milliseconds, and repeat
15091509
every 60 milliseconds. Touch steppers begin repeating after 600 milliseconds;
15101510
a shorter touch activates once on release. Leaving, cancellation, a disabled
1511-
or read-only update, and reaching a step boundary stop repetition. Automatic
1512-
localized stepper labels and live value announcements are not yet part of this
1513-
contract.
1511+
or read-only update, and reaching a step boundary stop repetition. The numeric
1512+
input also receives a localized role description. While it is focused,
1513+
normalized value changes are announced assertively through AppKit, GTK4, or
1514+
WinUI's native assistive-technology channel; empty values are localized as
1515+
well.
15141516

15151517
- `UiButton`
15161518
- `UiBadge`

src/accessibility.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,40 @@ pub use conformance::{
1010
#[cfg(test)]
1111
mod conformance_tests;
1212

13+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
14+
#[serde(rename_all = "camelCase")]
15+
pub enum AccessibilityAnnouncementPriority {
16+
#[default]
17+
Polite,
18+
Assertive,
19+
}
20+
21+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22+
#[serde(rename_all = "camelCase")]
23+
pub struct AccessibilityAnnouncement {
24+
pub node: HostNodeId,
25+
pub message: String,
26+
pub priority: AccessibilityAnnouncementPriority,
27+
}
28+
29+
impl AccessibilityAnnouncement {
30+
pub fn new(
31+
node: HostNodeId,
32+
message: impl Into<String>,
33+
priority: AccessibilityAnnouncementPriority,
34+
) -> Self {
35+
Self {
36+
node,
37+
message: message.into(),
38+
priority,
39+
}
40+
}
41+
42+
pub fn assertive(node: HostNodeId, message: impl Into<String>) -> Self {
43+
Self::new(node, message, AccessibilityAnnouncementPriority::Assertive)
44+
}
45+
}
46+
1347
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1448
#[serde(rename_all = "camelCase")]
1549
pub enum AccessibilityRole {

0 commit comments

Comments
 (0)