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