react-x11 2.52.0 → 2.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1096 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/imagedecode-worker.js +32 -0
  76. package/src/imagedecode.js +140 -2
  77. package/src/layouts.js +1 -1
  78. package/src/nodes/animation.js +1 -1
  79. package/src/nodes/invalidate.js +1 -1
  80. package/src/nodes/layouthost.js +1 -1
  81. package/src/nodes/video.js +19 -8
  82. package/src/nodes/window/droptarget.js +1 -1
  83. package/src/ntk.d.ts +12 -0
  84. package/src/pacing.js +1 -1
  85. package/src/refresh/index.js +2 -1
  86. package/src/types/video.d.ts +5 -2
  87. package/src/videobackend.js +28 -0
  88. package/src/videoframes.js +29 -16
  89. package/src/wayland/app.js +27 -0
  90. package/src/wayland/video.js +376 -0
  91. package/src/win32/a11y.js +1 -1
  92. package/src/win32/app.js +2 -2
  93. package/src/win32/fonts.js +1 -1
  94. package/src/win32/glarea.js +4 -4
  95. package/src/win32/overlay.js +1 -1
  96. package/src/win32/panewindow.js +3 -3
  97. package/src/win32/shell.js +1 -1
  98. package/src/win32/window.js +2 -2
  99. package/src/windowid.js +1 -1
  100. package/src/xvideo.js +189 -0
  101. package/docs/architecture/accessibility.md +0 -470
  102. package/docs/architecture/animation.md +0 -707
  103. package/docs/architecture/container-queries.md +0 -525
  104. package/docs/architecture/custom-layout.md +0 -334
  105. package/docs/architecture/drag-and-drop.md +0 -941
  106. package/docs/architecture/element-layer-contents.md +0 -622
  107. package/docs/architecture/frame-pacing.md +0 -255
  108. package/docs/architecture/grid-layout.md +0 -358
  109. package/docs/architecture/protocol-efficiency.md +0 -357
  110. package/docs/architecture/video.md +0 -1149
  111. package/docs/architecture/windowed-regions.md +0 -854
  112. package/docs/glx.md +0 -137
  113. package/docs/wayland.md +0 -545
  114. package/docs/windows-embedding.md +0 -271
  115. package/docs/windows-gl.md +0 -221
package/docs/events.md CHANGED
@@ -1,132 +1,86 @@
1
1
  # Events
2
2
 
3
- ntk delivers raw X11 events per window; react-x11 turns them into synthetic
4
- events dispatched over the drawn node tree with DOM-like semantics.
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
5
 
6
6
  ## Dispatch
7
7
 
8
- 1. **Hit test**: front-to-back walk of the stacking-ordered tree (zIndex,
9
- then document order), respecting `overflow` clipping and
10
- `pointerEvents` — `'none'` takes a node and everything in it out of the
11
- test, `'box-none'` only the node itself, so its children are still
12
- targets and a point over its own area lands on what is behind it. A
13
- `<glarea>` is asked first: its surface is stacked above everything 2D in
14
- the window, and its children above the surface, so a point over one lands
15
- on the child under it or on the `<glarea>` — or, with `'box-none'`, on
16
- whatever the tree has behind the surface
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
17
16
  ([elements.md](elements.md#glarea)).
18
17
  2. **Capture phase**: `on<Event>Capture` handlers from the window down to
19
18
  the target.
20
19
  3. **Target + bubble phase**: `on<Event>` handlers from the target up.
21
- 4. **Default action**: the element's built-in behavior (textinput editing,
20
+ 4. **Default action**: the element's built-in behaviour (textinput editing,
22
21
  wheel scrolling, then Tab traversal) — skipped if any handler called
23
22
  `ev.preventDefault()`.
24
23
 
25
- Step 4 is a documented seam, not a privilege of the built-in elements: an
26
- element added with `registerElement` implements the same `defaultKeyDown` /
27
- `defaultMouseDown` / `defaultMouseMove` / … methods and gets the same
28
- ordering ([extending.md](extending.md#behaviour-of-your-own)). The wheel has
29
- one more layer than the rest: the node under the pointer gets `defaultWheel`
30
- first — for a pane whose wheel is a zoom rather than a scroll — and unless it
31
- consumes the event, the scroll chain walks outward from the target asking
32
- each node `canScroll(deltaX, deltaY)`, which is how an element that scrolls
33
- content it painted joins in
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)`
34
30
  ([extending.md](extending.md#scrolling-content-you-painted)).
35
31
 
36
- `ev.stopPropagation()` stops the walk. Handlers always read from current
37
- props — they can never go stale.
38
-
39
32
  ### When the response is painted
40
33
 
41
- **Discrete input paints from its own handler**, in the same turn of the
42
- event loop as the press — not on the next frame. A click has exactly one
43
- visual answer (a button darkens, a caret lands, focus moves), so there is
44
- nothing a frame's wait could coalesce it with, and waiting would charge it
45
- up to a frame interval plus a server round trip on top of a paint that
46
- measures in the hundreds of microseconds. The dispatcher therefore lands
47
- React's discrete-priority commit and paints once the whole dispatch has
48
- unwound — the default action, your handlers and React's update all in one
49
- pass, never a half-updated frame followed by a second one.
50
-
51
- Discrete means everything ntk does not coalesce: `mouseDown`/`mouseUp`,
52
- `keyDown`/`keyUp`, focus and blur, and the window manager's close request.
53
-
54
- **Motion and the wheel stay on the frame clock.** `mouseMove`, and the hover
55
- enter/leave work it drives, are the opposite case — the pointer reports at
56
- device rate and only the newest position matters — so they coalesce to at
57
- most one repaint per frame. A scroll is the same case for a different reason:
58
- a touchpad reports one dozens of times a frame, and nobody can see those
59
- frames apart. What makes the wait free is that ntk coalesces a scroll by
60
- _adding it up_ rather than keeping the newest, so a frame's event carries the
61
- whole distance and the pacing costs a paint, never a pixel.
62
-
63
- **Hover follows content.** A scroll moves content under a pointer that did
64
- not move, and so can a layout that reflows it. After a frame that laid out
65
- — every frame of a scroll does — the window asks again what is under the
66
- pointer's last position, as a browser asks at the frame after a scroll:
67
- `:hover`, `onMouseEnter` and `onMouseLeave` change as a motion would change
68
- them, and the node under the pointer hears `defaultMouseMove` at that point,
69
- so an element that paints its own hover (a document, a graph) re-reads what
70
- it is over. `onMouseMove` is not called — the pointer did not move — and
71
- nothing changes while a press or a capture holds the pointer, or after the
72
- pointer has left the window.
73
-
74
- **Hover belongs to the active window, on macOS.** Nothing in CSS or the
75
- DOM ties hover to focus — `:hover` is "while the user designates an element
76
- with a pointing device", and a `mousemove` is owed whenever the pointer moves
77
- over an element — but the platform does. A window there that is not the
78
- active one does not track the pointer: AppKit's own controls roll over in
79
- the key window alone, and a browser drops the moves over a window that is
80
- neither key nor main and tells the page the pointer left. So on the Cocoa
81
- backend a window that loses the keyboard hears `onMouseLeave` down its hover
82
- path, `:hover` comes off, an element that paints its own hover hears
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
83
55
  `defaultMouseLeave`, and no motion, enter, cursor or attention follows until
84
- the window is the active one again — when the hover comes back at the point
85
- the pointer is at, without waiting for it to move. `<window onMouseOut>` is
86
- not called: the pointer is still over it.
87
-
88
- "Active" is the focus's answer, as it is for the caret
89
- ([below](#window-focus)): a window whose `<Dialog>` holds the keyboard still
90
- hovers. A `<popup>` nothing manages — a menu, a dropdown, a popover under a
91
- tray item — always does, since it is on the screen because the user asked
92
- for it, in an application that may have no active window at all. A press
93
- still lands where it was aimed, as the first click on a window always has
94
- here, and its gesture runs on; the wheel scrolls a background window as the
95
- platform has it. On X11, Wayland and Windows nothing changes: every toolkit
96
- there hovers in a window without the keyboard, and with focus-follows-mouse
97
- the question hardly arises.
98
-
99
- Bursts of discrete input stay bounded by the same mechanism. While the
100
- server has not acknowledged the last frame, the response goes back to the
101
- paced path: click twice quickly and the first press paints immediately while
102
- the second folds into one catch-up frame. Update instantly, then catch up.
103
-
104
- This needs ntk 5.2.0 or newer, which publishes the `frameInFlight()` gate
105
- the decision is made with. On an older ntk everything still works; the
106
- response simply waits for the next frame, as it did before.
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).
107
64
 
108
65
  ### When a handler throws
109
66
 
110
- **No error boundary can catch it.** A boundary only sees throws React
111
- itself invoked, and a handler runs from an X event, so React is not on the
112
- stack. react-x11 therefore catches it at the dispatcher: the error is
113
- reported with the element, the handler name and the component that rendered
114
- the node, `process.exitCode` is set to 1 so the crash is still visible to CI
115
- and to a supervisor, and **dispatch continues** — one bad tooltip handler
116
- must not stop the handlers after it, or the frame loop.
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.
117
73
 
118
74
  ```
119
75
  react-x11: onClick on <box> in Panel threw. It ran from an X event rather
120
76
  than a render, so no error boundary could catch it; dispatch continues.
121
77
  ```
122
78
 
123
- `onDrop` is the one handler that may be `async`, and a promise it rejects
124
- with arrives here too — same message, same channel — rather than becoming an
125
- unhandled rejection, which node exits on. It also refuses the drop; see
126
- [drag-and-drop.md](drag-and-drop.md#the-drop).
127
-
128
- `createRoot({ onUncaughtError })` takes it over completely, which is the
129
- point of it being overridable: for a kiosk, crashing loudly is correct.
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:
130
84
 
131
85
  ```js
132
86
  const root = await createRoot({
@@ -156,46 +110,34 @@ const root = await createRoot({
156
110
  }
157
111
  ```
158
112
 
159
- `ev.screenX`/`screenY` are where the pointer is on the virtual screen, in the
160
- same logical pixels `ev.x`/`y` are in — what to anchor a `<popup>` at, since
161
- `<popup x y>` are logical too. Undefined on an event with no pointer position.
162
-
163
- `nativeEvent.rootx/rooty` is the same point in **device** pixels, which is the
164
- unit a node's `abs` is in — reach for it when doing geometry against `abs`,
165
- and for nothing else. The split is the same one `ev.x` and `nativeEvent.x`
166
- have (docs/scale.md).
167
-
168
- `detail` counts a press as the next of a multi-click when it lands within the
169
- desktop's double-click time and distance of the last one
170
- ([system.md](system.md#usedesktopsettings--how-the-desktop-wants-an-app-to-feel)),
171
- and the gap is the one between the times the two presses carry,
172
- `nativeEvent.time`, not between when each was handled. So a first click
173
- whose render outlasts the window still makes a double click with a second
174
- the user pressed in time. Presses that carry no time (win32's) are timed as
175
- they are handled.
176
-
177
- On the Wayland backend a finger or a tablet tool drives the same mouse
178
- events, and those events alone carry `nativeEvent.pointerType` (`'touch'`,
179
- `'pen'`, `'eraser'` or `'mouse'`) with `pressure`, `tiltX`/`tiltY`,
180
- `rotation`, `distance` and `tangentialPressure` in the DOM's ranges; a mouse
181
- adds nothing, so a handler that never looks is unaffected. Every finger also
182
- arrives raw on the window as `touchstart`/`touchmove`/`touchend`/
183
- `touchcancel` (`wnd.on(...)`, not React props) — see
184
- [wayland-backend.md](wayland-backend.md).
185
-
186
- `x`/`y`/`localX`/`localY` and the wheel deltas are logical pixels in **the
187
- target's** unit — the display scale, times any `scale` prop above it
188
- ([scale.md](scale.md#a-subtree-of-its-own)). With no `scale` in the tree
189
- that is one unit for the whole window; with one, a handler on an unzoomed
190
- ancestor is reading the zoomed target's pixels, and `ev.nativeEvent.x`/`y`
191
- are the way back to the window's own.
192
-
193
- **A Ctrl chord is read from the keysym, not the code point.** `codepoint`
194
- comes from the _shifted_ keysym, so Ctrl+Shift+Z arrives as `Z` and Ctrl+Z as
195
- `z`; a handler that compares code points misses half of its own shortcut.
196
- `ctrlChordLetter(ev)` from `react-x11/keysyms` answers with the lowercase
197
- keysym either way — it is what `<textinput>` reads Ctrl+C/V/Z with, and it is
198
- exported because any widget with a chord of its own needs the same rule:
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.
199
141
 
200
142
  ```js
201
143
  onKeyDown={(ev) => {
@@ -203,16 +145,10 @@ onKeyDown={(ev) => {
203
145
  }}
204
146
  ```
205
147
 
206
- That keeps working under a Cyrillic or Greek layout too: `ev.keysym` is the
207
- Latin keysym for the key however the layout is switched — see
208
- [Layouts](#layouts). For a chord an application would otherwise write out by
209
- hand, [Accelerators](#accelerators) is the shorter way: a menu item's
210
- `shortcut` already binds itself, and `useAccelerator` takes the same chord.
211
-
212
148
  ### Modifiers
213
149
 
214
- Four booleans, DOM names, on **every** event rather than only the keyboard
215
- ones — a shift+click, an Alt+drag and a Super+wheel all need them:
150
+ Four booleans with DOM names, on **every** event — a shift+click, an
151
+ Alt+drag and a Super+wheel all need them:
216
152
 
217
153
  | property | X11 modifier | usually |
218
154
  | ---------- | ------------ | ------- |
@@ -227,25 +163,19 @@ onKeyDown={(ev) => {
227
163
  }}
228
164
  ```
229
165
 
230
- **Mod1 is Alt and Mod4 is Super by convention, not by protocol.** X says
231
- only that there are eight modifier rows and lets the keymap decide which
232
- keys sit in each; those two are where `setxkbmap` puts Alt and Super, and
233
- where every toolkit — GTK and Qt included — reads them from. The setup that
234
- disagrees still has the raw mask: `ev.nativeEvent.buttons` is the state
235
- field as it arrived, and `MOD` in `react-x11/keysyms` names its bits
236
- (`MOD.Mod3`, `MOD.Lock` for Caps Lock, and so on).
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, …).
237
170
 
238
171
  ### The primary modifier {#primary-modifier}
239
172
 
240
173
  **On the Cocoa backend the editing chords are ⌘, not Ctrl.** ⌘ arrives as
241
- `metaKey` (the DOM's own macOS mapping), and the app says which of the two its
242
- shortcuts are pressed with: `app.primaryModifier` is `'Super'` there and absent
243
- everywhere else, X11 under XQuartz included. The built-in editors read it, so
244
- `<textinput>`'s Undo, Cut, Copy, Paste and Select All are ⌘Z, ⌘X, ⌘C, ⌘V and
245
- ⌘A on a Mac, Redo is ⇧⌘Z, and the right-click menu prints them that way. A ⌃
246
- or ⌘ chord the field has no answer for is never typed as text — AppKit hands
247
- over the key's bare character with either held. A widget with chords of its
248
- own asks the same question with `primaryModifierHeld`:
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`:
249
179
 
250
180
  ```js
251
181
  import { ctrlChordLetter, keysymOf, primaryModifierHeld } from 'react-x11/keysyms';
@@ -256,72 +186,61 @@ onKeyDown={(ev) => {
256
186
  }}
257
187
  ```
258
188
 
259
- It is the same rule a menu bar's `shortcut` follows there, where `Control`
260
- means ⌘ ([globalmenu.md](globalmenu.md#shortcuts-are-a-list-not-a-string)),
261
- and the one [accelerators](#the-chord) follow: `useAccelerator([['Control',
262
- 'K']])` is ⌘K on a Mac, and a drawn `ContextMenu` prints and answers `⌘S`.
263
-
264
- The editing keys follow it as well. Where shortcuts are ⌘, `<textinput>` and
265
- `<textarea>` move and delete the Mac's way — AppKit's own bindings, the table
266
- every Cocoa field reads — so a Mac user's hands already know them:
267
-
268
- | keys | does |
269
- | ---------------------- | --------------------------------------------------------------------------- |
270
- | ⌥← ⌥→ | a word back or forward |
271
- | ⌘← ⌘→, and ⌃← ⌃→ | the start or end of the line |
272
- | ⌘↑ ⌘↓ | the start or end of the value |
273
- | ⌥↑ ⌥↓ | the start or end of the paragraph, and on to the next |
274
- | ↑ ↓ in a `<textinput>` | its start and its end, as in an NSTextField |
275
- | ⌥⌫ ⌥⌦ | delete a word |
276
- | ⌘⌫ | delete to the start of the line |
277
- | ⌃⌫ | take the accent off the character before the caret — é to e |
278
- | ⌃A ⌃E | the start or end of the paragraph |
279
- | ⌃B ⌃F, ⌃P ⌃N | a character back or forward, a line up or down |
280
- | ⌃H ⌃D | delete a character back or forward |
281
- | ⌃K ⌃Y | kill to the end of the paragraph — its break, at its end — and yank it back |
282
-
283
- Shift extends the selection with every move. "Line" is the line as it is
284
- drawn, wrapped, and "paragraph" runs between line breaks; in a
285
- `<textinput>` both are the whole value.
286
-
287
- Everywhere else the bindings are GTK's, Qt's and Windows': Ctrl+←/→ and
288
- Ctrl+⌫/⌦ by word, Ctrl+A to select all. The two sets disagree about Ctrl
289
- outright — ⌃← is the start of the line on a Mac, and ⌃A the start of the
290
- paragraph — so a backend has one set or the other, never a mix. Home and
291
- End move the caret to the ends of the line on every backend: AppKit scrolls without moving it, which a field — one
292
- line, its caret the point of the keystroke — has no use for.
293
-
294
- ⌃K's kill buffer is the application's and not the pasteboard, as it is in
295
- AppKit: a kill leaves what ⌘C copied alone, ⌃Y in one field yanks what ⌃K
296
- killed in another, and kills one after another add up, so the two that take
297
- a line and then its break yank back as the line. A `sensitive` field kills
298
- into nothing, for the reason it offers no Copy.
299
-
300
- Any of them is the field's default action, so an `onKeyDown` that calls
301
- `preventDefault()` keeps the key, as it does every other.
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.
302
222
 
303
223
  ## Handlers
304
224
 
305
- | handler | notes |
306
- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
307
- | `onClick` | fires on the nearest common ancestor of press & release; `detail` counts multi-clicks |
308
- | `onMouseDown` / `onMouseUp` / `onMouseMove` | move is coalesced to once per frame by ntk |
309
- | `onMouseEnter` / `onMouseLeave` | do not propagate; synthesized by hover-path diffing, on motion and when a scroll moves content under a still pointer |
310
- | `onWheel` | pixels, from the device or from X buttons 4–7; default action scrolls the nearest scroll container with somewhere to go |
311
- | `onContextMenu` | right-click (button 3), after `onMouseDown`; default action opens the element's menu |
312
- | `onKeyDown` / `onKeyUp` | delivered to the focused node (or the window); Space/Enter click an `onClick`, [accelerators](#accelerators) fire and Tab cycles focus, unless it took them |
313
- | `onCompositionStart` / `onCompositionUpdate` / `onCompositionEnd` | text still being typed — a dead key, a Compose sequence; see below |
314
- | `onFocus` / `onBlur` | focus follows mousedown (nearest `focusable` ancestor) and Tab traversal |
315
- | `onDragEnter` / `onDragLeave` | do not propagate; drag-path diffing, the same shape as the hover pair above |
316
- | `onDragOver` / `onDrop` | on a drop target; `onDrop` may be async — [drag-and-drop.md](drag-and-drop.md) |
317
- | `onDragStart` / `onDrag` / `onDragEnd` | on a `draggable` node; the press is a click until it moves 4px |
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 |
318
238
 
319
239
  ## Space and Enter are a click {#space-and-enter-are-a-click}
320
240
 
321
- **An `onClick` on a focusable node is operable from the keyboard, with
322
- nothing else written.** Space or Enter on it dispatches the click — the whole
323
- press gesture, mousedown then mouseup then click, through the ordinary path —
324
- so a control hand-built out of a `<box>` behaves like a `<Button>`:
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>`:
325
244
 
326
245
  ```jsx
327
246
  <box focusable role="option" aria-selected={on} onClick={() => open(channel)}>
@@ -329,42 +248,28 @@ so a control hand-built out of a `<box>` behaves like a `<Button>`:
329
248
  </box>
330
249
  ```
331
250
 
332
- The keys are a **default action**, which is the whole of the contract:
333
-
334
- - `preventDefault()` in the node's own `onKeyDown` takes the key back — that
335
- is how a widget that answers Enter with something other than its click
336
- (a disclosure opens a branch; the click only moves the selection) keeps it;
337
- - a node with no `onClick` is untouched, whatever its `role` says. A role
338
- advertises an action to an assistive technology, which has no other way to
339
- ask; a key press is already at the node, so there is nothing to advertise
340
- and nothing to activate. `role="button"` with only an `onKeyDown` stays
341
- yours;
342
- - `<textinput>` and `<textarea>` never activate: their own `defaultKeyDown`
343
- answers the keystroke, so Space types a space;
344
- - **a scroll pane keeps Space.** Space and Shift+Space page a scrolling `<box>`
345
- ([elements.md](elements.md#scrolling)) — that is a default action too, and
346
- the pane answers it before this one, so a clickable pane pages with Space
347
- and activates with Enter. A focusable row _inside_ a pane is not affected
348
- either way: default actions run on the focused node, and the pane is not
349
- it;
350
- - **a chord is not a press.** With Ctrl, Alt or Super held — ⌘ or ⌃ or ⌥ on
351
- the Cocoa backend — Space and Enter are a command, and the node leaves them
352
- to whoever bound it: a menu's Ctrl+Return fires with focus on a clickable
353
- box exactly as it does anywhere else ([accelerators](#accelerators)).
354
-
355
- Shift is not a command, and it travels: the synthesized click carries the key
356
- press's own modifier mask, so Shift+Enter reaches `onClick` as a shift-click.
357
-
358
- It is the same function an assistive technology's activation goes through, on
359
- purpose — one definition of "activate" for the pointer, the keyboard and
360
- Orca, which cannot then disagree about a control
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
361
267
  ([accessibility.md](accessibility.md#at-driven-controls)).
362
268
 
363
269
  ## Accelerators {#accelerators}
364
270
 
365
- **A menu's `shortcut` is a binding.** `MenuBar` and `ContextMenu` answer their
366
- own items' chords while they are mounted, so the array that draws the menu is
367
- the array that binds it:
271
+ **A menu's `shortcut` is a binding.** `MenuBar` and `ContextMenu` answer
272
+ their items' chords while mounted, unopened:
368
273
 
369
274
  ```jsx
370
275
  <MenuBar
@@ -384,17 +289,11 @@ the array that binds it:
384
289
  />
385
290
  ```
386
291
 
387
- Ctrl+S calls `save()` without the menu being opened. The same array is what
388
- `aria-keyshortcuts` announces and what crosses the bus when the desktop's
389
- panel draws the menu ([globalmenu.md](globalmenu.md)) — and it stays bound in
390
- that case, because the panel draws the rows but the key is pressed in your
391
- window and nothing else is going to deliver it. (The macOS menu bar is the
392
- exception, and answers the ⌘ chords it shows by itself —
393
- [below](#the-macos-menu-bar).)
394
-
395
- For a shortcut that is not in a menu — there are always some —
396
- `useAccelerator` takes the same chords, so one can be moved into or out of a
397
- menu without being rewritten:
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:
398
297
 
399
298
  ```jsx
400
299
  import { useAccelerator } from 'react-x11';
@@ -404,25 +303,20 @@ useAccelerator([['Control', 'K']], () => openPalette());
404
303
 
405
304
  ### The chord
406
305
 
407
- `[['Control', 'S'], ['F2']]` is dbusmenu's `aas`: a list of **alternatives**,
408
- each a list of modifier tokens — `Control`, `Alt`, `Shift`, `Super` — ending
409
- in the key. Any alternative fires; only the first is drawn, since a row has
410
- one shortcut column.
411
-
412
- The key is named the way X names it, which is what `gdk_keyval_name()` emits
413
- and what a panel's importer parses: `plus` rather than `+`, `Prior` rather
414
- than `PgUp`, `Return` rather than `Enter`. A one-character name is that
415
- character. `keysymFromName()` in `react-x11/keysyms` is the resolver, and
416
- `matchesShortcut(ev, shortcut, { primary })` the matcher, for an application
417
- dispatching chords its own way.
418
-
419
- **`Control` is the app's primary modifier**: Ctrl, except on the Cocoa
420
- backend, where it is ⌘ — the key a Mac user saves with, the key a drawn row
421
- prints (`⌘S`, `⇧⌘Z`) and the key the binding answers
422
- ([The primary modifier](#primary-modifier)). `Super` is ⌘ there too, so a
423
- chord cannot ask for ⌃ on a Mac; a chord is matched with it up. The menus and
424
- `useAccelerator` read the app's answer themselves, and `matchesShortcut` takes
425
- it as an option, since it is a pure function with no app to ask:
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:
426
320
 
427
321
  ```js
428
322
  import { matchesShortcut } from 'react-x11';
@@ -431,29 +325,23 @@ import { primaryModifier } from 'react-x11/keysyms';
431
325
  matchesShortcut(ev, [['Control', 'S']], { primary: primaryModifier(app) });
432
326
  ```
433
327
 
434
- Four rules, all of which a hand-rolled `onKeyDown` gets subtly differently:
435
-
436
- - **exact on the four modifiers.** Ctrl+S does not fire on Ctrl+Shift+S.
437
- - **indifferent to the locks.** Caps Lock and Num Lock are not modifiers here
438
- — `ctrlKey`/`altKey`/`shiftKey`/`metaKey` are the four named bits and
439
- nothing else, so a chord keeps working with either light on.
440
- - **the key, not the character.** Matching is against `ev.keysym`, the Latin
441
- keysym for the physical key, so a chord survives a layout switch
442
- ([Layouts](#layouts)) — and `['Control', 's']` and `['Control', 'S']` are
443
- one binding rather than two. A chord that names a **symbol** also matches
444
- the character the key produced, because `+` is the shifted `=` and
445
- `[['Control', 'plus']]` has to be pressable; letters stay out of that path,
446
- since it is exactly what would make Ctrl+Shift+S fire a Ctrl+S binding.
447
- - **`enabled` and `visible` gate it.** A dimmed item's chord fires nothing,
448
- and neither does an item inside a submenu whose parent is disabled — a
449
- command you cannot reach by opening the menu is not one a key should reach.
450
- - **`Control` is the primary modifier**, above — exact on it as on the rest,
451
- so ⌃⌘S does not fire a ⌘S binding either.
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.
452
340
 
453
341
  ### Who gets the key first
454
342
 
455
- Accelerators sit at the end of the same default-action chain Tab and
456
- Space/Enter are in, and `preventDefault()` is the word at every step:
343
+ Accelerators sit in the default-action chain, and `preventDefault()` stops it
344
+ at any step:
457
345
 
458
346
  1. the application's own `onKeyDown`, capture then bubble;
459
347
  2. composition — a dead key, a Compose sequence;
@@ -462,83 +350,57 @@ Space/Enter are in, and `preventDefault()` is the word at every step:
462
350
  4. **accelerators**;
463
351
  5. focus traversal, for Tab.
464
352
 
465
- So **a focused text field keeps the chords it answers**: Ctrl+C copies the
466
- selection and never reaches a menu item that also says Ctrl+C. Ctrl+S is not
467
- one of the field's, so it falls through to the menu — a control that swallowed
468
- every chord would be a control no application could put a shortcut behind.
469
-
470
- Return is the same rule. A `<textinput>` answers Return and Shift+Return with
471
- `onSubmit`, and nothing else: Ctrl+Return, Alt+Return and Super+Return — ⌘↩
472
- on the Cocoa backend — are chords, and reach the menu that prints them. A
473
- field with **no** `onSubmit` has no answer for Return at all, so the key goes
474
- on to whatever binds it, a form's `[['Return']]` default button included. A
475
- `<textarea>` keeps Return and Shift+Return for the newline and Ctrl+Return
476
- (⌘↩) for `onSubmit` when it has one; Alt and Super on Return are chords there
477
- too, except on the Cocoa backend, where ⌃↩ and ⌥↩ are AppKit's newline.
478
-
479
- Two things suppress them entirely, both because something else has the
480
- keyboard:
481
-
482
- - **an open menu.** It is holding a pointer grab, and a chord firing a second
483
- command out from under the one being pointed at is a bug nobody reproduces.
484
- - **a modal `<popup>`** (`trapFocus`, which is what `Dialog` sets). Ctrl+S
485
- while a confirmation is up saves nothing. A menu declared _inside_ the modal
486
- still works — the containment rule is the one Tab traversal already follows
487
- — and `useAccelerator`'s `scope` option is how a hook-registered shortcut
488
- joins it:
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:
489
370
 
490
371
  ```jsx
491
372
  const inDialog = useRef(null);
492
373
  useAccelerator([['Control', 'Return']], confirm, { scope: inDialog });
493
374
  ```
494
375
 
495
- A chord belongs to the window whose menu declared it, so a second top-level
496
- window does not answer it, and `enabled: false` unbinds one without unmounting
497
- anything. `accelerators={false}` on a `MenuBar` or `ContextMenu` turns the
498
- whole of this off for an application that already has a dispatcher.
499
-
500
- **An app with no `<window>` at all** — a menu-bar app, whose whole UI is the
501
- popover a tray click opens — binds against the root-level `<popup>` that took
502
- the keyboard instead
503
- ([elements.md](elements.md#a-popover-under-a-tray-item)). There is nothing
504
- else for a shortcut to belong to, and it is the window the keys are arriving
505
- on. A binding that lands on nothing at all — no window and no popup either —
506
- warns once in development and names `scope`, rather than leaving a chord that
507
- is drawn, announced and dead.
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`.
508
383
 
509
384
  ### The macOS menu bar {#the-macos-menu-bar}
510
385
 
511
- On the Cocoa backend a `MenuBar` is drawn by the system
512
- ([globalmenu.md](globalmenu.md)), which installs each item's chord as an
513
- `NSMenu` key equivalent where it can spell one — a single chord on a
514
- character key — and AppKit then runs the item on a ⌘ chord by itself. It does
515
- so _after_ the window has seen the key, whatever the window did with it. So
516
- those chords are **reserved**: the window's accelerators are not offered them
517
- at all, neither the bar's own binding nor a `useAccelerator` on the same
518
- chord, whichever mounted last, and the item runs once. A chord the menu bar
519
- cannot spell — a function key, a second alternative, `plus` — or one with no
520
- `Control` or `Super` in it stays the window's, and fires the way it does
521
- anywhere else.
522
-
523
- The reservation cannot reach steps 1 and 3 above, which run before it: an
524
- `onKeyDown` that answers ⌘S for itself, or a focused field answering ⌘C beside
525
- an Edit menu whose Copy says ⌘C, runs alongside the menu item rather than
526
- instead of it — `preventDefault()` there does not reach AppKit. Leave a chord
527
- the menu bar shows to the menu bar.
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.
528
397
 
529
398
  ## The wheel {#wheel}
530
399
 
531
- `ev.deltaX` / `ev.deltaY` are **pixels**, positive down and right, and one
532
- notch of a wheel is 48 of them — the step an arrow key takes, so the two
533
- input routes move a list by the same amount.
534
-
535
- Where the server has XI2, those pixels are the distance the device actually
536
- measured. The core protocol has no wheel at all: a scroll arrives as a click
537
- of button 4/5/6/7, and a click carries no magnitude, which is why every
538
- scroll used to be exactly one notch. XI2 carries the same gesture as the
539
- device's own _scroll valuators_, so a touchpad's two-finger scroll reports
540
- the fractions of a notch it really was, and `deltaY` is `14.5` rather than
541
- `48` twice a second.
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.
542
404
 
543
405
  ```jsx
544
406
  <box
@@ -550,64 +412,39 @@ the fractions of a notch it really was, and `deltaY` is `14.5` rather than
550
412
  />
551
413
  ```
552
414
 
553
- `ev.smooth` says which kind of device this was, not whether this particular
554
- delta happened to be fractional: `true` means the valuators are flowing and
555
- the next event may be a third of a notch, `false` means the emulated button,
556
- which can only ever say one. It is the flag to branch on for anything that
557
- wants to animate a scroll — a whole notch is worth easing towards, a stream
558
- of measured pixels is not.
559
-
560
- **Nothing is opted into.** A `<window>` selects XI2 the first time it is
561
- scrolled and falls back to the wheel buttons where the server has none, so a
562
- handler written against `deltaY` works on both and the difference is the
563
- value. A `<popup>` is on the buttons deliberately — it holds a pointer grab,
564
- and a core grab delivers core events — so a scroll inside an open menu moves
565
- a notch at a time. Needs ntk >= 7.5.0.
566
-
567
- The selection waits for the first wheel because it is not free. An XI2
568
- selection _replaces_ the core one for the same event type, and an `XIMotion`
569
- is around 136 bytes on the wire where a core `MotionNotify` is 32 — measured
570
- against Xorg 21.1. Motion is the one event that keeps arriving at frame rate
571
- for as long as the pointer is over the window, so selecting it up front bills
572
- every window about 8 KB/s of pointer traffic while the pointer crosses it,
573
- including the dialogs, toolbars and forms that never see a wheel at all.
574
-
575
- What waiting costs is the opening event of the first gesture in a window's
576
- life, and it costs less than it sounds: a mouse wheel reports whole notches
577
- either way, and ntk treats the first valuator event as a seed with no
578
- distance to report, so under an eager selection that same first event moves
579
- nothing at all.
580
-
581
- `<window xi2>` selects at creation anyway, for an app whose whole interaction
582
- is a touchpad and which wants the very first flick smooth. `<window
583
- xi2={false}>` refuses the selection for the window's whole life.
584
-
585
- Shift turns a vertical scroll sideways for a device with only one axis (the
586
- convention every toolkit follows); a device that reports its own horizontal
587
- axis keeps what it reported. The default action is the hit node's own
588
- `defaultWheel` and then the scroll chain — see
589
- [extending.md](extending.md#behaviour-of-your-own) for the element whose
590
- wheel is a zoom rather than a scroll, and
591
- [extending.md](extending.md#scrolling-content-you-painted) for how anything
592
- joins the chain.
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)).
593
433
 
594
434
  ## Composition: dead keys and Compose {#composition}
595
435
 
596
- `é` is not a key. On a French, German or us-intl layout it is `dead_acute`
597
- then `e`, and a dead key is a keysym with **no code point at all** — so a
598
- renderer that types from `ev.codepoint` types the letter and drops the
599
- accent. That is most accented input in Europe, and it is invisible to anyone
600
- testing on a US layout (issue #272).
601
-
602
- A composition is text the user is still typing. It shows at the caret,
603
- underlined, and it is not the value until it commits:
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:
604
441
 
605
442
  ```
606
443
  dead_acute → the field shows ´ (underlined). value unchanged.
607
444
  e → the field shows é. value is now …é, one undo step.
608
445
  ```
609
446
 
610
- Everything about it is on by default and needs no configuration:
447
+ On by default, no configuration:
611
448
 
612
449
  | you press | you get |
613
450
  | ---------------------------------- | ------------------------------------------------- |
@@ -620,17 +457,15 @@ Everything about it is on by default and needs no configuration:
620
457
  | Compose `-` `-` `-` | `—` |
621
458
  | Escape mid-sequence | nothing typed, and the accent goes away |
622
459
 
623
- The dead keys are Unicode's own composition rather than a table somebody
624
- maintains, so `dead_breve` + `и` is `й` and `dead_acute` + `α` is `ά`
625
- without anyone having written those down. What is written down is the part
626
- Unicode has no rule for: `dead_stroke` + `o` is `ø`, and the `Multi_key`
627
- symbol sequences, which are conventions rather than compositions.
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).
628
463
 
629
464
  ### Handling it yourself
630
465
 
631
- The three events mirror the DOM's. `data` is empty on the start, the text
632
- showing at the caret on each update, and the **committed text** at the end —
633
- empty when the sequence was abandoned.
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).
634
469
 
635
470
  ```jsx
636
471
  <textinput
@@ -639,59 +474,38 @@ empty when the sequence was abandoned.
639
474
  />
640
475
  ```
641
476
 
642
- **A key an open composition took carries no text of its own.** `ev.key` and
643
- `ev.codepoint` are `undefined` on it and `ev.composing` is true, because its
644
- text arrives on the composition event instead — an application that types
645
- from `onKeyDown` would otherwise insert `o`, `c` and then `©`. The keysym is
646
- still there, so a chord still matches.
647
-
648
- The order is **application chords → composition → the element → focus
649
- traversal**: an `onKeyDown` that calls `preventDefault()` keeps its key and
650
- the composer never sees it, and a key the composer does take never reaches
651
- the element's `defaultKeyDown` — so a dead key cannot also fire an
652
- accelerator, and a Compose sequence containing `z` cannot undo halfway
653
- through.
654
-
655
- A composition is abandoned, never half-committed, when focus leaves, when the
656
- window loses the keyboard, or when a press puts the caret somewhere else.
657
-
658
- A screen reader is told the same story: the preedit is a text run of its
659
- own and its churn is marked as text the user did not type, so the accent is
660
- not read out and the character it commits is —
661
- [accessibility.md](accessibility.md#while-a-composition-is-open).
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)).
662
487
 
663
488
  ### Input methods
664
489
 
665
- On the [Wayland backend](wayland-backend.md) the compositor's input method
666
- (IBus under GNOME) drives the same three events over `zwp_text_input_v3`,
667
- and on the Windows backend the system IME drives them over IMM32
668
- (`src/win32/ime.js`): its preedit arrives on `onCompositionUpdate` and the
669
- text it commits on `onCompositionEnd`, inserted the way a typed character
670
- is. Two things are only there:
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:
671
494
 
672
495
  - `ev.cursorBegin` / `ev.cursorEnd` on an update — code-point offsets into
673
- `data` — when the engine says where inside its preedit the cursor is, or
674
- which segment of a phrase it is converting (drawn with a heavier
675
- underline). A dead key never sets them.
676
- - a commit with no preedit before it — an emoji picker, an engine that
677
- commits directly — is a start and an end in one.
678
-
679
- Keys the input method consumes never reach the application, so a dead key
680
- composed by IBus does not also reach the client-side table; a key that does
681
- arrive is one the input method declined. `<textinput sensitive>` offers the
682
- input method none of its text, and `inputMode` on a `<textinput>` tells it
683
- what to expect ([elements.md](elements.md#textinput)).
684
-
685
- The **Cocoa backend has no input method yet** — `NSTextInputClient` is
686
- [macos.md](macos.md)'s IME work — so its text is what each key event says it
687
- typed. A dead key such as ⌥E types nothing and shows nothing, and the key
688
- after it types what macOS put on that key: `é` where the event carries the
689
- dead key's composition, the bare letter where it does not, never the two
690
- letters reading the key's own character used to type. A key that typed more
691
- than one character — the accent and a letter it does not combine with, `´s`
692
- — commits them as one composition, a start and an end in one, the way an
693
- input method's commit arrives. Press-and-Hold accents and CJK input wait on
694
- the same work.
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.
695
509
 
696
510
  ### Changing the table
697
511
 
@@ -708,59 +522,36 @@ const root = await createRoot({
708
522
  });
709
523
  ```
710
524
 
711
- `compose: 'system'` is the whole file shorthand, `compose: false` turns
712
- composition off for an app that does its own, and later definitions win over
713
- earlier ones — which is what makes a Compose file an override of the
714
- built-ins rather than an addition beside them. Parsing is X's own format;
715
- `include` directives are ignored, and a line naming keysyms outside ASCII
716
- and the dead-key block (`<Greek_alpha>`, `<Cyrillic_a>`) is skipped rather
717
- than guessed at.
718
-
719
- **What this is not is an input method.** There is no preedit arriving from
720
- another process and no candidate list, so CJK still needs XIM or an
721
- IBus/Fcitx client — issue #272 tracks it. The events above are the surface it
722
- will arrive on.
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.
723
532
 
724
533
  ## Layouts {#layouts}
725
534
 
726
- A key event answers two questions and, the moment a non-Latin layout is
727
- active, it has to answer them differently:
535
+ A key event answers two questions, differently under a non-Latin layout:
728
536
 
729
- - **what the key typed** — `ev.key` and `ev.codepoint`, the active layout,
730
- the active level. A Cyrillic layout types Cyrillic.
537
+ - **what the key typed** — `ev.key` and `ev.codepoint`, from the active
538
+ layout and level. A Cyrillic layout types Cyrillic.
731
539
  - **what the key is called** — `ev.keysym`, the **Latin** keysym for that
732
- key, whichever group holds it. Shortcuts do not move with the layout, in
733
- GTK, in Qt and in the browsers, and Ctrl+Z has to keep undoing while the
734
- user is typing Russian.
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.
735
542
 
736
543
  So `ctrlChordLetter(ev) === keysymOf('z')` matches the physical Z key under
737
- every layout, and nothing in a widget has to know that a layout exists.
738
-
739
- **⌥ types, on the Cocoa backend.** It is the layout's third and fourth level
740
- there rather than a modifier: ⌥S is `ß` on a US keyboard, and on a German one
741
- ⌥L is `@` and ⌥5 and ⌥8 are `[` and `{` — the only keys that type them. So
742
- `ev.key` and `ev.codepoint` are what the layout typed, with ⌥, Shift and Caps
743
- Lock applied, while `ev.keysym` is still the key (`s`) and `ev.altKey` is
744
- true: an Alt+S accelerator still matches, and a field types `ß`. A ⌘ or ⌃
745
- chord types nothing and is named by its key, so ⌘⌥S is a chord on S — the
746
- same split the DOM makes on a Mac.
747
- `ev.group` is the active XKB group, 0-3, for an application that wants to
748
- show which layout is live — a switch sends no other notice.
749
-
750
- Where the Latin keysym comes from depends on how the layout got there, and
751
- the two mechanisms have nothing in common:
752
-
753
- - **Linux/XKB** loads every layout at once as a group and switches which one
754
- is live; the keymap never changes and **no MappingNotify is sent**. The
755
- Latin keysym is on the same keycode, in whichever group is the Latin one —
756
- which under `ru,us` is the second.
757
- - **XQuartz** has no groups. It synthesizes the keymap from the active macOS
758
- layout and _overwrites_ it when the input menu changes (Preferences →
759
- Input → "Follow system keyboard layout"; without that, nothing downstream
760
- can help), then sends MappingNotify. After a switch to Russian **no group
761
- anywhere holds Latin**, and the only thing left that still means "the Z
762
- key" is the keycode — so that is what is used, against the US position of
763
- whichever keycode scheme the server speaks.
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.
764
555
 
765
556
  ```js
766
557
  const root = await createRoot({
@@ -771,20 +562,14 @@ const root = await createRoot({
771
562
  ```
772
563
 
773
564
  `accelerators` also takes a keycode→keysym table (`{ 52: 'z' }`) for a
774
- server whose keycodes are neither evdev's nor macOS's, which is the only
775
- case the two built-in ones do not cover.
776
-
777
- One thing the core protocol cannot express is the difference between two
778
- groups of two levels and one group of four, so **the AltGr row is not
779
- reachable**: `ev.keysym` and `ev.codepoint` read groups only. Resolving it
780
- needs XkbGetMap, which node-x11 does not implement.
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.
781
567
 
782
568
  ## Pointer capture
783
569
 
784
570
  `ev.capturePointer()` routes every following `mousemove` and `mouseup` to
785
- the capturing node instead of whatever is under the pointer, so a drag
786
- keeps working past the widget's own bounds — and a release far outside it
787
- still ends the gesture. This is what `Slider` is built on:
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:
788
573
 
789
574
  ```jsx
790
575
  onMouseDown: (ev) => {
@@ -795,54 +580,39 @@ onMouseMove: (ev) => { if (dragging) setValue(valueAt(ev)); },
795
580
  onMouseUp: () => setDragging(false),
796
581
  ```
797
582
 
798
- Capture is released automatically on `mouseup` (like the DOM's implicit
799
- pointer capture) and when the capturing node unmounts; `ev.releasePointer()`
800
- ends it early. While captured, hover stays where it was — dragging must not
801
- light up every widget the pointer crosses.
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.
802
585
 
803
586
  ## Focus
804
587
 
805
- `focusable` opts a node into focus (`<textinput>` is focusable by
806
- default, and so is a scroll container with somewhere to scroll), `autoFocus`
807
- takes it at mount, and every drawn node has
808
- `focus()` / `blur()` / `focused` on its ref — plus `focusWithin`, which is
809
- true while focus is on the node **or inside it**, the question a modal asks
810
- before taking focus itself. To _draw_ that rather than read it, there is a
811
- `':focus-within'` style block
812
- ([styling.md](styling.md#inline-pseudo-states)), which is what a row
813
- containing a field wants. `focus()` hands the node back, so a component
814
- can forward it straight out of an imperative handle. Focusing a node inside a
815
- scroll container scrolls it into view — unless a press focused it, since
816
- the node is under the pointer. Mousedown focuses the nearest
817
- focusable ancestor of the hit node; Tab / Shift+Tab cycle through focusable
818
- nodes in tree order. Keyboard
819
- events route to the focused node's ancestor chain. `disabled` opts a node
820
- back out of focus, whatever else it says.
821
-
822
- **A focused node shows a ring**, and how focus arrived decides whether it
823
- does. A press sets `:focus`; everything else — Tab, `autoFocus`,
824
- `node.focus()`, a modal handing focus back as it closes — also sets
825
- `:focus-visible`, and that is the state the ring is drawn on. A press on a
826
- **text control** is the one press that sets both, because a field the
827
- keyboard is about to go to has to say so however focus got there. It costs no
828
- layout and needs no opt-in; see
829
- [styling.md](styling.md#the-focus-ring) for restyling it.
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.
830
604
 
831
605
  ### Tab order
832
606
 
833
- `tabIndex` sets the sequential focus order, following the DOM's rules:
607
+ `tabIndex` follows the DOM's rules:
834
608
 
835
- - nodes with a **positive** `tabIndex` come first, in ascending order;
836
- - then everything focusable without one (the implicit `0` group), in tree
837
- order;
838
- - `tabIndex={-1}` is focusable — by a press, and by `focus()` — but Tab
839
- never lands on it;
840
- - an explicit `tabIndex` makes a node focusable without also passing
841
- `focusable`.
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`.
842
613
 
843
- Tab out of a node that is not in the tab order (`tabIndex={-1}`, or a node
844
- outside the current focus scope) and traversal starts from the beginning of
845
- the order.
614
+ Tab from a node outside the order (`tabIndex={-1}`, or outside the current
615
+ focus scope) starts from the beginning.
846
616
 
847
617
  ```jsx
848
618
  <box tabIndex={1} /> {/* visited first */}
@@ -851,27 +621,19 @@ the order.
851
621
  <box tabIndex={-1} /> {/* clickable, never tabbed to */}
852
622
  ```
853
623
 
854
- Traversal is a **default action** like any other, and it runs last: an
855
- `onKeyDown` that calls `preventDefault()` on Tab keeps it, and so does an
856
- element whose own `defaultKeyDown` consumes it — an editor indenting with
857
- Tab. An element that does owes the keyboard user a way out of it, which is
858
- the Escape-arms-one-Tab convention in
859
- [extending.md](extending.md#behaviour-of-your-own).
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)).
860
628
 
861
629
  ### Focus scopes (modals)
862
630
 
863
- `trapFocus` makes a node own a **focus scope**. While it is the innermost
864
- scope:
865
-
866
- - Tab / Shift+Tab only visit focusables inside it — the rest of the window
867
- is unreachable by keyboard;
868
- - a press outside it does not move focus (poking at the window behind a
869
- modal is inert as far as focus goes — the press itself still dispatches,
870
- so pair it with `grab` + `onDismiss` for a real modal);
871
- - when the scope unmounts, focus goes back to whatever had it before the
872
- scope opened — no bookkeeping in the widget.
631
+ `trapFocus` makes a node own a **focus scope**. While it is the innermost:
873
632
 
874
- That is a modal dialog, with the popup taking focus at mount:
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.
875
637
 
876
638
  ```jsx
877
639
  function Dialog({ open, x, y, onClose }) {
@@ -893,106 +655,65 @@ function Dialog({ open, x, y, onClose }) {
893
655
  }
894
656
  ```
895
657
 
896
- Scopes nest (a modal opened from a modal); the innermost one wins, and
897
- popping it hands focus back to the outer one. Programmatic `focus()` is
898
- never blocked by a scope — the trap is about Tab and presses.
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.
899
661
 
900
662
  ### Focus inside a `<popup>`
901
663
 
902
- An override-redirect window never receives the X input focus, so a popup
903
- cannot hold focus itself: the **owner window** does, and a node inside the
904
- popup can be the focused node of that window. Keys arrive at the owner
905
- window and are dispatched to it, then bubble out through the popup's
906
- position in the JSX tree into the owner's handlers, so nothing has to
907
- proxy them.
908
-
909
- One consequence worth knowing: a press inside a popup that lands on nothing
910
- focusable leaves the owner window's focus alone. Menus depend on it —
911
- their rows are not focusable, and the trigger keeps handling keys while the
912
- menu is open (`Menu`, `Select`).
913
-
914
- A popup with no owner window — a popover a tray click opened, in an app with
915
- no window of its own — has nowhere for its keys to arrive, and takes them
916
- itself with `grabKeyboard`
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`
917
670
  ([elements.md](elements.md#a-popover-under-a-tray-item)).
918
671
 
919
672
  ### Window focus
920
673
 
921
- Node focus is per `<window>`, and the window itself may or may not be the
922
- one the X server sends keys to — that is the window manager's call.
923
- `<window onFocus>` / `<window onBlur>` report it (ntk ≥ 3.7.0, X
924
- `FocusIn`/`FocusOut`).
925
-
926
- While the window is unfocused the focused node **keeps** focus, exactly as
927
- `document.activeElement` survives a window blur in a browser — it just
928
- stops looking active: a `<textinput>` caret stops blinking, and resumes
929
- when the window is focused again. Calling `focus()` on a node in a window
930
- that does not have the input focus asks for it (X `SetInputFocus`), though
931
- a window manager is free to refuse.
932
-
933
- Which of an application's windows a key is _delivered_ to is a separate
934
- question again, and not one the application answers: the server sends it to
935
- the window holding the input focus, or to whichever descendant of that
936
- window the pointer happens to be over. A nested `<window>` and a `<popup>`
937
- put the two apart — the field is focused inside them, the keyboard belongs
938
- to the top-level — so a key is dispatched to **the node that has focus**,
939
- whichever of that top-level's windows it arrived at, and bubbles from there
940
- out through the JSX tree. Where more than one window holds a focused node,
941
- the one focused last is where the user is typing and the one that answers.
942
-
943
- "Unfocused" is likewise the **focus's** answer rather than any one window's.
944
- A `<popup>` shares its owner's focus, and a managed one — a `<Dialog>` — is a
945
- real window the window manager focuses in its own right, so opening one takes
946
- the X focus off the owner at the moment the field inside the dialog starts
947
- receiving keys. The caret follows the window that actually holds the
948
- keyboard, whichever of the ones sharing that focus it is; asking the owner
949
- alone is how a focused field ends up with a `:focus` ring and no caret. By
950
- the same token, `focus()` on a node inside an open dialog does not ask the
951
- server for the input focus — the dialog already has it.
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.
952
688
 
953
689
  ### Focus and visibility
954
690
 
955
- **Focus follows visibility.** A subtree that goes off the screen gives up
956
- the keyboard: a `<Suspense>` boundary showing its fallback, an `<Activity
957
- mode="hidden">`, a `display: 'none'`, a `<popup>` being unmapped. The
958
- focused node fires `onBlur`, `:focus`/`:focus-within` come off, and keys go
959
- to the window instead — because a control nobody can see must not be
960
- collecting keystrokes, and the application's state must not advance from
961
- them. Tab skips invisible nodes for the same reason.
962
-
963
- **And it comes back with it.** When the subtree is revealed, focus returns
964
- to where it was, ring and all, so a boundary that re-suspended mid-edit
965
- leaves the user typing where they were. Two rules keep that from being
966
- focus stealing:
967
-
968
- - it only happens when **nothing else has the keyboard** — anything focused
969
- while the subtree was away keeps focus;
970
- - it is a restore rather than a navigation: no scrolling into view, and no
971
- X `SetInputFocus`, so a background window revealing something never takes
972
- the keyboard off another application.
973
-
974
- `createRoot({ restoreFocusOnReveal: false })` turns the second half off, for
975
- an app that wants the browser's answer — focus that fell to nothing stays
976
- there, and coming back is the user's own Tab. The release is not optional.
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.
977
704
 
978
705
  ## Cursors
979
706
 
980
707
  The `cursor` style property (`'pointer'`, `'text'`, `'wait'`, `'move'`,
981
- `'crosshair'`, `'ew-resize'`, `'ns-resize'`, `'grab'`, `'not-allowed'`, …)
982
- applies the deepest hovered node's cursor to the window, via ntk's cursor
983
- cache over the standard X cursor font.
984
-
985
- `cursor: 'none'` hides the pointer (ntk ≥ 4.2.0) — the kiosk case. It is
986
- the one name that is not a glyph in that font, so ntk builds it from an
987
- empty 1×1 mask. Note it is **not** the same as setting no cursor at all:
988
- with none of a node's ancestors naming one, the window inherits the root
989
- window's cursor and the pointer stays visible.
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.
990
712
 
991
713
  ## Drag and drop {#drag-and-drop}
992
714
 
993
- Drag and drop rides this event system — same hit testing, same
994
- capture/bubble dispatch, same style states — so the props sit alongside the
995
- pointer handlers on any drawn element, `<window>` or `<popup>`:
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>`:
996
717
 
997
718
  ```jsx
998
719
  <box
@@ -1002,24 +723,20 @@ pointer handlers on any drawn element, `<window>` or `<popup>`:
1002
723
  />
1003
724
  ```
1004
725
 
1005
- | handler | notes |
1006
- | -------------------------------------- | --------------------------------------------------------------------------------------- |
1007
- | `onDragEnter` / `onDragLeave` | do not propagate — path diffing, like hover; paired with the `':drag-over'` state |
1008
- | `onDragOver` | per pointer position; `e.accept(action)` / `e.reject()` answer the source |
1009
- | `onDrop` | may be async — the protocol reply waits for the returned promise, bounded by a watchdog |
1010
- | `onDragStart` / `onDrag` / `onDragEnd` | the source side of a `draggable` node; `':dragging'` styles it for the duration |
1011
-
1012
- Two things that differ from the events above. A press on a `draggable` node
1013
- is still a **click** until the pointer moves 4px, and a completed drag
1014
- suppresses the click that would otherwise follow — the same bargain the DOM
1015
- makes. And a drag that leaves the app's own windows keeps working: it is
1016
- promoted to the platform's own protocol — XDND on X11, AppKit's dragging on
1017
- Cocoa, the data device on Wayland, OLE on Windows — and can be dropped into
1018
- a file manager, an editor or another react-x11 process, with the handlers
1019
- unchanged.
1020
-
1021
- **The full reference is [drag-and-drop.md](drag-and-drop.md)**: the
1022
- `dropAccept` matching rules, the event payload (`e.files`, `e.text`,
1023
- `e.getData`, `e.items`), `dragData` and lazy payloads, the `useDropTarget`
1024
- and `useDragSource` hooks, drag previews, interoperating with GTK and
1025
- Firefox, and how to drive a drag in a test.
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.