react-x11 2.52.0 → 2.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/docs/README.md +160 -314
- package/docs/accessibility.md +176 -229
- package/docs/appearance.md +77 -176
- package/docs/click-to-component.md +18 -51
- package/docs/clipboard.md +40 -59
- package/docs/components.md +518 -1036
- package/docs/dbus.md +61 -133
- package/docs/debugging.md +152 -245
- package/docs/desktop-calendar.md +51 -88
- package/docs/desktop.md +222 -347
- package/docs/devtools.md +50 -167
- package/docs/drag-and-drop.md +126 -204
- package/docs/ecosystem/animation.md +91 -125
- package/docs/ecosystem/data-fetching.md +38 -53
- package/docs/ecosystem/dev-tooling.md +159 -202
- package/docs/ecosystem/forms.md +61 -97
- package/docs/ecosystem/headless.md +64 -89
- package/docs/ecosystem/i18n.md +18 -21
- package/docs/ecosystem/icons.md +93 -126
- package/docs/ecosystem/layout.md +42 -52
- package/docs/ecosystem/routing.md +18 -29
- package/docs/ecosystem/state.md +71 -88
- package/docs/ecosystem/testing.md +131 -157
- package/docs/ecosystem/theming.md +119 -165
- package/docs/ecosystem.md +145 -179
- package/docs/elements.md +1096 -1774
- package/docs/embedding.md +83 -132
- package/docs/events.md +412 -695
- package/docs/extending.md +784 -1326
- package/docs/eyedropper.md +63 -122
- package/docs/filedialog.md +71 -107
- package/docs/frame.md +195 -339
- package/docs/gl.md +65 -62
- package/docs/globalmenu.md +63 -146
- package/docs/macos.md +531 -3234
- package/docs/notifications.md +33 -47
- package/docs/packaging.md +166 -309
- package/docs/permissions.md +9 -14
- package/docs/react-features.md +81 -106
- package/docs/remote.md +32 -76
- package/docs/scale.md +72 -186
- package/docs/styling.md +329 -547
- package/docs/system.md +200 -280
- package/docs/testing.md +63 -83
- package/docs/typescript.md +6 -48
- package/docs/uri-schemes.md +67 -107
- package/docs/wayland-backend.md +200 -459
- package/docs/windows-integrations.md +75 -131
- package/docs/windows.md +343 -1367
- package/package.json +3 -2
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/dnd.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/imagedecode-worker.js +32 -0
- package/src/imagedecode.js +140 -2
- package/src/layouts.js +1 -1
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/ntk.d.ts +12 -0
- package/src/pacing.js +1 -1
- package/src/refresh/index.js +2 -1
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +27 -0
- package/src/wayland/video.js +376 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
- package/docs/architecture/accessibility.md +0 -470
- package/docs/architecture/animation.md +0 -707
- package/docs/architecture/container-queries.md +0 -525
- package/docs/architecture/custom-layout.md +0 -334
- package/docs/architecture/drag-and-drop.md +0 -941
- package/docs/architecture/element-layer-contents.md +0 -622
- package/docs/architecture/frame-pacing.md +0 -255
- package/docs/architecture/grid-layout.md +0 -358
- package/docs/architecture/protocol-efficiency.md +0 -357
- package/docs/architecture/video.md +0 -1149
- package/docs/architecture/windowed-regions.md +0 -854
- package/docs/glx.md +0 -137
- package/docs/wayland.md +0 -545
- package/docs/windows-embedding.md +0 -271
- package/docs/windows-gl.md +0 -221
|
@@ -1,470 +0,0 @@
|
|
|
1
|
-
# Accessibility: an AT-SPI2 bridge with no mirror, and where it lives
|
|
2
|
-
|
|
3
|
-
Design record for the accessibility layer that shipped in core — the _why_
|
|
4
|
-
behind [accessibility.md](../accessibility.md), which is the reference for
|
|
5
|
-
what the API is. Written as built, not as proposed: the alternatives below
|
|
6
|
-
were live options at the time and the reasons they lost are the useful part.
|
|
7
|
-
|
|
8
|
-
_Checked at react-x11 `a937436` (master, 2026-08-07), **ntk 7.2.0**,
|
|
9
|
-
`dbus-native` 0.15.1, against at-spi2-core 2.60.4 (the installed
|
|
10
|
-
introspection data) and at-spi2-core's `xml/` interface definitions (the
|
|
11
|
-
wire spec). Verified live against this desktop's registry with libatspi
|
|
12
|
-
(python3 `gi.repository.Atspi`) and with Orca's own speech log. File:line
|
|
13
|
-
references drift with the code._
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## 0. TL;DR
|
|
18
|
-
|
|
19
|
-
- **Linux accessibility is D-Bus, not X.** An app exposes a tree of objects
|
|
20
|
-
implementing `org.a11y.atspi.*` on a dedicated _accessibility bus_ and
|
|
21
|
-
registers with the AT-SPI registry daemon; Orca walks that. Being a
|
|
22
|
-
pure-JS X11 client blocks nothing.
|
|
23
|
-
- **Two modules.** `src/a11y.js` is the model: pure functions from the
|
|
24
|
-
retained node tree to roles/names/states, plus null hook slots the
|
|
25
|
-
renderer polls (the trace-registry pattern — one property read per event
|
|
26
|
-
when off). `src/atspi.js` is the bridge: D-Bus plumbing, dynamically
|
|
27
|
-
imported once per process when a root exists. The model is unit-testable
|
|
28
|
-
with no bus anywhere; the bridge is testable against an in-process bus.
|
|
29
|
-
- **No mirror tree.** Every AT call is answered from the live node tree at
|
|
30
|
-
the moment it arrives. The only per-node bridge state is the object path,
|
|
31
|
-
the exported interface list, and a snapshot of what the bus was last
|
|
32
|
-
_told_ — kept solely to turn "something changed" into the precise AT-SPI
|
|
33
|
-
event diff.
|
|
34
|
-
- **The vocabulary is the web's** (`role`, `aria-*`), not React Native's
|
|
35
|
-
`accessibilityRole` family that NEXT_STEPS §11.3 originally recommended.
|
|
36
|
-
RN itself adopted the ARIA names in 0.71, and the widgets here had been
|
|
37
|
-
carrying web role strings for months. Role→AT-SPI mappings follow what
|
|
38
|
-
Chromium and Firefox send on Linux, so Orca sees from a react-x11 app
|
|
39
|
-
what it sees from a browser.
|
|
40
|
-
- **In core, not `@react-x11/a11y`.** The dependency argument for a sibling
|
|
41
|
-
package dissolved when D-Bus landed in core as an `optionalDependency`;
|
|
42
|
-
what remained was a peer-version matrix and an opt-in accessibility
|
|
43
|
-
layer, which is an accessibility failure.
|
|
44
|
-
- **Off is free and silent.** A four-rung ladder (transport → session bus →
|
|
45
|
-
`GetAddress` → connect+`Embed`), every rung failing into "off" with no
|
|
46
|
-
log and no mode; the bridge's socket is `unref()`d so it never holds the
|
|
47
|
-
process open. Discovery dials a **private, short-lived connection** —
|
|
48
|
-
deliberately not the shared `sessionBus()` — see §7.
|
|
49
|
-
|
|
50
|
-
## 1. The constraints that shaped it
|
|
51
|
-
|
|
52
|
-
Three personas had veto power:
|
|
53
|
-
|
|
54
|
-
1. **The machine with no accessibility stack** — ssh -X, CI, containers,
|
|
55
|
-
macOS/XQuartz, bare `startx`. react-x11's flagship configurations. The
|
|
56
|
-
layer must cost them nothing: no import weight, no connection attempts
|
|
57
|
-
that log, no event-loop hold, no new mode. This is the same rule
|
|
58
|
-
[dbus.md](../dbus.md) established: "no bus" is a first-class
|
|
59
|
-
configuration.
|
|
60
|
-
2. **The screen-reader user on a desktop.** For them the tree has to be
|
|
61
|
-
_correct_, not merely present: states that match the pixels, focus
|
|
62
|
-
events at the moment focus moves, text deltas as they type, and controls
|
|
63
|
-
that can actually be driven. Half an implementation — a tree with stale
|
|
64
|
-
states — is worse than none, because the user cannot tell which half to
|
|
65
|
-
trust.
|
|
66
|
-
3. **The component-library author targeting react-x11.** They must not need
|
|
67
|
-
a react-x11-specific integration. Standard props on host elements had to
|
|
68
|
-
be the entire seam.
|
|
69
|
-
|
|
70
|
-
And one engineering constraint: the renderer's hot paths (focus moves,
|
|
71
|
-
every `applyProps`, every text repaint) get touched. The cost of
|
|
72
|
-
accessibility being _off_ had to be one nullable property read per site,
|
|
73
|
-
which is the same bar `trace-registry.js` set for the protocol tracer.
|
|
74
|
-
|
|
75
|
-
## 2. Where accessibility actually happens
|
|
76
|
-
|
|
77
|
-
The at-spi2-core `xml/` directory is the wire spec, and the shape is:
|
|
78
|
-
|
|
79
|
-
- A **separate bus**, not the session bus. `org.a11y.Bus.GetAddress` on the
|
|
80
|
-
session bus answers its address (activating `at-spi-bus-launcher` on
|
|
81
|
-
demand); `AT_SPI_BUS_ADDRESS` overrides.
|
|
82
|
-
- The app exports objects at `/org/a11y/atspi/accessible/<id>` implementing
|
|
83
|
-
`Accessible` (tree + role + states) and per-capability interfaces:
|
|
84
|
-
`Component` (geometry, hit test, focus), `Action`, `Value`,
|
|
85
|
-
`Text`/`EditableText`. The app root at `…/accessible/root` adds
|
|
86
|
-
`Application` and calls `org.a11y.atspi.Socket.Embed` on the registry,
|
|
87
|
-
which replies with the desktop ref the root reports as its `Parent`.
|
|
88
|
-
- ATs bulk-load through `org.a11y.atspi.Cache.GetItems` (one struct per
|
|
89
|
-
node) and then track **signals**: `org.a11y.atspi.Event.Object.*`
|
|
90
|
-
(`StateChanged`, `ChildrenChanged`, `TextChanged`, `TextCaretMoved`,
|
|
91
|
-
`PropertyChange`, `Announcement`, …) and `Event.Window.*`
|
|
92
|
-
(`Activate`/`Deactivate`/`Create`/`Destroy`), all with the same
|
|
93
|
-
`siiva{sv}` body. Orca's model of the app is built almost entirely from
|
|
94
|
-
these; getting their _details_ right (state nicks like `"focused"`,
|
|
95
|
-
insert/delete offsets in code points) is most of the work.
|
|
96
|
-
- Toolkit-less apps are expected to drive this themselves rather than
|
|
97
|
-
inherit it from GTK. That is what `src/atspi.js` is.
|
|
98
|
-
|
|
99
|
-
Enum values (roles, states, relations) were generated from the installed
|
|
100
|
-
`gi.repository.Atspi` — the very tables Orca reads — and are append-only
|
|
101
|
-
upstream, so they are pasted into `a11y.js` as frozen data with the
|
|
102
|
-
regeneration one-liner in a comment, not fetched or computed.
|
|
103
|
-
|
|
104
|
-
## 3. The two-module split, and the hook-slot seam
|
|
105
|
-
|
|
106
|
-
```
|
|
107
|
-
src/nodes/, events.js, Reconciler.js (hot paths)
|
|
108
|
-
│ a11yHooks.focus?.(…) — null when off
|
|
109
|
-
▼
|
|
110
|
-
src/a11y.js the model: role/name/states/tree-projection as pure
|
|
111
|
-
functions; the hook slots; startA11y() gate
|
|
112
|
-
│ dynamic import, once, when a root exists and the gate passes
|
|
113
|
-
▼
|
|
114
|
-
src/atspi.js the bridge: bus dial, Embed, per-node export, the
|
|
115
|
-
event queue/flush, the interface implementations
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
The precedent is `trace-registry.js`: an always-imported module whose whole
|
|
119
|
-
idle cost is null slots, with the heavy half behind a dynamic import. Two
|
|
120
|
-
consequences fall out of the split being _exactly at the D-Bus line_:
|
|
121
|
-
|
|
122
|
-
- Everything semantically interesting — what role a node has, what its name
|
|
123
|
-
resolves to, which states are set, what the accessible tree looks like
|
|
124
|
-
after pruning — is computable and testable with **no bus in the
|
|
125
|
-
process** (`test/a11y.test.js`). The bridge test then only has to prove
|
|
126
|
-
plumbing, not semantics.
|
|
127
|
-
- The bridge can be rewritten (Wayland's a11y future, a different
|
|
128
|
-
transport) without touching the model or the renderer wiring.
|
|
129
|
-
|
|
130
|
-
The renderer chokepoints wired to slots: `insertBefore`/`removeChild`
|
|
131
|
-
(tree), `applyProps` (props), `TextChunkNode.setText` (label content),
|
|
132
|
-
`TextInputNode._repaint` (value/caret/selection — every edit path funnels
|
|
133
|
-
through it), `EventManager.focus` (the one place focus moves),
|
|
134
|
-
`_onWindowFocus` (WM focus), `appendChildToContainer`/
|
|
135
|
-
`removeChildFromContainer` (toplevels), `resetAfterCommit` (flush). Focus
|
|
136
|
-
and focusability deserve a note: the focusable rule used to exist twice
|
|
137
|
-
(`EventManager._isFocusable` and `Node._focusableForRing`); it now lives
|
|
138
|
-
once in `a11y.js` and both call it, so the keyboard, the focus ring and the
|
|
139
|
-
FOCUSABLE state cannot disagree by construction.
|
|
140
|
-
|
|
141
|
-
## 4. No mirror
|
|
142
|
-
|
|
143
|
-
The obvious design — GTK's, historically — is a shadow "accessible object"
|
|
144
|
-
per widget, kept in sync. The whole class of bugs that produces (stale
|
|
145
|
-
name, stale index, remove-before-add races) comes from the mirror being a
|
|
146
|
-
second copy of the truth.
|
|
147
|
-
|
|
148
|
-
Here there is no copy. `GetChildren` walks the live `node.children` through
|
|
149
|
-
the projection (§5) at call time; `GetState` recomputes from live props and
|
|
150
|
-
live focus; `GetExtents` reads `node.abs` and the window's live
|
|
151
|
-
`_screenOrigin`. An answer cannot be stale because nothing is cached to go
|
|
152
|
-
stale.
|
|
153
|
-
|
|
154
|
-
What _is_ kept per exported node — `{ id, ifaces, snapshot }` — exists for
|
|
155
|
-
one reason: **events are diffs, and a diff needs a "before".** The snapshot
|
|
156
|
-
holds the last name/description/states/value/text the bus was told.
|
|
157
|
-
`syncNode()` recomputes, XORs the two u32 state words, and emits one
|
|
158
|
-
`StateChanged` per flipped bit with its enum nick; `_diffText` runs a
|
|
159
|
-
common-prefix/suffix diff over the code-point arrays and emits the
|
|
160
|
-
`delete`+`insert` pair with exact offsets. This is also what makes the
|
|
161
|
-
wiring correct without per-prop plumbing: _any_ prop change queues the
|
|
162
|
-
node, and whatever actually changed is what gets emitted.
|
|
163
|
-
|
|
164
|
-
Object **export is lazy**: a node is exported the first time a ref to it is
|
|
165
|
-
handed out (`refFor`), which transitively means "the first time an AT could
|
|
166
|
-
possibly call back on it". Per (node, interface) the impl object is
|
|
167
|
-
`{ __proto__: sharedProto, b: bridge, n: node }` — the prototypes hold all
|
|
168
|
-
behaviour, dbus-native reads properties through the chain, and the per-node
|
|
169
|
-
cost is a couple of two-field objects. Signals are emitted through raw
|
|
170
|
-
`bus.sendSignal` rather than dbus-native's EventEmitter hook-up, so impls
|
|
171
|
-
need no emitter machinery. On detach, the subtree is walked, each exported
|
|
172
|
-
node unexported with a `Cache.RemoveAccessible`, and the parent gets one
|
|
173
|
-
`ChildrenChanged remove` — but only if the AT had ever seen the child;
|
|
174
|
-
what the bus was never told about, it does not need to hear removed.
|
|
175
|
-
|
|
176
|
-
## 5. The tree projection
|
|
177
|
-
|
|
178
|
-
The accessible tree is the node tree through three rules:
|
|
179
|
-
|
|
180
|
-
- **Pruned subtrees**: `aria-hidden`, the internal content kinds
|
|
181
|
-
(`textchunk`, `svgchild`), `<text>` spans (the outer `<text>` speaks for
|
|
182
|
-
the run), and everything under `<glarea>` (scene nodes have no
|
|
183
|
-
rectangles). Gone entirely.
|
|
184
|
-
- **Erased nodes**: `role="none"`/`"presentation"` — the node vanishes and
|
|
185
|
-
its children take its place, ARIA's semantics. This is what keeps a
|
|
186
|
-
styling wrapper from adding a filler level around every widget.
|
|
187
|
-
- **Defaults**: `<box>` is FILLER — the role GTK gives its own layout
|
|
188
|
-
containers and screen readers step over silently — so an unlabelled tree
|
|
189
|
-
is _boring_ rather than noisy. Everything with real semantics defaults to
|
|
190
|
-
them (window→FRAME, text→LABEL, textinput→ENTRY, image→IMAGE, …).
|
|
191
|
-
|
|
192
|
-
…plus a fourth source of children that is not in the node tree at all: what
|
|
193
|
-
a registered element says it drew (§8b), appended after whatever it holds
|
|
194
|
-
retained.
|
|
195
|
-
|
|
196
|
-
Two structural notes. `<popup>`s stay **nested**: a popup's `parent` is the
|
|
197
|
-
JSX node it was written under, so a menu appears inside the widget that
|
|
198
|
-
opened it — GTK4 does the same with popovers, and it preserves context that
|
|
199
|
-
promoting popups to application children would lose. Screen coordinates
|
|
200
|
-
still resolve correctly because extents go through the _owning window's_
|
|
201
|
-
`_screenOrigin`, and the popup is its own window. And name-from-contents
|
|
202
|
-
(button/menuitem/tab/… with no `aria-label`) concatenates the subtree's
|
|
203
|
-
chunk text — which is why `MenuRow` sets `aria-label` explicitly: without
|
|
204
|
-
it the shortcut column and the submenu arrow would read into the name
|
|
205
|
-
("New Ctrl+N ▸").
|
|
206
|
-
|
|
207
|
-
## 6. Events: queue, collapse, flush
|
|
208
|
-
|
|
209
|
-
Hook calls do not emit; they queue. The flush runs at `resetAfterCommit`
|
|
210
|
-
(so a commit's worth of mutations is one batch) and, for changes that
|
|
211
|
-
happen outside commits — pointer-driven focus, typing into an uncontrolled
|
|
212
|
-
input — on a `queueMicrotask` fallback. Collapse rules:
|
|
213
|
-
|
|
214
|
-
- **Removals first**, so a node that left and rejoined in one batch reads
|
|
215
|
-
as its add, never as a stale remove.
|
|
216
|
-
- **Attaches collapse to the topmost node** per subtree: the AT gets one
|
|
217
|
-
`ChildrenChanged add` + `Cache.AddAccessible` and descends on its own.
|
|
218
|
-
The initial bottom-up construction of a tree costs _nothing_ — a detached
|
|
219
|
-
subtree is not "live" (§3's `_live` walk) until its top joins a mounted
|
|
220
|
-
toplevel, so React building a window queues zero events until the
|
|
221
|
-
container mount, which then announces exactly one child.
|
|
222
|
-
- Focus emits `StateChanged focused` on both ends **plus** the legacy
|
|
223
|
-
`Event.Focus.Focus` on the gaining node, because that is what the GTK
|
|
224
|
-
bridge emits and older ATs still listen for the pair.
|
|
225
|
-
|
|
226
|
-
## 7. The ladder, and why discovery dials its own socket
|
|
227
|
-
|
|
228
|
-
`startA11y()` (called from `createRoot`, never awaited): gate →
|
|
229
|
-
`import('./atspi.js')` → transport → session-bus address → `GetAddress` →
|
|
230
|
-
connect → export root + `Cache` → `Embed` → install hooks → walk
|
|
231
|
-
already-mounted toplevels (via `trace-registry.onApp`). Every failure is a
|
|
232
|
-
silent off; `REACT_X11_A11Y=1` makes each rung print why it stopped, which
|
|
233
|
-
is the entire debugging story for "Orca doesn't see my app".
|
|
234
|
-
|
|
235
|
-
The one non-obvious decision: **the `GetAddress` probe uses a private
|
|
236
|
-
connection, opened and closed inside the call, not the shared
|
|
237
|
-
`sessionBus()`**. The shared machinery is process-global state, and the
|
|
238
|
-
climb is an unowned background task — the combination raced this repo's own
|
|
239
|
-
test harness, which swaps `DBUS_SESSION_BUS_ADDRESS` to a per-test broker
|
|
240
|
-
and hard-resets the shared state between cases; an in-flight climb from an
|
|
241
|
-
earlier test corrupted a later test's portal reads. The fix is not "be
|
|
242
|
-
careful", it is _no shared state_: a probe that owns its socket cannot
|
|
243
|
-
interfere with anyone, and as a bonus the shared session connection is no
|
|
244
|
-
longer dialled (and kept warm) as the side effect of a probe on machines
|
|
245
|
-
where nothing else wants D-Bus.
|
|
246
|
-
|
|
247
|
-
Related hygiene, borrowed from the ecosystem: `NO_AT_BRIDGE` is honoured —
|
|
248
|
-
it is what at-spi2-atk checks and what GTK's own test suite sets — and
|
|
249
|
-
`react-x11/test` sets it, so a suite running on a developer's desktop does
|
|
250
|
-
not parade hundreds of phantom applications through a live screen reader.
|
|
251
|
-
Priority order: `REACT_X11_A11Y` (explicit, either way) →
|
|
252
|
-
`AT_SPI_BUS_ADDRESS` (an address is intent — it is also the hermetic test
|
|
253
|
-
seam) → `NO_AT_BRIDGE` → on.
|
|
254
|
-
|
|
255
|
-
The bridge holds no event-loop reference (`stream.unref()`): it is a
|
|
256
|
-
passenger, not cargo, and a process with no other work exits — the
|
|
257
|
-
registry sees the name drop, which is AT-SPI's own liveness model. A dead
|
|
258
|
-
accessibility bus is not redialled, the same no-resurrection contract as
|
|
259
|
-
`bus.js`; the registry _restarting_ is survivable though — the bridge
|
|
260
|
-
watches `NameOwnerChanged` on `org.a11y.atspi.Registry` and re-embeds,
|
|
261
|
-
exactly as the GTK bridge does.
|
|
262
|
-
|
|
263
|
-
## 8. AT-driven input goes through the front door
|
|
264
|
-
|
|
265
|
-
- **Activation** (`Action.DoAction("activate")`) dispatches a synthetic
|
|
266
|
-
**full press gesture** — MouseDown, MouseUp, Click — through
|
|
267
|
-
`EventManager.dispatch` at discrete priority with the frame flush, so an
|
|
268
|
-
AT press is indistinguishable from a finger: capture/bubble order,
|
|
269
|
-
`:active` styling, the commit, the paint. The full gesture rather than a
|
|
270
|
-
bare Click was a live-testing find: `Select` and `MenuBar` open their
|
|
271
|
-
menus **on the press**, as real menus do, and a Click-only activation
|
|
272
|
-
silently did nothing on exactly those controls. Coordinate-driven
|
|
273
|
-
_default_ actions (caret placement, drag arming) are deliberately not
|
|
274
|
-
invoked — a synthesized centre point is not a place the user chose.
|
|
275
|
-
- **Value writes** (`Value.CurrentValue` set) route to the node's
|
|
276
|
-
`onAccessibilityAction({ action: 'setValue', value })`, wrapped in
|
|
277
|
-
`callHandler` so a throwing handler cannot unwind into dbus-native's
|
|
278
|
-
message dispatch. `Slider` wires it into the same clamp/quantize/emit as
|
|
279
|
-
the pointer and the keyboard — one state path, three input routes.
|
|
280
|
-
- **Edits** (`EditableText`) go through `TextInputNode._commit`, the same
|
|
281
|
-
single mutation point typing uses, so `onChange` fires and undo history
|
|
282
|
-
records — an AT edit _is_ a user edit. Offsets need no conversion
|
|
283
|
-
anywhere: AT-SPI counts code points, and `_chars()`/`_caret` already do.
|
|
284
|
-
Every write is expressed as **one replacement** — `[start, end) := text`
|
|
285
|
-
— because that is the primitive both implementations can offer: the
|
|
286
|
-
built-ins splice and `_commit`, and a registered element implements
|
|
287
|
-
`a11yReplaceText`, which its own editing needs anyway. Set-the-lot,
|
|
288
|
-
insert and delete are that call with one end moved.
|
|
289
|
-
|
|
290
|
-
## 8a. Text from elements core did not write (#257)
|
|
291
|
-
|
|
292
|
-
A registered element draws text core cannot see: `<textinput>`'s state is
|
|
293
|
-
read off `_chars()`/`_caret`/`_selection`, which a `<codeeditor>` in
|
|
294
|
-
another package does not have. The seam is a **pull** — `a11yTextState()`
|
|
295
|
-
answers `{ value, caret, selectionStart, selectionEnd, editable,
|
|
296
|
-
multiline, preedit }` — plus a **push** that says it is worth pulling,
|
|
297
|
-
`Node.notifyA11yTextChanged()`, which is the same `hooks.textState` slot
|
|
298
|
-
`_repaint` already fed. Three consequences make it the cheap shape:
|
|
299
|
-
|
|
300
|
-
- **One normalizer, no second model.** `customTextState()` turns the
|
|
301
|
-
element's answer into the `{ chars, caret, selection, preedit }` the
|
|
302
|
-
bridge and the spy already speak, clamping the offsets on the way — so
|
|
303
|
-
granularity, attribute runs, the diff, `:system` composition marking and
|
|
304
|
-
the spy's transcript are the same code paths `<textinput>` uses. A
|
|
305
|
-
third-party editor cannot be read _differently_ from core's, only less.
|
|
306
|
-
- **Two tiers, because viewers are not degenerate editors.** `editable`
|
|
307
|
-
splits them: without it an element is `Text` and a `document` role,
|
|
308
|
-
which is exactly the markdown/code-block/terminal case (selection,
|
|
309
|
-
Ctrl+C, no caret semantics); with it, EDITABLE, an `entry` role, and —
|
|
310
|
-
only if `a11yReplaceText` exists — the `EditableText` interface.
|
|
311
|
-
Exporting an interface whose every method answered false would be a lie
|
|
312
|
-
an AT has no way to test.
|
|
313
|
-
- **Declared, not inferred.** `a11yRole` is the element's own default
|
|
314
|
-
role (below a `role` prop, above the scroller rule, so an editor that
|
|
315
|
-
scrolls is still an entry), and `multiline` is a claim rather than a
|
|
316
|
-
guess off the value — inferring it from a `\n` would report the
|
|
317
|
-
element's _shape_ changing the first time somebody pressed Enter, the
|
|
318
|
-
same objection that keeps the scroll-pane role off `isScroller()`
|
|
319
|
-
flicker.
|
|
320
|
-
|
|
321
|
-
Deliberately not in the seam: per-character extents (they need a layout
|
|
322
|
-
only the element has; the fallback is the element's own rect, which is
|
|
323
|
-
honest and keeps a magnifier tracking something real) and AT-driven
|
|
324
|
-
copy/paste, which stay the built-ins' clipboard round trip.
|
|
325
|
-
|
|
326
|
-
## 8b. Structure from elements core did not write (#304)
|
|
327
|
-
|
|
328
|
-
The same gap one level up. An element that _draws_ N interactive things is
|
|
329
|
-
one node and therefore one accessible — a graph pane reads as "group" with
|
|
330
|
-
nothing inside — and unlike the text case there is no prop that could have
|
|
331
|
-
said otherwise, because the objects an AT needs do not exist anywhere.
|
|
332
|
-
`a11yScene()` is the pull that produces them: `[{ id, rect, role, name,
|
|
333
|
-
states, props, children }]`, with `Node.notifyA11ySceneChanged()` as the
|
|
334
|
-
push, routed through the existing `hooks.propsChanged` slot because
|
|
335
|
-
"re-read this node's accessible facts and announce the difference" is
|
|
336
|
-
exactly what that slot already means.
|
|
337
|
-
|
|
338
|
-
Four decisions, in the order they bite:
|
|
339
|
-
|
|
340
|
-
- **Objects, not descriptors.** Everything else here is answered from the
|
|
341
|
-
live tree (§4), and a scene has no live tree to answer from — the
|
|
342
|
-
element rebuilds its description every frame. So a11y.js reconciles the
|
|
343
|
-
frame's descriptions against the last frame's by `id` and returns the
|
|
344
|
-
_same_ objects where the id survived. That is what an export can be keyed
|
|
345
|
-
on: paths, snapshots and every ref an AT is holding stay valid across a
|
|
346
|
-
scene rebuilt sixty times a second, and an id that disappears is marked
|
|
347
|
-
defunct rather than forgotten, so the bridge can still walk what it is
|
|
348
|
-
losing. Identity is the one thing core cannot derive, which is why `id`
|
|
349
|
-
is the only required field besides the rect.
|
|
350
|
-
- **A scene item is read by the model, not beside it.** It carries `props`
|
|
351
|
-
in the same `role`/`aria-*` vocabulary and an `abs` rect in the same
|
|
352
|
-
window coordinates, so `atspiRoleOf`, `a11yName`, `a11yStates`,
|
|
353
|
-
`a11yAttributes`, the extents, the cache items, the snapshot diff and the
|
|
354
|
-
test spy are all the code that already existed — the same "no second
|
|
355
|
-
model" rule §8a's normalizer follows. The friendly `states: { selected,
|
|
356
|
-
checked, expanded, disabled, busy }` is a table of five entries into
|
|
357
|
-
those props; `focused` is the exception, because element-internal focus
|
|
358
|
-
never reaches a window's focus manager and there is no ARIA spelling for
|
|
359
|
-
it.
|
|
360
|
-
- **Actions fall back rather than fail.** `a11ySceneAction(id, action)`
|
|
361
|
-
claims with `true`; anything else takes core's answer — for `activate`, a
|
|
362
|
-
synthetic click through the ordinary dispatch at the item's own rect. An
|
|
363
|
-
element hit-tests its scene already, so that is not an approximation of
|
|
364
|
-
the user's click, it _is_ it. A single multiplexed hook (rather than one
|
|
365
|
-
method per action) is what makes claiming `focus` and leaving `activate`
|
|
366
|
-
to core the default reading of the code rather than a trap.
|
|
367
|
-
- **An action is offered where one lands.** The role table decides as it
|
|
368
|
-
does everywhere else, plus: an element that implements the seam makes
|
|
369
|
-
every item it drew activatable, because implementing it is the same
|
|
370
|
-
promise a handler is — and the roles a scene reaches for (`listitem` for
|
|
371
|
-
a graph node) are mostly ones ARIA promises nothing about.
|
|
372
|
-
|
|
373
|
-
Deferred with it: hit testing descends into items (`GetAccessibleAtPoint`
|
|
374
|
-
answers with the item under the point, which is what a magnifier following
|
|
375
|
-
the pointer needs), but the **testing queries** still walk retained nodes
|
|
376
|
-
only, so `getByRole` cannot find a drawn item — the same shape as #260,
|
|
377
|
-
and it wants that issue's answer rather than a second one here.
|
|
378
|
-
|
|
379
|
-
## 9. Testing
|
|
380
|
-
|
|
381
|
-
Four layers, each catching what the one below cannot:
|
|
382
|
-
|
|
383
|
-
1. **Model** (`test/a11y.test.js`) — no bus: roles, names, states,
|
|
384
|
-
projection, widget wiring, over the mock app.
|
|
385
|
-
2. **The spy** (`src/testing/a11y.js`, `test/a11y-spy.test.js`, and the
|
|
386
|
-
layer applications are meant to use via `renderX11({ a11y: true })`) —
|
|
387
|
-
the hook slots are the seam the bridge itself consumes, so a spy
|
|
388
|
-
filling the same slots observes the same contract with no transport:
|
|
389
|
-
synchronous, bus-free, Node-20-and-macOS-safe. It reuses the bridge's
|
|
390
|
-
own snapshot-and-diff idea in miniature, and it doubles as a renderer
|
|
391
|
-
regression net — a chokepoint that stops calling its hook fails these
|
|
392
|
-
tests exactly as it would silence Orca. The utterance strings it (and
|
|
393
|
-
`scripts/a11y-probe.mjs`) produce come from one shared `utteranceOf`,
|
|
394
|
-
deliberately documented as react-x11's model rather than an imitation
|
|
395
|
-
of Orca's wording, which is presentation policy that shifts between
|
|
396
|
-
releases.
|
|
397
|
-
3. **Hermetic bridge** (`test/atspi.test.js`) — dbus-native's
|
|
398
|
-
`createBroker()` is a real in-process message bus (routing, names, match
|
|
399
|
-
rules); a stub client owns `org.a11y.atspi.Registry` and answers
|
|
400
|
-
`Embed`; `AT_SPI_BUS_ADDRESS` points the bridge at it, so everything
|
|
401
|
-
from the address onward is the production path. A second client plays
|
|
402
|
-
the screen reader with raw `invoke` calls and a signal tap, asserting
|
|
403
|
-
wire-level facts: struct shapes, state bits, text-diff offsets, the
|
|
404
|
-
press gesture, cache items. Skips itself on Node 20 where the transport
|
|
405
|
-
legitimately is not installed.
|
|
406
|
-
4. **Live** — not automated, but scripted and repeatable:
|
|
407
|
-
`scripts/a11y-probe.mjs` is the AT side of the wire in dbus-native
|
|
408
|
-
(tree dumps, an event tail, `--speak` through speech-dispatcher), and
|
|
409
|
-
python3 `gi.repository.Atspi` is libatspi, Orca's own client stack —
|
|
410
|
-
`get_desktop(0)` → find the app → walk/`do_action`/`set_current_value`
|
|
411
|
-
exercises the real registry, with `orca --debug-file=…` grepped for
|
|
412
|
-
`SPEECH OUTPUT` as ground truth. Those runs confirmed the whole chain
|
|
413
|
-
("Press me — button", "check box checked", "horizontal slider — 80
|
|
414
|
-
percent", menus opening from AT activation) — and found a real bug on
|
|
415
|
-
first contact: `GetRoleName` answered the ARIA vocabulary while
|
|
416
|
-
`GetRole` answered the AT-SPI number. Invisible to libatspi, which
|
|
417
|
-
derives role names locally from the number and never asks — exactly
|
|
418
|
-
the class of defect only a second, independent client exposes, and the
|
|
419
|
-
argument for the probe existing at all.
|
|
420
|
-
|
|
421
|
-
## 10. Alternatives rejected, for the record
|
|
422
|
-
|
|
423
|
-
- **A sibling `@react-x11/a11y` package** (the original §11.3 sketch).
|
|
424
|
-
Made sense when D-Bus was going to be an external dependency; once
|
|
425
|
-
`dbus-native` became core's own optionalDependency the split bought a
|
|
426
|
-
version matrix and an opt-in accessibility story, and "install another
|
|
427
|
-
package to be accessible" is the wrong default in a way no engineering
|
|
428
|
-
argument offsets.
|
|
429
|
-
- **The RN prop vocabulary** (`accessibilityRole`, `accessibilityState`).
|
|
430
|
-
Overtaken by RN itself (0.71 added the ARIA names); and the widgets'
|
|
431
|
-
existing `role` strings — written before anything read them — were
|
|
432
|
-
already the web's.
|
|
433
|
-
- **A mirror/shadow accessible tree.** §4; the sync-bug class it invites
|
|
434
|
-
is the thing GTK4's rewrite spent years digging out of.
|
|
435
|
-
- **Gating on `org.a11y.Status.IsEnabled`** (Qt's behaviour) rather than
|
|
436
|
-
connecting whenever the bus exists (GTK's). One more mode, one more
|
|
437
|
-
property watch, to save a tree that is only maintained when something
|
|
438
|
-
queries it anyway. GTK's answer is smaller.
|
|
439
|
-
- **Per-node dbus-native interface exports with EventEmitter signal
|
|
440
|
-
hook-up.** dbus-native wraps `emit` per exported object; with thousands
|
|
441
|
-
of nodes that is machinery nobody asked for. Raw `sendSignal` plus
|
|
442
|
-
prototype-shared impls keeps per-node cost to two small objects.
|
|
443
|
-
- **`aria-live` region tracking.** The DOM needs it because content
|
|
444
|
-
changes are the only signal; here `announce()` is explicit, cannot fire
|
|
445
|
-
on renders you did not mean, and maps onto the modern
|
|
446
|
-
`Event.Object.Announcement` Orca ≥ 45 speaks.
|
|
447
|
-
|
|
448
|
-
## 11. Deferred, and what each needs
|
|
449
|
-
|
|
450
|
-
| gap | what it waits on |
|
|
451
|
-
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
452
|
-
| relations (`aria-labelledby`, label-for) | an element-id registry; `aria-label` + name-from-contents cover the widgets |
|
|
453
|
-
| `Selection` interface on containers | per-item SELECTED states + actions already carry selection; this is polish |
|
|
454
|
-
| `Table` interface (row/col navigation calls) | honest `table`/`row`/`cell` roles + posinset shipped; the interface matters most with virtualization, where only rendered rows exist at all |
|
|
455
|
-
| key-event forwarding (`DeviceEventController.NotifyListenersSync`) | per-keystroke echo of keys themselves; typed characters already arrive as text-changed events |
|
|
456
|
-
| soft-wrap line granularity in `<textarea>` | ntk layout lines are available (`caretPosition(...).line`); the work is boundary bookkeeping in the Text impl |
|
|
457
|
-
| `GetApplicationBusAddress` peer-to-peer mode | niche; empty string is a valid "no" |
|
|
458
|
-
|
|
459
|
-
## 12. Sources
|
|
460
|
-
|
|
461
|
-
- at-spi2-core `xml/` — the D-Bus interface definitions (fetched from the
|
|
462
|
-
GNOME repo at implementation time; `Accessible.xml`, `Cache.xml`,
|
|
463
|
-
`Socket.xml`, `Event.xml`, `Text.xml`, …).
|
|
464
|
-
- `gi.repository.Atspi` 2.60.4 on this machine — enum values, and the
|
|
465
|
-
client stack used for live verification.
|
|
466
|
-
- The GTK (at-spi2-atk / GTK4) and Qt (`atspiadaptor`) bridges, and
|
|
467
|
-
Chromium's `ax_platform_node_auralinux` — prior art for role mappings,
|
|
468
|
-
event pairs, `NO_AT_BRIDGE`, and registry-restart handling.
|
|
469
|
-
- Orca — both as the consumer whose event diet defines "enough", and
|
|
470
|
-
directly: its debug speech log is the acceptance test.
|