react-x11 2.51.1 → 2.52.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.
Files changed (91) hide show
  1. package/README.md +87 -54
  2. package/docs/README.md +410 -0
  3. package/docs/accessibility.md +628 -0
  4. package/docs/appearance.md +416 -0
  5. package/docs/architecture/accessibility.md +470 -0
  6. package/docs/architecture/animation.md +707 -0
  7. package/docs/architecture/container-queries.md +525 -0
  8. package/docs/architecture/custom-layout.md +334 -0
  9. package/docs/architecture/drag-and-drop.md +941 -0
  10. package/docs/architecture/element-layer-contents.md +622 -0
  11. package/docs/architecture/frame-pacing.md +255 -0
  12. package/docs/architecture/grid-layout.md +358 -0
  13. package/docs/architecture/protocol-efficiency.md +357 -0
  14. package/docs/architecture/video.md +1149 -0
  15. package/docs/architecture/windowed-regions.md +854 -0
  16. package/docs/click-to-component.md +125 -0
  17. package/docs/clipboard.md +281 -0
  18. package/docs/components.md +1749 -0
  19. package/docs/dbus.md +277 -0
  20. package/docs/debugging.md +464 -0
  21. package/docs/desktop-calendar.md +284 -0
  22. package/docs/desktop.md +648 -0
  23. package/docs/devtools.md +250 -0
  24. package/docs/drag-and-drop.md +663 -0
  25. package/docs/ecosystem/animation.md +394 -0
  26. package/docs/ecosystem/data-fetching.md +201 -0
  27. package/docs/ecosystem/dev-tooling.md +436 -0
  28. package/docs/ecosystem/forms.md +313 -0
  29. package/docs/ecosystem/headless.md +219 -0
  30. package/docs/ecosystem/i18n.md +89 -0
  31. package/docs/ecosystem/icons.md +410 -0
  32. package/docs/ecosystem/layout.md +216 -0
  33. package/docs/ecosystem/routing.md +146 -0
  34. package/docs/ecosystem/state.md +361 -0
  35. package/docs/ecosystem/testing.md +500 -0
  36. package/docs/ecosystem/theming.md +540 -0
  37. package/docs/ecosystem.md +473 -0
  38. package/docs/elements.md +2762 -0
  39. package/docs/embedding.md +288 -0
  40. package/docs/events.md +1025 -0
  41. package/docs/extending.md +2242 -0
  42. package/docs/eyedropper.md +223 -0
  43. package/docs/filedialog.md +258 -0
  44. package/docs/frame.md +580 -0
  45. package/docs/gl.md +170 -0
  46. package/docs/globalmenu.md +278 -0
  47. package/docs/glx.md +137 -0
  48. package/docs/macos.md +3295 -0
  49. package/docs/notifications.md +164 -0
  50. package/docs/packaging.md +611 -0
  51. package/docs/permissions.md +148 -0
  52. package/docs/react-features.md +430 -0
  53. package/docs/remote.md +223 -0
  54. package/docs/scale.md +339 -0
  55. package/docs/security.md +132 -0
  56. package/docs/styling.md +1928 -0
  57. package/docs/system.md +599 -0
  58. package/docs/testing.md +514 -0
  59. package/docs/typescript.md +117 -0
  60. package/docs/uri-schemes.md +380 -0
  61. package/docs/wayland-backend.md +591 -0
  62. package/docs/wayland.md +545 -0
  63. package/docs/windows-embedding.md +271 -0
  64. package/docs/windows-gl.md +221 -0
  65. package/docs/windows-integrations.md +233 -0
  66. package/docs/windows.md +1404 -0
  67. package/package.json +9 -2
  68. package/src/bus.js +39 -26
  69. package/src/cli/create.d.ts +43 -0
  70. package/src/cli/create.js +305 -0
  71. package/src/cli/env.js +484 -0
  72. package/src/cli/index.js +29 -2
  73. package/src/cli/templates/AGENTS.template.md +195 -0
  74. package/src/cli/templates/DESIGN.template.md +269 -0
  75. package/src/desktopintegration.js +2 -1
  76. package/src/desktopsettings.js +193 -24
  77. package/src/filedialog.js +3 -3
  78. package/src/keyboardstate.js +28 -0
  79. package/src/node.d.ts +10 -10
  80. package/src/portal.js +31 -0
  81. package/src/screencolor.js +5 -10
  82. package/src/types/system.d.ts +8 -6
  83. package/src/wayland/app.js +59 -0
  84. package/src/wayland/clipboard.js +14 -7
  85. package/src/wayland/connection.js +3 -0
  86. package/src/wayland/context2d.js +129 -10
  87. package/src/wayland/protocols/index.json +6 -0
  88. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  89. package/src/wayland/seat.js +28 -1
  90. package/src/wayland/swapchain.js +23 -0
  91. package/src/wayland/xkb.js +137 -0
@@ -0,0 +1,628 @@
1
+ # Accessibility
2
+
3
+ react-x11 applications are visible to screen readers. The renderer carries a
4
+ built-in [AT-SPI2](https://gitlab.gnome.org/GNOME/at-spi2-core) bridge — the
5
+ same interface GTK and Qt applications register themselves through — so Orca
6
+ reads a react-x11 window the way it reads any other: roles, names, states,
7
+ focus, live text editing, and the actions to drive controls without a
8
+ pointer.
9
+
10
+ It is on by default and costs nothing to have: with no accessibility bus on
11
+ the machine the bridge never comes up, and the render path pays one property
12
+ read per event for the hooks it would have filled.
13
+
14
+ > **This is the Linux bridge; the win32 backend has one of its own and Cocoa
15
+ > has none.** On **win32** the same tree is pushed to UI Automation instead
16
+ > (`src/win32/a11y.js`, [windows.md](windows.md#accessibility-ui-automation)),
17
+ > because UIA asks synchronously and is answered from a pushed copy rather
18
+ > than from the live tree. AT-SPI
19
+ > is a freedesktop protocol with nothing to register against on macOS, so on
20
+ > the **Cocoa** backend `createRoot()` leaves it off (`desktop: { a11y:
21
+ false }` by default there) and no `NSAccessibility` bridge has been built
22
+ > in its place — a Cocoa-backend app is not yet reachable by VoiceOver.
23
+ > Everything else on this page still earns its keep there: `role`, `aria-*`
24
+ > and `announce()` are properties of the shared node tree, keyboard
25
+ > traversal and the widgets' own semantics are shared code, and all of it is
26
+ > what an `NSAccessibility` bridge will read when it lands
27
+ > ([macos.md](macos.md), Phase 5). Write them now; nothing changes when it
28
+ > does.
29
+
30
+ ```jsx
31
+ <box role="button" aria-label="Close" onClick={close} focusable>
32
+ <text>×</text>
33
+ </box>
34
+ ```
35
+
36
+ That is the entire API for most code. `role` and `aria-*` are the web's
37
+ names, accepted on every host element — the same vocabulary react-dom takes
38
+ and React Native adopted — so a component library that sets them is
39
+ accessible here with no react-x11-specific knowledge, and the built-in
40
+ widgets (`Button`, `Checkbox`, `Select`, `Menu`, `Tabs`, …) already
41
+ carry them.
42
+
43
+ > This page is the reference. The design record — why there is no mirror
44
+ > tree, why the vocabulary is the web's rather than React Native's, why
45
+ > discovery dials its own socket, what was rejected and what is deferred —
46
+ > is [architecture/accessibility.md](architecture/accessibility.md).
47
+
48
+ ## What a screen reader hears without any props
49
+
50
+ Accessibility is not opt-in; the defaults are chosen so an unlabelled tree
51
+ is _boring_ to a screen reader rather than broken:
52
+
53
+ | element | reads as |
54
+ | ----------------------- | ----------------------------------------------------------- |
55
+ | `<window>` | frame, named by `title` |
56
+ | `<popup>` | window (give the content a `role` — `menu`, `dialog`, …) |
57
+ | `<box>` | filler — skipped silently, like GTK's own layout boxes |
58
+ | `<text>` | label, reading its content — and its selection, if any |
59
+ | `<textinput>` | entry: editable, single-line, caret and selection live |
60
+ | `<textarea>` | entry: editable, multi-line |
61
+ | `<image>` / `<svg>` | image, named by `alt` |
62
+ | `overflow: 'scroll'` | scroll pane (and a tab stop when it can scroll — see below) |
63
+ | `<canvas>` / `<glarea>` | drawing area / canvas — name them if they carry meaning |
64
+
65
+ Focus, enabled/disabled, checked, selected, expanded and the rest are read
66
+ from the same live state the widgets draw from — the `focusable` rule, the
67
+ `disabled` prop, the `aria-*` props — so what the screen reader says cannot
68
+ drift from what the screen shows.
69
+
70
+ ## The props
71
+
72
+ All of these are accepted on every host element and are inert (and
73
+ harmless) when no assistive technology is listening.
74
+
75
+ | prop | AT-SPI meaning |
76
+ | ----------------------------------------------------------- | ---------------------------------------------------- |
77
+ | `role` | what the element _is_ (ARIA role names; table below) |
78
+ | `aria-label` | the accessible name, over every other source |
79
+ | `aria-description` | supplementary description |
80
+ | `aria-hidden` | remove this subtree from the accessible tree |
81
+ | `aria-checked` (`true`/`false`/`'mixed'`) | CHECKABLE + CHECKED / INDETERMINATE |
82
+ | `aria-selected` | SELECTABLE + SELECTED |
83
+ | `aria-expanded` | EXPANDABLE + EXPANDED / COLLAPSED |
84
+ | `aria-pressed` | PRESSED (toggle buttons) |
85
+ | `aria-busy`, `aria-modal`, `aria-required`, `aria-readonly` | the states of the same names |
86
+ | `aria-haspopup` | HAS_POPUP (`'menu'`, `'listbox'`, `'dialog'`, …) |
87
+ | `aria-orientation` | HORIZONTAL / VERTICAL |
88
+ | `aria-valuenow` / `-valuemin` / `-valuemax` / `-valuetext` | the Value interface (sliders, progress) |
89
+ | `aria-level`, `aria-posinset`, `aria-setsize` | "level 2", "3 of 7" — headings, trees, lists |
90
+ | `aria-keyshortcuts` | the shortcut announced with the item ("Control+N") |
91
+ | `onAccessibilityAction` | the AT drove the control (below) |
92
+
93
+ Roles: `alert alertdialog article banner blockquote button caption cell
94
+ checkbox columnheader combobox comment complementary contentinfo dialog
95
+ document form grid gridcell group heading img link list listbox listitem log
96
+ main marquee math menu menubar menuitem menuitemcheckbox menuitemradio meter
97
+ navigation note option paragraph progressbar radio radiogroup region row
98
+ rowheader scrollbar search searchbox separator slider spinbutton status
99
+ switch tab table tablist tabpanel term textbox timer toolbar tooltip tree
100
+ treegrid treeitem window`, plus `none`/`presentation` to erase a wrapper
101
+ from the accessible tree while keeping its children. An unknown role warns
102
+ in development and falls back to the element default — a typo must not
103
+ silently turn a button into a filler, and it must not crash either.
104
+
105
+ Two deliberate absences. There is no `aria-disabled`: `disabled` is already
106
+ a host prop with real behaviour (it blocks focus and drives `:disabled`
107
+ style blocks), and a parallel prop that only _said_ disabled would let the
108
+ two disagree. And there is no `aria-labelledby`/`aria-describedby`: there
109
+ are no element ids to point them at — use `aria-label`, or lean on
110
+ name-from-contents.
111
+
112
+ ### Names
113
+
114
+ The accessible name resolves in order: `aria-label` → what the element
115
+ itself carries (a window's `title`, an image's `alt`, an input's
116
+ `placeholder` — the fallback the web engines also use) → for roles whose
117
+ contents are their label (buttons, checkboxes, menu items, tabs, links,
118
+ options, tree items, …), the visible text inside. So
119
+ `<Button>Save</Button>` is named "Save" with nobody writing a label, and
120
+ `announce`-worthy custom controls need one prop, not three.
121
+
122
+ ### AT-driven controls
123
+
124
+ Activation needs no wiring at all: every element whose role promises it
125
+ (button, checkbox, menu item, tab, …) or that has an `onClick` exposes an
126
+ AT-SPI `activate` action, and the bridge performs it by dispatching a
127
+ synthetic click through the normal event path — capture, bubble, default
128
+ actions, the discrete-priority commit — so Orca pressing a button is
129
+ indistinguishable from a finger.
130
+
131
+ (Under the hood the activation is a full press gesture — mousedown, mouseup,
132
+ click — because several controls act on the press: `Select` and `MenuBar`
133
+ drop their menus on it, the way real menus do.)
134
+
135
+ **The keyboard shares that click.** Space and Enter on a focused element with
136
+ an `onClick` dispatch the same gesture through the same function, so a
137
+ keyboard user and a screen-reader user are never served differently by the
138
+ same element — see [events.md](events.md#space-and-enter-are-a-click). The
139
+ role half of the rule above stays the AT's: a role _advertises_ an action to
140
+ something that cannot press a key, and a `role="button"` with no `onClick`
141
+ has no click for a key to be.
142
+
143
+ The one thing that does need a handler is a _value_ write — Orca adjusting
144
+ a slider through the Value interface. The handler receives one argument:
145
+
146
+ ```ts
147
+ { action: 'setValue', value: number }
148
+ ```
149
+
150
+ (`action` is a string so the shape can grow; `'setValue'` is the only
151
+ action delivered today — activation never arrives here, it goes through the
152
+ event system above.) Wire it into the same state setter as the pointer and
153
+ the keyboard:
154
+
155
+ ```jsx
156
+ <box
157
+ role="slider"
158
+ aria-valuenow={value}
159
+ aria-valuemin={0}
160
+ aria-valuemax={100}
161
+ onAccessibilityAction={({ action, value }) => {
162
+ if (action === 'setValue') setValue(clamp(value));
163
+ }}
164
+ />
165
+ ```
166
+
167
+ `Slider` already does exactly this.
168
+
169
+ ### Announcements
170
+
171
+ ```js
172
+ import { announce } from 'react-x11';
173
+
174
+ announce('Form saved');
175
+ announce('Connection lost', { assertive: true });
176
+ ```
177
+
178
+ The explicit counterpart of an ARIA live region: say something through the
179
+ screen reader without moving focus. Returns `true` when a bridge was live
180
+ to carry it; `false` means nobody is listening and a visible fallback may
181
+ be warranted. (Orca ≥ 45 speaks these; there is no `aria-live` region
182
+ tracking — an explicit call is smaller, and it cannot fire on renders you
183
+ did not mean.)
184
+
185
+ ## What the built-in widgets announce
186
+
187
+ Every widget in [components.md](components.md) is wired; none of them need
188
+ anything from you beyond their ordinary props. What a screen reader hears:
189
+
190
+ | widget | reads as |
191
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
192
+ | `Button` | button, named by its label; disabled buttons say so |
193
+ | `Checkbox` | check box, checked/unchecked, named by its label |
194
+ | `Radio` / `RadioGroup` | radio buttons with the selected one checked |
195
+ | `Switch` | switch, on/off — **give it an `aria-label`**, it has no visible label of its own |
196
+ | `Slider` | horizontal slider with value and range; Orca can adjust it |
197
+ | `ProgressBar` | progress bar; the value reads as a percentage of its range, and an `indeterminate` one is busy with no value rather than 0% |
198
+ | `Select` | combo box named by the current choice, expanded/collapsed; its menu is a listbox of options, each "n of m" and the current one selected |
199
+ | `MenuBar` / `ContextMenu` | menu bar and menus; items carry their shortcut as the standard `keyshortcuts` attribute ("Control+N" — UI Events spelling, where the row draws "Ctrl+N" and the key path [binds it](events.md#accelerators)), a `toggleType` as checked/radio, `toggleState: -1` as `mixed`, submenus as expandable |
200
+ | `Tooltip` | tooltip |
201
+ | `Dialog` | modal dialog, named by its title |
202
+ | `Tabs` | tab list with orientation; each tab selected or not; the panel is a tab panel |
203
+ | `Table` | table with column headers and rows ("n of m", selected). Virtualization means only rendered rows exist in the tree — the same rows a sighted user can see |
204
+ | `SplitPane` | the divider is a separator with orientation and position — the ARIA window-splitter pattern |
205
+
206
+ ## Text controls
207
+
208
+ `<textinput>` and `<textarea>` implement the AT-SPI `Text` and
209
+ `EditableText` interfaces in full working order: character count, reading
210
+ by character/word/line, caret position (offsets are code points, on both
211
+ sides), selections, per-character screen rectangles for magnifiers and
212
+ caret tracking, and edits — a screen reader can read, navigate, select,
213
+ and type. Every edit and caret move emits the precise
214
+ `text-changed`/`text-caret-moved` deltas Orca narrates from.
215
+
216
+ One honest limit: "line" granularity follows hard newlines, not the soft
217
+ wraps of a `<textarea>`'s layout. Orca still reads everything; a
218
+ line-by-line walk of one long wrapped paragraph is one stop, not several.
219
+
220
+ A `<text>` has the `Text` interface too — readable, with no caret to move.
221
+ What it now also reports is its **selection**: a paragraph inside a
222
+ `selectable` surface ([elements.md](elements.md#selecting-text)) hands over
223
+ the range the user has dragged across, and says so as it moves, so a
224
+ magnifier's highlight and a braille display's cursor sit on the same
225
+ characters the highlight is painted over.
226
+
227
+ ### While a composition is open
228
+
229
+ A dead key or a half-typed Compose sequence puts text on the screen that is
230
+ in nobody's value yet ([events.md](events.md#composition)). The `Text`
231
+ interface answers with **what is drawn**, that preedit included, and its
232
+ offsets index that string — because every geometric answer beside it is
233
+ read off the layout of the same string, and a magnifier tracking character
234
+ 3 has to land on the glyph a sighted user sees. `CaretOffset` is at the far
235
+ end of the composition, where the next keystroke of the sequence appears.
236
+
237
+ Which part of it is uncommitted is said twice, so a reader can act on
238
+ either:
239
+
240
+ - the preedit is its own **attribute run** — `underline: single`, the
241
+ registered AT-SPI attribute for what is literally drawn, plus
242
+ `composition: true`, which is this renderer's own because AT-SPI
243
+ registers nothing for a preedit;
244
+ - its churn is **`:system`** text — `text-changed:insert:system` when the
245
+ accent appears, `delete:system` when it changes or is abandoned. That is
246
+ the suffix Gecko established for a change the user did not type, and it
247
+ is what lets a reader stay quiet through a composition.
248
+
249
+ What the sequence **commits** is a plain `insert` of the finished
250
+ character: `é` is what the user typed, and it is spoken, once, as one
251
+ insertion — the same event an ordinary keystroke produces, which is also
252
+ why it is one undo step and not two.
253
+
254
+ Nothing here is announced through `announce()`. A live-region announcement
255
+ on top of the text-changed feed would have a screen reader say the accent
256
+ twice; an application that wants to narrate composition state can call
257
+ `announce()` itself from `onCompositionUpdate`.
258
+
259
+ ### Text of your own
260
+
261
+ A [registered element](extending.md) that draws its own text — a code
262
+ editor, a markdown viewer, a terminal, a canvas-backed table with cell
263
+ editing — reports it through one method and is then read by everything
264
+ above: character count, reading by character/word/line, caret, selection,
265
+ the text-changed deltas, and an AT that can navigate and type.
266
+
267
+ ```js
268
+ class EditorNode extends Node {
269
+ a11yTextState() {
270
+ return {
271
+ value: this.text, // what is drawn, composition included
272
+ caret: this.caret, // code-point offsets into `value`
273
+ selectionStart: this.anchor,
274
+ selectionEnd: this.caret,
275
+ editable: true, // an editor rather than a viewer
276
+ multiline: true,
277
+ };
278
+ }
279
+
280
+ insert(text) {
281
+ /* …the edit… */ this.notifyA11yTextChanged();
282
+ }
283
+ }
284
+ ```
285
+
286
+ | member | |
287
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------- |
288
+ | `a11yTextState()` | `{ value, caret, selectionStart, selectionEnd, editable, multiline, preedit }` — `null` for no text |
289
+ | `notifyA11yTextChanged()` | the text moved. Free when nothing is listening, so no guard around it |
290
+ | `a11ySetSelection(start, end)` | an AT moved the caret or made a selection |
291
+ | `a11yReplaceText(start, end, text)` | an AT edited — insert, delete and replace all arrive as this |
292
+ | `a11yRole` | the role the element is when the app writes none; `role` still wins |
293
+
294
+ Two tiers, because the read-only one is a real element and not a
295
+ degenerate editor. `{ value, selectionStart, selectionEnd }` alone is a
296
+ document: the `Text` interface, a `document` role, a selection an AT reads
297
+ and sets — which is what a markdown view with Ctrl+C needs and all it
298
+ needs. `editable: true` adds the EDITABLE state, an `entry` role and, once
299
+ `a11yReplaceText` exists, the `EditableText` interface. Nothing is exposed
300
+ that would not work: an element that reports editable text but implements
301
+ no write is announced as editable and reports its own edits, but is not
302
+ offered to an AT as one to type into.
303
+
304
+ Offsets are code points and index the string the element **draws** — an
305
+ open composition included, for the same reason `<textinput>`'s do. Report
306
+ which part is uncommitted as `preedit: { offset, text }` and its churn is
307
+ marked `:system`, so a reader stays quiet through a dead key and speaks the
308
+ character it commits. Offsets are clamped on the way out, so a stale caret
309
+ cannot become an out-of-range answer on the wire.
310
+
311
+ The known limits of the tier, none of which stop a screen reader working:
312
+ per-character screen rectangles fall back to the element's own rect (a
313
+ magnifier tracks the element, not the glyph), and AT-driven paste and copy
314
+ are the built-ins' — an element's own Ctrl+C/Ctrl+V is unaffected. Both
315
+ want a layout the element alone has; say so on the issue if you have one.
316
+
317
+ The seam is observed by the test spy exactly as core's is, so an editor in
318
+ another package can assert what it says with no bus and no desktop —
319
+ [below](#asserting-what-a-screen-reader-would-hear).
320
+
321
+ ### A scene of your own
322
+
323
+ The other half of the same gap. A [registered element](extending.md) that
324
+ _draws_ interactive things — a graph pane, a chart, a timeline, a seating
325
+ plan — is one node, so it is one accessible: "Flow graph, group", with
326
+ three hundred selectable nodes inside it that no assistive technology can
327
+ see. It says what it drew and each one becomes a child:
328
+
329
+ ```js
330
+ class FlowNode extends Node {
331
+ a11yScene() {
332
+ return this.nodes.map((node) => ({
333
+ id: node.id, // stable while it is on screen: this is identity
334
+ role: 'listitem',
335
+ name: node.label,
336
+ rect: node.rect, // window coordinates
337
+ states: {
338
+ selected: this.selection.has(node.id),
339
+ focused: this.cursor === node.id,
340
+ },
341
+ }));
342
+ }
343
+
344
+ select(id) {
345
+ /* …the element's own selection… */ this.notifyA11ySceneChanged();
346
+ }
347
+ }
348
+ ```
349
+
350
+ | member | |
351
+ | ----------------------------- | ----------------------------------------------------------------------------------------------------- |
352
+ | `a11yScene()` | `[{ id, rect, role, name, description, focusable, states, props, children }]` |
353
+ | `notifyA11ySceneChanged()` | the scene moved between commits — a drag, an animation, your own arrow keys |
354
+ | `a11ySceneAction(id, action)` | an AT sent `'activate'`, `'focus'` or `'scroll'`. `true` claims it; anything else keeps core's answer |
355
+
356
+ They are read by the same model everything else is: `role` and the
357
+ `aria-*`-shaped states above become the AT-SPI role and state set, `rect`
358
+ becomes the Component extents a magnifier tracks and focus is drawn around,
359
+ and a change to any of it is announced as that child changing rather than as
360
+ the pane being replaced. Keyboard navigation _between_ items stays the
361
+ element's own — the window's focus manager holds the element, which is why
362
+ `states.focused` is the only way the cursor inside it is reported.
363
+
364
+ Activation with no `a11ySceneAction` is a synthetic click at the item's own
365
+ rect, which is exactly what a mouse user does to it; the seam is for the
366
+ elements where that is not the truth. See
367
+ [extending.md](extending.md#a-scene-a-screen-reader-can-walk) for the whole
368
+ contract.
369
+
370
+ ## The compatibility ladder
371
+
372
+ `createRoot()` starts one climb per process, off the critical path:
373
+
374
+ 1. is `dbus-native` installed? (it is an optionalDependency; Node < 22.12
375
+ skips it)
376
+ 2. is there a session bus?
377
+ 3. does `org.a11y.Bus` answer `GetAddress`? (this also starts the a11y bus
378
+ launcher on desktops where it is not running yet)
379
+ 4. connect, export the tree, `Embed` with the AT-SPI registry.
380
+
381
+ Every "no" is a normal, **silent** off: ssh sessions, CI, containers, bare
382
+ `startx`, macOS and Windows X servers — react-x11's own flagship
383
+ configurations — simply have no accessibility stack, and an app must not
384
+ log, slow down or grow a mode because of it. The bridge holds no
385
+ event-loop reference either, so a process that is done exits; the registry
386
+ sees the name drop and forgets the app.
387
+
388
+ Environment switches, in priority order:
389
+
390
+ - `REACT_X11_A11Y=0` — off. Any other value forces the climb **and makes
391
+ it loud**: each rung prints why it stopped, which is the answer to "why
392
+ does Orca not see my app", and the bridge says when it goes quiet and when
393
+ it wakes ([with nobody listening](#with-nobody-listening)).
394
+ - `AT_SPI_BUS_ADDRESS` — skip discovery and connect here. The seam
395
+ sandboxes use, and the one the hermetic tests use.
396
+ - `REACT_X11_A11Y_INTERVAL=<ms>` — how often changes are pushed to the
397
+ platform, 500 by default; `0` pushes every change. See
398
+ [How often the bridge speaks](#how-often-the-bridge-speaks), which also
399
+ applies to the Windows bridge.
400
+ - `NO_AT_BRIDGE=1` — off; the ecosystem-wide switch every toolkit honours.
401
+ `react-x11/test` sets it, so a test run on a desktop does not parade
402
+ phantom applications through a running screen reader.
403
+
404
+ And one that is not environmental, for an app that has to turn the bridge off
405
+ for itself without setting a variable every child process will inherit:
406
+
407
+ ```jsx
408
+ await createRoot({ desktop: { a11y: false } });
409
+ ```
410
+
411
+ It outranks every switch above, `REACT_X11_A11Y=1` included — see
412
+ [desktop.md](desktop.md#turning-the-desktop-off), where `desktop: false` turns
413
+ this off along with the other two integrations that talk to the session bus.
414
+ Pass it on the first root: the climb happens once per process, and a second
415
+ root's switch cannot unstart a bridge that is already up.
416
+
417
+ A dead accessibility bus is not redialled — the same contract as
418
+ [dbus.md](dbus.md): the app simply stops being accessible until restarted.
419
+
420
+ ## How often the bridge speaks
421
+
422
+ A bridge mirrors the tree for a platform that reads it on its own schedule,
423
+ and pushing the mirror on every commit is paid by everything that commits
424
+ often: a scroll, a pan, a value ticking. That was mostly paid for nobody.
425
+ An AT-SPI bus runs in most Linux sessions whether or not a screen reader
426
+ does, and a 100,000-row `<Table>` flung through it exported, diffed and
427
+ unexported every row that scrolled past: 31 frames a second against 38 with
428
+ the bridge off. On Windows the same push was 1.6 ms of every frame of a
429
+ graph pane's pan. So both bridges pace what they push (`src/a11ypace.js`),
430
+ the way browsers do. Chromium batches non-interactive accessibility updates
431
+ 150 ms apart once a page has loaded, and bounds 500 ms apart, and serializes
432
+ focus and selection at once.
433
+
434
+ - **A change after a quiet spell is pushed at once.** A click or a key that
435
+ changes one thing is heard immediately.
436
+ - **A stream of changes is pushed at most once per interval**, 500 ms by
437
+ default, and the last push carries the last state. A row that scrolls in
438
+ and out between two pushes is never exported at all, and an AT that was
439
+ never told of it is not told it left.
440
+ - **What the user is waiting on is pushed at once, whatever the interval:**
441
+ focus moving, an announcement, and a change to the focused element itself,
442
+ such as the characters typed into it or its toggled state.
443
+ - **Reads are never paced** (AT-SPI). A call from an AT first flushes
444
+ whatever the pacer is holding, so every object and interface it asks
445
+ about is as the tree has it now. Only the unsolicited signals wait.
446
+
447
+ `REACT_X11_A11Y_INTERVAL` sets the interval in milliseconds, and `0` goes
448
+ back to a push per change.
449
+
450
+ ### With nobody listening
451
+
452
+ Pacing bounds what a push costs. It does not help when nobody hears the
453
+ push, which is the usual case: the bus runs whether or not a screen reader
454
+ does. Dragging a scrollbar thumb down a 100,000-row `<Table>` ran at 19
455
+ frames a second with the AT-SPI bridge on and 24 with it off, and nothing on
456
+ that desktop was listening. So the bridge asks the registry.
457
+
458
+ An AT registers the events it wants with the registry. The registry answers
459
+ `GetRegisteredEvents` and announces each new listener, and GTK 4 and
460
+ at-spi2-atk use that to hold back the events nobody asked for. Both still
461
+ send the cache's signals and `children-changed` regardless, because libatspi
462
+ listens for those in every client without registering. So an empty list is
463
+ not the whole answer. An AT that registered for nothing still has to call in
464
+ to read anything, and a call from anybody but the registry counts too.
465
+
466
+ - **With no listener and no caller, the bridge falls silent.** Nothing is
467
+ queued, pushed or exported, and what it had exported before the registry
468
+ answered is withdrawn. The tree still answers every read, starting from the
469
+ application root.
470
+ - **The first listener or the first call wakes it for good.** The AT reads
471
+ the tree as it is, which is where it would start anyway, and every change
472
+ after that is pushed as above.
473
+ - **An announcement made during the silence goes unheard**, and
474
+ `announce()` returns `false` for it, as it does with no bridge at all.
475
+ - **A registry that cannot say who listens** is taken to have someone
476
+ listening, as GTK 4 does with one it cannot ask.
477
+
478
+ ## For component libraries
479
+
480
+ There is nothing to integrate against. Set the standard props on the host
481
+ elements you render, exactly as the built-in widgets do; the bridge mirrors
482
+ whatever the tree says. The seams, in increasing order of involvement:
483
+
484
+ - `role` + `aria-*` on any element — the whole story for almost everything.
485
+ - `disabled`, `focusable`, `tabIndex` — already doubling as the ENABLED and
486
+ FOCUSABLE states; nothing separate to maintain.
487
+ - `onAccessibilityAction` — when an AT can _set_ something on your control.
488
+ - A [registered element](extending.md) is a host element like any other:
489
+ its instances take all of the above, and a class that should default to a
490
+ role assigns `this.a11yRole` in its constructor (a `role` prop still
491
+ wins).
492
+ - `a11yTextState()` + `notifyA11yTextChanged()` — when your element draws
493
+ text of its own, which is the one thing props cannot express
494
+ ([above](#text-of-your-own)).
495
+ - `a11yScene()` + `notifyA11ySceneChanged()` — when your element draws
496
+ interactive _things_ of its own, which is the other one
497
+ ([above](#a-scene-of-your-own)).
498
+
499
+ ## Testing and verifying
500
+
501
+ ### Asserting what a screen reader would hear
502
+
503
+ `renderX11(element, { a11y: true })` hands back `at`, an in-process
504
+ assistive-technology spy: the same semantic feed the AT-SPI bridge serves
505
+ to Orca, observed before it becomes D-Bus — so it is synchronous, needs no
506
+ bus, and runs everywhere the test suite runs (the mock backend, Node 20,
507
+ macOS included). Input stays real: `userEvent.tab()` is a Tab through the
508
+ in-process X server's focus machinery, not a shortcut around it.
509
+
510
+ ```js
511
+ import { renderX11, userEvent, cleanup, XK_SPACE } from 'react-x11/test';
512
+
513
+ const { at } = await renderX11(<Preferences />, { a11y: true });
514
+
515
+ await userEvent.tab();
516
+ assert.equal(at.focused().utterance, 'Save, button');
517
+
518
+ await userEvent.tab();
519
+ await userEvent.key(XK_SPACE);
520
+ assert.ok(at.since().some((e) => e.type === 'state' && e.state === 'checked'));
521
+ ```
522
+
523
+ Every entry is a precise fact (`{ type, state, on, node, … }`) plus a
524
+ one-line `summary`; `at.transcript()` is the summaries, made for
525
+ `deepEqual`. `at.focused()` and `at.focusables()` describe nodes the way an
526
+ AT would — name, role, states, and an `utterance` string following the
527
+ model documented on this page (name, role, the states worth speaking, a
528
+ value as a percentage). The utterance is deliberately **this library's
529
+ model, not an imitation of Orca**, whose wording is presentation policy;
530
+ what it buys you is that a control nobody named renders as
531
+ `"(no accessible name)"` instead of passing because no assertion happened
532
+ to mention its name.
533
+
534
+ Four tests worth copying into any application — each one a check a sighted
535
+ test author cannot make by looking (`test/a11y-spy.test.js` is the worked
536
+ example of all four):
537
+
538
+ - **Focus order** — tab through, `deepEqual` the focus summaries.
539
+ - **No keyboard trap** — `at.focusables().length` + 1 Tabs lands back on
540
+ the first stop, and Shift+Tab wraps the other way.
541
+ - **Nothing nameless** — for each of `at.focusables()`, assert the
542
+ utterance is not `"(no accessible name)"`. The highest value per line in
543
+ this document.
544
+ - **State changes are announced** — press Space on a checkbox, assert a
545
+ `checked` state entry arrived; a widget that repaints without one is the
546
+ most common accessibility regression there is.
547
+
548
+ ### Testing the bridge itself
549
+
550
+ The wire half is testable without a desktop too: point
551
+ `AT_SPI_BUS_ADDRESS` at an in-process bus (dbus-native's broker), own
552
+ `org.a11y.atspi.Registry` on it with a stub `Embed`, and walk your app with
553
+ real D-Bus calls — `test/atspi.test.js` is the worked example, and the
554
+ model half (roles, names, states as pure functions over the tree) needs no
555
+ bus at all (`test/a11y.test.js`). Applications rarely need this layer; it
556
+ is how react-x11 proves the transport.
557
+
558
+ ### Seeing it without learning a screen reader
559
+
560
+ `npm run a11y:probe` is the client side of AT-SPI — the side Orca is on —
561
+ with no GNOME libraries and nothing to configure:
562
+
563
+ ```bash
564
+ npm run a11y:probe # what is on the accessibility bus
565
+ npm run a11y:probe -- widgets # dump that app's accessible tree
566
+ npm run a11y:probe -- widgets --watch # follow its events as you click
567
+ npm run a11y:probe -- widgets --watch --speak # ...said out loud
568
+ npm run a11y:probe -- nautilus # someone else's toolkit, for comparison
569
+ ```
570
+
571
+ It talks to whatever is on the bus, so pointing it at a GTK app and at
572
+ yours is the sharpest check available: if the shapes match, a screen reader
573
+ will treat them the same. The `say:` lines it prints are a **toy** model —
574
+ what Orca really utters is Orca's own policy — but they make a missing
575
+ accessible name audible instead of merely absent, which is the failure that
576
+ matters most and is easiest to miss.
577
+
578
+ ### With a real screen reader
579
+
580
+ ```bash
581
+ orca --replace --debug-file=/tmp/orca.log # then Tab through your app
582
+ grep "SPEECH OUTPUT" /tmp/orca.log
583
+ ```
584
+
585
+ The debug log is the ground truth, and it is easier to read than listening.
586
+ For audio, speech-dispatcher does the talking (`spd-say hello` tests it on
587
+ its own). Orca's own toggle is `gsettings set
588
+ org.gnome.desktop.a11y.applications screen-reader-enabled true`; the GUI
589
+ inspector, if you want one, is `accerciser`.
590
+
591
+ ### Scripting the AT side
592
+
593
+ The AT stack is also scriptable through the same library Orca uses:
594
+
595
+ ```python
596
+ import gi; gi.require_version('Atspi', '2.0')
597
+ from gi.repository import Atspi
598
+ desktop = Atspi.get_desktop(0)
599
+ apps = [desktop.get_child_at_index(i) for i in range(desktop.get_child_count())]
600
+ app = next(a for a in apps if a and a.get_name() == 'myapp')
601
+ # walk it, read roles/names/states, do_action(0), set values…
602
+ ```
603
+
604
+ Accerciser gives the same view interactively, and running `orca` while
605
+ Tab-walking your app is the ground truth. Under `react-x11/test`,
606
+ remember, the bridge is off by default (`NO_AT_BRIDGE`) so tests stay
607
+ deterministic.
608
+
609
+ ## What is deliberately not there (yet)
610
+
611
+ - **Relations** (`aria-labelledby`, label-for) — needs an id registry;
612
+ `aria-label` and name-from-contents cover the widgets.
613
+ - **The Selection interface** on containers — Orca reads per-item
614
+ SELECTED states and selection is driven through actions and focus, so
615
+ this is polish, not a gap in coverage.
616
+ - **The Table interface** — `Table` exposes honest `table`/`row`/`cell`
617
+ roles with positions, but not the row/column navigation calls; note that
618
+ virtualization means only rendered rows exist in the accessible tree,
619
+ which is also all a sighted user can see.
620
+ - **Key-event forwarding** (`DeviceEventController.NotifyListenersSync`) —
621
+ Orca's per-keystroke echo of _keys themselves_; typed characters already
622
+ arrive through text-changed events.
623
+ - **Soft-wrap line granularity** in `<textarea>`, per above.
624
+ - **An input method** — dead keys and Compose are the whole composition
625
+ story today, so a script that needs a candidate window (any CJK) cannot
626
+ be typed at all, accessibly or otherwise. What an IME would report is
627
+ already here: the preedit is a text run and its own `:system` feed, so
628
+ the work left is the input method, not its accessibility.