react-x11 2.51.0 → 2.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +10 -3
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/xkb.js +137 -0
package/docs/events.md
ADDED
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
# Events
|
|
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.
|
|
5
|
+
|
|
6
|
+
## Dispatch
|
|
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
|
|
17
|
+
([elements.md](elements.md#glarea)).
|
|
18
|
+
2. **Capture phase**: `on<Event>Capture` handlers from the window down to
|
|
19
|
+
the target.
|
|
20
|
+
3. **Target + bubble phase**: `on<Event>` handlers from the target up.
|
|
21
|
+
4. **Default action**: the element's built-in behavior (textinput editing,
|
|
22
|
+
wheel scrolling, then Tab traversal) — skipped if any handler called
|
|
23
|
+
`ev.preventDefault()`.
|
|
24
|
+
|
|
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
|
|
34
|
+
([extending.md](extending.md#scrolling-content-you-painted)).
|
|
35
|
+
|
|
36
|
+
`ev.stopPropagation()` stops the walk. Handlers always read from current
|
|
37
|
+
props — they can never go stale.
|
|
38
|
+
|
|
39
|
+
### When the response is painted
|
|
40
|
+
|
|
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
|
|
83
|
+
`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.
|
|
107
|
+
|
|
108
|
+
### When a handler throws
|
|
109
|
+
|
|
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.
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
react-x11: onClick on <box> in Panel threw. It ran from an X event rather
|
|
120
|
+
than a render, so no error boundary could catch it; dispatch continues.
|
|
121
|
+
```
|
|
122
|
+
|
|
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.
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
const root = await createRoot({
|
|
133
|
+
onUncaughtError: (error, info) => {
|
|
134
|
+
telemetry.report(error, info.componentStack);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Event object
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
{
|
|
144
|
+
type, target, currentTarget, // public instances
|
|
145
|
+
x, y, // window coordinates
|
|
146
|
+
localX, localY, // target-relative
|
|
147
|
+
nativeEvent, // the raw ntk/X11 event
|
|
148
|
+
shiftKey, ctrlKey, // on every event, not just keys
|
|
149
|
+
altKey, metaKey, // Mod1 and Mod4 — see Modifiers below
|
|
150
|
+
preventDefault(), stopPropagation(),
|
|
151
|
+
capturePointer(), releasePointer(), // see Pointer capture below
|
|
152
|
+
// mouse: button, detail (DOM-style click count: 2 = double, 3 = triple)
|
|
153
|
+
// wheel: deltaX, deltaY (pixels), smooth
|
|
154
|
+
// keyboard: keycode, keysym, group, codepoint, key, composing
|
|
155
|
+
// composition: data
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
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:
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
onKeyDown={(ev) => {
|
|
202
|
+
if (ev.ctrlKey && ctrlChordLetter(ev) === keysymOf('d')) duplicateLine();
|
|
203
|
+
}}
|
|
204
|
+
```
|
|
205
|
+
|
|
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
|
+
### Modifiers
|
|
213
|
+
|
|
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:
|
|
216
|
+
|
|
217
|
+
| property | X11 modifier | usually |
|
|
218
|
+
| ---------- | ------------ | ------- |
|
|
219
|
+
| `shiftKey` | Shift | Shift |
|
|
220
|
+
| `ctrlKey` | Control | Ctrl |
|
|
221
|
+
| `altKey` | Mod1 | Alt |
|
|
222
|
+
| `metaKey` | Mod4 | Super |
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
onKeyDown={(ev) => {
|
|
226
|
+
if (ev.altKey && ev.keysym === keysymOf('b')) wordLeft();
|
|
227
|
+
}}
|
|
228
|
+
```
|
|
229
|
+
|
|
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).
|
|
237
|
+
|
|
238
|
+
### The primary modifier {#primary-modifier}
|
|
239
|
+
|
|
240
|
+
**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`:
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
import { ctrlChordLetter, keysymOf, primaryModifierHeld } from 'react-x11/keysyms';
|
|
252
|
+
|
|
253
|
+
onKeyDown={(ev) => {
|
|
254
|
+
if (primaryModifierHeld(ev, app) && ctrlChordLetter(ev) === keysymOf('d'))
|
|
255
|
+
duplicateLine();
|
|
256
|
+
}}
|
|
257
|
+
```
|
|
258
|
+
|
|
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.
|
|
302
|
+
|
|
303
|
+
## Handlers
|
|
304
|
+
|
|
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 |
|
|
318
|
+
|
|
319
|
+
## Space and Enter are a click {#space-and-enter-are-a-click}
|
|
320
|
+
|
|
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>`:
|
|
325
|
+
|
|
326
|
+
```jsx
|
|
327
|
+
<box focusable role="option" aria-selected={on} onClick={() => open(channel)}>
|
|
328
|
+
<text>{channel}</text>
|
|
329
|
+
</box>
|
|
330
|
+
```
|
|
331
|
+
|
|
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
|
|
361
|
+
([accessibility.md](accessibility.md#at-driven-controls)).
|
|
362
|
+
|
|
363
|
+
## Accelerators {#accelerators}
|
|
364
|
+
|
|
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:
|
|
368
|
+
|
|
369
|
+
```jsx
|
|
370
|
+
<MenuBar
|
|
371
|
+
menus={[
|
|
372
|
+
{
|
|
373
|
+
label: 'File',
|
|
374
|
+
items: [
|
|
375
|
+
{ label: 'Save', shortcut: [['Control', 'S']], onSelect: save },
|
|
376
|
+
{
|
|
377
|
+
label: 'Save As…',
|
|
378
|
+
shortcut: [['Control', 'Shift', 'S']],
|
|
379
|
+
onSelect: saveAs,
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
},
|
|
383
|
+
]}
|
|
384
|
+
/>
|
|
385
|
+
```
|
|
386
|
+
|
|
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:
|
|
398
|
+
|
|
399
|
+
```jsx
|
|
400
|
+
import { useAccelerator } from 'react-x11';
|
|
401
|
+
|
|
402
|
+
useAccelerator([['Control', 'K']], () => openPalette());
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### The chord
|
|
406
|
+
|
|
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:
|
|
426
|
+
|
|
427
|
+
```js
|
|
428
|
+
import { matchesShortcut } from 'react-x11';
|
|
429
|
+
import { primaryModifier } from 'react-x11/keysyms';
|
|
430
|
+
|
|
431
|
+
matchesShortcut(ev, [['Control', 'S']], { primary: primaryModifier(app) });
|
|
432
|
+
```
|
|
433
|
+
|
|
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.
|
|
452
|
+
|
|
453
|
+
### Who gets the key first
|
|
454
|
+
|
|
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:
|
|
457
|
+
|
|
458
|
+
1. the application's own `onKeyDown`, capture then bubble;
|
|
459
|
+
2. composition — a dead key, a Compose sequence;
|
|
460
|
+
3. the focused element's own behaviour (`<textinput>` editing, a scroll pane's
|
|
461
|
+
page keys);
|
|
462
|
+
4. **accelerators**;
|
|
463
|
+
5. focus traversal, for Tab.
|
|
464
|
+
|
|
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:
|
|
489
|
+
|
|
490
|
+
```jsx
|
|
491
|
+
const inDialog = useRef(null);
|
|
492
|
+
useAccelerator([['Control', 'Return']], confirm, { scope: inDialog });
|
|
493
|
+
```
|
|
494
|
+
|
|
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.
|
|
508
|
+
|
|
509
|
+
### The macOS menu bar {#the-macos-menu-bar}
|
|
510
|
+
|
|
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.
|
|
528
|
+
|
|
529
|
+
## The wheel {#wheel}
|
|
530
|
+
|
|
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.
|
|
542
|
+
|
|
543
|
+
```jsx
|
|
544
|
+
<box
|
|
545
|
+
style={{ overflow: 'scroll' }}
|
|
546
|
+
onWheel={(ev) => {
|
|
547
|
+
ev.deltaY; // pixels — fractional from a touchpad
|
|
548
|
+
ev.smooth; // whether the device can report a fraction at all
|
|
549
|
+
}}
|
|
550
|
+
/>
|
|
551
|
+
```
|
|
552
|
+
|
|
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.
|
|
593
|
+
|
|
594
|
+
## Composition: dead keys and Compose {#composition}
|
|
595
|
+
|
|
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:
|
|
604
|
+
|
|
605
|
+
```
|
|
606
|
+
dead_acute → the field shows ´ (underlined). value unchanged.
|
|
607
|
+
e → the field shows é. value is now …é, one undo step.
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Everything about it is on by default and needs no configuration:
|
|
611
|
+
|
|
612
|
+
| you press | you get |
|
|
613
|
+
| ---------------------------------- | ------------------------------------------------- |
|
|
614
|
+
| `dead_acute` `e` | `é` — and any base letter, in any script |
|
|
615
|
+
| `dead_circumflex` `dead_acute` `e` | `ế` — dead keys stack |
|
|
616
|
+
| `dead_acute` space | `´` — the accent on its own |
|
|
617
|
+
| `dead_acute` `q` | `´q` — no such character, so nothing is swallowed |
|
|
618
|
+
| Compose `o` `c` | `©` |
|
|
619
|
+
| Compose `'` `e` (or `e` `'`) | `é` |
|
|
620
|
+
| Compose `-` `-` `-` | `—` |
|
|
621
|
+
| Escape mid-sequence | nothing typed, and the accent goes away |
|
|
622
|
+
|
|
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.
|
|
628
|
+
|
|
629
|
+
### Handling it yourself
|
|
630
|
+
|
|
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.
|
|
634
|
+
|
|
635
|
+
```jsx
|
|
636
|
+
<textinput
|
|
637
|
+
onCompositionUpdate={(ev) => setStatus(`composing ${ev.data}`)}
|
|
638
|
+
onCompositionEnd={(ev) => setStatus('')}
|
|
639
|
+
/>
|
|
640
|
+
```
|
|
641
|
+
|
|
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).
|
|
662
|
+
|
|
663
|
+
### Input methods
|
|
664
|
+
|
|
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:
|
|
671
|
+
|
|
672
|
+
- `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.
|
|
695
|
+
|
|
696
|
+
### Changing the table
|
|
697
|
+
|
|
698
|
+
```js
|
|
699
|
+
import { XK_MULTI_KEY } from 'react-x11/keysyms';
|
|
700
|
+
|
|
701
|
+
const root = await createRoot({
|
|
702
|
+
compose: {
|
|
703
|
+
// this machine's Compose file, which is how a personal ~/.XCompose is
|
|
704
|
+
// picked up — usually absent on macOS, where nothing is lost
|
|
705
|
+
file: 'system',
|
|
706
|
+
sequences: [[[XK_MULTI_KEY, 'l', 'd'], '🦆']],
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
```
|
|
710
|
+
|
|
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.
|
|
723
|
+
|
|
724
|
+
## Layouts {#layouts}
|
|
725
|
+
|
|
726
|
+
A key event answers two questions and, the moment a non-Latin layout is
|
|
727
|
+
active, it has to answer them differently:
|
|
728
|
+
|
|
729
|
+
- **what the key typed** — `ev.key` and `ev.codepoint`, the active layout,
|
|
730
|
+
the active level. A Cyrillic layout types Cyrillic.
|
|
731
|
+
- **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.
|
|
735
|
+
|
|
736
|
+
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.
|
|
764
|
+
|
|
765
|
+
```js
|
|
766
|
+
const root = await createRoot({
|
|
767
|
+
// 'latin' is the default; 'layout' reports the keysym the layout put on
|
|
768
|
+
// the key, for an app matching shortcuts its own way
|
|
769
|
+
accelerators: 'layout',
|
|
770
|
+
});
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
`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.
|
|
781
|
+
|
|
782
|
+
## Pointer capture
|
|
783
|
+
|
|
784
|
+
`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:
|
|
788
|
+
|
|
789
|
+
```jsx
|
|
790
|
+
onMouseDown: (ev) => {
|
|
791
|
+
ev.capturePointer();
|
|
792
|
+
setDragging(true);
|
|
793
|
+
},
|
|
794
|
+
onMouseMove: (ev) => { if (dragging) setValue(valueAt(ev)); },
|
|
795
|
+
onMouseUp: () => setDragging(false),
|
|
796
|
+
```
|
|
797
|
+
|
|
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.
|
|
802
|
+
|
|
803
|
+
## Focus
|
|
804
|
+
|
|
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.
|
|
830
|
+
|
|
831
|
+
### Tab order
|
|
832
|
+
|
|
833
|
+
`tabIndex` sets the sequential focus order, following the DOM's rules:
|
|
834
|
+
|
|
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`.
|
|
842
|
+
|
|
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.
|
|
846
|
+
|
|
847
|
+
```jsx
|
|
848
|
+
<box tabIndex={1} /> {/* visited first */}
|
|
849
|
+
<box tabIndex={2} /> {/* then this */}
|
|
850
|
+
<box focusable /> {/* then tree order */}
|
|
851
|
+
<box tabIndex={-1} /> {/* clickable, never tabbed to */}
|
|
852
|
+
```
|
|
853
|
+
|
|
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).
|
|
860
|
+
|
|
861
|
+
### Focus scopes (modals)
|
|
862
|
+
|
|
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.
|
|
873
|
+
|
|
874
|
+
That is a modal dialog, with the popup taking focus at mount:
|
|
875
|
+
|
|
876
|
+
```jsx
|
|
877
|
+
function Dialog({ open, x, y, onClose }) {
|
|
878
|
+
if (!open) return null;
|
|
879
|
+
return (
|
|
880
|
+
<popup
|
|
881
|
+
trapFocus
|
|
882
|
+
grab
|
|
883
|
+
x={x}
|
|
884
|
+
y={y}
|
|
885
|
+
width={280}
|
|
886
|
+
height={120}
|
|
887
|
+
onDismiss={onClose}
|
|
888
|
+
>
|
|
889
|
+
<textinput autoFocus />
|
|
890
|
+
<Button label="OK" onPress={onClose} />
|
|
891
|
+
</popup>
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
```
|
|
895
|
+
|
|
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.
|
|
899
|
+
|
|
900
|
+
### Focus inside a `<popup>`
|
|
901
|
+
|
|
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`
|
|
917
|
+
([elements.md](elements.md#a-popover-under-a-tray-item)).
|
|
918
|
+
|
|
919
|
+
### Window focus
|
|
920
|
+
|
|
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.
|
|
952
|
+
|
|
953
|
+
### Focus and visibility
|
|
954
|
+
|
|
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.
|
|
977
|
+
|
|
978
|
+
## Cursors
|
|
979
|
+
|
|
980
|
+
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.
|
|
990
|
+
|
|
991
|
+
## Drag and drop {#drag-and-drop}
|
|
992
|
+
|
|
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>`:
|
|
996
|
+
|
|
997
|
+
```jsx
|
|
998
|
+
<box
|
|
999
|
+
dropAccept={['files']}
|
|
1000
|
+
onDrop={(e) => setPaths(e.files.map((f) => f.path))}
|
|
1001
|
+
style={{ ':drag-over': { borderColor: '$accent' } }}
|
|
1002
|
+
/>
|
|
1003
|
+
```
|
|
1004
|
+
|
|
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.
|