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