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