react-x11 2.52.0 → 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 +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 +1091 -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/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/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/components.md
CHANGED
|
@@ -11,31 +11,25 @@ wins by position rather than by clobbering a computed value:
|
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Props that a component consumes itself are never style — `ProgressBar`'s
|
|
14
|
-
`color`, `Dialog`'s `width`/`height` (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
[styling.md](styling.md).
|
|
14
|
+
`color`, `Dialog`'s `width`/`height` (its window's size), `ContextMenu`'s
|
|
15
|
+
`fontSize` (it measures labels with it). Everything else is forwarded to the
|
|
16
|
+
host box. See [styling.md](styling.md).
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
thing a widget can need from you — [accessibility.md](accessibility.md)
|
|
25
|
-
lists what each one announces.
|
|
18
|
+
The widgets are plain React over the host elements, exported from the package
|
|
19
|
+
root. All are screen-reader ready: roles, names, states and AT-driven control
|
|
20
|
+
are built in, and an `aria-label` (or a label child) is the only thing a
|
|
21
|
+
widget can need from you — [accessibility.md](accessibility.md) lists what
|
|
22
|
+
each one announces.
|
|
26
23
|
|
|
27
24
|
## Theming
|
|
28
25
|
|
|
29
26
|
**Widgets follow the desktop with nothing declared** — dark on a dark desktop,
|
|
30
|
-
light on a light one
|
|
31
|
-
|
|
32
|
-
or override it.
|
|
27
|
+
light on a light one. See [system appearance](appearance.md) for how that is
|
|
28
|
+
worked out and how to pin or override it.
|
|
33
29
|
|
|
34
30
|
`<ThemeProvider value={palette}>` gives every widget beneath it a palette,
|
|
35
|
-
layered over the scheme in force
|
|
36
|
-
changes
|
|
37
|
-
colour** — corner radius, border weight, text size and the padding inside a
|
|
38
|
-
control are most of what separates one platform's controls from another's:
|
|
31
|
+
layered over the scheme in force, so a partial one names only what your app
|
|
32
|
+
changes. It carries **shape as well as colour**:
|
|
39
33
|
|
|
40
34
|
| token | |
|
|
41
35
|
| --------------------------------------------------------------- | ------------------------------------ |
|
|
@@ -55,37 +49,24 @@ control are most of what separates one platform's controls from another's:
|
|
|
55
49
|
|
|
56
50
|
### The ground and what is on it
|
|
57
51
|
|
|
58
|
-
`background` is
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
A palette that has one ground says so by naming one: **`surface` follows
|
|
66
|
-
`background` unless you name it**, so a theme that only recolours the app
|
|
67
|
-
keeps working and only a design that really raises its cards has a second
|
|
68
|
-
token to fill in.
|
|
52
|
+
`background` is the **window**, the fill under everything. `surface` is what
|
|
53
|
+
is raised off it: a control's fill, a card, the sheet of a menu or dialog.
|
|
54
|
+
The light palette makes them the same colour; the dark one lifts `surface` a
|
|
55
|
+
step. **`surface` follows `background` unless you name it**, so only a design
|
|
56
|
+
that really raises its cards has a second token to fill in.
|
|
57
|
+
`surfaceHover` and `surfaceActive` are that surface's interaction steps.
|
|
69
58
|
|
|
70
59
|
```jsx
|
|
71
60
|
<ThemeProvider value={{ background: '#f6f8fa', surface: '#ffffff' }}>
|
|
72
61
|
```
|
|
73
62
|
|
|
74
|
-
`surfaceHover` and `surfaceActive` are that surface's own interaction steps,
|
|
75
|
-
which is what they have always been — before there was a `surface` to be the
|
|
76
|
-
steps of.
|
|
77
|
-
|
|
78
63
|
### What the app says with colour
|
|
79
64
|
|
|
80
|
-
`danger`, `success`, `warning` and `info`
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Each works as **ink as well as fill**: every one clears 4.5:1 against its own
|
|
87
|
-
palette's ground, so the message under a field and the badge beside it are one
|
|
88
|
-
token.
|
|
65
|
+
`danger`, `success`, `warning` and `info` — failed, worked, look at this, a
|
|
66
|
+
note — colour alerts, badges, validation messages and toasts, and keep a
|
|
67
|
+
hard-coded `#e74c3c` (a bruise on a dark desktop) out of app code. Each
|
|
68
|
+
clears 4.5:1 against its palette's ground, so it works as **ink as well as
|
|
69
|
+
fill**:
|
|
89
70
|
|
|
90
71
|
```jsx
|
|
91
72
|
<text style={{ color: '$danger' }}>Password too short</text>
|
|
@@ -94,8 +75,7 @@ token.
|
|
|
94
75
|
</box>
|
|
95
76
|
```
|
|
96
77
|
|
|
97
|
-
For
|
|
98
|
-
than the colour — tint the fill and leave the ink alone:
|
|
78
|
+
For a tinted panel, tint the fill and leave the ink alone:
|
|
99
79
|
|
|
100
80
|
```jsx
|
|
101
81
|
import { tint } from 'react-x11/style';
|
|
@@ -104,59 +84,39 @@ import { tint } from 'react-x11/style';
|
|
|
104
84
|
</box>;
|
|
105
85
|
```
|
|
106
86
|
|
|
107
|
-
Only `danger` has `…Hover` and `…Active
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
or `fontFamily` in a style still wins, the way a style property always wins
|
|
124
|
-
over what a node inherits.
|
|
125
|
-
|
|
126
|
-
The widgets that take a size of their own keep taking it: `MenuBar` and
|
|
127
|
-
`ContextMenu` have a `fontSize` prop because they measure their labels before
|
|
128
|
-
they have anywhere to measure them in, and `Table` has `rowHeight` because
|
|
129
|
-
every row has to be the same height for it to skip the ones off screen.
|
|
130
|
-
|
|
131
|
-
Both are CSS-style family lists — `'"JetBrains Mono", monospace'` names a
|
|
132
|
-
preference and a fallback.
|
|
133
|
-
|
|
134
|
-
`monoFamily` is the second face, and nothing unstyled reads it: it is there so
|
|
135
|
-
that every code surface in an app — a listing, a log pane, a hex dump, all
|
|
136
|
-
written by different components — can say `fontFamily: '$monoFamily'` and be
|
|
137
|
-
set from one place.
|
|
87
|
+
Only `danger` has `…Hover` and `…Active` — the only status fill anyone
|
|
88
|
+
presses. The `…Text` four (and `accentText`) are derived from their fill as
|
|
89
|
+
the more legible of the palette's `text` and `background`, so a palette
|
|
90
|
+
almost never sets them: a yellow `warning` gets dark letters unasked.
|
|
91
|
+
|
|
92
|
+
`fontFamily` and `fontSize` are the app's type, and text that names neither
|
|
93
|
+
**takes them** — a `<text>`, a `<textinput>`, a widget's label — so
|
|
94
|
+
`<ThemeProvider value={{ fontFamily: 'Inter', fontSize: 16 }}>` sets it once,
|
|
95
|
+
and a `Select`'s rows grow with it. A style's `fontSize`/`fontFamily` still
|
|
96
|
+
wins. Both are CSS-style family lists (`'"JetBrains Mono", monospace'`).
|
|
97
|
+
`MenuBar` and `ContextMenu` keep a `fontSize` prop (they measure labels
|
|
98
|
+
before they have a window to measure in), and `Table` keeps `rowHeight`
|
|
99
|
+
(every row must be the same height to skip the ones off screen).
|
|
100
|
+
|
|
101
|
+
`monoFamily` is read by nothing unstyled: it is there so every code surface
|
|
102
|
+
in an app can say `fontFamily: '$monoFamily'` and be set from one place.
|
|
138
103
|
|
|
139
104
|
### Which way it reads
|
|
140
105
|
|
|
141
|
-
`direction`
|
|
142
|
-
|
|
143
|
-
|
|
106
|
+
`direction` (`'ltr'` or `'rtl'`) is **seeded from the locale**, so an app
|
|
107
|
+
started under an RTL locale is mirrored unconfigured. Set it here to mirror a
|
|
108
|
+
whole UI:
|
|
144
109
|
|
|
145
110
|
```jsx
|
|
146
111
|
<ThemeProvider value={{ direction: settings.rtl ? 'rtl' : 'ltr' }}>
|
|
147
112
|
```
|
|
148
113
|
|
|
149
|
-
The provider plants the
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
layout mirrors and those do not.
|
|
156
|
-
|
|
157
|
-
Most of the set needs no help — `Button`, `Checkbox`, `Radio`, `Switch` and
|
|
158
|
-
`ProgressBar` are rows and flex ratios, and come out mirrored on their own.
|
|
159
|
-
The rest each have one thing yoga could not answer:
|
|
114
|
+
The provider plants the `direction` style property in the node tree, so
|
|
115
|
+
boxes **and** widgets mirror together. A bare
|
|
116
|
+
`<box style={{ direction: 'rtl' }}>` mirrors only the layout, not the widget
|
|
117
|
+
decisions read through `useDirection()` — which way an arrow key steps, a
|
|
118
|
+
chevron points or a submenu opens. `Button`, `Checkbox`, `Radio`, `Switch` and
|
|
119
|
+
`ProgressBar` need nothing more; the rest mirror one thing each:
|
|
160
120
|
|
|
161
121
|
| widget | what mirrors |
|
|
162
122
|
| ------------------------- | --------------------------------------------------- |
|
|
@@ -164,67 +124,44 @@ The rest each have one thing yoga could not answer:
|
|
|
164
124
|
| `Tabs` | Left/Right walk the strip the way it is drawn |
|
|
165
125
|
| `Table` | column resizing, the sort mark, the header's scroll |
|
|
166
126
|
| `SplitPane` | which pane is first, the drag and its arrows |
|
|
167
|
-
| `MenuBar` / `ContextMenu` | submenus open
|
|
127
|
+
| `MenuBar` / `ContextMenu` | submenus open leftwards, and their arrows |
|
|
168
128
|
| `Select`, `PasswordInput` | the field's own insets follow the text |
|
|
169
129
|
|
|
170
|
-
`<textinput>` and `<textarea>` mirror
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
screen.
|
|
130
|
+
`<textinput>` and `<textarea>` mirror inside too: value, placeholder and caret
|
|
131
|
+
sit against the start edge, and the value is shaped at the box's base level
|
|
132
|
+
([styling.md](styling.md#inside-an-editable-field)); Left/Right and Home/End
|
|
133
|
+
still step through the string, not across the screen. The style property and
|
|
134
|
+
logical edges: [styling.md](styling.md#direction-and-the-logical-edges).
|
|
176
135
|
|
|
177
|
-
|
|
178
|
-
property and the logical edges.
|
|
136
|
+
### Size and shape
|
|
179
137
|
|
|
180
|
-
`paddingY` is the space **you can see
|
|
181
|
-
baseline
|
|
182
|
-
widget label is trimmed to its letters
|
|
138
|
+
`paddingY` is the space **you can see**, above the capitals and below the
|
|
139
|
+
baseline: every widget label is trimmed to its letters
|
|
183
140
|
([styling.md](styling.md#measuring-text-to-its-letters)), so a control is its
|
|
184
|
-
label band plus
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
`
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
The
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
`radiusPopupItem` is a **ceiling**, not the radius a row gets. A rounded rect
|
|
210
|
-
inside a rounded rect only reads as one shape when the two curves share a
|
|
211
|
-
centre, which happens exactly when the inner radius is the outer one less the
|
|
212
|
-
gap between them — so a row's corner is `radiusPopup` minus the sheet's
|
|
213
|
-
hairline border minus the inset the row is padded in by, and the token caps
|
|
214
|
-
it. A theme that wants rounder highlights rounds the sheet they sit on: that
|
|
215
|
-
is the only change that can round both and keep them concentric.
|
|
216
|
-
|
|
217
|
-
They are only ever _seen_ where the display composites — a popup gives up
|
|
218
|
-
its corners by not painting them, and with nothing blending them those
|
|
219
|
-
pixels are black rather than the desktop, so the widgets ask the window
|
|
220
|
-
(`'@supports transparency'`, [styling.md](styling.md)) and stay square
|
|
221
|
-
where the answer is no.
|
|
222
|
-
|
|
223
|
-
There are two consumers of a palette, and one provider feeds both. Widgets
|
|
224
|
-
read it as React context through `useTheme()`; a `$token` in a style
|
|
225
|
-
resolves against the nearest `theme` **prop** in the node tree, which knows
|
|
226
|
-
nothing about React ([styling.md](styling.md#theme-tokens)). `ThemeProvider`
|
|
227
|
-
puts the merged palette on the context _and_ plants it in the tree, so
|
|
141
|
+
label band plus `paddingY` twice — a larger number than CSS would need, and
|
|
142
|
+
the same in every typeface. Menu rows, `Select` options and `<textinput>`
|
|
143
|
+
follow the rule, so a field, a `Button` and a `Select` on one row match in
|
|
144
|
+
height.
|
|
145
|
+
|
|
146
|
+
The `…Active` four are the colour **while held**, derived one step past the
|
|
147
|
+
hover (`accent` → `accentHover` → `accentActive`), so a palette rarely sets
|
|
148
|
+
them; one that is set wins. See [The press state](#the-press-state).
|
|
149
|
+
|
|
150
|
+
The popup radii follow the **text size**, not `radius`: `radiusPopup` is half
|
|
151
|
+
the body size (7px at 14px), and `radiusPopupItem` (a menu row's highlight)
|
|
152
|
+
and `radiusTooltip` (the bubble) step in from it. Naming one pins it.
|
|
153
|
+
`radiusPopupItem` is a **ceiling**: a row's corner is `radiusPopup` less the
|
|
154
|
+
sheet's border and the row's inset, so the curves stay concentric — round the
|
|
155
|
+
sheet to round the highlights. Rounded corners only show where the display
|
|
156
|
+
composites (`'@supports transparency'`, [styling.md](styling.md)); elsewhere
|
|
157
|
+
the widgets stay square.
|
|
158
|
+
|
|
159
|
+
### `ThemeProvider` and `useTheme()`
|
|
160
|
+
|
|
161
|
+
Widgets read the palette as React context through `useTheme()`; a `$token` in
|
|
162
|
+
a style resolves against the nearest `theme` **prop** in the node tree
|
|
163
|
+
([styling.md](styling.md#theme-tokens)). `ThemeProvider` feeds both, so this
|
|
164
|
+
paints the palette, and an inner provider merges over an outer one:
|
|
228
165
|
|
|
229
166
|
```jsx
|
|
230
167
|
<ThemeProvider value={dark}>
|
|
@@ -232,11 +169,10 @@ puts the merged palette on the context _and_ plants it in the tree, so
|
|
|
232
169
|
</ThemeProvider>
|
|
233
170
|
```
|
|
234
171
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
`
|
|
239
|
-
above set — and is the same object token resolution sees:
|
|
172
|
+
`useTheme()` returns the complete palette in force — the object token
|
|
173
|
+
resolution sees. It indexes like a bag of tokens: `theme[name]` needs no cast;
|
|
174
|
+
named tokens keep their types, anything else is `unknown`. What a provider
|
|
175
|
+
_accepts_ stays closed (`Partial<Theme>`), so a palette typo is caught.
|
|
240
176
|
|
|
241
177
|
```jsx
|
|
242
178
|
function Panel() {
|
|
@@ -245,20 +181,10 @@ function Panel() {
|
|
|
245
181
|
}
|
|
246
182
|
```
|
|
247
183
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
What a provider _accepts_ stays closed (`Partial<Theme>`), because a typo in a
|
|
253
|
-
palette has nowhere else to be caught.
|
|
254
|
-
|
|
255
|
-
Where the palette reaches the tree depends on where the provider is written.
|
|
256
|
-
|
|
257
|
-
**Above the windows**, at the root of the tree, it draws nothing, and every
|
|
258
|
-
window under it takes the palette — its background and the type every
|
|
259
|
-
unstyled `<text>` falls back to as well as the `$token`s inside. Whatever
|
|
260
|
-
shape the windows arrive in: a component that renders one, a window that is
|
|
261
|
-
closed for now, several at once, a provider nested in another.
|
|
184
|
+
**Above the windows**, at the root of the tree, a provider draws nothing and
|
|
185
|
+
every window under it takes the palette — background, the fallback type of
|
|
186
|
+
unstyled `<text>`, the `$token`s — however the windows arrive (a component
|
|
187
|
+
that renders one, a closed window, several, a nested provider):
|
|
262
188
|
|
|
263
189
|
```jsx
|
|
264
190
|
root.render(
|
|
@@ -268,26 +194,20 @@ root.render(
|
|
|
268
194
|
);
|
|
269
195
|
```
|
|
270
196
|
|
|
271
|
-
**Inside a window** it is a `<box
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
error says to move the provider.
|
|
278
|
-
|
|
279
|
-
Widgets plant the merged palette on their own root node too, so a `$token`
|
|
280
|
-
in a style you pass one resolves even with no provider anywhere.
|
|
197
|
+
**Inside a window** it is a `<box>` styled `{ flexGrow: 1 }`; pass `style` to
|
|
198
|
+
change that (`{ flexGrow: 0 }` around a single control). A nested `<window>`
|
|
199
|
+
under a provider written directly in a window takes the palette with it; a
|
|
200
|
+
provider deeper inside a `<box>` has no window to hand it to, and the error
|
|
201
|
+
says to move it. Widgets plant the palette on their own root node too, so a
|
|
202
|
+
`$token` in a style you pass one resolves with no provider at all.
|
|
281
203
|
|
|
282
|
-
`examples/themes.js` has
|
|
283
|
-
|
|
284
|
-
them at runtime.
|
|
204
|
+
`examples/themes.js` has GitHub, macOS and Windows palettes in light and dark;
|
|
205
|
+
`npm run examples:theming` switches between them.
|
|
285
206
|
|
|
286
207
|
## System icons
|
|
287
208
|
|
|
288
|
-
The glyphs the widgets
|
|
289
|
-
|
|
290
|
-
you did not should not disagree about which way a chevron points.
|
|
209
|
+
The glyphs the widgets are drawn with, exported so your toolbar and a
|
|
210
|
+
`Select` agree about which way a chevron points.
|
|
291
211
|
|
|
292
212
|
```jsx
|
|
293
213
|
import { Icon } from 'react-x11';
|
|
@@ -312,45 +232,28 @@ import { Icon } from 'react-x11';
|
|
|
312
232
|
Everything else goes to the host `<canvas>`, so a clickable one is
|
|
313
233
|
`<Icon name="close" onClick={…} focusable />`.
|
|
314
234
|
|
|
315
|
-
**`size` is the mark
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
`capBand(fontSize)` because it should be as wide as the capitals beside it.
|
|
321
|
-
|
|
322
|
-
A chevron is the one glyph whose two axes differ: the arms are at 45°, so it
|
|
323
|
-
is as long as `size` along the way it points across and half that the other
|
|
324
|
-
way. `<Icon name="chevronDown" size={10} />` is 10 wide and 5 tall; the same
|
|
325
|
-
size of `chevronRight` is 5 wide and 10 tall.
|
|
235
|
+
**`size` is the mark**, ink corner to corner — not a 24px grid with 14px of
|
|
236
|
+
ink in it as in lucide-style sets — so pick the size you want to see. A
|
|
237
|
+
chevron is the one glyph whose axes differ (45° arms):
|
|
238
|
+
`<Icon name="chevronDown" size={10} />` is 10 wide and 5 tall, and
|
|
239
|
+
`chevronRight` at the same size is 5 wide and 10 tall.
|
|
326
240
|
|
|
327
241
|
### Affordances, not nouns
|
|
328
242
|
|
|
329
|
-
The set holds glyphs
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
than an action the control affords, it is not going to be here. Bringing
|
|
337
|
-
your own set is expected and supported — a `<svg>` or a `<canvas>` goes
|
|
338
|
-
anywhere an `<Icon>` does, and `ContextMenu`'s `icon` takes either. The
|
|
339
|
-
platform's own set is there too:
|
|
340
|
-
[`<image src={{ symbol }}>`](elements.md#src-symbol---the-platforms-own-icons)
|
|
341
|
-
draws an SF Symbol on macOS and an icon from the desktop's icon theme
|
|
342
|
-
elsewhere, in the text colour.
|
|
343
|
-
|
|
344
|
-
The **drawings are not themable** for the same reason: the geometry is the
|
|
345
|
-
widget set's vocabulary, and an application that wants a different chevron
|
|
346
|
-
wants an icon library. Colour and size are yours; the shape is not.
|
|
243
|
+
The set holds glyphs about **the control** — more here, chosen, close,
|
|
244
|
+
hidden — and no nouns (folder, document, printer). Bring your own icon set:
|
|
245
|
+
a `<svg>` or `<canvas>` goes anywhere an `<Icon>` does, and `ContextMenu`'s
|
|
246
|
+
`icon` takes either. The platform's own set is
|
|
247
|
+
[`<image src={{ symbol }}>`](elements.md#src-symbol---the-platforms-own-icons):
|
|
248
|
+
an SF Symbol on macOS, the desktop's icon theme elsewhere, in the text colour.
|
|
249
|
+
The drawings are not themable: colour and size are yours, the shape is not.
|
|
347
250
|
|
|
348
251
|
### Colour inherits; size does not
|
|
349
252
|
|
|
350
|
-
`color`
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
253
|
+
`color` inherits ([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)),
|
|
254
|
+
so an icon takes the ink it is written inside, hover included; the `color`
|
|
255
|
+
prop is for ink the surrounding text does not have (a destructive mark, a
|
|
256
|
+
check on an accent fill):
|
|
354
257
|
|
|
355
258
|
```jsx
|
|
356
259
|
<box style={{ color: theme.textMuted, ':hover': { color: theme.accent } }}>
|
|
@@ -359,39 +262,18 @@ at the call site:
|
|
|
359
262
|
</box>
|
|
360
263
|
```
|
|
361
264
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
`
|
|
365
|
-
not: a destructive action's mark, or a check drawn on an accent fill.
|
|
366
|
-
|
|
367
|
-
**`size` does not inherit, and is deliberately not `fontSize`.** A glyph is
|
|
368
|
-
a drawing rather than a letter — no baseline to sit on, no ascent to be
|
|
369
|
-
measured against — so it takes its default from the palette's `fontSize` and
|
|
370
|
-
stays put when a label beside it shrinks. Pass `size` for the one icon that
|
|
371
|
-
has to be bigger.
|
|
265
|
+
**`size` does not inherit, and is not `fontSize`**: it defaults from the
|
|
266
|
+
palette's `fontSize` and stays put when a label beside it shrinks. Pass
|
|
267
|
+
`size` for the one icon that must be bigger.
|
|
372
268
|
|
|
373
269
|
### What it costs to draw one
|
|
374
270
|
|
|
375
|
-
Each glyph is a drawing over `<canvas mono>`
|
|
376
|
-
([elements.md](elements.md#canvas)),
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
The entry is **coverage**, so the colour is applied at composite time and
|
|
382
|
-
stays out of the key: one rendered copy of `chevronDown` at 12px serves the
|
|
383
|
-
resting row, the highlighted row, the disabled one and both colour schemes —
|
|
384
|
-
the trick the glyph cache runs on text, and the one `<svg>` gets for a
|
|
385
|
-
`fill="currentColor"` document. The size cannot leave the key, since a
|
|
386
|
-
coverage surface is pixels at a fixed size, so one icon at two sizes is two
|
|
387
|
-
entries.
|
|
388
|
-
|
|
389
|
-
The drawings are module-level, so re-rendering a list invalidates none of
|
|
390
|
-
its icons: `<canvas>` compares `onDraw` by identity, and a fresh closure per
|
|
391
|
-
render is a repaint per glyph.
|
|
392
|
-
|
|
393
|
-
`icons` is the map of raw drawings, for a widget that wants the glyph
|
|
394
|
-
without the component:
|
|
271
|
+
Each glyph is a module-level drawing over `<canvas mono>`
|
|
272
|
+
([elements.md](elements.md#canvas)), cached as coverage and coloured at
|
|
273
|
+
composite time, so one copy per glyph and size serves every colour and state.
|
|
274
|
+
`icons` is the map of raw drawings, for a widget that wants the glyph without
|
|
275
|
+
the component; keep `onDraw` identity-stable as these are, or every render
|
|
276
|
+
repaints:
|
|
395
277
|
|
|
396
278
|
```jsx
|
|
397
279
|
import { icons } from 'react-x11';
|
|
@@ -404,28 +286,21 @@ import { icons } from 'react-x11';
|
|
|
404
286
|
/>;
|
|
405
287
|
```
|
|
406
288
|
|
|
407
|
-
|
|
408
|
-
|
|
289
|
+
Icons are **`aria-hidden` by default** — the meaning is on the control's
|
|
290
|
+
`role` and `aria-expanded`. Name one
|
|
409
291
|
(`aria-hidden={false} aria-label="Close"`) only when the icon _is_ the
|
|
410
|
-
control
|
|
292
|
+
control.
|
|
411
293
|
|
|
412
294
|
## Basic controls
|
|
413
295
|
|
|
414
|
-
`Button`, `Checkbox`, `Radio`/`RadioGroup`, `Switch` and `ProgressBar`
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
and
|
|
421
|
-
|
|
422
|
-
— so moving the pointer over a control repaints one node instead of
|
|
423
|
-
rendering.
|
|
424
|
-
|
|
425
|
-
`Button`, `Checkbox`, `Switch` and `ProgressBar` take `style`, merged after
|
|
426
|
-
their own so an override wins by position, and forward any remaining props
|
|
427
|
-
to the host box. `Radio` is the exception: it takes only the props listed
|
|
428
|
-
below, and the group around it carries the layout.
|
|
296
|
+
`Button`, `Checkbox`, `Radio`/`RadioGroup`, `Switch` and `ProgressBar` are
|
|
297
|
+
focusable, activate on click — and so on Space and Enter, unless Ctrl, Alt or
|
|
298
|
+
Super make it a shortcut ([events.md](events.md#space-and-enter-are-a-click))
|
|
299
|
+
— set the pointer cursor, and draw hover, press and focus as
|
|
300
|
+
`:hover`/`:active`/`:focus` style blocks, so hovering repaints one node
|
|
301
|
+
rather than rendering. All but `Radio` take `style` (merged after their own)
|
|
302
|
+
and forward other props to the host box; `Radio` takes only its listed props,
|
|
303
|
+
and its group carries the layout.
|
|
429
304
|
|
|
430
305
|
```jsx
|
|
431
306
|
import { Button, Checkbox, RadioGroup, Radio, Switch, ProgressBar } from 'react-x11';
|
|
@@ -444,93 +319,59 @@ import { Button, Checkbox, RadioGroup, Radio, Switch, ProgressBar } from 'react-
|
|
|
444
319
|
```
|
|
445
320
|
|
|
446
321
|
Label text is the children (or a `label` prop); a bare string is wrapped in
|
|
447
|
-
a `<text>` for you
|
|
322
|
+
a `<text>` for you. All five are demoed in `examples/widgets.jsx`.
|
|
448
323
|
|
|
449
324
|
### Native bezels
|
|
450
325
|
|
|
451
|
-
On
|
|
452
|
-
|
|
453
|
-
`
|
|
454
|
-
focus, keyboard and accessibility stay this
|
|
455
|
-
|
|
456
|
-
an embedded `NSView`: the press model and the event routing are behaviour
|
|
457
|
-
this project considers part of its identity, and a real `NSControl` would
|
|
458
|
-
take them over wholesale. [macos.md](macos.md#native-controls) is the
|
|
459
|
-
design record.
|
|
460
|
-
|
|
461
|
-
**Default native, seam out**, at three levels:
|
|
326
|
+
On the **Cocoa** backend (never X11) `Button`, `Checkbox`, `Radio`, `Switch`,
|
|
327
|
+
`Slider` and `Select` wear AppKit's bezels by default; on **win32** all but
|
|
328
|
+
`Switch` wear the system's own while the desktop is light, and are drawn in
|
|
329
|
+
dark mode. Interaction, focus, keyboard and accessibility stay this
|
|
330
|
+
library's. Opt out at three levels:
|
|
462
331
|
|
|
463
332
|
```jsx
|
|
333
|
+
// the app's identity
|
|
464
334
|
<ThemeProvider value={{ controls: 'drawn' }}>
|
|
465
|
-
{
|
|
466
|
-
{
|
|
467
|
-
<Button native={false}>Brand</Button> {/* one control */}
|
|
335
|
+
{/* one control */}
|
|
336
|
+
<Button native={false}>Brand</Button>
|
|
468
337
|
</ThemeProvider>;
|
|
469
338
|
|
|
470
339
|
const canGoNative = useSupports('nativeControls'); // what the backend has
|
|
471
340
|
```
|
|
472
341
|
|
|
473
|
-
`controls`
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
`
|
|
487
|
-
|
|
488
|
-
one size. The slack around it is not part of the control: a press up there
|
|
489
|
-
does nothing, as it does in AppKit. Drawn controls stretch as they always
|
|
490
|
-
have — their chrome is drawn to the box and their label is centred in it,
|
|
491
|
-
so a stretched drawn control is merely roomy.
|
|
492
|
-
|
|
493
|
-
The one thing that makes a native button taller is its own label. Squeezed
|
|
494
|
-
narrower than its label, a `<Button>` wraps it at its words — native or
|
|
495
|
-
drawn — and grows by the lines it gained, which on macOS is drawn with
|
|
496
|
-
AppKit's flexible-height push bezel, the one it has for a title that wraps.
|
|
497
|
-
A row that should never wrap its buttons wants `flexWrap: 'wrap'` on the
|
|
498
|
-
row instead, so the buttons move to a second row rather than their labels
|
|
499
|
-
to a second line.
|
|
342
|
+
`controls` (a theme token, since looking native is an app-wide decision) is
|
|
343
|
+
`'auto'` (bezels where the backend has them), `'native'` (ask, and hear about
|
|
344
|
+
it in development where there are none) or `'drawn'`. Give `native={false}`
|
|
345
|
+
to a control that names its own colours in `style` — a system bezel ignores
|
|
346
|
+
them.
|
|
347
|
+
|
|
348
|
+
Under a native bezel, `style` sizes the slot the control _sits in_: a
|
|
349
|
+
`<Button>` or `<Select>` keeps AppKit's height, centred, so
|
|
350
|
+
`height: 44`, `height: '100%'`, `flexGrow: 1` or a taller neighbour in a
|
|
351
|
+
stretching row leave a 22pt control in a 44pt slot, and a press in the slack
|
|
352
|
+
does nothing. Drawn controls stretch to the box. A `<Button>` squeezed
|
|
353
|
+
narrower than its label wraps it at its words and grows by the lines —
|
|
354
|
+
native or drawn (on macOS with AppKit's flexible-height bezel); give the row
|
|
355
|
+
`flexWrap: 'wrap'` to move buttons to a second row instead. Design notes:
|
|
356
|
+
[macos.md](macos.md#native-controls).
|
|
500
357
|
|
|
501
358
|
### The press state
|
|
502
359
|
|
|
503
|
-
Every control
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
does nothing** — it acknowledges the input, it does not promise the outcome.
|
|
512
|
-
A press dragged off the control drops it, and picking the control back up
|
|
513
|
-
restores it, so the way it looks always agrees with whether releasing there
|
|
514
|
-
would activate anything.
|
|
515
|
-
|
|
516
|
-
Nothing is needed to get this: it is what the widgets do. Writing a control
|
|
517
|
-
of your own, `:active` is the state block for it —
|
|
518
|
-
[styling.md](styling.md#inline-pseudo-states) — and the palette's
|
|
519
|
-
`accentActive`/`surfaceActive`/`textMutedActive`/`dangerActive` are the
|
|
520
|
-
colours.
|
|
360
|
+
Every control activates on the **release**, and looks distinct while
|
|
361
|
+
**held** — on the press, though the press does nothing yet — so a slow click
|
|
362
|
+
is visibly heard. Four states: resting, hovered, held, hovered again on
|
|
363
|
+
release. Dragging off the control drops the held look and coming back
|
|
364
|
+
restores it, so the look always says whether releasing there would activate.
|
|
365
|
+
For a control of your own, `:active`
|
|
366
|
+
([styling.md](styling.md#inline-pseudo-states)) is the state block and
|
|
367
|
+
`accentActive`/`surfaceActive`/`textMutedActive`/`dangerActive` the colours.
|
|
521
368
|
|
|
522
369
|
### The change event, and `name`
|
|
523
370
|
|
|
524
371
|
Every value control — `Checkbox`, `Switch`, `RadioGroup`, `Select`,
|
|
525
|
-
`Slider` — calls `onChange(ev)` with
|
|
372
|
+
`Slider` — calls `onChange(ev)` with the **same change event**
|
|
526
373
|
`<textinput>` and `<textarea>` use
|
|
527
|
-
([elements.md](elements.md#the-change-event))
|
|
528
|
-
|
|
529
|
-
```jsx
|
|
530
|
-
<Checkbox checked={agreed} onChange={(ev) => setAgreed(ev.value)}>
|
|
531
|
-
I agree
|
|
532
|
-
</Checkbox>
|
|
533
|
-
```
|
|
374
|
+
([elements.md](elements.md#the-change-event)); the new value is `ev.value`:
|
|
534
375
|
|
|
535
376
|
```js
|
|
536
377
|
{ type: 'change',
|
|
@@ -539,29 +380,22 @@ Every value control — `Checkbox`, `Switch`, `RadioGroup`, `Select`,
|
|
|
539
380
|
name: 'agree', value: true }
|
|
540
381
|
```
|
|
541
382
|
|
|
542
|
-
|
|
543
|
-
passed straight to any control in the library, with no per-widget adapter:
|
|
383
|
+
So a form library's handler goes straight onto any control:
|
|
544
384
|
|
|
545
385
|
```jsx
|
|
546
386
|
<textinput name="host" value={f.values.host} onChange={f.handleChange} />
|
|
547
387
|
<Checkbox name="agree" checked={f.values.agree} onChange={f.handleChange} />
|
|
548
388
|
```
|
|
549
389
|
|
|
550
|
-
`target` is a plain descriptor
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
destructure — `target.type` is how they tell a checkbox from a text field,
|
|
555
|
-
which is why it is set even though nothing in react-x11 reads it. There is no
|
|
556
|
-
`preventDefault`: the value has already changed by the time the handler runs.
|
|
557
|
-
|
|
558
|
-
**The line is `name`.** A widget that takes one is a form field and reports
|
|
559
|
-
an event. `Tabs`, `Table` and the menus are not form fields — you would never
|
|
560
|
-
register a tab strip with formik — and keep their plain callbacks (`Tabs`
|
|
561
|
-
calls `onChange(id)`).
|
|
390
|
+
`target` is a plain descriptor, not a node (a widget has no single node
|
|
391
|
+
holding its value), shaped as formik's `handleChange` and react-hook-form
|
|
392
|
+
read it — `target.type` tells a checkbox from a text field. There is no
|
|
393
|
+
`preventDefault`: the value has already changed.
|
|
562
394
|
|
|
563
|
-
`name
|
|
564
|
-
|
|
395
|
+
**The line is `name`**: a widget that takes one is a form field and reports
|
|
396
|
+
an event. `Tabs`, `Table` and the menus are not, and keep plain callbacks
|
|
397
|
+
(`Tabs` calls `onChange(id)`). `name` is otherwise inert. See
|
|
398
|
+
[docs/ecosystem/forms.md](ecosystem/forms.md).
|
|
565
399
|
|
|
566
400
|
### `Button`
|
|
567
401
|
|
|
@@ -574,31 +408,18 @@ one. See [docs/ecosystem/forms.md](ecosystem/forms.md).
|
|
|
574
408
|
| `primary` | speaks in the accent: the fill when solid, the ink and border otherwise |
|
|
575
409
|
| `disabled` | inert, dimmed, not focusable |
|
|
576
410
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
moves both sizes together, which a hand-made `style={{ height: 22 }}` would
|
|
590
|
-
not.
|
|
591
|
-
|
|
592
|
-
A label with less room than it needs wraps at its words, the lines centred
|
|
593
|
-
under one another, and the button grows to hold them — a row too narrow
|
|
594
|
-
for its buttons squeezes each to its longest word first, as it would any
|
|
595
|
-
paragraph ([Native bezels](#native-bezels) has what that does to a native
|
|
596
|
-
button).
|
|
597
|
-
|
|
598
|
-
An **element** child inherits the resolved label colour — the ink is set on
|
|
599
|
-
the button's box and `color` inherits ([styling.md](styling.md)) — so an
|
|
600
|
-
icon+label button is correct by construction in every state, disabled
|
|
601
|
-
included, with nothing passed to the icon:
|
|
411
|
+
`variant` and `primary` compose: `solid` is a fill, `outline` a border,
|
|
412
|
+
`ghost` neither (for a `✕` on a chip, or a delete inside a form row); a
|
|
413
|
+
dialog footer is `outline` beside `primary`, a toolbar's loudest icon
|
|
414
|
+
`primary variant="ghost"`. Every variant keeps the widget states and the
|
|
415
|
+
border _width_, so a mixed row lines up. `size="small"` halves the control
|
|
416
|
+
padding and follows the palette's `paddingY`, which a hand-set `height`
|
|
417
|
+
would not.
|
|
418
|
+
|
|
419
|
+
A label with too little room wraps at its words, centred, and the button
|
|
420
|
+
grows — a too-narrow row squeezes each button to its longest word first. An
|
|
421
|
+
**element** child inherits the label's colour in every state, disabled
|
|
422
|
+
included:
|
|
602
423
|
|
|
603
424
|
```jsx
|
|
604
425
|
<Button primary disabled={busy} onPress={run}>
|
|
@@ -623,23 +444,18 @@ included, with nothing passed to the icon:
|
|
|
623
444
|
|
|
624
445
|
### `Radio` / `RadioGroup`
|
|
625
446
|
|
|
626
|
-
`RadioGroup` takes `value`, `onChange(ev)`, `name`, `style` and any
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
`
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
Arrow keys move the selection through the group in **mount order**,
|
|
633
|
-
wrapping — Up/Left back, Down/Right forward — which is how a native radio
|
|
634
|
-
group behaves; click or Space selects the focused one.
|
|
447
|
+
`RadioGroup` takes `value`, `onChange(ev)`, `name`, `style` and any box
|
|
448
|
+
props; each `Radio` takes only its `value`, `children`/`label` and
|
|
449
|
+
`disabled`, so per-radio styling goes on the group. A `Radio` outside a
|
|
450
|
+
`RadioGroup` throws. Arrow keys move the selection in **mount order**,
|
|
451
|
+
wrapping (Up/Left back, Down/Right forward); click or Space selects the
|
|
452
|
+
focused one.
|
|
635
453
|
|
|
636
454
|
### `Switch`
|
|
637
455
|
|
|
638
|
-
`checked`, `onChange(ev)`, `name` and `disabled`,
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
would flip between the ends with nothing in between to animate — the worked
|
|
642
|
-
example in [styling.md](styling.md#transitions).
|
|
456
|
+
`checked`, `onChange(ev)`, `name` and `disabled`, as `Checkbox`, in a
|
|
457
|
+
sliding pill (the thumb's `left` transition is the worked example in
|
|
458
|
+
[styling.md](styling.md#transitions)).
|
|
643
459
|
|
|
644
460
|
### `ProgressBar`
|
|
645
461
|
|
|
@@ -658,29 +474,17 @@ Determinate with a `value`, indeterminate without one.
|
|
|
658
474
|
<ProgressBar indeterminate style={{ width: 200 }} />
|
|
659
475
|
```
|
|
660
476
|
|
|
661
|
-
The
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
[style loop](styling.md#loops): the block is absolutely positioned and
|
|
668
|
-
travels on the logical `start` edge from `-40%` to `100%` of the track, so it
|
|
669
|
-
mirrors with the text direction, is right at any width without measuring
|
|
670
|
-
anything, and repaints the bar rather than the window. It reports
|
|
671
|
-
`aria-busy` **with no value at all**, which is what tells a screen reader
|
|
672
|
-
that a percentage is not coming.
|
|
673
|
-
|
|
674
|
-
Under [reduced motion](system.md#usedesktopsettings--how-the-desktop-wants-an-app-to-feel)
|
|
675
|
-
the loop never starts — that part is core's, and every loop in every app gets
|
|
676
|
-
it — and the block parks in the middle of the track instead of at its
|
|
677
|
-
off-screen starting point. A still bar that shows nothing would say the app
|
|
678
|
-
had stopped; a full one would say it had finished.
|
|
477
|
+
The indeterminate block is a [style loop](styling.md#loops) on the logical
|
|
478
|
+
`start` edge, so it mirrors with the text direction and fits any width; it
|
|
479
|
+
reports `aria-busy` **with no value**, telling a screen reader no percentage
|
|
480
|
+
is coming. Under
|
|
481
|
+
[reduced motion](system.md#usedesktopsettings--how-the-desktop-wants-an-app-to-feel)
|
|
482
|
+
it does not move and parks mid-track.
|
|
679
483
|
|
|
680
484
|
## `Select`
|
|
681
485
|
|
|
682
|
-
A dropdown whose menu is
|
|
683
|
-
|
|
486
|
+
A dropdown whose menu is an override-redirect `<popup>` anchored below the
|
|
487
|
+
trigger.
|
|
684
488
|
|
|
685
489
|
```jsx
|
|
686
490
|
import { Select } from 'react-x11';
|
|
@@ -709,11 +513,9 @@ import { Select } from 'react-x11';
|
|
|
709
513
|
| `chevronStyle` | the chevron's style, over `textMuted` |
|
|
710
514
|
| `native`, `nativeMenu` | the platform's bezel, and its menu |
|
|
711
515
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
keyboard, type-ahead and the focus ring. A borderless select that sits in a
|
|
716
|
-
line of text:
|
|
516
|
+
`style` restyles the trigger and the two slots what is inside it, keeping the
|
|
517
|
+
menu, keyboard, type-ahead and focus ring. A borderless select in a line of
|
|
518
|
+
text:
|
|
717
519
|
|
|
718
520
|
```jsx
|
|
719
521
|
<Select
|
|
@@ -732,96 +534,57 @@ line of text:
|
|
|
732
534
|
/>
|
|
733
535
|
```
|
|
734
536
|
|
|
735
|
-
The
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
would only say that the widgets were written at different times.
|
|
775
|
-
|
|
776
|
-
Opening on the press rather than the release is deliberate, and it is the
|
|
777
|
-
one control whose answer to a press is more than a colour: a dropdown exists
|
|
778
|
-
to be looked at, so waiting for the button to come back up before showing it
|
|
779
|
-
wastes the whole time the button is down. It is what every desktop toolkit
|
|
780
|
-
does. A press while the menu is up dismisses it through the popup's pointer
|
|
781
|
-
grab, so the two never fight over the toggle.
|
|
782
|
-
|
|
783
|
-
Keyboard, while the trigger is focused (the popup is override-redirect and
|
|
784
|
-
never takes focus, so the trigger keeps handling keys with the menu open):
|
|
785
|
-
|
|
786
|
-
| key | closed | open |
|
|
787
|
-
| ----------------- | -------------- | --------------------------------------- |
|
|
788
|
-
| `Down` / `Up` | opens the menu | move the active option, wrapping around |
|
|
789
|
-
| `Home` / `End` | — | first / last option |
|
|
790
|
-
| `Enter` / `Space` | opens the menu | pick the active option |
|
|
791
|
-
| `Escape` | — | close without picking |
|
|
792
|
-
|
|
793
|
-
The menu opens with the current value active, hovering an option makes it
|
|
794
|
-
active (pointer and keyboard share one highlight), and the active option is
|
|
795
|
-
scrolled into view.
|
|
796
|
-
|
|
797
|
-
**PageUp/PageDown** move by a menu viewport (`MAX_MENU_HEIGHT / ITEM_HEIGHT`
|
|
798
|
-
options), clamping at the ends rather than wrapping the way the arrows do.
|
|
799
|
-
|
|
800
|
-
**Losing the window** closes it too. That is not the same event as the
|
|
801
|
-
trigger's own blur: a window losing the window manager's focus leaves the
|
|
802
|
-
node inside it focused and merely stops it looking active — so without this
|
|
803
|
-
the menu would still be up over an application you have switched away from,
|
|
804
|
-
holding the pointer grab it opened with. The menus do the same.
|
|
805
|
-
|
|
806
|
-
**Type-ahead.** Typing letters jumps to the matching option: with the menu
|
|
807
|
-
open it moves the highlight, and with it closed it changes the value
|
|
808
|
-
outright, the way a native select does. Keystrokes within 700ms accumulate
|
|
809
|
-
into one query (`b`,`l` finds _blueberry_, not _banana_); repeating a
|
|
810
|
-
single letter cycles through the options starting with it.
|
|
537
|
+
The chevron is sized against the caption's numeric `fontSize`;
|
|
538
|
+
`chevronStyle={{ display: 'none' }}` drops it. A select with either slot is
|
|
539
|
+
drawn even under `controls: 'auto'`. The menu itself is not restyled.
|
|
540
|
+
|
|
541
|
+
**The menu can be the platform's.** On macOS with `@windowkit/appkit` 0.20 or
|
|
542
|
+
later, a select under the native bezel opens a real `NSMenu`: the chosen row
|
|
543
|
+
over the trigger and checked, at least the trigger's width, with AppKit's
|
|
544
|
+
tracking, type-select and VoiceOver. `nativeMenu` makes that choice alone: it
|
|
545
|
+
follows the trigger by default; `true` uses the platform menu under a drawn
|
|
546
|
+
trigger too (as a browser does for a styled `<select>`, and as `<Html>`
|
|
547
|
+
does), at the caption's size and family when the system has it; `false`
|
|
548
|
+
keeps the drawn menu. Elsewhere, or on an older bridge, the menu is drawn. A
|
|
549
|
+
platform menu dismisses itself (Escape, a click outside, the app losing
|
|
550
|
+
focus), and the select hears a pick or nothing.
|
|
551
|
+
|
|
552
|
+
**Behaviour.** The menu opens on the **press**, as in every desktop toolkit;
|
|
553
|
+
a press while it is up closes it. Space and Enter toggle it; Escape, focus
|
|
554
|
+
loss, losing the window (switching applications) or picking close it. The
|
|
555
|
+
list scrolls past 220px, is sized to its longest option (rows end in `…`
|
|
556
|
+
only on a screen narrower than that), and is drawn as a menu is. The
|
|
557
|
+
trigger is a Tab stop, and a caption with no room ends in `…` rather than
|
|
558
|
+
wrapping.
|
|
559
|
+
|
|
560
|
+
Keys go to the focused trigger, menu open or not:
|
|
561
|
+
|
|
562
|
+
| key | closed | open |
|
|
563
|
+
| --------------------- | -------------- | --------------------------------------- |
|
|
564
|
+
| `Down` / `Up` | opens the menu | move the active option, wrapping around |
|
|
565
|
+
| `Home` / `End` | — | first / last option |
|
|
566
|
+
| `PageUp` / `PageDown` | — | a menu's height of options, clamped |
|
|
567
|
+
| `Enter` / `Space` | opens the menu | pick the active option |
|
|
568
|
+
| `Escape` | — | close without picking |
|
|
569
|
+
|
|
570
|
+
The menu opens with the current value active; hovering an option makes it
|
|
571
|
+
active (one highlight for pointer and keyboard), and it is scrolled into view.
|
|
572
|
+
**Type-ahead**: letters typed within 700ms form one query (`b`,`l` finds
|
|
573
|
+
_blueberry_, not _banana_), and a repeated single letter cycles; with the
|
|
574
|
+
menu open this moves the highlight, closed it changes the value, as a native
|
|
575
|
+
select does.
|
|
811
576
|
|
|
812
577
|
### Theming
|
|
813
578
|
|
|
814
|
-
`Select`
|
|
815
|
-
|
|
816
|
-
`
|
|
817
|
-
|
|
818
|
-
highlight is `hoverBackground`/`hoverText`.
|
|
579
|
+
`Select` reads the palette from [`ThemeProvider`](#theming) like every
|
|
580
|
+
widget: the trigger is `background`/`text` in a `border` box that turns
|
|
581
|
+
`borderFocus` when focused or open; chevron and placeholder are `textMuted`;
|
|
582
|
+
the menu highlight is `hoverBackground`/`hoverText`.
|
|
819
583
|
|
|
820
584
|
## `Calendar` / `DatePicker`
|
|
821
585
|
|
|
822
|
-
Moved to [`@react-x11/components`](ecosystem.md)
|
|
823
|
-
problem before it is a widget
|
|
824
|
-
range semantics people need do not belong in a renderer.
|
|
586
|
+
Moved to [`@react-x11/components`](ecosystem.md): a date picker is a locale
|
|
587
|
+
problem before it is a widget.
|
|
825
588
|
|
|
826
589
|
## `PasswordInput`
|
|
827
590
|
|
|
@@ -849,81 +612,35 @@ A masked field whose mask is a **scribble**, not a row of bullets.
|
|
|
849
612
|
|
|
850
613
|
### Why a scribble
|
|
851
614
|
|
|
852
|
-
Bullets
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
So the mask is a single stroke through points chosen by a generator seeded
|
|
860
|
-
from the window id and a hash of the value. Every keystroke reseeds it, so
|
|
861
|
-
**the whole curve moves on every character** — feedback you cannot miss — and
|
|
862
|
-
nothing in the shape is per-character: the pen visits a number of points taken
|
|
863
|
-
from the mask's _width_, where a character is worth about half a point, so no
|
|
864
|
-
part of the stroke can be matched to anything typed.
|
|
865
|
-
|
|
866
|
-
It is a **scribble rather than a waveform**, and that is a decision rather
|
|
867
|
-
than a look. A stroke whose `x` only ever increases is the plot of a
|
|
868
|
-
function, and the eye reads it as one — value against position, meaning in
|
|
869
|
-
the peaks — however wild the `y` is. So the points are laid out one per
|
|
870
|
-
column, which is what makes the stroke cover the width it was given, and then
|
|
871
|
-
visited **out of order**: the pen doubles back, crosses what it has already
|
|
872
|
-
drawn, and leaves loops. The shuffle is local rather than global, because in
|
|
873
|
-
a box seventeen pixels tall a jump right across it is a long shallow scratch,
|
|
874
|
-
and a maskful of those is not a scribble either.
|
|
875
|
-
|
|
876
|
-
What does grow is the width, because a mask that did not would say nothing
|
|
877
|
-
about progress. Each position contributes an advance drawn from a **second,
|
|
878
|
-
window-seeded** stream, so the width only ever grows as you type, never
|
|
879
|
-
twitches when a character is replaced, and is not a clean multiple of
|
|
880
|
-
anything a glance could divide.
|
|
881
|
-
|
|
882
|
-
The honest limit, since a mask that oversells itself is worse than one that
|
|
883
|
-
does not: **this hides a glance, not a recording.** Someone watching the
|
|
884
|
-
field grow keystroke by keystroke still counts the keystrokes, and a long
|
|
885
|
-
password still sits in a visibly different bracket from a short one.
|
|
615
|
+
Bullets show how many characters were typed and barely show the keystroke
|
|
616
|
+
that just landed. The scribble is the reverse: **the whole stroke changes on
|
|
617
|
+
every character**, nothing in it is per-character, and its width only grows
|
|
618
|
+
(never twitching when a character is replaced, never a clean multiple of
|
|
619
|
+
anything). **It hides a glance, not a recording**: someone watching it grow
|
|
620
|
+
still counts keystrokes. Design notes:
|
|
621
|
+
[design/components.md](../design/components.md#why-a-scribble).
|
|
886
622
|
|
|
887
623
|
### Two states, and what holds across both
|
|
888
624
|
|
|
889
|
-
**Masked
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
selection,
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
What holds in **both** states is that nothing leaves by a selection. Ctrl+C
|
|
902
|
-
and Ctrl+X do nothing, the revealed input carries
|
|
903
|
-
[`sensitive`](elements.md#sensitive) so its menu has no Cut or Copy, and
|
|
904
|
-
neither state ever takes PRIMARY, so a middle click in another window cannot
|
|
905
|
-
spend it. The line is between what is on screen and what is on the clipboard:
|
|
906
|
-
the first stops being visible when the field is hidden, the second is
|
|
907
|
-
readable by every client on the display until something else takes it.
|
|
908
|
-
|
|
909
|
-
While the value is masked it is **never laid out and never drawn**: the mask
|
|
910
|
-
is measured from one reference character, so the secret does not enter ntk's
|
|
911
|
-
shaping cache and its glyphs never reach the X server. Revealing it costs
|
|
912
|
-
what revealing it costs. The eye is a pointer affordance and not a tab stop,
|
|
913
|
-
as GTK's peek icon is; `revealed` + `onRevealChange` put the toggle wherever
|
|
914
|
-
your keyboard can reach it.
|
|
915
|
-
|
|
916
|
-
Caps Lock is reported while it is on, from the modifier state on the keys as
|
|
917
|
-
they arrive — the mistake that a masked field otherwise lets you make four
|
|
918
|
-
times before telling you.
|
|
625
|
+
**Masked**: type, Backspace, Ctrl+Backspace / Ctrl+U / Delete to clear,
|
|
626
|
+
Ctrl+V or Shift+Insert to paste, Enter to submit. No caret, no selection, no
|
|
627
|
+
undo history. **Revealed**: a real `<textinput>` takes its place — caret,
|
|
628
|
+
selection, arrows, clicks, undo, the edit menu. Focus follows the swap both
|
|
629
|
+
ways, and the reveal ends when the keyboard leaves the widget.
|
|
630
|
+
|
|
631
|
+
In **both**, nothing leaves by a selection: Ctrl+C and Ctrl+X do nothing, the
|
|
632
|
+
revealed input is [`sensitive`](elements.md#sensitive) (no Cut or Copy in its
|
|
633
|
+
menu), and neither state takes PRIMARY. While masked the value is **never
|
|
634
|
+
laid out or drawn**, so its glyphs never reach the display server. The eye is
|
|
635
|
+
a pointer affordance, not a tab stop; `revealed` + `onRevealChange` put the
|
|
636
|
+
toggle where the keyboard can reach it. Caps Lock is reported while it is on.
|
|
919
637
|
|
|
920
638
|
### Drawing your own mask
|
|
921
639
|
|
|
922
|
-
`drawMask(ctx, info)` replaces the scribble
|
|
923
|
-
`{ width, height, seed, color, length }`,
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
before replacing it — the reasoning for each number is there.
|
|
640
|
+
`drawMask(ctx, info)` replaces the scribble; `info` is
|
|
641
|
+
`{ width, height, seed, color, length }`, with the mask width the field
|
|
642
|
+
worked out and the value-and-window seed. The scribble is `strokeScribble` in
|
|
643
|
+
`src/components/scribble.js`, with the reasoning for each number.
|
|
927
644
|
|
|
928
645
|
```jsx
|
|
929
646
|
<PasswordInput
|
|
@@ -936,12 +653,10 @@ before replacing it — the reasoning for each number is there.
|
|
|
936
653
|
|
|
937
654
|
### Password managers
|
|
938
655
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
window title has to do with it, and the seams that are _not_ a field —
|
|
944
|
-
Secret Service, the sandbox portal, AT-SPI — are in
|
|
656
|
+
XTEST auto-type arrives as ordinary key events, and Ctrl+V pastes with
|
|
657
|
+
control characters stripped (so a manager's trailing newline stays out).
|
|
658
|
+
Which manager does what, the window title's part, and Secret Service, the
|
|
659
|
+
portal and AT-SPI:
|
|
945
660
|
[desktop.md](desktop.md#password-fields-and-password-managers).
|
|
946
661
|
|
|
947
662
|
## `Slider`
|
|
@@ -972,9 +687,8 @@ import { Slider } from 'react-x11';
|
|
|
972
687
|
| `trackStyle` | the track's style, over `height` and `track` |
|
|
973
688
|
| `fillStyle` | the filled part of the track, over `accent` |
|
|
974
689
|
|
|
975
|
-
The
|
|
976
|
-
|
|
977
|
-
size. The flat slider most design systems draw is:
|
|
690
|
+
The style slots restyle the parts and keep the drag, keyboard, `aria-*`
|
|
691
|
+
values, RTL and target size. A flat slider:
|
|
978
692
|
|
|
979
693
|
```jsx
|
|
980
694
|
<Slider
|
|
@@ -992,22 +706,15 @@ size. The flat slider most design systems draw is:
|
|
|
992
706
|
/>
|
|
993
707
|
```
|
|
994
708
|
|
|
995
|
-
The thumb
|
|
996
|
-
|
|
997
|
-
as the taller of thumb and track,
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
otherwise use the platform's bezel, whose pixels no style reaches.
|
|
1001
|
-
|
|
1002
|
-
Dragging uses [pointer capture](events.md#pointer-capture): the press
|
|
1003
|
-
captures, so the thumb keeps following a pointer that has wandered far
|
|
1004
|
-
outside the widget, and releasing out there still ends the drag.
|
|
1005
|
-
|
|
1006
|
-
Keyboard: arrows step, `Home`/`End` jump to the ends, `PageUp`/`PageDown`
|
|
1007
|
-
move by ten steps.
|
|
709
|
+
The thumb is centred on its value, so the travel is the track less one thumb
|
|
710
|
+
(numeric `thumbStyle` `width`/`height` are what the widget uses); the control
|
|
711
|
+
is as tall as the taller of thumb and track, its target at least 24. Half a
|
|
712
|
+
thumb of `marginStart`/`marginEnd` on the track lets the thumb overhang the
|
|
713
|
+
ends. A slider with any slot is drawn even under `controls: 'auto'`.
|
|
1008
714
|
|
|
1009
|
-
|
|
1010
|
-
|
|
715
|
+
Dragging uses [pointer capture](events.md#pointer-capture), so it follows
|
|
716
|
+
the pointer anywhere and ends wherever it is released. Keyboard: arrows
|
|
717
|
+
step, `Home`/`End` jump to the ends, `PageUp`/`PageDown` move ten steps.
|
|
1011
718
|
|
|
1012
719
|
## `Tooltip`
|
|
1013
720
|
|
|
@@ -1030,36 +737,20 @@ import { Tooltip } from 'react-x11';
|
|
|
1030
737
|
| `width`, `height` | pin an axis of the bubble exactly; anything not pinned is measured |
|
|
1031
738
|
| `maxWidth`, `maxHeight` | cap what an element label measures out to, inside the screen's own cap |
|
|
1032
739
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
mousedown
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
and
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
not
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
`
|
|
1047
|
-
takes the first side the hint fits on, preferring above — measured against
|
|
1048
|
-
the **screen**, because a popup is a real X window and the screen is what
|
|
1049
|
-
bounds it. A named side is a preference rather than a promise either way:
|
|
1050
|
-
it still flips to its opposite instead of opening off-screen. (`placement`
|
|
1051
|
-
is the older name for the same prop and still wins where it is given.)
|
|
1052
|
-
|
|
1053
|
-
A **string** `label` is measured and the popup sized around it, because a
|
|
1054
|
-
`<popup>` is a real X window and needs its size before layout. An
|
|
1055
|
-
**element** label is measured too — nothing to size, nothing to guess: the
|
|
1056
|
-
popup is rendered once [`hidden`](elements.md#hidden--realized-laid-out-not-on-screen)
|
|
1057
|
-
at its natural size (a real layout of the real content, capped at the screen
|
|
1058
|
-
and at `maxWidth`/`maxHeight`), the size is read back, and the same
|
|
1059
|
-
placement math runs before anything is mapped. Both commits land in the same
|
|
1060
|
-
task and the popup maps last, so it is only ever on screen placed and at its
|
|
1061
|
-
final size. The element gets the bubble to fill, with no padding imposed on
|
|
1062
|
-
it:
|
|
740
|
+
It wraps its children in a row box carrying the hover handlers and anchor ref,
|
|
741
|
+
so it composes around any element, and hides at once on leave and on
|
|
742
|
+
mousedown. **One at a time** per connection: a trigger taking the hover
|
|
743
|
+
dismisses any showing hint immediately. `direction` is a preference: `'auto'`
|
|
744
|
+
takes the first side that fits on the **screen**, preferring above, and a
|
|
745
|
+
named side still flips rather than open off-screen. (`placement` is the older
|
|
746
|
+
name and still wins where given.)
|
|
747
|
+
|
|
748
|
+
An **element** `label` is measured like a string one: rendered
|
|
749
|
+
[`hidden`](elements.md#hidden--realized-laid-out-not-on-screen) at its
|
|
750
|
+
natural size (capped by the screen and `maxWidth`/`maxHeight`) and placed
|
|
751
|
+
before it maps, so it never appears misplaced. It fills the bubble with no
|
|
752
|
+
padding imposed; anything renders there, widgets included
|
|
753
|
+
(`examples/tooltips.jsx`):
|
|
1063
754
|
|
|
1064
755
|
```jsx
|
|
1065
756
|
<Tooltip
|
|
@@ -1076,40 +767,25 @@ it:
|
|
|
1076
767
|
</Tooltip>
|
|
1077
768
|
```
|
|
1078
769
|
|
|
1079
|
-
`width
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
another panel of the app — so inside it, `$background` is the bubble and
|
|
1092
|
-
`$text` is the ink on it, published through a `ThemeProvider` so that
|
|
1093
|
-
`useTheme()` agrees. Write the content the way you would write anything
|
|
1094
|
-
else and it is legible in both schemes; hard-code a light text colour and
|
|
1095
|
-
it will be invisible on the light bubble a dark palette gives it. `textMuted`
|
|
1096
|
-
is derived for the same reason — the palette's own is a grey chosen against
|
|
1097
|
-
the app's background, not against this one — and `$surface` is the bubble
|
|
1098
|
-
too, so a card written for the app does not light up inside a tooltip.
|
|
1099
|
-
|
|
1100
|
-
Where the display composites, the popup is an ARGB window: a bubble rounded
|
|
1101
|
-
at `radiusTooltip`, no border, and a small arrow pointing back at the middle
|
|
1102
|
-
of the trigger with everything neither covers left transparent. Without a
|
|
1103
|
-
compositor it is the square opaque rectangle it has always been and there is
|
|
1104
|
-
no arrow — those pixels would be black rather than empty.
|
|
770
|
+
`width` and `height` together skip measuring; one alone fixes that axis and
|
|
771
|
+
measures the other (`width={340}` is a column whose height fits). The size is
|
|
772
|
+
settled at open and kept until the next.
|
|
773
|
+
|
|
774
|
+
**The palette inside is inverted**: the bubble is drawn in the palette's ink,
|
|
775
|
+
so inside it `$background` (and `$surface`) is the bubble and `$text` the ink,
|
|
776
|
+
published through a `ThemeProvider` so `useTheme()` agrees, with `textMuted`
|
|
777
|
+
derived to match. Write content with tokens and it reads in both schemes; a
|
|
778
|
+
hard-coded light text colour vanishes on the light bubble of a dark palette.
|
|
779
|
+
Where the display composites the bubble is rounded at `radiusTooltip`, with
|
|
780
|
+
no border and a small arrow at the trigger's middle; elsewhere it is a
|
|
781
|
+
square rectangle with no arrow.
|
|
1105
782
|
|
|
1106
783
|
## `Dialog`
|
|
1107
784
|
|
|
1108
|
-
A
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
window manager) stacked above its owner and iconified with it.
|
|
785
|
+
A `<popup>` centred over its owner that the **window manager knows is a
|
|
786
|
+
dialog**: managed and transient for its owner (`WM_TRANSIENT_FOR` on X11) —
|
|
787
|
+
framed, movable, closable through the WM, out of the taskbar and alt-tab, and
|
|
788
|
+
on a full EWMH window manager stacked above its owner and iconified with it.
|
|
1113
789
|
|
|
1114
790
|
```jsx
|
|
1115
791
|
import { Button, Dialog } from 'react-x11';
|
|
@@ -1129,100 +805,65 @@ import { Button, Dialog } from 'react-x11';
|
|
|
1129
805
|
</Dialog>;
|
|
1130
806
|
```
|
|
1131
807
|
|
|
1132
|
-
| prop |
|
|
1133
|
-
| ---------- |
|
|
1134
|
-
| `open` | renders nothing when false; the popup exists only while true
|
|
1135
|
-
| `title` | names the dialog
|
|
1136
|
-
| `children` | body content; strings become `<text>`
|
|
1137
|
-
| `actions` | elements for the right-aligned button row
|
|
1138
|
-
| `onClose` | Escape, or the window manager's close button
|
|
1139
|
-
| `managed` | `false` for
|
|
1140
|
-
| `width` | popup width (default 360); `'auto'` for the content's
|
|
1141
|
-
| `height` |
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
Escape closes because keys go to the focused node inside the popup and
|
|
1152
|
-
bubble out through the popup's place in the JSX tree; so does the window
|
|
1153
|
-
manager's close button, through `onCloseRequest`. **Pointer modality is not
|
|
1154
|
-
enforced** — widgets in the owner window stay clickable behind the dialog —
|
|
1155
|
-
so it is for confirmations, not for guarding state. (`_NET_WM_STATE_MODAL`
|
|
1156
|
-
is the mechanism that would enforce it, and it means nothing without the
|
|
1157
|
-
`WM_TRANSIENT_FOR` this now sets; that is why the property had to come
|
|
1158
|
-
first.)
|
|
808
|
+
| prop | |
|
|
809
|
+
| ---------- | ------------------------------------------------------------------- |
|
|
810
|
+
| `open` | renders nothing when false; the popup exists only while true |
|
|
811
|
+
| `title` | names the dialog; drawn once ([below](#managed)) |
|
|
812
|
+
| `children` | body content; strings become `<text>` |
|
|
813
|
+
| `actions` | elements for the right-aligned button row |
|
|
814
|
+
| `onClose` | Escape, or the window manager's close button |
|
|
815
|
+
| `managed` | `false` for an unframed override-redirect popup ([below](#managed)) |
|
|
816
|
+
| `width` | popup width (default 360); `'auto'` for the content's |
|
|
817
|
+
| `height` | default `'auto'`, the content's ([below](#size-and-placement)) |
|
|
818
|
+
|
|
819
|
+
Focus is the renderer's `trapFocus` ([Focus scopes](events.md#focus-scopes-modals)):
|
|
820
|
+
Tab stays inside, presses elsewhere do not move focus, and focus returns to
|
|
821
|
+
whatever had it (usually the opening button) on close. `autoFocus` picks the
|
|
822
|
+
first stop; with nothing focusable the dialog surface takes focus
|
|
823
|
+
(`tabIndex={-1}`), so Escape and Tab work at once. **Pointer modality is not
|
|
824
|
+
enforced** — the owner stays clickable — so it is for confirmations, not
|
|
825
|
+
guarding state.
|
|
1159
826
|
|
|
1160
827
|
### `managed`
|
|
1161
828
|
|
|
1162
|
-
| `managed` | what you get
|
|
1163
|
-
| ---------------- |
|
|
1164
|
-
| `true` (default) |
|
|
1165
|
-
| `false` |
|
|
1166
|
-
|
|
1167
|
-
The
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
They are one choice, not two: a client-side pointer grab over a window the
|
|
1173
|
-
window manager is trying to let the user drag swallows the press that would
|
|
1174
|
-
start the drag, so `grab` and the frame turn on and off together. A managed
|
|
1175
|
-
dialog staying open when you click elsewhere is not a regression — it is what
|
|
1176
|
-
a dialog does. `managed={false}` is the right shape for a transient
|
|
1177
|
-
confirmation on a display with no window manager at all.
|
|
1178
|
-
|
|
1179
|
-
The owner is resolved automatically: `Dialog` already keeps an out-of-flow
|
|
1180
|
-
`<box>` inside the owner window for placement, and a ref to any drawn node
|
|
1181
|
-
resolves to the window that owns it
|
|
829
|
+
| `managed` | what you get |
|
|
830
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
831
|
+
| `true` (default) | frame, titlebar (the `title`), movable, WM close button, transient for its owner; a press outside does not close it |
|
|
832
|
+
| `false` | no frame, not movable, a pointer grab, the `title` as a bold heading; a press outside calls `onClose` |
|
|
833
|
+
|
|
834
|
+
The frame and the grab go together (a grab would swallow the press that
|
|
835
|
+
starts a window drag). `managed={false}` suits a display with no window
|
|
836
|
+
manager. Either way the `title` names the dialog for accessibility, and the
|
|
837
|
+
owner is found automatically
|
|
1182
838
|
([`transientFor`](elements.md#transientfor--a-window-that-belongs-to-another)).
|
|
1183
839
|
|
|
1184
840
|
### Size and placement
|
|
1185
841
|
|
|
1186
|
-
**The height is the content's**
|
|
1187
|
-
|
|
1188
|
-
popup measures its content, centres itself over the owner window at that
|
|
1189
|
-
size and only then opens — it is born the right size in the right place,
|
|
1190
|
-
never shown at a guess and corrected
|
|
842
|
+
**The height is the content's** (`height="auto"`): the dialog measures its
|
|
843
|
+
content, centres over the owner at that size, and only then opens
|
|
1191
844
|
([`placement: 'center'`](elements.md#centred-over-something)).
|
|
1192
845
|
|
|
1193
|
-
- **Capped at the monitor
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
- **
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
`width` stays a number by default, 360: a body of prose wraps to it, where a
|
|
1209
|
-
natural width would be as wide as its longest sentence. `width="auto"` is
|
|
1210
|
-
there for a dialog whose content has a width of its own (a grid of fields, an
|
|
1211
|
-
image). A number for either axis is that size exactly.
|
|
1212
|
-
|
|
1213
|
-
`centerRect(node, {width, height})` is the same centring as a function,
|
|
1214
|
-
exported alongside `anchorRect` for window-centred popups of your own that
|
|
1215
|
-
know their size; one that does not uses `<popup anchor={{ rect, placement:
|
|
1216
|
-
'center' }}>` and lets the window work it out.
|
|
846
|
+
- **Capped at the monitor**: taller content scrolls in the body while the
|
|
847
|
+
`actions` row stays pinned; the body is a tab stop only while it can
|
|
848
|
+
scroll. A numeric `height` scrolls the same way.
|
|
849
|
+
- **Following its content**: content that changes while open re-fits the
|
|
850
|
+
dialog from its **top edge**, moving it up only to stay on the monitor; a
|
|
851
|
+
managed dialog the user dragged grows where they put it.
|
|
852
|
+
- **Centred once**, over where the owner was at open; moving the owner does
|
|
853
|
+
not move it.
|
|
854
|
+
|
|
855
|
+
`width` defaults to 360 so prose wraps; `width="auto"` is for content with a
|
|
856
|
+
width of its own (a grid of fields, an image). A number is exact on either
|
|
857
|
+
axis. `centerRect(node, {width, height})` is the same centring as a function,
|
|
858
|
+
for popups of your own that know their size; one that does not uses
|
|
859
|
+
`<popup anchor={{ rect, placement: 'center' }}>`.
|
|
1217
860
|
|
|
1218
861
|
## `MenuBar` / `ContextMenu`
|
|
1219
862
|
|
|
1220
|
-
Pull-down and right-click menus
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
sizes its own — 22pt rows, the 13pt menu font — so a context menu sits
|
|
1225
|
-
beside the system's like one of them ([macos.md](macos.md#menus-the-global-menu-finally-at-home)).
|
|
863
|
+
Pull-down and right-click menus in `<popup>` windows, anchored with
|
|
864
|
+
`useAnchor`. On the Cocoa backend with native controls they are sized as
|
|
865
|
+
`NSMenu` sizes its own — 22pt rows, the 13pt menu font
|
|
866
|
+
([macos.md](macos.md#menus-the-global-menu-finally-at-home)).
|
|
1226
867
|
|
|
1227
868
|
```jsx
|
|
1228
869
|
import { MenuBar, ContextMenu } from 'react-x11';
|
|
@@ -1253,16 +894,28 @@ import { MenuBar, ContextMenu } from 'react-x11';
|
|
|
1253
894
|
|
|
1254
895
|
Item shape: `{ label, onSelect, type, items, enabled, visible, shortcut,
|
|
1255
896
|
toggleType, toggleState, icon, iconName, disposition, key }`. Both take an
|
|
1256
|
-
`onSelect(item)` prop
|
|
1257
|
-
the **press**,
|
|
897
|
+
`onSelect(item)` prop too, fired after the item's own. A bar menu opens on
|
|
898
|
+
the **press**, like `Select`.
|
|
899
|
+
|
|
900
|
+
**The vocabulary is `com.canonical.dbusmenu`'s** — `type: 'separator'`,
|
|
901
|
+
`enabled: false`, `toggleType`/`toggleState` (`0`, `1`, or `-1` for
|
|
902
|
+
indeterminate), `shortcut` as modifier tokens — because on a desktop whose
|
|
903
|
+
panel shows application menus, `MenuBar` hands the array over and stops
|
|
904
|
+
drawing, with the same JSX ([globalmenu.md](globalmenu.md), also for
|
|
905
|
+
`globalMenu={false}`).
|
|
906
|
+
|
|
907
|
+
**`shortcut` is a binding, not a label**: `[['Control', 'S']]` makes Ctrl+S
|
|
908
|
+
fire the item's `onSelect` while the menu is mounted, unopened, gated by its
|
|
909
|
+
`enabled` and `visible`, and still when a panel has taken the menu over. The
|
|
910
|
+
same array is the drawn shortcut, the dbusmenu payload and the
|
|
911
|
+
`aria-keyshortcuts` announcement. Chord rules, key names, `useAccelerator`
|
|
912
|
+
and `accelerators={false}`: [events.md](events.md#accelerators).
|
|
1258
913
|
|
|
1259
914
|
**A context menu's items can come from the right-click.** The click records
|
|
1260
|
-
only where it happened;
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
right-click. Items still empty in that render open nothing: no popup, no
|
|
1265
|
-
pointer grab, and the focus left where the press put it.
|
|
915
|
+
only where it happened; the menu's content, size and placement come from the
|
|
916
|
+
`items` of the next render, so items derived in a row's `onContextMenu` give
|
|
917
|
+
a menu for that row on the first right-click. Items still empty open nothing
|
|
918
|
+
— no popup, no grab, focus left where the press put it.
|
|
1266
919
|
|
|
1267
920
|
```jsx
|
|
1268
921
|
const [target, setTarget] = useState(null);
|
|
@@ -1276,87 +929,23 @@ const [target, setTarget] = useState(null);
|
|
|
1276
929
|
</ContextMenu>;
|
|
1277
930
|
```
|
|
1278
931
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
list of modifier tokens rather than a display string, all follow from that.
|
|
1297
|
-
`toggleState` is `0`, `1` or `-1`, the last being indeterminate. See
|
|
1298
|
-
[globalmenu.md](globalmenu.md), which also covers `globalMenu={false}`.
|
|
1299
|
-
|
|
1300
|
-
**The sheet.** Where the display composites, a menu is an ARGB window
|
|
1301
|
-
rounded at `radiusPopup` with a hairline border, and the highlight on a row
|
|
1302
|
-
is a pill: inset from the popup's edge by the list's own padding and rounded
|
|
1303
|
-
a step tighter (`radiusPopupItem`), so it reads as sitting _on_ the sheet
|
|
1304
|
-
rather than cutting across it. The fill is `hoverBackground` — the palette's
|
|
1305
|
-
selection colour, shared with `Select`'s active option and `Table`'s current
|
|
1306
|
-
row, so one token moves every highlight in the app. Without a compositor
|
|
1307
|
-
both go square, which is what those pixels can honestly be.
|
|
1308
|
-
|
|
1309
|
-
**One selection at a time.** The bar item, the row under it and the row
|
|
1310
|
-
under that are one trail, and only its deepest level wears the selection
|
|
1311
|
-
colour; everything behind is drawn in `surfaceActive`.
|
|
1312
|
-
They are still chosen — they are the way back — but two selection-coloured
|
|
1313
|
-
rows in two menus claim the same thing twice, and only one of them is where
|
|
1314
|
-
the keys are going. A submenu that is open with _nothing_ selected in it has
|
|
1315
|
-
not taken over yet: the pointer is still on the row that opened it, so that
|
|
1316
|
-
row stays lit — which is why a menu bar item stays coloured until you touch
|
|
1317
|
-
a row in the menu it opened, and goes quiet then.
|
|
1318
|
-
|
|
1319
|
-
The bar item wears the same pill as the rows, at `radiusPopupItem` — the same
|
|
1320
|
-
padding around the same capitals, so a title and the first row of the menu it
|
|
1321
|
-
opens are one shape in one size, inset into the bar by a few pixels of margin
|
|
1322
|
-
that sit outside that padding.
|
|
1323
|
-
|
|
1324
|
-
**Titles that do not fit.** A bar narrower than its menus paints as many as
|
|
1325
|
-
fit and moves the rest behind a chevron at the end of it — the system set's
|
|
1326
|
-
`moreVertical`, drawn rather than a `»`, so it is a mark and not a font's
|
|
1327
|
-
opinion of one. The chevron is an ordinary bar entry whose items are the
|
|
1328
|
-
menus that were cut, so each row opens the menu it stands for as a submenu,
|
|
1329
|
-
and it is a real stop for Left/Right: **the keyboard walks exactly what is
|
|
1330
|
-
painted**, never a title laid out past the window's edge. Widen the window
|
|
1331
|
-
and the titles come back one at a time. The bar measures itself, so the
|
|
1332
|
-
first frame of a new bar always shows everything and the cut lands on the
|
|
1333
|
-
next — and none of this happens on a desktop whose panel has taken the menu,
|
|
1334
|
-
where there is no bar of ours to overflow.
|
|
1335
|
-
|
|
1336
|
-
**Icons.** `icon` fills the column left of the label — the same column a
|
|
1337
|
-
`toggleType` mark uses, so an item that is both checked and iconned shows
|
|
1338
|
-
the check: the check is state, the icon is only identity. `icon` is drawn by
|
|
1339
|
-
react-x11 and never crosses the bus; `iconName` is the icon-theme name a
|
|
1340
|
-
panel uses ([globalmenu.md](globalmenu.md)). One column rather than
|
|
1341
|
-
two, because a second would indent every label in the menu to reserve room
|
|
1342
|
-
for icons most items do not have.
|
|
1343
|
-
|
|
1344
|
-
The column is there for the menus that draw in it and gone from the ones
|
|
1345
|
-
that do not. What counts is what a row actually paints: one icon, or one
|
|
1346
|
-
toggle that is _on_, anywhere in a menu indents every label in it — and a
|
|
1347
|
-
menu of plain commands, or of checkboxes that all happen to be off, has no
|
|
1348
|
-
column at all and its labels sit where a label sits.
|
|
1349
|
-
|
|
1350
|
-
So ticking the first item in such a menu moves its labels sideways, once.
|
|
1351
|
-
Toolkits that reserve the column unconditionally are buying stillness in
|
|
1352
|
-
that one frame, and paying for it with an indent on every menu that has a
|
|
1353
|
-
toggle anywhere in it — this one makes the other trade. A menu that already
|
|
1354
|
-
has a mark in it keeps the column whatever happens to the rest.
|
|
1355
|
-
|
|
1356
|
-
Pass a **function** for anything real. It is called with the colour the
|
|
1357
|
-
row's label is being drawn in and the size the column allows, which is what
|
|
1358
|
-
lets one drawing follow its row into the highlight and into the disabled
|
|
1359
|
-
grey:
|
|
932
|
+
**Look.** Where the display composites, a menu is rounded (`radiusPopup`,
|
|
933
|
+
hairline border) and the row highlight an inset pill (`radiusPopupItem`) in
|
|
934
|
+
`hoverBackground`, the token shared with `Select` and `Table` highlights;
|
|
935
|
+
without a compositor both are square. Only the deepest open level wears
|
|
936
|
+
`hoverBackground`; the bar item and parent rows behind it are
|
|
937
|
+
`surfaceActive`. A bar too narrow for its titles moves the rest behind a
|
|
938
|
+
`moreVertical` entry whose rows open them as submenus; the keyboard walks
|
|
939
|
+
only what is painted. Design notes:
|
|
940
|
+
[design/components.md](../design/components.md#menubar--contextmenu).
|
|
941
|
+
|
|
942
|
+
**Icons.** `icon` fills the column left of the label, shared with the
|
|
943
|
+
`toggleType` mark (a checked item with an icon shows the check). It is drawn
|
|
944
|
+
locally and never crosses the bus; `iconName` is the icon-theme name a panel
|
|
945
|
+
uses. The column exists only while some row paints in it, so ticking the
|
|
946
|
+
first item of an unticked menu shifts its labels once. Pass a **function**:
|
|
947
|
+
it gets the row's ink and the column size, so one drawing follows the
|
|
948
|
+
highlight and the disabled grey —
|
|
1360
949
|
|
|
1361
950
|
```jsx
|
|
1362
951
|
const save = ({ color, size }) => (
|
|
@@ -1364,29 +953,19 @@ const save = ({ color, size }) => (
|
|
|
1364
953
|
);
|
|
1365
954
|
|
|
1366
955
|
{ label: 'Save', icon: save, shortcut: [['Control', 'S']], onSelect: onSave }
|
|
1367
|
-
```
|
|
1368
|
-
|
|
1369
|
-
Paint the SVG in `currentColor` and the renderer caches it as coverage
|
|
1370
|
-
rather than as pixels, so recolouring per row is a composite and every
|
|
1371
|
-
colour of the icon shares one rendered copy
|
|
1372
|
-
([elements.md](elements.md#svg)). A [system icon](#system-icons) is already
|
|
1373
|
-
that, and takes the same two arguments:
|
|
1374
|
-
|
|
1375
|
-
```jsx
|
|
1376
956
|
{ label: 'Close', icon: (p) => <Icon name="close" {...p} />, onSelect: onClose }
|
|
1377
957
|
```
|
|
1378
958
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
`check`, a radio one a `dot`, and the indeterminate third state a `dash`.
|
|
1385
|
-
They used to be `✓`, `●` and `–` set in the row's font, which is the same
|
|
1386
|
-
gamble this paragraph tells you not to take.
|
|
959
|
+
— an SVG painted in `currentColor` is cached once for every colour
|
|
960
|
+
([elements.md](elements.md#svg)), as a [system icon](#system-icons) is. A
|
|
961
|
+
**string** icon is drawn as text and is only as good as the font (`✂` or `⏻`
|
|
962
|
+
is an empty box without one); an element renders as-is. Toggle marks are
|
|
963
|
+
the `check`, `dot` and `dash` icons. `examples/menu.jsx` has a worked set.
|
|
1387
964
|
|
|
1388
|
-
**Submenus.**
|
|
1389
|
-
|
|
965
|
+
**Submenus.** An item with its own `items` is a submenu parent, marked with a
|
|
966
|
+
`chevronRight`. Nesting is unlimited; each level is a `<popup>` opening to the
|
|
967
|
+
end side, its first item level with its parent row, flipping at a screen
|
|
968
|
+
edge.
|
|
1390
969
|
|
|
1391
970
|
```jsx
|
|
1392
971
|
{ label: 'Export', items: [
|
|
@@ -1395,72 +974,35 @@ marked with a `chevronRight` and opening to the side:
|
|
|
1395
974
|
] }
|
|
1396
975
|
```
|
|
1397
976
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
disabled entries** and wrapping; Home/End jump to the ends; Right opens a
|
|
1413
|
-
submenu (selecting its first item) and Left leaves one; Enter/Space
|
|
1414
|
-
activate — or open a submenu, for a parent row; Escape closes **one level
|
|
1415
|
-
at a time**. In a `MenuBar`, Left/Right walk between menus _when there is
|
|
1416
|
-
no submenu to move through_, and with one menu open, hovering another
|
|
1417
|
-
switches to it. Hovering a submenu parent opens it with nothing selected
|
|
1418
|
-
inside.
|
|
1419
|
-
|
|
1420
|
-
**PageUp/PageDown** step ten rows and then settle on the nearest selectable
|
|
1421
|
-
entry in the direction of travel, so a page never lands on a separator or a
|
|
1422
|
-
disabled row. Menus size to their content rather than scrolling, so the
|
|
1423
|
-
stride is fixed — deriving one from the menu height would just equal
|
|
1424
|
-
Home/End.
|
|
1425
|
-
|
|
1426
|
-
**Type-ahead.** Typing letters jumps to the entry whose label starts with
|
|
1427
|
-
them, in whichever level is deepest open. Keystrokes within 700ms
|
|
1428
|
-
accumulate into one query, so `c`,`a` finds _Carrot_ rather than jumping to
|
|
1429
|
-
_Apple_ first; repeating a single letter cycles through the entries
|
|
1430
|
-
starting with it. Disabled entries and separators are never matched.
|
|
1431
|
-
|
|
1432
|
-
Open state is a single path of active indices — one per open level — so
|
|
1433
|
-
moving the selection at any level truncates the path and closes deeper
|
|
1434
|
-
levels for free.
|
|
1435
|
-
|
|
1436
|
-
Both keep focus on a node in the _owner_ window — the popup is
|
|
1437
|
-
override-redirect and never takes focus — which is the same arrangement
|
|
1438
|
-
`Select` uses. `ContextMenu`'s wrapper is focusable for that reason and
|
|
1439
|
-
takes focus when the menu opens.
|
|
1440
|
-
|
|
1441
|
-
Switching between `MenuBar` menus reuses the same X window and moves it
|
|
1442
|
-
rather than destroying and recreating one, so there is no flicker.
|
|
977
|
+
**Keyboard.** Up/Down move, **skipping separators and disabled entries**,
|
|
978
|
+
wrapping; Home/End jump to the ends; PageUp/PageDown step ten rows, then to
|
|
979
|
+
the nearest selectable one (menus size to content, never scroll); Right opens
|
|
980
|
+
a submenu at its first item and Left leaves it; Enter/Space activate or open;
|
|
981
|
+
Escape closes **one level at a time**. In a `MenuBar`, Left/Right move between
|
|
982
|
+
menus when there is no submenu to move through, and with one menu open,
|
|
983
|
+
hovering another switches to it. Hovering a parent opens its submenu with
|
|
984
|
+
nothing selected. **Type-ahead** works in the deepest open level: 700ms
|
|
985
|
+
queries (`c`,`a` finds _Carrot_), a repeated letter cycles, disabled entries
|
|
986
|
+
and separators never match.
|
|
987
|
+
|
|
988
|
+
Focus stays in the _owner_ window, as with `Select`; `ContextMenu`'s wrapper
|
|
989
|
+
is focusable and takes focus when its menu opens, but is not a Tab stop
|
|
990
|
+
(`tabIndex={-1}`). Losing the window closes an open menu.
|
|
1443
991
|
|
|
1444
992
|
### Safe-polygon hover
|
|
1445
993
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
naive hover handling closes both just as the user aims at them. `MenuBar`,
|
|
1449
|
-
`ContextMenu` and `Tooltip` therefore use
|
|
994
|
+
`MenuBar`, `ContextMenu` and `Tooltip` keep a submenu or hint open while the
|
|
995
|
+
pointer crosses towards it, using
|
|
1450
996
|
[floating-ui's safePolygon](https://floating-ui.com/docs/usehover#safepolygon)
|
|
1451
997
|
idea: the triangle between where the pointer was and the near edge of the
|
|
1452
|
-
open surface counts as still hovering the parent.
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
but only for `SAFE_HOVER_DELAY` (320 ms), so a pointer that stops there
|
|
1456
|
-
still means what it landed on. Leaving the triangle switches immediately,
|
|
1457
|
-
and reaching the surface keeps it open for as long as the pointer stays.
|
|
998
|
+
open surface counts as still hovering the parent. Inside it, hover changes
|
|
999
|
+
wait at most `SAFE_HOVER_DELAY` (320 ms); leaving it switches at once, and
|
|
1000
|
+
reaching the surface keeps it open.
|
|
1458
1001
|
|
|
1459
1002
|
The helpers are exported for widgets of your own:
|
|
1460
1003
|
`movingToward(point, apex, rect)`, `safePolygon(apex, rect, buffer)`,
|
|
1461
|
-
`pointInPolygon(point, polygon)` and `screenPoint(ev)
|
|
1462
|
-
|
|
1463
|
-
windows.
|
|
1004
|
+
`pointInPolygon(point, polygon)` and `screenPoint(ev)`, all in **screen**
|
|
1005
|
+
coordinates, since trigger and popup are different windows.
|
|
1464
1006
|
|
|
1465
1007
|
## `Tabs`
|
|
1466
1008
|
|
|
@@ -1484,21 +1026,18 @@ One panel visible at a time, switched by a strip of tabs.
|
|
|
1484
1026
|
| `orientation` | `'horizontal'` (default) or `'vertical'` |
|
|
1485
1027
|
| `manual` | arrows move focus only; Enter or Space commits |
|
|
1486
1028
|
|
|
1487
|
-
`content` may be a node or a function
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
and wrap, Home/End jump to the ends, disabled tabs are skipped. Arrows
|
|
1495
|
-
select as they move, the way a desktop notebook behaves; `manual` splits
|
|
1496
|
-
focus from selection, which is what you want when a panel is expensive.
|
|
1029
|
+
`content` may be a node or a function, called only while its tab is
|
|
1030
|
+
selected. Items with no `content` make `Tabs` a pure navigator (in
|
|
1031
|
+
`examples/app.jsx` the strip is in one `SplitPane` half, the panel in the
|
|
1032
|
+
other). The strip is a **single tab stop**: Left/Right (Up/Down when
|
|
1033
|
+
vertical) move and wrap, Home/End jump, disabled tabs are skipped. Arrows
|
|
1034
|
+
select as they move; `manual` splits focus from selection, for expensive
|
|
1035
|
+
panels.
|
|
1497
1036
|
|
|
1498
1037
|
## `Table`
|
|
1499
1038
|
|
|
1500
|
-
A grid with a header
|
|
1501
|
-
|
|
1039
|
+
A grid with a fixed header, resizable columns, and only the rows in view
|
|
1040
|
+
built.
|
|
1502
1041
|
|
|
1503
1042
|
```jsx
|
|
1504
1043
|
<Table
|
|
@@ -1522,12 +1061,8 @@ view actually built.
|
|
|
1522
1061
|
| `onColumnResize(id, width)` | after a header drag |
|
|
1523
1062
|
|
|
1524
1063
|
`value(row)` feeds sorting and the default cell text; `render(row, { selected,
|
|
1525
|
-
column })` replaces the cell
|
|
1526
|
-
|
|
1527
|
-
A `render` is **told when its row is selected**, because the selection is a
|
|
1528
|
-
filled bar and a colour picked against the resting background disappears
|
|
1529
|
-
into it — a directory in the accent, a failure in red. Fall back to
|
|
1530
|
-
`hoverText` there and let the glyph carry the meaning:
|
|
1064
|
+
column })` replaces the cell. `selected` is there because a colour picked for
|
|
1065
|
+
the resting row can vanish into the selection bar — fall back to `hoverText`:
|
|
1531
1066
|
|
|
1532
1067
|
```jsx
|
|
1533
1068
|
render: (row, { selected }) => (
|
|
@@ -1537,35 +1072,23 @@ render: (row, { selected }) => (
|
|
|
1537
1072
|
);
|
|
1538
1073
|
```
|
|
1539
1074
|
|
|
1540
|
-
**
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
`ev.detail` like any other multi-click.
|
|
1553
|
-
|
|
1554
|
-
The header scrolls sideways with the body but never vertically. Between two
|
|
1555
|
-
headers there is a hairline rule and, just left of it, a grab band seven
|
|
1556
|
-
pixels wide: the separator you see and the handle you hit are not the same
|
|
1557
|
-
size, because a boundary wants to be thin and a handle wants to be easy. The
|
|
1558
|
-
band is invisible until the pointer is on it and takes the accent while it is
|
|
1559
|
-
held, and it is a **sibling** of the header rather than a child of it — a
|
|
1560
|
-
click fires on the nearest common ancestor of press and release, so a handle
|
|
1561
|
-
inside the header would end every resize with a sort of the column that was
|
|
1562
|
-
just dragged. The handle takes focus on the press, so Left/Right resize by
|
|
1563
|
-
16px from the keyboard as well.
|
|
1075
|
+
**Every row must be `rowHeight` tall**: only the ~20 rows in view are
|
|
1076
|
+
mounted, swapped as you scroll, with spacers keeping the scrollbar true.
|
|
1077
|
+
Sorting a hundred thousand rows is still yours — pass `sort` and sort the
|
|
1078
|
+
data yourself.
|
|
1079
|
+
|
|
1080
|
+
The **table** holds focus, not the row (rows unmount off screen). Up/Down
|
|
1081
|
+
move the selection, PageUp/PageDown by a viewport, Home/End to the ends; a
|
|
1082
|
+
click selects and a double click (`ev.detail`) or Enter fires `onActivate`.
|
|
1083
|
+
The header scrolls sideways with the body, never vertically. A seven-pixel
|
|
1084
|
+
grab band left of each header rule resizes the column (accent while held,
|
|
1085
|
+
no sort on release); it takes focus on the press, so Left/Right then resize
|
|
1086
|
+
by 16px.
|
|
1564
1087
|
|
|
1565
1088
|
## `Tree`
|
|
1566
1089
|
|
|
1567
|
-
Moved to [`@react-x11/components`](ecosystem.md):
|
|
1568
|
-
drag-to-reorder and lazy children
|
|
1090
|
+
Moved to [`@react-x11/components`](ecosystem.md): virtualisation,
|
|
1091
|
+
drag-to-reorder and lazy children do not belong in core.
|
|
1569
1092
|
|
|
1570
1093
|
## `SplitPane`
|
|
1571
1094
|
|
|
@@ -1585,26 +1108,22 @@ Two panes with a divider you can drag.
|
|
|
1585
1108
|
| `onResize(size)` | after a drag or a key step |
|
|
1586
1109
|
| `min` / `minSecond` | how small either pane may get |
|
|
1587
1110
|
|
|
1588
|
-
Only the first pane's size is stored; the second takes
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
The divider is focusable: arrows move it by 16px, Home/End drive it to
|
|
1594
|
-
either limit. Dragging captures the pointer, so it keeps tracking after the
|
|
1595
|
-
pointer leaves the six pixels it started on, and it keeps the grip where it
|
|
1596
|
-
was taken rather than jumping to the pointer.
|
|
1111
|
+
Only the first pane's size is stored; the second takes the rest, so a window
|
|
1112
|
+
resize never leaves a gap, and the drag clamps against the container as laid
|
|
1113
|
+
out at that moment. The divider is focusable: arrows move it 16px, Home/End
|
|
1114
|
+
to either limit. A drag captures the pointer and keeps the grip where it was
|
|
1115
|
+
taken.
|
|
1597
1116
|
|
|
1598
1117
|
## `Canvas3D`
|
|
1599
1118
|
|
|
1600
|
-
Moved to [`@react-x11/components/three`](ecosystem.md) as `<Canvas
|
|
1601
|
-
|
|
1602
|
-
|
|
1119
|
+
Moved to [`@react-x11/components/three`](ecosystem.md) as `<Canvas>`. Core
|
|
1120
|
+
keeps the `<glarea>` surface under it — [gl.md](gl.md),
|
|
1121
|
+
`examples/viewer3d.jsx`.
|
|
1603
1122
|
|
|
1604
1123
|
## `useAnchor(ref)` / `anchorRect(node, options)`
|
|
1605
1124
|
|
|
1606
|
-
The placement math behind `Select` and `Tooltip`,
|
|
1607
|
-
|
|
1125
|
+
The placement math behind `Select` and `Tooltip`, for popup widgets of your
|
|
1126
|
+
own.
|
|
1608
1127
|
|
|
1609
1128
|
```jsx
|
|
1610
1129
|
const ref = useRef(null);
|
|
@@ -1623,39 +1142,29 @@ const rect = measure({ placement: 'bottom', align: 'center', width, height });
|
|
|
1623
1142
|
| `at` | a rect **inside** the node to anchor to instead — a caret, a cell |
|
|
1624
1143
|
| `width`, `height` | size of the popup you are positioning |
|
|
1625
1144
|
| `alignTo` | node the alignment reads, when it is not the anchor |
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
back as `placement`.
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
when the popup is above or below its trigger — with one beside it the
|
|
1642
|
-
alignment axis is vertical, and nothing vertical mirrors. The direction is
|
|
1643
|
-
read off the anchoring node, so a menu inside a mirrored panel needs no
|
|
1644
|
-
argument.
|
|
1645
|
-
|
|
1646
|
-
`alignTo` takes the two axes from **different nodes**: the placement edge
|
|
1647
|
-
from the anchor, the alignment from `alignTo`. A submenu is the case that
|
|
1648
|
-
needs it — it belongs against the outer edge of the menu it comes out of,
|
|
1649
|
-
but level with the row that opened it, and that row is inset by the menu's
|
|
1650
|
-
border and padding. Both nodes have to be in the same window.
|
|
1145
|
+
| `alignOffset` | shift along the alignment axis in px, applied before clamping |
|
|
1146
|
+
|
|
1147
|
+
`placement` is a **preference**: near a screen edge the popup flips to the
|
|
1148
|
+
opposite side, and it is clamped into the screen either way; the side used
|
|
1149
|
+
comes back as `placement`. With no screen geometry it does not clamp.
|
|
1150
|
+
`'center'` puts the popup **over** the anchor, middle on middle, only
|
|
1151
|
+
clamped (`align`/`offset` ignored) — what [`Dialog`](#size-and-placement)
|
|
1152
|
+
uses.
|
|
1153
|
+
|
|
1154
|
+
`'start'`/`'end'` are **logical** (a submenu opens towards `'end'`, leftwards
|
|
1155
|
+
when mirrored); `'left'`/`'right'` are physical. `align` mirrors too, but
|
|
1156
|
+
only above or below the anchor. The direction is read off the anchoring
|
|
1157
|
+
node. `alignTo` takes the alignment from another node in the same window — a
|
|
1158
|
+
submenu sits against its menu's outer edge but level with the row that
|
|
1159
|
+
opened it, and `alignOffset` lines its first item up with that row's text.
|
|
1651
1160
|
|
|
1652
1161
|
### `at` — anchoring to part of a node
|
|
1653
1162
|
|
|
1654
|
-
`at` is a rect in the **anchor node's own coordinates
|
|
1655
|
-
anchor
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1163
|
+
`at` is a rect in the **anchor node's own coordinates** that becomes the
|
|
1164
|
+
anchor — for a caret, a table cell, a datapoint, a highlighted span. `{x, y}`
|
|
1165
|
+
alone is a point; the popup `width` then defaults to the sub-rect's. Being
|
|
1166
|
+
node-relative, it lets `useAnchorTracking` follow a caret for free, out-of-view
|
|
1167
|
+
test included.
|
|
1659
1168
|
|
|
1660
1169
|
```jsx
|
|
1661
1170
|
const measure = useAnchor(editorRef);
|
|
@@ -1667,51 +1176,33 @@ const rect = measure({
|
|
|
1667
1176
|
});
|
|
1668
1177
|
```
|
|
1669
1178
|
|
|
1670
|
-
`width` and `height` are optional (`{x, y}` alone is a point), and `width`
|
|
1671
|
-
on the popup then defaults to the sub-rect's rather than the node's.
|
|
1672
|
-
|
|
1673
|
-
Node-relative rather than screen-relative on purpose: the offset stays true
|
|
1674
|
-
through everything that moves the node, so `useAnchorTracking` follows a
|
|
1675
|
-
caret with no extra work — and its out-of-view test becomes the caret's,
|
|
1676
|
-
which is what you want, since an editor's own lines scroll away long before
|
|
1677
|
-
the editor does.
|
|
1678
|
-
|
|
1679
1179
|
### `anchorScreenRect(app, rect, options)` — a rect with no node behind it
|
|
1680
1180
|
|
|
1681
|
-
The same placement against a rect
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
display. With no node to ask, `options.scale` is the display's — it
|
|
1687
|
-
defaults to 1 — and `options.direction` decides what `'start'` and `'end'`
|
|
1688
|
-
mean. A `<popup>` does this itself with
|
|
1181
|
+
The same placement against a **screen** rect in logical pixels — a tray
|
|
1182
|
+
item's frame, where the pointer was; `{x, y}` alone is a point. The popup
|
|
1183
|
+
stays on the monitor the whole rect overlaps most (the nearest if none).
|
|
1184
|
+
`options.scale` is the display's (default 1) and `options.direction` decides
|
|
1185
|
+
`'start'`/`'end'`. A `<popup>` does this itself with
|
|
1689
1186
|
[`anchor={{ rect }}`](elements.md#a-popover-under-a-tray-item).
|
|
1690
1187
|
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
already rendered. `<popup anchor={{ to: ref, at }}>` hands the placement to
|
|
1695
|
-
the popup, which does it in the one place the size is known —
|
|
1188
|
+
A popup that must **measure itself first** (rows sized to labels) cannot
|
|
1189
|
+
know its size at render; `<popup anchor={{ to: ref, at }}>` lets the popup
|
|
1190
|
+
place itself once laid out —
|
|
1696
1191
|
[elements.md](elements.md#anchor--a-popup-that-places-itself).
|
|
1697
1192
|
|
|
1698
1193
|
### `anchorArea(node)`
|
|
1699
1194
|
|
|
1700
|
-
The
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
What needs it is a surface sizing itself: a menu measured to its longest
|
|
1706
|
-
label is never usefully wider than the screen it opens on, and `anchorRect`
|
|
1707
|
-
can slide a popup back from an edge but nothing rescues one that does not
|
|
1708
|
-
fit. `Select` and `ContextMenu` both cap themselves this way.
|
|
1195
|
+
The usable part of the monitor `node` is on — panels and docks taken off, the
|
|
1196
|
+
cap [`<window width="auto">`](elements.md#natural-size) uses — or `null` with
|
|
1197
|
+
no display. A surface sizing itself caps itself to it (`Select` and
|
|
1198
|
+
`ContextMenu` do), since `anchorRect` can slide a popup back from an edge but
|
|
1199
|
+
not rescue one too big for the screen.
|
|
1709
1200
|
|
|
1710
1201
|
## `useDropTarget(options)` / `useDragSource(options)`
|
|
1711
1202
|
|
|
1712
|
-
The render-state layer over the drag-and-drop props, for
|
|
1713
|
-
`':drag-over'` or `':dragging'` style block cannot
|
|
1714
|
-
disabled sibling, a
|
|
1203
|
+
The render-state layer over the drag-and-drop props, for what a
|
|
1204
|
+
`':drag-over'` or `':dragging'` style block cannot do — a hint label, a
|
|
1205
|
+
disabled sibling, a preview that follows the pointer.
|
|
1715
1206
|
|
|
1716
1207
|
```jsx
|
|
1717
1208
|
const { dropProps, isOver, isAccepted } = useDropTarget({
|
|
@@ -1725,25 +1216,16 @@ const { dragProps, isDragging, position } = useDragSource({
|
|
|
1725
1216
|
});
|
|
1726
1217
|
```
|
|
1727
1218
|
|
|
1728
|
-
Spread `dropProps` / `dragProps` on any drawn element
|
|
1729
|
-
props
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
[drag-and-drop.md](drag-and-drop.md).
|
|
1219
|
+
Spread `dropProps` / `dragProps` on any drawn element; they are the same
|
|
1220
|
+
props you could write by hand, so use them when the render changes and the
|
|
1221
|
+
plain props when it does not. `position` is `{ x, y, accepted }` in screen
|
|
1222
|
+
coordinates during a drag — what a `<popup dragPreview>` follows. Full
|
|
1223
|
+
reference: [drag-and-drop.md](drag-and-drop.md).
|
|
1734
1224
|
|
|
1735
1225
|
---
|
|
1736
1226
|
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
must escape the window bounds, and a ref to the trigger node for anchoring
|
|
1743
|
-
(`node.abs` + `node.root.window.x/y`).
|
|
1744
|
-
|
|
1745
|
-
The window's screen position comes from the server (`TranslateCoordinates`),
|
|
1746
|
-
refreshed when the window is realized and whenever it is configured — not
|
|
1747
|
-
from `window.x`/`y`. Once a reparenting window manager has put the window
|
|
1748
|
-
inside its frame, those are relative to the _frame_, and a popup placed with
|
|
1749
|
-
them lands near the corner of the screen instead of under its trigger.
|
|
1227
|
+
To build a popup widget of your own, `src/components/Select.js` is the
|
|
1228
|
+
reference: state in React, `:hover` style blocks, a `<popup>` for anything
|
|
1229
|
+
that must escape the window, `useAnchor` on the trigger. Do not place a popup
|
|
1230
|
+
from a window's own `x`/`y` — under a reparenting window manager they are
|
|
1231
|
+
frame-relative; `useAnchor` and `ev.screenX`/`screenY` come from the server.
|