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
package/docs/events.md ADDED
@@ -0,0 +1,742 @@
1
+ # Events
2
+
3
+ Raw input arrives per window from the backend; react-x11 dispatches it as
4
+ synthetic events over the drawn node tree with DOM-like semantics.
5
+
6
+ ## Dispatch
7
+
8
+ 1. **Hit test**: front-to-back over the stacking-ordered tree (zIndex, then
9
+ document order), respecting `overflow` clipping and `pointerEvents` —
10
+ `'none'` removes a node and its subtree, `'box-none'` only the node
11
+ itself, so its children are still targets and a point over its own area
12
+ lands on what is behind it. A `<glarea>` is asked first: its surface is
13
+ above everything 2D in the window and its children above the surface, so
14
+ a point over it lands on a child, on the `<glarea>`, or (with
15
+ `'box-none'`) on what is behind the surface
16
+ ([elements.md](elements.md#glarea)).
17
+ 2. **Capture phase**: `on<Event>Capture` handlers from the window down to
18
+ the target.
19
+ 3. **Target + bubble phase**: `on<Event>` handlers from the target up.
20
+ 4. **Default action**: the element's built-in behaviour (textinput editing,
21
+ wheel scrolling, then Tab traversal) — skipped if any handler called
22
+ `ev.preventDefault()`.
23
+
24
+ `ev.stopPropagation()` stops the walk. Handlers always read current props.
25
+ Step 4 is open to elements added with `registerElement`, through the same
26
+ `defaultKeyDown` / `defaultMouseDown` / `defaultMouseMove` / … methods
27
+ ([extending.md](extending.md#behaviour-of-your-own)). For the wheel, the node
28
+ under the pointer gets `defaultWheel` first (a pane that zooms), then the
29
+ scroll chain asks each node outward `canScroll(deltaX, deltaY)`
30
+ ([extending.md](extending.md#scrolling-content-you-painted)).
31
+
32
+ ### When the response is painted
33
+
34
+ **Discrete input** — `mouseDown`/`mouseUp`, `keyDown`/`keyUp`, focus, blur,
35
+ the window manager's close request — paints in the same turn of the event
36
+ loop, once the default action, your handlers and React's update have all
37
+ run: one frame, never a half-updated one. A burst faster than frames are
38
+ acknowledged folds into one catch-up frame.
39
+
40
+ **Motion and the wheel stay on the frame clock**: `mouseMove` (and the hover
41
+ it drives) coalesces to at most one per frame, keeping the newest position;
42
+ wheel events coalesce by _adding up_, so one event carries the frame's whole
43
+ distance.
44
+
45
+ **Hover follows content.** After a frame that laid out — every frame of a
46
+ scroll — the window re-hit-tests the pointer's last position, as a browser
47
+ does: `:hover`, `onMouseEnter` and `onMouseLeave` update, and the node under
48
+ the pointer hears `defaultMouseMove` (so self-painted hover re-reads what it
49
+ is over). `onMouseMove` is not called, and nothing changes while a press or
50
+ capture holds the pointer or after it has left the window.
51
+
52
+ **On macOS, hover belongs to the active window**, as in AppKit and the
53
+ browsers there. A Cocoa window that loses the keyboard hears `onMouseLeave`
54
+ down its hover path, `:hover` comes off, a self-painting element hears
55
+ `defaultMouseLeave`, and no motion, enter, cursor or attention follows until
56
+ it is active again — when hover returns at the pointer without waiting for a
57
+ move. `<window onMouseOut>` is not called. "Active" is the focus's answer
58
+ ([Window focus](#window-focus)): a window whose `<Dialog>` holds the keyboard
59
+ still hovers, and an unmanaged `<popup>` (menu, dropdown, tray popover)
60
+ always does. Presses still land where aimed and the wheel scrolls a
61
+ background window. On X11, Wayland and Windows windows hover without the
62
+ keyboard, as every toolkit there does. Design notes:
63
+ [design/events.md](../design/events.md#when-the-response-is-painted).
64
+
65
+ ### When a handler throws
66
+
67
+ **No error boundary can catch it**: a handler runs from an X event, with
68
+ React not on the stack. The dispatcher catches it, reports the element, the
69
+ handler name and the component that rendered the node, sets
70
+ `process.exitCode = 1` (so CI and a supervisor still see it), and **dispatch
71
+ continues** — one bad handler must not stop the handlers after it, or the
72
+ frame loop.
73
+
74
+ ```
75
+ react-x11: onClick on <box> in Panel threw. It ran from an X event rather
76
+ than a render, so no error boundary could catch it; dispatch continues.
77
+ ```
78
+
79
+ `onDrop` may be `async`; a promise it rejects with arrives here too (same
80
+ message, not an unhandled rejection, which node exits on) and refuses the
81
+ drop ([drag-and-drop.md](drag-and-drop.md#the-drop)).
82
+ `createRoot({ onUncaughtError })` takes over completely — for a kiosk,
83
+ crashing loudly is correct:
84
+
85
+ ```js
86
+ const root = await createRoot({
87
+ onUncaughtError: (error, info) => {
88
+ telemetry.report(error, info.componentStack);
89
+ process.exit(1);
90
+ },
91
+ });
92
+ ```
93
+
94
+ ## Event object
95
+
96
+ ```
97
+ {
98
+ type, target, currentTarget, // public instances
99
+ x, y, // window coordinates
100
+ localX, localY, // target-relative
101
+ nativeEvent, // the raw ntk/X11 event
102
+ shiftKey, ctrlKey, // on every event, not just keys
103
+ altKey, metaKey, // Mod1 and Mod4 — see Modifiers below
104
+ preventDefault(), stopPropagation(),
105
+ capturePointer(), releasePointer(), // see Pointer capture below
106
+ // mouse: button, detail (DOM-style click count: 2 = double, 3 = triple)
107
+ // wheel: deltaX, deltaY (pixels), smooth
108
+ // keyboard: keycode, keysym, group, codepoint, key, composing
109
+ // composition: data
110
+ }
111
+ ```
112
+
113
+ - `x`/`y`/`localX`/`localY` and the wheel deltas are logical pixels in **the
114
+ target's** unit: the display scale times any `scale` prop above it
115
+ ([scale.md](scale.md#a-subtree-of-its-own)). A handler on an unzoomed
116
+ ancestor reads the zoomed target's pixels; `ev.nativeEvent.x`/`y` are the
117
+ window's own.
118
+ - `ev.screenX`/`screenY`: the pointer on the virtual screen in the same
119
+ logical pixels — what to anchor a `<popup>` at. Undefined without a
120
+ pointer position. `nativeEvent.rootx`/`rooty` is the same point in
121
+ **device** pixels, the unit of a node's `abs`; use it only for geometry
122
+ against `abs` ([scale.md](scale.md)).
123
+ - `detail` counts a multi-click within the desktop's double-click time and
124
+ distance ([system.md](system.md#usedesktopsettings--how-the-desktop-wants-an-app-to-feel)),
125
+ timed by the presses' own `nativeEvent.time`, so a slow first render does
126
+ not break a double click. win32 presses carry no time and are timed as
127
+ handled.
128
+ - On the Wayland backend a finger or tablet tool drives the mouse events,
129
+ which then carry `nativeEvent.pointerType` (`'touch'`, `'pen'`, `'eraser'`,
130
+ `'mouse'`) and `pressure`, `tiltX`/`tiltY`, `rotation`, `distance`,
131
+ `tangentialPressure` in the DOM's ranges; a mouse adds nothing. Raw
132
+ `touchstart`/`touchmove`/`touchend`/`touchcancel` arrive on the window
133
+ (`wnd.on(...)`, not React props) — [wayland-backend.md](wayland-backend.md).
134
+
135
+ **Read a Ctrl chord from the keysym, not the code point.** `codepoint` comes
136
+ from the _shifted_ keysym (Ctrl+Shift+Z is `Z`, Ctrl+Z is `z`).
137
+ `ctrlChordLetter(ev)` from `react-x11/keysyms` gives the lowercase keysym
138
+ either way, as `<textinput>` uses for Ctrl+C/V/Z, and keeps working under a
139
+ Cyrillic or Greek layout ([Layouts](#layouts)). For most chords,
140
+ [Accelerators](#accelerators) is shorter.
141
+
142
+ ```js
143
+ onKeyDown={(ev) => {
144
+ if (ev.ctrlKey && ctrlChordLetter(ev) === keysymOf('d')) duplicateLine();
145
+ }}
146
+ ```
147
+
148
+ ### Modifiers
149
+
150
+ Four booleans with DOM names, on **every** event — a shift+click, an
151
+ Alt+drag and a Super+wheel all need them:
152
+
153
+ | property | X11 modifier | usually |
154
+ | ---------- | ------------ | ------- |
155
+ | `shiftKey` | Shift | Shift |
156
+ | `ctrlKey` | Control | Ctrl |
157
+ | `altKey` | Mod1 | Alt |
158
+ | `metaKey` | Mod4 | Super |
159
+
160
+ ```js
161
+ onKeyDown={(ev) => {
162
+ if (ev.altKey && ev.keysym === keysymOf('b')) wordLeft();
163
+ }}
164
+ ```
165
+
166
+ Mod1/Mod4 as Alt/Super is the convention every toolkit reads, not protocol.
167
+ For a keymap that disagrees, `ev.nativeEvent.buttons` is the raw mask and
168
+ `MOD` in `react-x11/keysyms` names its bits (`MOD.Mod3`, `MOD.Lock` for Caps
169
+ Lock, …).
170
+
171
+ ### The primary modifier {#primary-modifier}
172
+
173
+ **On the Cocoa backend the editing chords are ⌘, not Ctrl.** ⌘ arrives as
174
+ `metaKey`, and `app.primaryModifier` is `'Super'` there and absent elsewhere
175
+ (X11 under XQuartz included). The built-in editors read it: `<textinput>`'s
176
+ Undo, Cut, Copy, Paste and Select All are ⌘Z, ⌘X, ⌘C, ⌘V, ⌘A, Redo is ⇧⌘Z,
177
+ and the context menu prints them so. A ⌃ or ⌘ chord the field does not answer
178
+ is never typed as text. Ask the same question with `primaryModifierHeld`:
179
+
180
+ ```js
181
+ import { ctrlChordLetter, keysymOf, primaryModifierHeld } from 'react-x11/keysyms';
182
+
183
+ onKeyDown={(ev) => {
184
+ if (primaryModifierHeld(ev, app) && ctrlChordLetter(ev) === keysymOf('d'))
185
+ duplicateLine();
186
+ }}
187
+ ```
188
+
189
+ Menus and accelerators follow the same rule: `Control` in a `shortcut` means
190
+ ⌘ there ([globalmenu.md](globalmenu.md#shortcuts-are-a-list-not-a-string),
191
+ [The chord](#the-chord)), so `useAccelerator([['Control', 'K']])` is ⌘K and a
192
+ drawn `ContextMenu` prints and answers `⌘S`.
193
+
194
+ Where shortcuts are ⌘, `<textinput>` and `<textarea>` also move and delete
195
+ with AppKit's bindings:
196
+
197
+ | keys | does |
198
+ | ---------------------- | -------------------------------------------------------------------- |
199
+ | ⌥← ⌥→ | a word back or forward |
200
+ | ⌘← ⌘→, and ⌃← ⌃→ | the start or end of the line |
201
+ | ⌘↑ ⌘↓ | the start or end of the value |
202
+ | ⌥↑ ⌥↓ | the start or end of the paragraph, and on to the next |
203
+ | ↑ ↓ in a `<textinput>` | its start and its end, as in an NSTextField |
204
+ | ⌥⌫ ⌥⌦ | delete a word |
205
+ | ⌘⌫ | delete to the start of the line |
206
+ | ⌃⌫ | take the accent off the character before the caret — é to e |
207
+ | ⌃A ⌃E | the start or end of the paragraph |
208
+ | ⌃B ⌃F, ⌃P ⌃N | a character back or forward, a line up or down |
209
+ | ⌃H ⌃D | delete a character back or forward |
210
+ | ⌃K ⌃Y | kill to the paragraph's end (its break, if at the end); yank it back |
211
+
212
+ Shift extends the selection. "Line" is the drawn, wrapped line; "paragraph"
213
+ runs between line breaks; in a `<textinput>` both are the whole value.
214
+ Elsewhere the bindings are GTK's, Qt's and Windows' (Ctrl+←/→ and Ctrl+⌫/⌦ by
215
+ word, Ctrl+A select all) — never a mix. Home and End go to the ends of the
216
+ line on every backend.
217
+
218
+ ⌃K's kill buffer is the app's, not the pasteboard: ⌘C's copy is untouched,
219
+ ⌃Y in one field yanks what ⌃K killed in another, and consecutive kills add
220
+ up. A `sensitive` field kills into nothing. All of these are the field's
221
+ default action, so `preventDefault()` in `onKeyDown` keeps the key.
222
+
223
+ ## Handlers
224
+
225
+ | handler | notes |
226
+ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
227
+ | `onClick` | on the nearest common ancestor of press & release; `detail` counts clicks |
228
+ | `onMouseDown` / `onMouseUp` / `onMouseMove` | move is coalesced to once per frame |
229
+ | `onMouseEnter` / `onMouseLeave` | do not propagate; hover-path diffing, on motion and when content scrolls under the pointer |
230
+ | `onWheel` | pixels ([below](#wheel)); default action scrolls the nearest container with somewhere to go |
231
+ | `onContextMenu` | right-click, after `onMouseDown`; default action opens the element's menu |
232
+ | `onKeyDown` / `onKeyUp` | to the focused node (or window); defaults: Space/Enter click, [accelerators](#accelerators), Tab |
233
+ | `onCompositionStart` / `onCompositionUpdate` / `onCompositionEnd` | text still being typed — [below](#composition) |
234
+ | `onFocus` / `onBlur` | focus follows mousedown (nearest `focusable` ancestor) and Tab |
235
+ | `onDragEnter` / `onDragLeave` | do not propagate; drag-path diffing, like hover |
236
+ | `onDragOver` / `onDrop` | on a drop target; `onDrop` may be async — [drag-and-drop.md](drag-and-drop.md) |
237
+ | `onDragStart` / `onDrag` / `onDragEnd` | on a `draggable` node; the press is a click until it moves 4px |
238
+
239
+ ## Space and Enter are a click {#space-and-enter-are-a-click}
240
+
241
+ **An `onClick` on a focusable node is keyboard-operable with nothing else
242
+ written.** Space or Enter dispatches the whole press gesture — mousedown,
243
+ mouseup, click — so a `<box>` control behaves like a `<Button>`:
244
+
245
+ ```jsx
246
+ <box focusable role="option" aria-selected={on} onClick={() => open(channel)}>
247
+ <text>{channel}</text>
248
+ </box>
249
+ ```
250
+
251
+ It is a **default action**, so:
252
+
253
+ - `preventDefault()` in the node's `onKeyDown` keeps the key (a disclosure
254
+ that opens on Enter while its click only selects);
255
+ - a node with no `onClick` is untouched whatever its `role` —
256
+ `role="button"` with only `onKeyDown` stays yours;
257
+ - `<textinput>` and `<textarea>` never activate: Space types a space;
258
+ - **a scroll pane keeps Space**: Space and Shift+Space page a scrolling
259
+ `<box>` ([elements.md](elements.md#scrolling)), so a clickable pane pages
260
+ with Space and activates with Enter. A focused row _inside_ a pane is
261
+ unaffected;
262
+ - **a chord is not a press**: with Ctrl, Alt or Super (⌘, ⌃, ⌥ on Cocoa)
263
+ held, Space and Enter go to whoever bound them, e.g. a menu's Ctrl+Return.
264
+
265
+ Shift travels: the click carries the key's modifier mask, so Shift+Enter is a
266
+ shift-click. Assistive-technology activation goes through the same function
267
+ ([accessibility.md](accessibility.md#at-driven-controls)).
268
+
269
+ ## Accelerators {#accelerators}
270
+
271
+ **A menu's `shortcut` is a binding.** `MenuBar` and `ContextMenu` answer
272
+ their items' chords while mounted, unopened:
273
+
274
+ ```jsx
275
+ <MenuBar
276
+ menus={[
277
+ {
278
+ label: 'File',
279
+ items: [
280
+ { label: 'Save', shortcut: [['Control', 'S']], onSelect: save },
281
+ {
282
+ label: 'Save As…',
283
+ shortcut: [['Control', 'Shift', 'S']],
284
+ onSelect: saveAs,
285
+ },
286
+ ],
287
+ },
288
+ ]}
289
+ />
290
+ ```
291
+
292
+ The same array is announced as `aria-keyshortcuts` and crosses the bus when
293
+ a desktop panel draws the menu ([globalmenu.md](globalmenu.md)); it stays
294
+ bound then, since the key is still pressed in your window. (The macOS menu
295
+ bar answers its ⌘ chords itself — [below](#the-macos-menu-bar).) For a
296
+ shortcut in no menu, `useAccelerator` takes the same chords:
297
+
298
+ ```jsx
299
+ import { useAccelerator } from 'react-x11';
300
+
301
+ useAccelerator([['Control', 'K']], () => openPalette());
302
+ ```
303
+
304
+ ### The chord
305
+
306
+ `[['Control', 'S'], ['F2']]` is dbusmenu's `aas`: **alternatives**, each
307
+ modifier tokens (`Control`, `Alt`, `Shift`, `Super`) ending in the key. Any
308
+ alternative fires; only the first is drawn. Keys use X keysym names (as
309
+ `gdk_keyval_name()` emits): `plus` not `+`, `Prior` not `PgUp`, `Return` not
310
+ `Enter`; a one-character name is that character. `keysymFromName()` in
311
+ `react-x11/keysyms` resolves names, and
312
+ `matchesShortcut(ev, shortcut, { primary })` matches, for apps dispatching
313
+ chords their own way.
314
+
315
+ **`Control` is the app's primary modifier**: ⌘ on the Cocoa backend (what a
316
+ drawn row prints, `⌘S`, `⇧⌘Z`, and what it answers —
317
+ [The primary modifier](#primary-modifier)), Ctrl elsewhere. `Super` is ⌘
318
+ there too, so a chord cannot ask for ⌃ on a Mac. `matchesShortcut` is pure,
319
+ so it takes the answer as an option:
320
+
321
+ ```js
322
+ import { matchesShortcut } from 'react-x11';
323
+ import { primaryModifier } from 'react-x11/keysyms';
324
+
325
+ matchesShortcut(ev, [['Control', 'S']], { primary: primaryModifier(app) });
326
+ ```
327
+
328
+ Matching is:
329
+
330
+ - **exact on the four modifiers**, the primary one included: Ctrl+S does not
331
+ fire on Ctrl+Shift+S, nor ⌘S on ⌃⌘S.
332
+ - **indifferent to Caps Lock and Num Lock.**
333
+ - **by key, not character**: against `ev.keysym`, the Latin keysym of the
334
+ physical key, so a chord survives a layout switch ([Layouts](#layouts)) and
335
+ `'s'`/`'S'` are one binding. A chord naming a **symbol** also matches the
336
+ character produced (`+` is shifted `=`, and `[['Control', 'plus']]` must be
337
+ pressable); letters never do, or Ctrl+Shift+S would fire Ctrl+S.
338
+ - **gated by `enabled` and `visible`**, including a disabled parent
339
+ submenu's.
340
+
341
+ ### Who gets the key first
342
+
343
+ Accelerators sit in the default-action chain, and `preventDefault()` stops it
344
+ at any step:
345
+
346
+ 1. the application's own `onKeyDown`, capture then bubble;
347
+ 2. composition — a dead key, a Compose sequence;
348
+ 3. the focused element's own behaviour (`<textinput>` editing, a scroll pane's
349
+ page keys);
350
+ 4. **accelerators**;
351
+ 5. focus traversal, for Tab.
352
+
353
+ So **a focused text field keeps the chords it answers** (Ctrl+C copies the
354
+ selection, never reaching a menu's Ctrl+C) and passes the rest (Ctrl+S
355
+ reaches the menu). A `<textinput>` answers Return and Shift+Return with
356
+ `onSubmit`; Ctrl, Alt or Super+Return (⌘↩ on Cocoa) are chords and reach the
357
+ menu. With **no** `onSubmit` it does not answer Return at all, so a form's
358
+ `[['Return']]` default button gets it. A `<textarea>` keeps Return and
359
+ Shift+Return for the newline and Ctrl+Return (⌘↩) for `onSubmit` if it has
360
+ one; Alt and Super+Return are chords, except on Cocoa, where ⌃↩ and ⌥↩ are
361
+ AppKit's newline.
362
+
363
+ Nothing fires while:
364
+
365
+ - **a menu is open** (it holds a pointer grab);
366
+ - **a modal `<popup>`** (`trapFocus`, as `Dialog` sets) is up — Ctrl+S under
367
+ a confirmation saves nothing. A menu declared _inside_ the modal still
368
+ works, as Tab traversal does, and `useAccelerator`'s `scope` joins a hook
369
+ binding to it:
370
+
371
+ ```jsx
372
+ const inDialog = useRef(null);
373
+ useAccelerator([['Control', 'Return']], confirm, { scope: inDialog });
374
+ ```
375
+
376
+ A chord belongs to the window whose menu declared it; another top-level
377
+ window does not answer it. `enabled: false` unbinds one; `accelerators={false}`
378
+ on a `MenuBar` or `ContextMenu` turns the whole mechanism off for an app with
379
+ its own dispatcher. **An app with no `<window>`** (a menu-bar app) binds
380
+ against the root-level `<popup>` that took the keyboard
381
+ ([elements.md](elements.md#a-popover-under-a-tray-item)); a binding with
382
+ neither warns once in development and names `scope`.
383
+
384
+ ### The macOS menu bar {#the-macos-menu-bar}
385
+
386
+ On the Cocoa backend the system draws a `MenuBar`
387
+ ([globalmenu.md](globalmenu.md)) and runs an item on its ⌘ chord itself,
388
+ after the window has seen the key, wherever it can spell the chord as a key
389
+ equivalent (one chord on a character key). Those chords are **reserved**: the
390
+ window's accelerators — the bar's own binding or a `useAccelerator` on the
391
+ same chord — never see them, and the item runs once. A chord it cannot spell
392
+ (a function key, a second alternative, `plus`) or one without `Control` or
393
+ `Super` stays the window's. Steps 1 and 3 above run first and AppKit still
394
+ runs the item afterwards, so an `onKeyDown` or a focused field answering ⌘C
395
+ beside an Edit menu's ⌘C runs alongside it, `preventDefault()` or not —
396
+ leave a chord the menu bar shows to the menu bar.
397
+
398
+ ## The wheel {#wheel}
399
+
400
+ `ev.deltaX` / `ev.deltaY` are **pixels**, positive down and right; one wheel
401
+ notch is 48 — the step an arrow key takes. On X11 with XI2 they are the
402
+ distance the device measured, so a touchpad reports fractions (`14.5`, not
403
+ `48` twice a second); without XI2 every event is one whole notch.
404
+
405
+ ```jsx
406
+ <box
407
+ style={{ overflow: 'scroll' }}
408
+ onWheel={(ev) => {
409
+ ev.deltaY; // pixels — fractional from a touchpad
410
+ ev.smooth; // whether the device can report a fraction at all
411
+ }}
412
+ />
413
+ ```
414
+
415
+ `ev.smooth` is the kind of device, not of this delta: `true`, the next event
416
+ may be a third of a notch; `false`, an emulated button that only ever says
417
+ one. Branch on it to animate scrolling — ease towards a whole notch, never a
418
+ stream of measured pixels.
419
+
420
+ **Nothing is opted into.** A `<window>` selects XI2 on its first scroll (it
421
+ costs pointer-motion traffic, so windows that never scroll never pay) and
422
+ falls back to buttons where the server has none. A `<popup>` stays on
423
+ buttons, since it holds a pointer grab, so an open menu scrolls a notch at a
424
+ time. `<window xi2>` selects at creation, for a touchpad app that wants its
425
+ very first flick smooth; `<window xi2={false}>` never selects. Design notes:
426
+ [design/events.md](../design/events.md#wheel).
427
+
428
+ Shift turns a vertical scroll sideways on a one-axis device; a device with
429
+ its own horizontal axis keeps it. The default action is the hit node's
430
+ `defaultWheel`, then the scroll chain
431
+ ([extending.md](extending.md#behaviour-of-your-own),
432
+ [extending.md](extending.md#scrolling-content-you-painted)).
433
+
434
+ ## Composition: dead keys and Compose {#composition}
435
+
436
+ `é` is not a key: on a French, German or us-intl layout it is `dead_acute`
437
+ then `e`, and a dead key has **no code point** — code typing from
438
+ `ev.codepoint` drops the accent. Test text input on such a layout, not only
439
+ US. A composition is text still being typed: it shows underlined at the
440
+ caret, and is not the value until it commits:
441
+
442
+ ```
443
+ dead_acute → the field shows ´ (underlined). value unchanged.
444
+ e → the field shows é. value is now …é, one undo step.
445
+ ```
446
+
447
+ On by default, no configuration:
448
+
449
+ | you press | you get |
450
+ | ---------------------------------- | ------------------------------------------------- |
451
+ | `dead_acute` `e` | `é` — and any base letter, in any script |
452
+ | `dead_circumflex` `dead_acute` `e` | `ế` — dead keys stack |
453
+ | `dead_acute` space | `´` — the accent on its own |
454
+ | `dead_acute` `q` | `´q` — no such character, so nothing is swallowed |
455
+ | Compose `o` `c` | `©` |
456
+ | Compose `'` `e` (or `e` `'`) | `é` |
457
+ | Compose `-` `-` `-` | `—` |
458
+ | Escape mid-sequence | nothing typed, and the accent goes away |
459
+
460
+ Dead keys use Unicode composition, so `dead_breve` + `и` is `й` and
461
+ `dead_acute` + `α` is `ά`; only what Unicode has no rule for is tabled
462
+ (`dead_stroke` + `o` is `ø`, and the `Multi_key` sequences).
463
+
464
+ ### Handling it yourself
465
+
466
+ The three events mirror the DOM's: `data` is empty on start, the text at the
467
+ caret on each update, and the **committed text** at the end (empty if
468
+ abandoned).
469
+
470
+ ```jsx
471
+ <textinput
472
+ onCompositionUpdate={(ev) => setStatus(`composing ${ev.data}`)}
473
+ onCompositionEnd={(ev) => setStatus('')}
474
+ />
475
+ ```
476
+
477
+ **A key the composition took carries no text**: `ev.key` and `ev.codepoint`
478
+ are `undefined` and `ev.composing` is true (its text comes on the
479
+ composition event), but the keysym is there, so chords still match. The order
480
+ is **application chords → composition → the element → focus traversal**:
481
+ `preventDefault()` in `onKeyDown` hides a key from the composer, and a key
482
+ the composer takes never reaches the element — a dead key cannot fire an
483
+ accelerator, nor a Compose `z` undo. A composition is abandoned, never
484
+ half-committed, when focus leaves, the window loses the keyboard, or a press
485
+ moves the caret. Screen readers hear the committed character, not the accent
486
+ ([accessibility.md](accessibility.md#while-a-composition-is-open)).
487
+
488
+ ### Input methods
489
+
490
+ The [Wayland backend](wayland-backend.md)'s compositor input method (IBus
491
+ under GNOME, via `zwp_text_input_v3`) and the Windows IME (IMM32) drive the
492
+ same three events: preedit on `onCompositionUpdate`, the commit on
493
+ `onCompositionEnd`, inserted like typing. Only there:
494
+
495
+ - `ev.cursorBegin` / `ev.cursorEnd` on an update — code-point offsets into
496
+ `data` for the engine's cursor or the segment being converted (drawn with
497
+ a heavier underline). A dead key never sets them.
498
+ - a commit with no preedit (an emoji picker) is a start and an end in one.
499
+
500
+ Keys the input method consumes never reach the app, nor the client-side
501
+ table. `<textinput sensitive>` offers it none of its text, and `inputMode`
502
+ tells it what to expect ([elements.md](elements.md#textinput)).
503
+
504
+ The **Cocoa backend has no input method yet**: ⌥E (a dead key) types and
505
+ shows nothing, the next key types what macOS put there (`é`, or the bare
506
+ letter), and a key that typed several characters (`´s`) commits them as one
507
+ composition. Press-and-Hold accents and CJK input are not available there
508
+ yet.
509
+
510
+ ### Changing the table
511
+
512
+ ```js
513
+ import { XK_MULTI_KEY } from 'react-x11/keysyms';
514
+
515
+ const root = await createRoot({
516
+ compose: {
517
+ // this machine's Compose file, which is how a personal ~/.XCompose is
518
+ // picked up — usually absent on macOS, where nothing is lost
519
+ file: 'system',
520
+ sequences: [[[XK_MULTI_KEY, 'l', 'd'], '🦆']],
521
+ },
522
+ });
523
+ ```
524
+
525
+ `compose: 'system'` is shorthand for the file alone; `compose: false` turns
526
+ composition off for an app that does its own. Later definitions override
527
+ earlier ones. The format is X's; `include` is ignored, and lines naming
528
+ keysyms outside ASCII and the dead-key block (`<Greek_alpha>`) are skipped.
529
+ **On X11 this is not an input method** — no preedit from another process, no
530
+ candidate list — so CJK input there needs XIM or an IBus/Fcitx client, which
531
+ react-x11 does not have yet.
532
+
533
+ ## Layouts {#layouts}
534
+
535
+ A key event answers two questions, differently under a non-Latin layout:
536
+
537
+ - **what the key typed** — `ev.key` and `ev.codepoint`, from the active
538
+ layout and level. A Cyrillic layout types Cyrillic.
539
+ - **what the key is called** — `ev.keysym`, the **Latin** keysym for that
540
+ key, whichever group holds it, because shortcuts do not move with the
541
+ layout (GTK, Qt and browsers agree): Ctrl+Z keeps undoing in Russian.
542
+
543
+ So `ctrlChordLetter(ev) === keysymOf('z')` matches the physical Z key under
544
+ every layout. `ev.group` is the active XKB group (0–3), for showing which
545
+ layout is live; a switch sends no other notice. Under XQuartz, layout
546
+ switches reach X only with Preferences → Input → "Follow system keyboard
547
+ layout" on. Design notes: [design/events.md](../design/events.md#layouts).
548
+
549
+ **⌥ types, on the Cocoa backend**: it is the layout's third and fourth level
550
+ (⌥S is `ß` on US; on German ⌥L is `@`, ⌥5 and ⌥8 `[` and `{`). `ev.key` and
551
+ `ev.codepoint` are what was typed, with ⌥, Shift and Caps Lock applied, while
552
+ `ev.keysym` is still the key (`s`) and `ev.altKey` is true — an Alt+S
553
+ accelerator matches and a field types `ß`. A ⌘ or ⌃ chord types nothing and
554
+ is named by its key (⌘⌥S is a chord on S), as in the DOM on a Mac.
555
+
556
+ ```js
557
+ const root = await createRoot({
558
+ // 'latin' is the default; 'layout' reports the keysym the layout put on
559
+ // the key, for an app matching shortcuts its own way
560
+ accelerators: 'layout',
561
+ });
562
+ ```
563
+
564
+ `accelerators` also takes a keycode→keysym table (`{ 52: 'z' }`) for a
565
+ server whose keycodes are neither evdev's nor macOS's. **The AltGr row is not
566
+ reachable** on X11: `ev.keysym` and `ev.codepoint` read groups only.
567
+
568
+ ## Pointer capture
569
+
570
+ `ev.capturePointer()` routes every following `mousemove` and `mouseup` to
571
+ the capturing node, so a drag keeps working past the widget and a release
572
+ far outside still ends it. `Slider` is built on it:
573
+
574
+ ```jsx
575
+ onMouseDown: (ev) => {
576
+ ev.capturePointer();
577
+ setDragging(true);
578
+ },
579
+ onMouseMove: (ev) => { if (dragging) setValue(valueAt(ev)); },
580
+ onMouseUp: () => setDragging(false),
581
+ ```
582
+
583
+ Capture ends on `mouseup` (like the DOM's implicit capture), when the node
584
+ unmounts, or on `ev.releasePointer()`. While captured, hover stays put.
585
+
586
+ ## Focus
587
+
588
+ `focusable` opts a node in (`<textinput>` and a scroll container with
589
+ somewhere to scroll are focusable by default); `disabled` opts it out
590
+ whatever else it says; `autoFocus` focuses at mount. Every drawn node's ref
591
+ has `focus()` (which returns the node, for forwarding from an imperative
592
+ handle), `blur()`, `focused`, and `focusWithin` — focus on the node **or
593
+ inside it**; `':focus-within'` draws that
594
+ ([styling.md](styling.md#inline-pseudo-states)). Mousedown focuses the
595
+ nearest focusable ancestor of the hit node; keyboard events route up the
596
+ focused node's ancestor chain. Focusing a node in a scroll container scrolls
597
+ it into view, unless a press focused it.
598
+
599
+ **A focused node shows a ring** depending on how focus arrived: a press sets
600
+ `:focus`; Tab, `autoFocus`, `node.focus()` or a modal handing focus back
601
+ also set `:focus-visible`, which the ring is drawn on. A press on a **text
602
+ control** sets both. No opt-in;
603
+ [styling.md](styling.md#the-focus-ring) restyles it.
604
+
605
+ ### Tab order
606
+
607
+ `tabIndex` follows the DOM's rules:
608
+
609
+ - **positive** `tabIndex` first, ascending;
610
+ - then everything focusable without one (the implicit `0`), in tree order;
611
+ - `tabIndex={-1}` is focusable by a press or `focus()`, never by Tab;
612
+ - an explicit `tabIndex` makes a node focusable without `focusable`.
613
+
614
+ Tab from a node outside the order (`tabIndex={-1}`, or outside the current
615
+ focus scope) starts from the beginning.
616
+
617
+ ```jsx
618
+ <box tabIndex={1} /> {/* visited first */}
619
+ <box tabIndex={2} /> {/* then this */}
620
+ <box focusable /> {/* then tree order */}
621
+ <box tabIndex={-1} /> {/* clickable, never tabbed to */}
622
+ ```
623
+
624
+ Traversal is the last **default action**: `preventDefault()` on Tab keeps
625
+ it, as does an element whose `defaultKeyDown` consumes it (an editor
626
+ indenting) — which then owes the user the Escape-arms-one-Tab way out
627
+ ([extending.md](extending.md#behaviour-of-your-own)).
628
+
629
+ ### Focus scopes (modals)
630
+
631
+ `trapFocus` makes a node own a **focus scope**. While it is the innermost:
632
+
633
+ - Tab / Shift+Tab visit only focusables inside it;
634
+ - a press outside does not move focus (the press still dispatches — pair it
635
+ with `grab` + `onDismiss` for a real modal);
636
+ - when it unmounts, focus returns to whatever had it before it opened.
637
+
638
+ ```jsx
639
+ function Dialog({ open, x, y, onClose }) {
640
+ if (!open) return null;
641
+ return (
642
+ <popup
643
+ trapFocus
644
+ grab
645
+ x={x}
646
+ y={y}
647
+ width={280}
648
+ height={120}
649
+ onDismiss={onClose}
650
+ >
651
+ <textinput autoFocus />
652
+ <Button label="OK" onPress={onClose} />
653
+ </popup>
654
+ );
655
+ }
656
+ ```
657
+
658
+ Scopes nest; the innermost wins, and closing it hands focus back to the
659
+ outer. Programmatic `focus()` is never blocked — the trap is about Tab and
660
+ presses.
661
+
662
+ ### Focus inside a `<popup>`
663
+
664
+ A popup never holds input focus itself: its **owner window** does, and a
665
+ node inside the popup can be that window's focused node. Keys arrive at the
666
+ owner and bubble out through the popup's place in the JSX tree. A press in a
667
+ popup on nothing focusable leaves focus alone — which is how menus and
668
+ `Select` keep the trigger handling keys while open. A popup with no owner (a
669
+ tray popover in a windowless app) takes keys itself with `grabKeyboard`
670
+ ([elements.md](elements.md#a-popover-under-a-tray-item)).
671
+
672
+ ### Window focus
673
+
674
+ Node focus is per `<window>`; whether the window gets the keys is the window
675
+ manager's call, reported by `<window onFocus>` / `<window onBlur>`. In an
676
+ unfocused window the focused node **keeps** focus (as
677
+ `document.activeElement` does) but stops looking active: a `<textinput>`
678
+ caret stops blinking until the window is focused again. `focus()` in an
679
+ unfocused window asks for the input focus; the window manager may refuse.
680
+
681
+ A key goes to **the node that has focus**, whichever of a top-level's windows
682
+ (nested `<window>`, `<popup>`) it arrived at; with several focused nodes, the
683
+ last focused answers. "Unfocused" is the **focus's** answer, not one
684
+ window's: a `<popup>` shares its owner's focus, a managed `<Dialog>` is
685
+ focused by the window manager in its own right, and the caret follows
686
+ whichever holds the keyboard. `focus()` inside an open dialog does not ask
687
+ for input focus — the dialog has it.
688
+
689
+ ### Focus and visibility
690
+
691
+ **Focus follows visibility.** A subtree leaving the screen — a `<Suspense>`
692
+ fallback, `<Activity mode="hidden">`, `display: 'none'`, an unmapped
693
+ `<popup>` — gives up the keyboard: the focused node fires `onBlur`,
694
+ `:focus`/`:focus-within` come off, and keys go to the window, so a hidden
695
+ control never collects keystrokes. Tab skips invisible nodes.
696
+
697
+ **And it comes back with it**, ring and all, when the subtree is revealed —
698
+ but only if **nothing else has the keyboard** by then, and as a restore: no
699
+ scrolling into view and no request for the window system's focus, so a
700
+ background window never takes the keyboard from another app.
701
+ `createRoot({ restoreFocusOnReveal: false })` turns the restore off (focus
702
+ that fell to nothing stays there, as in a browser); the release always
703
+ happens.
704
+
705
+ ## Cursors
706
+
707
+ The `cursor` style property (`'pointer'`, `'text'`, `'wait'`, `'move'`,
708
+ `'crosshair'`, `'ew-resize'`, `'ns-resize'`, `'grab'`, `'not-allowed'`, …) of
709
+ the deepest hovered node applies to the window. `cursor: 'none'` hides the
710
+ pointer (the kiosk case) — unlike naming no cursor anywhere, which inherits
711
+ the root window's visible one.
712
+
713
+ ## Drag and drop {#drag-and-drop}
714
+
715
+ Drag and drop rides this event system — same hit testing, capture/bubble
716
+ dispatch and style states — on any drawn element, `<window>` or `<popup>`:
717
+
718
+ ```jsx
719
+ <box
720
+ dropAccept={['files']}
721
+ onDrop={(e) => setPaths(e.files.map((f) => f.path))}
722
+ style={{ ':drag-over': { borderColor: '$accent' } }}
723
+ />
724
+ ```
725
+
726
+ | handler | notes |
727
+ | -------------------------------------- | ------------------------------------------------------------------------- |
728
+ | `onDragEnter` / `onDragLeave` | do not propagate — path diffing, like hover; paired with `':drag-over'` |
729
+ | `onDragOver` | per pointer position; `e.accept(action)` / `e.reject()` answer the source |
730
+ | `onDrop` | may be async — the reply waits for the promise, bounded by a watchdog |
731
+ | `onDragStart` / `onDrag` / `onDragEnd` | the source side of a `draggable` node; `':dragging'` styles it meanwhile |
732
+
733
+ A press on a `draggable` node is a **click** until it moves 4px, and a
734
+ completed drag suppresses the click, as in the DOM. A drag leaving the app's
735
+ windows becomes the platform's own — XDND on X11, AppKit dragging on Cocoa,
736
+ the data device on Wayland, OLE on Windows — and drops into a file manager,
737
+ an editor or another react-x11 process with the handlers unchanged.
738
+
739
+ **Full reference: [drag-and-drop.md](drag-and-drop.md)** — `dropAccept`
740
+ matching, the payload (`e.files`, `e.text`, `e.getData`, `e.items`),
741
+ `dragData` and lazy payloads, `useDropTarget`/`useDragSource`, previews, GTK
742
+ and Firefox interop, and driving a drag in a test.