react-x11 2.51.1 → 2.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -61
- package/docs/README.md +256 -0
- package/docs/accessibility.md +575 -0
- package/docs/appearance.md +317 -0
- package/docs/click-to-component.md +92 -0
- package/docs/clipboard.md +262 -0
- package/docs/components.md +1231 -0
- package/docs/dbus.md +205 -0
- package/docs/debugging.md +371 -0
- package/docs/desktop-calendar.md +247 -0
- package/docs/desktop.md +523 -0
- package/docs/devtools.md +133 -0
- package/docs/drag-and-drop.md +585 -0
- package/docs/ecosystem/animation.md +360 -0
- package/docs/ecosystem/data-fetching.md +186 -0
- package/docs/ecosystem/dev-tooling.md +393 -0
- package/docs/ecosystem/forms.md +277 -0
- package/docs/ecosystem/headless.md +194 -0
- package/docs/ecosystem/i18n.md +86 -0
- package/docs/ecosystem/icons.md +377 -0
- package/docs/ecosystem/layout.md +206 -0
- package/docs/ecosystem/routing.md +135 -0
- package/docs/ecosystem/state.md +344 -0
- package/docs/ecosystem/testing.md +474 -0
- package/docs/ecosystem/theming.md +494 -0
- package/docs/ecosystem.md +439 -0
- package/docs/elements.md +2079 -0
- package/docs/embedding.md +239 -0
- package/docs/events.md +742 -0
- package/docs/extending.md +1700 -0
- package/docs/eyedropper.md +164 -0
- package/docs/filedialog.md +222 -0
- package/docs/frame.md +436 -0
- package/docs/gl.md +173 -0
- package/docs/globalmenu.md +195 -0
- package/docs/macos.md +592 -0
- package/docs/notifications.md +150 -0
- package/docs/packaging.md +468 -0
- package/docs/permissions.md +143 -0
- package/docs/react-features.md +405 -0
- package/docs/remote.md +179 -0
- package/docs/scale.md +225 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1710 -0
- package/docs/system.md +519 -0
- package/docs/testing.md +494 -0
- package/docs/typescript.md +75 -0
- package/docs/uri-schemes.md +340 -0
- package/docs/wayland-backend.md +332 -0
- package/docs/windows-integrations.md +177 -0
- package/docs/windows.md +380 -0
- package/package.json +11 -3
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/dnd.js +2 -2
- package/src/filedialog.js +3 -3
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/keyboardstate.js +28 -0
- package/src/layouts.js +1 -1
- package/src/node.d.ts +10 -10
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/portal.js +31 -0
- package/src/refresh/index.js +2 -1
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +86 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/video.js +376 -0
- package/src/wayland/xkb.js +137 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
|
@@ -0,0 +1,1231 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
Every component takes **`style`** and merges it after its own, so an override
|
|
4
|
+
wins by position rather than by clobbering a computed value:
|
|
5
|
+
|
|
6
|
+
```jsx
|
|
7
|
+
<Button
|
|
8
|
+
label="Sign"
|
|
9
|
+
style={{ marginTop: 8, ':hover': { borderColor: '#333' } }}
|
|
10
|
+
/>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Props that a component consumes itself are never style — `ProgressBar`'s
|
|
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).
|
|
17
|
+
|
|
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.
|
|
23
|
+
|
|
24
|
+
## Theming
|
|
25
|
+
|
|
26
|
+
**Widgets follow the desktop with nothing declared** — dark on a dark desktop,
|
|
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.
|
|
29
|
+
|
|
30
|
+
`<ThemeProvider value={palette}>` gives every widget beneath it a palette,
|
|
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**:
|
|
33
|
+
|
|
34
|
+
| token | |
|
|
35
|
+
| --------------------------------------------------------------- | ------------------------------------ |
|
|
36
|
+
| `background` `surface` | the ground, and what is raised on it |
|
|
37
|
+
| `text` `textMuted` `border` | ink, secondary ink, edges |
|
|
38
|
+
| `accent` `accentHover` `accentText` | primary buttons, checks, fills |
|
|
39
|
+
| `hoverBackground` `hoverText` | selected rows, menu highlights |
|
|
40
|
+
| `surfaceHover` `track` `borderFocus` | hover fills, tracks, focus |
|
|
41
|
+
| `selection` `caret` `link` | selected text, the caret, links |
|
|
42
|
+
| `danger` `success` `warning` `info` (+`…Text`) | what the app says with colour |
|
|
43
|
+
| `accentActive` `surfaceActive` `textMutedActive` `dangerActive` | the pressed step of each fill |
|
|
44
|
+
| `radius` `radiusSmall` `borderWidth` | control shape |
|
|
45
|
+
| `radiusPopup` `radiusPopupItem` `radiusTooltip` | floating-surface shape |
|
|
46
|
+
| `fontSize` `paddingX` `paddingY` | control size |
|
|
47
|
+
| `fontFamily` `monoFamily` | the app's two faces |
|
|
48
|
+
| `direction` | which way the app reads |
|
|
49
|
+
|
|
50
|
+
### The ground and what is on it
|
|
51
|
+
|
|
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.
|
|
58
|
+
|
|
59
|
+
```jsx
|
|
60
|
+
<ThemeProvider value={{ background: '#f6f8fa', surface: '#ffffff' }}>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### What the app says with colour
|
|
64
|
+
|
|
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**:
|
|
70
|
+
|
|
71
|
+
```jsx
|
|
72
|
+
<text style={{ color: '$danger' }}>Password too short</text>
|
|
73
|
+
<box style={{ backgroundColor: '$danger' }}>
|
|
74
|
+
<text style={{ color: '$dangerText' }}>Delete</text>
|
|
75
|
+
</box>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For a tinted panel, tint the fill and leave the ink alone:
|
|
79
|
+
|
|
80
|
+
```jsx
|
|
81
|
+
import { tint } from 'react-x11/style';
|
|
82
|
+
<box style={{ backgroundColor: tint(theme.danger, 0.12) }}>
|
|
83
|
+
<text style={{ color: '$danger' }}>Could not save</text>
|
|
84
|
+
</box>;
|
|
85
|
+
```
|
|
86
|
+
|
|
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.
|
|
103
|
+
|
|
104
|
+
### Which way it reads
|
|
105
|
+
|
|
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:
|
|
109
|
+
|
|
110
|
+
```jsx
|
|
111
|
+
<ThemeProvider value={{ direction: settings.rtl ? 'rtl' : 'ltr' }}>
|
|
112
|
+
```
|
|
113
|
+
|
|
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:
|
|
120
|
+
|
|
121
|
+
| widget | what mirrors |
|
|
122
|
+
| ------------------------- | --------------------------------------------------- |
|
|
123
|
+
| `Slider` | the drag, and Left/Right (Up/Down do not) |
|
|
124
|
+
| `Tabs` | Left/Right walk the strip the way it is drawn |
|
|
125
|
+
| `Table` | column resizing, the sort mark, the header's scroll |
|
|
126
|
+
| `SplitPane` | which pane is first, the drag and its arrows |
|
|
127
|
+
| `MenuBar` / `ContextMenu` | submenus open leftwards, and their arrows |
|
|
128
|
+
| `Select`, `PasswordInput` | the field's own insets follow the text |
|
|
129
|
+
|
|
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).
|
|
135
|
+
|
|
136
|
+
### Size and shape
|
|
137
|
+
|
|
138
|
+
`paddingY` is the space **you can see**, above the capitals and below the
|
|
139
|
+
baseline: every widget label is trimmed to its letters
|
|
140
|
+
([styling.md](styling.md#measuring-text-to-its-letters)), so a control is its
|
|
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:
|
|
165
|
+
|
|
166
|
+
```jsx
|
|
167
|
+
<ThemeProvider value={dark}>
|
|
168
|
+
<box style={{ backgroundColor: '$background' }}>…</box>
|
|
169
|
+
</ThemeProvider>
|
|
170
|
+
```
|
|
171
|
+
|
|
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.
|
|
176
|
+
|
|
177
|
+
```jsx
|
|
178
|
+
function Panel() {
|
|
179
|
+
const theme = useTheme();
|
|
180
|
+
return <box style={{ borderColor: theme.border }} />;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
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):
|
|
188
|
+
|
|
189
|
+
```jsx
|
|
190
|
+
root.render(
|
|
191
|
+
<ThemeProvider value={brand} colorScheme={settings.theme}>
|
|
192
|
+
<App />
|
|
193
|
+
</ThemeProvider>,
|
|
194
|
+
);
|
|
195
|
+
```
|
|
196
|
+
|
|
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.
|
|
203
|
+
|
|
204
|
+
`examples/themes.js` has GitHub, macOS and Windows palettes in light and dark;
|
|
205
|
+
`npm run examples:theming` switches between them.
|
|
206
|
+
|
|
207
|
+
## System icons
|
|
208
|
+
|
|
209
|
+
The glyphs the widgets are drawn with, exported so your toolbar and a
|
|
210
|
+
`Select` agree about which way a chevron points.
|
|
211
|
+
|
|
212
|
+
```jsx
|
|
213
|
+
import { Icon } from 'react-x11';
|
|
214
|
+
|
|
215
|
+
<Icon name="chevronDown" />
|
|
216
|
+
<Icon name="check" size={10} color={theme.accentText} />
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
| name | |
|
|
220
|
+
| ------------------------------------------------------ | ------------------------------------ |
|
|
221
|
+
| `chevronRight` `chevronLeft` `chevronDown` `chevronUp` | more this way: menus, twisties, sort |
|
|
222
|
+
| `check` `dash` `dot` | chosen, partly chosen, one of many |
|
|
223
|
+
| `close` `plus` `moreVertical` | dismiss, add, overflow |
|
|
224
|
+
| `eye` `eyeOff` | reveal a secret, hide it again |
|
|
225
|
+
|
|
226
|
+
| prop | |
|
|
227
|
+
| ------- | ----------------------------------------------------- |
|
|
228
|
+
| `name` | one of the above; anything else throws |
|
|
229
|
+
| `size` | the mark, px. Default: a shade under `theme.fontSize` |
|
|
230
|
+
| `color` | the ink. Default: `theme.text` |
|
|
231
|
+
|
|
232
|
+
Everything else goes to the host `<canvas>`, so a clickable one is
|
|
233
|
+
`<Icon name="close" onClick={…} focusable />`.
|
|
234
|
+
|
|
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.
|
|
240
|
+
|
|
241
|
+
### Affordances, not nouns
|
|
242
|
+
|
|
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.
|
|
250
|
+
|
|
251
|
+
### Colour inherits; size does not
|
|
252
|
+
|
|
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):
|
|
257
|
+
|
|
258
|
+
```jsx
|
|
259
|
+
<box style={{ color: theme.textMuted, ':hover': { color: theme.accent } }}>
|
|
260
|
+
<text>Open recent</text>
|
|
261
|
+
<Icon name="chevronRight" />
|
|
262
|
+
</box>
|
|
263
|
+
```
|
|
264
|
+
|
|
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.
|
|
268
|
+
|
|
269
|
+
### What it costs to draw one
|
|
270
|
+
|
|
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:
|
|
277
|
+
|
|
278
|
+
```jsx
|
|
279
|
+
import { icons } from 'react-x11';
|
|
280
|
+
|
|
281
|
+
<canvas
|
|
282
|
+
mono
|
|
283
|
+
cacheKey="check"
|
|
284
|
+
onDraw={icons.check}
|
|
285
|
+
style={{ width: 12, height: 12, color: '$textMuted' }}
|
|
286
|
+
/>;
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Icons are **`aria-hidden` by default** — the meaning is on the control's
|
|
290
|
+
`role` and `aria-expanded`. Name one
|
|
291
|
+
(`aria-hidden={false} aria-label="Close"`) only when the icon _is_ the
|
|
292
|
+
control.
|
|
293
|
+
|
|
294
|
+
## Basic controls
|
|
295
|
+
|
|
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.
|
|
304
|
+
|
|
305
|
+
```jsx
|
|
306
|
+
import { Button, Checkbox, RadioGroup, Radio, Switch, ProgressBar } from 'react-x11';
|
|
307
|
+
|
|
308
|
+
<Button primary onPress={save}>
|
|
309
|
+
Save
|
|
310
|
+
</Button>
|
|
311
|
+
<Checkbox checked={wrap} onChange={(ev) => setWrap(ev.value)}>Wrap lines</Checkbox>
|
|
312
|
+
<Switch checked={live} onChange={(ev) => setLive(ev.value)} />
|
|
313
|
+
<ProgressBar value={0.4} style={{ width: 200 }} />
|
|
314
|
+
|
|
315
|
+
<RadioGroup value={size} onChange={(ev) => setSize(ev.value)}>
|
|
316
|
+
<Radio value="s">Small</Radio>
|
|
317
|
+
<Radio value="m">Medium</Radio>
|
|
318
|
+
</RadioGroup>;
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Label text is the children (or a `label` prop); a bare string is wrapped in
|
|
322
|
+
a `<text>` for you. All five are demoed in `examples/widgets.jsx`.
|
|
323
|
+
|
|
324
|
+
### Native bezels
|
|
325
|
+
|
|
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:
|
|
331
|
+
|
|
332
|
+
```jsx
|
|
333
|
+
// the app's identity
|
|
334
|
+
<ThemeProvider value={{ controls: 'drawn' }}>
|
|
335
|
+
{/* one control */}
|
|
336
|
+
<Button native={false}>Brand</Button>
|
|
337
|
+
</ThemeProvider>;
|
|
338
|
+
|
|
339
|
+
const canGoNative = useSupports('nativeControls'); // what the backend has
|
|
340
|
+
```
|
|
341
|
+
|
|
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).
|
|
357
|
+
|
|
358
|
+
### The press state
|
|
359
|
+
|
|
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.
|
|
368
|
+
|
|
369
|
+
### The change event, and `name`
|
|
370
|
+
|
|
371
|
+
Every value control — `Checkbox`, `Switch`, `RadioGroup`, `Select`,
|
|
372
|
+
`Slider` — calls `onChange(ev)` with the **same change event**
|
|
373
|
+
`<textinput>` and `<textarea>` use
|
|
374
|
+
([elements.md](elements.md#the-change-event)); the new value is `ev.value`:
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
{ type: 'change',
|
|
378
|
+
target: { type: 'checkbox', name: 'agree', value: true, checked: true },
|
|
379
|
+
currentTarget: /* the same object */,
|
|
380
|
+
name: 'agree', value: true }
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
So a form library's handler goes straight onto any control:
|
|
384
|
+
|
|
385
|
+
```jsx
|
|
386
|
+
<textinput name="host" value={f.values.host} onChange={f.handleChange} />
|
|
387
|
+
<Checkbox name="agree" checked={f.values.agree} onChange={f.handleChange} />
|
|
388
|
+
```
|
|
389
|
+
|
|
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.
|
|
394
|
+
|
|
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).
|
|
399
|
+
|
|
400
|
+
### `Button`
|
|
401
|
+
|
|
402
|
+
| prop | |
|
|
403
|
+
| -------------------- | ----------------------------------------------------------------------- |
|
|
404
|
+
| `children` / `label` | the label |
|
|
405
|
+
| `onPress()` | click, Space or Enter |
|
|
406
|
+
| `variant` | `'solid'` (default), `'outline'`, `'ghost'` — the chrome |
|
|
407
|
+
| `size` | `'medium'` (default) or `'small'` — the compact metric |
|
|
408
|
+
| `primary` | speaks in the accent: the fill when solid, the ink and border otherwise |
|
|
409
|
+
| `disabled` | inert, dimmed, not focusable |
|
|
410
|
+
|
|
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:
|
|
423
|
+
|
|
424
|
+
```jsx
|
|
425
|
+
<Button primary disabled={busy} onPress={run}>
|
|
426
|
+
<Icon name="chevronRight" />
|
|
427
|
+
Continue
|
|
428
|
+
</Button>
|
|
429
|
+
|
|
430
|
+
<Button variant="ghost" size="small" aria-label="Remove" onPress={remove}>
|
|
431
|
+
<Icon name="close" />
|
|
432
|
+
</Button>
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### `Checkbox`
|
|
436
|
+
|
|
437
|
+
| prop | |
|
|
438
|
+
| -------------------- | ----------------------------------------- |
|
|
439
|
+
| `checked` | current value (controlled) |
|
|
440
|
+
| `onChange(ev)` | a change event; the value is `ev.value` |
|
|
441
|
+
| `name` | field name, for form libraries |
|
|
442
|
+
| `children` / `label` | label to the right of the 16px check well |
|
|
443
|
+
| `disabled` | inert, dimmed |
|
|
444
|
+
|
|
445
|
+
### `Radio` / `RadioGroup`
|
|
446
|
+
|
|
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.
|
|
453
|
+
|
|
454
|
+
### `Switch`
|
|
455
|
+
|
|
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)).
|
|
459
|
+
|
|
460
|
+
### `ProgressBar`
|
|
461
|
+
|
|
462
|
+
Determinate with a `value`, indeterminate without one.
|
|
463
|
+
|
|
464
|
+
| prop | |
|
|
465
|
+
| --------------- | ------------------------------------------------------- |
|
|
466
|
+
| `value` | 0 to 1, clamped |
|
|
467
|
+
| `indeterminate` | working, extent unknown — a block slides across forever |
|
|
468
|
+
| `color` | fill colour (defaults to the theme accent) |
|
|
469
|
+
| `trackColor` | the groove (defaults to the theme track) |
|
|
470
|
+
| `height` | bar thickness, default 8; the corner radius follows it |
|
|
471
|
+
|
|
472
|
+
```jsx
|
|
473
|
+
<ProgressBar value={0.4} style={{ width: 200 }} />
|
|
474
|
+
<ProgressBar indeterminate style={{ width: 200 }} />
|
|
475
|
+
```
|
|
476
|
+
|
|
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.
|
|
483
|
+
|
|
484
|
+
## `Select`
|
|
485
|
+
|
|
486
|
+
A dropdown whose menu is an override-redirect `<popup>` anchored below the
|
|
487
|
+
trigger.
|
|
488
|
+
|
|
489
|
+
```jsx
|
|
490
|
+
import { Select } from 'react-x11';
|
|
491
|
+
|
|
492
|
+
<Select
|
|
493
|
+
style={{ width: 160 }}
|
|
494
|
+
value={color}
|
|
495
|
+
options={[
|
|
496
|
+
{ value: '#2980b9', label: 'Blue' },
|
|
497
|
+
{ value: '#c0392b', label: 'Red' },
|
|
498
|
+
'green', // shorthand: value === label
|
|
499
|
+
]}
|
|
500
|
+
onChange={(ev) => setColor(ev.value)}
|
|
501
|
+
placeholder="Pick a color…"
|
|
502
|
+
/>;
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
| prop | |
|
|
506
|
+
| ----------------------- | ----------------------------------------- |
|
|
507
|
+
| `options` | array of `{value, label}` or plain values |
|
|
508
|
+
| `value`, `onChange(ev)` | selection |
|
|
509
|
+
| `name` | field name, for form libraries |
|
|
510
|
+
| `placeholder` | trigger text when nothing is selected |
|
|
511
|
+
| `style` + any box props | forwarded to the trigger box |
|
|
512
|
+
| `labelStyle` | the caption's style, over `text` |
|
|
513
|
+
| `chevronStyle` | the chevron's style, over `textMuted` |
|
|
514
|
+
| `native`, `nativeMenu` | the platform's bezel, and its menu |
|
|
515
|
+
|
|
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:
|
|
519
|
+
|
|
520
|
+
```jsx
|
|
521
|
+
<Select
|
|
522
|
+
value={sort}
|
|
523
|
+
options={['Newest', 'Oldest', 'Most replies']}
|
|
524
|
+
onChange={(ev) => setSort(ev.value)}
|
|
525
|
+
style={{
|
|
526
|
+
borderWidth: 0,
|
|
527
|
+
paddingLeft: 0,
|
|
528
|
+
paddingRight: 0,
|
|
529
|
+
backgroundColor: 'transparent',
|
|
530
|
+
':hover': { backgroundColor: 'transparent' },
|
|
531
|
+
}}
|
|
532
|
+
labelStyle={{ color: theme.accent, fontWeight: 'bold' }}
|
|
533
|
+
chevronStyle={{ color: theme.accent }}
|
|
534
|
+
/>
|
|
535
|
+
```
|
|
536
|
+
|
|
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.
|
|
576
|
+
|
|
577
|
+
### Theming
|
|
578
|
+
|
|
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`.
|
|
583
|
+
|
|
584
|
+
## `Calendar` / `DatePicker`
|
|
585
|
+
|
|
586
|
+
Moved to [`@react-x11/components`](ecosystem.md): a date picker is a locale
|
|
587
|
+
problem before it is a widget.
|
|
588
|
+
|
|
589
|
+
## `PasswordInput`
|
|
590
|
+
|
|
591
|
+
A masked field whose mask is a **scribble**, not a row of bullets.
|
|
592
|
+
|
|
593
|
+
```jsx
|
|
594
|
+
<PasswordInput
|
|
595
|
+
value={secret}
|
|
596
|
+
onChange={(ev) => setSecret(ev.value)}
|
|
597
|
+
onSubmit={() => signIn()}
|
|
598
|
+
/>
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
| prop | |
|
|
602
|
+
| ----------------------------- | -------------------------------------- |
|
|
603
|
+
| `value` / `defaultValue` | the secret; controlled with `onChange` |
|
|
604
|
+
| `onChange(ev)` | `ev.value` is the new value |
|
|
605
|
+
| `onSubmit(value)` | Enter |
|
|
606
|
+
| `placeholder` | shown while empty (`'Password'`) |
|
|
607
|
+
| `revealable` | show the eye at all, default `true` |
|
|
608
|
+
| `revealed` / `onRevealChange` | drive the reveal yourself |
|
|
609
|
+
| `maxLength` | in code points |
|
|
610
|
+
| `drawMask(ctx, info)` | draw the mask yourself |
|
|
611
|
+
| `disabled` | not focusable, not editable |
|
|
612
|
+
|
|
613
|
+
### Why a scribble
|
|
614
|
+
|
|
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).
|
|
622
|
+
|
|
623
|
+
### Two states, and what holds across both
|
|
624
|
+
|
|
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.
|
|
637
|
+
|
|
638
|
+
### Drawing your own mask
|
|
639
|
+
|
|
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.
|
|
644
|
+
|
|
645
|
+
```jsx
|
|
646
|
+
<PasswordInput
|
|
647
|
+
drawMask={(ctx, { width, height, color }) => {
|
|
648
|
+
ctx.fillStyle = color;
|
|
649
|
+
ctx.fillRect(0, height / 2 - 1, width, 2);
|
|
650
|
+
}}
|
|
651
|
+
/>
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### Password managers
|
|
655
|
+
|
|
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:
|
|
660
|
+
[desktop.md](desktop.md#password-fields-and-password-managers).
|
|
661
|
+
|
|
662
|
+
## `Slider`
|
|
663
|
+
|
|
664
|
+
A draggable value control.
|
|
665
|
+
|
|
666
|
+
```jsx
|
|
667
|
+
import { Slider } from 'react-x11';
|
|
668
|
+
|
|
669
|
+
<Slider
|
|
670
|
+
value={volume}
|
|
671
|
+
min={0}
|
|
672
|
+
max={100}
|
|
673
|
+
step={5}
|
|
674
|
+
style={{ width: 200 }}
|
|
675
|
+
onChange={(ev) => setVolume(ev.value)}
|
|
676
|
+
/>;
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
| prop | |
|
|
680
|
+
| ----------------------- | ------------------------------------------------------- |
|
|
681
|
+
| `value`, `onChange(ev)` | current value (controlled) |
|
|
682
|
+
| `name` | field name, for form libraries |
|
|
683
|
+
| `min`, `max`, `step` | range and quantisation (defaults 0, 100, 1) |
|
|
684
|
+
| `height` | the bar thickness (default 4); width comes from `style` |
|
|
685
|
+
| `disabled` | inert, dimmed |
|
|
686
|
+
| `thumbStyle` | the thumb's style, over its default |
|
|
687
|
+
| `trackStyle` | the track's style, over `height` and `track` |
|
|
688
|
+
| `fillStyle` | the filled part of the track, over `accent` |
|
|
689
|
+
|
|
690
|
+
The style slots restyle the parts and keep the drag, keyboard, `aria-*`
|
|
691
|
+
values, RTL and target size. A flat slider:
|
|
692
|
+
|
|
693
|
+
```jsx
|
|
694
|
+
<Slider
|
|
695
|
+
value={volume}
|
|
696
|
+
onChange={(ev) => setVolume(ev.value)}
|
|
697
|
+
style={{ width: 240 }}
|
|
698
|
+
thumbStyle={{
|
|
699
|
+
width: 14,
|
|
700
|
+
height: 14,
|
|
701
|
+
borderWidth: 0,
|
|
702
|
+
backgroundColor: theme.accent,
|
|
703
|
+
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
|
|
704
|
+
}}
|
|
705
|
+
trackStyle={{ height: 3, marginStart: 7, marginEnd: 7 }}
|
|
706
|
+
/>
|
|
707
|
+
```
|
|
708
|
+
|
|
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'`.
|
|
714
|
+
|
|
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.
|
|
718
|
+
|
|
719
|
+
## `Tooltip`
|
|
720
|
+
|
|
721
|
+
A hover hint in a `<popup>`, so it can extend past the owner window.
|
|
722
|
+
|
|
723
|
+
```jsx
|
|
724
|
+
import { Tooltip } from 'react-x11';
|
|
725
|
+
|
|
726
|
+
<Tooltip label="Back to zero">
|
|
727
|
+
<Button onPress={reset}>Reset</Button>
|
|
728
|
+
</Tooltip>;
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
| prop | |
|
|
732
|
+
| ----------------------- | ---------------------------------------------------------------------- |
|
|
733
|
+
| `label` | the hint: a string, or an element (nothing shows without it) |
|
|
734
|
+
| `direction` | `'auto'` (default), `'top'`, `'bottom'`, `'left'`, `'right'` |
|
|
735
|
+
| `delay` | ms of hover before showing (default 500) |
|
|
736
|
+
| `fontSize` | label size, also used to size the popup |
|
|
737
|
+
| `width`, `height` | pin an axis of the bubble exactly; anything not pinned is measured |
|
|
738
|
+
| `maxWidth`, `maxHeight` | cap what an element label measures out to, inside the screen's own cap |
|
|
739
|
+
|
|
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`):
|
|
754
|
+
|
|
755
|
+
```jsx
|
|
756
|
+
<Tooltip
|
|
757
|
+
label={
|
|
758
|
+
<box style={{ flexGrow: 1, padding: 10, gap: 7 }}>
|
|
759
|
+
<text style={{ color: '#f5f6fa' }}>Scratch volume</text>
|
|
760
|
+
<ProgressBar value={used} />
|
|
761
|
+
</box>
|
|
762
|
+
}
|
|
763
|
+
maxWidth={260}
|
|
764
|
+
direction="right"
|
|
765
|
+
>
|
|
766
|
+
<box>…</box>
|
|
767
|
+
</Tooltip>
|
|
768
|
+
```
|
|
769
|
+
|
|
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.
|
|
782
|
+
|
|
783
|
+
## `Dialog`
|
|
784
|
+
|
|
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.
|
|
789
|
+
|
|
790
|
+
```jsx
|
|
791
|
+
import { Button, Dialog } from 'react-x11';
|
|
792
|
+
|
|
793
|
+
<Dialog
|
|
794
|
+
open={confirming}
|
|
795
|
+
title="Clear the form?"
|
|
796
|
+
onClose={() => setConfirming(false)}
|
|
797
|
+
actions={
|
|
798
|
+
<>
|
|
799
|
+
<Button label="Cancel" onPress={() => setConfirming(false)} />
|
|
800
|
+
<Button primary autoFocus label="Clear" onPress={clear} />
|
|
801
|
+
</>
|
|
802
|
+
}
|
|
803
|
+
>
|
|
804
|
+
The name and the greeting below it will be discarded.
|
|
805
|
+
</Dialog>;
|
|
806
|
+
```
|
|
807
|
+
|
|
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.
|
|
826
|
+
|
|
827
|
+
### `managed`
|
|
828
|
+
|
|
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
|
|
838
|
+
([`transientFor`](elements.md#transientfor--a-window-that-belongs-to-another)).
|
|
839
|
+
|
|
840
|
+
### Size and placement
|
|
841
|
+
|
|
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
|
|
844
|
+
([`placement: 'center'`](elements.md#centred-over-something)).
|
|
845
|
+
|
|
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' }}>`.
|
|
860
|
+
|
|
861
|
+
## `MenuBar` / `ContextMenu`
|
|
862
|
+
|
|
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)).
|
|
867
|
+
|
|
868
|
+
```jsx
|
|
869
|
+
import { MenuBar, ContextMenu } from 'react-x11';
|
|
870
|
+
|
|
871
|
+
<MenuBar
|
|
872
|
+
menus={[
|
|
873
|
+
{
|
|
874
|
+
label: 'File',
|
|
875
|
+
items: [
|
|
876
|
+
{ label: 'New', shortcut: [['Control', 'N']], onSelect: newFile },
|
|
877
|
+
{ type: 'separator' },
|
|
878
|
+
{ label: 'Save As…', enabled: false },
|
|
879
|
+
{
|
|
880
|
+
label: 'Wrap lines',
|
|
881
|
+
toggleType: 'checkmark',
|
|
882
|
+
toggleState: wrap ? 1 : 0,
|
|
883
|
+
onSelect: toggleWrap,
|
|
884
|
+
},
|
|
885
|
+
],
|
|
886
|
+
},
|
|
887
|
+
]}
|
|
888
|
+
/>;
|
|
889
|
+
|
|
890
|
+
<ContextMenu items={items} style={{ flexGrow: 1 }}>
|
|
891
|
+
<text>Right-click me</text>
|
|
892
|
+
</ContextMenu>;
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
Item shape: `{ label, onSelect, type, items, enabled, visible, shortcut,
|
|
896
|
+
toggleType, toggleState, icon, iconName, disposition, key }`. Both take an
|
|
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).
|
|
913
|
+
|
|
914
|
+
**A context menu's items can come from the right-click.** The click records
|
|
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.
|
|
919
|
+
|
|
920
|
+
```jsx
|
|
921
|
+
const [target, setTarget] = useState(null);
|
|
922
|
+
|
|
923
|
+
<ContextMenu items={target ? itemsFor(target) : []}>
|
|
924
|
+
{rows.map((row) => (
|
|
925
|
+
<box key={row.id} onContextMenu={() => setTarget(row)}>
|
|
926
|
+
<text>{row.name}</text>
|
|
927
|
+
</box>
|
|
928
|
+
))}
|
|
929
|
+
</ContextMenu>;
|
|
930
|
+
```
|
|
931
|
+
|
|
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 —
|
|
949
|
+
|
|
950
|
+
```jsx
|
|
951
|
+
const save = ({ color, size }) => (
|
|
952
|
+
<svg source={SAVE_SVG} style={{ width: size, height: size, color }} />
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
{ label: 'Save', icon: save, shortcut: [['Control', 'S']], onSelect: onSave }
|
|
956
|
+
{ label: 'Close', icon: (p) => <Icon name="close" {...p} />, onSelect: onClose }
|
|
957
|
+
```
|
|
958
|
+
|
|
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.
|
|
964
|
+
|
|
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.
|
|
969
|
+
|
|
970
|
+
```jsx
|
|
971
|
+
{ label: 'Export', items: [
|
|
972
|
+
{ label: 'PNG', onSelect: exportPng },
|
|
973
|
+
{ label: 'SVG', onSelect: exportSvg },
|
|
974
|
+
] }
|
|
975
|
+
```
|
|
976
|
+
|
|
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.
|
|
991
|
+
|
|
992
|
+
### Safe-polygon hover
|
|
993
|
+
|
|
994
|
+
`MenuBar`, `ContextMenu` and `Tooltip` keep a submenu or hint open while the
|
|
995
|
+
pointer crosses towards it, using
|
|
996
|
+
[floating-ui's safePolygon](https://floating-ui.com/docs/usehover#safepolygon)
|
|
997
|
+
idea: the triangle between where the pointer was and the near edge of the
|
|
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.
|
|
1001
|
+
|
|
1002
|
+
The helpers are exported for widgets of your own:
|
|
1003
|
+
`movingToward(point, apex, rect)`, `safePolygon(apex, rect, buffer)`,
|
|
1004
|
+
`pointInPolygon(point, polygon)` and `screenPoint(ev)`, all in **screen**
|
|
1005
|
+
coordinates, since trigger and popup are different windows.
|
|
1006
|
+
|
|
1007
|
+
## `Tabs`
|
|
1008
|
+
|
|
1009
|
+
One panel visible at a time, switched by a strip of tabs.
|
|
1010
|
+
|
|
1011
|
+
```jsx
|
|
1012
|
+
<Tabs
|
|
1013
|
+
items={[
|
|
1014
|
+
{ id: 'general', label: 'General', content: <GeneralPage /> },
|
|
1015
|
+
{ id: 'advanced', label: 'Advanced', content: () => <AdvancedPage /> },
|
|
1016
|
+
{ id: 'legacy', label: 'Legacy', disabled: true },
|
|
1017
|
+
]}
|
|
1018
|
+
/>
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
| prop | |
|
|
1022
|
+
| ------------------------ | -------------------------------------------------- |
|
|
1023
|
+
| `items` | `{ id, label, content, disabled }[]` |
|
|
1024
|
+
| `value` / `defaultValue` | selected id — controlled with `value` + `onChange` |
|
|
1025
|
+
| `onChange(id)` | a tab was chosen |
|
|
1026
|
+
| `orientation` | `'horizontal'` (default) or `'vertical'` |
|
|
1027
|
+
| `manual` | arrows move focus only; Enter or Space commits |
|
|
1028
|
+
|
|
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.
|
|
1036
|
+
|
|
1037
|
+
## `Table`
|
|
1038
|
+
|
|
1039
|
+
A grid with a fixed header, resizable columns, and only the rows in view
|
|
1040
|
+
built.
|
|
1041
|
+
|
|
1042
|
+
```jsx
|
|
1043
|
+
<Table
|
|
1044
|
+
columns={[
|
|
1045
|
+
{ id: 'name', label: 'Name', width: 220 },
|
|
1046
|
+
{ id: 'size', label: 'Size', width: 90, align: 'right' },
|
|
1047
|
+
]}
|
|
1048
|
+
rows={files}
|
|
1049
|
+
onSelect={(id, row) => open(row)}
|
|
1050
|
+
/>
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
| prop | |
|
|
1054
|
+
| ------------------------------ | --------------------------------------------------- |
|
|
1055
|
+
| `columns` | `{ id, label, width, align, value, render }[]` |
|
|
1056
|
+
| `rows` | `{ id, … }[]` — `id` identifies the row |
|
|
1057
|
+
| `rowHeight` | every row is this tall (24 by default) |
|
|
1058
|
+
| `sort` / `defaultSort` | `{ column, direction }`; reported by `onSortChange` |
|
|
1059
|
+
| `selected` / `defaultSelected` | selected row id; reported by `onSelect(id, row)` |
|
|
1060
|
+
| `onActivate(id, row)` | a double click, or Enter on the selection |
|
|
1061
|
+
| `onColumnResize(id, width)` | after a header drag |
|
|
1062
|
+
|
|
1063
|
+
`value(row)` feeds sorting and the default cell text; `render(row, { selected,
|
|
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`:
|
|
1066
|
+
|
|
1067
|
+
```jsx
|
|
1068
|
+
render: (row, { selected }) => (
|
|
1069
|
+
<text style={{ color: selected ? '$hoverText' : statusColour(row) }}>
|
|
1070
|
+
{row.state}
|
|
1071
|
+
</text>
|
|
1072
|
+
);
|
|
1073
|
+
```
|
|
1074
|
+
|
|
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.
|
|
1087
|
+
|
|
1088
|
+
## `Tree`
|
|
1089
|
+
|
|
1090
|
+
Moved to [`@react-x11/components`](ecosystem.md): virtualisation,
|
|
1091
|
+
drag-to-reorder and lazy children do not belong in core.
|
|
1092
|
+
|
|
1093
|
+
## `SplitPane`
|
|
1094
|
+
|
|
1095
|
+
Two panes with a divider you can drag.
|
|
1096
|
+
|
|
1097
|
+
```jsx
|
|
1098
|
+
<SplitPane direction="row" defaultSize={220} min={120} minSecond={300}>
|
|
1099
|
+
<Sidebar />
|
|
1100
|
+
<Editor />
|
|
1101
|
+
</SplitPane>
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
| prop | |
|
|
1105
|
+
| ---------------------- | ----------------------------------------------- |
|
|
1106
|
+
| `direction` | `'row'` (default) or `'column'` |
|
|
1107
|
+
| `size` / `defaultSize` | the **first** pane's width or height, in pixels |
|
|
1108
|
+
| `onResize(size)` | after a drag or a key step |
|
|
1109
|
+
| `min` / `minSecond` | how small either pane may get |
|
|
1110
|
+
|
|
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.
|
|
1116
|
+
|
|
1117
|
+
## `Canvas3D`
|
|
1118
|
+
|
|
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`.
|
|
1122
|
+
|
|
1123
|
+
## `useAnchor(ref)` / `anchorRect(node, options)`
|
|
1124
|
+
|
|
1125
|
+
The placement math behind `Select` and `Tooltip`, for popup widgets of your
|
|
1126
|
+
own.
|
|
1127
|
+
|
|
1128
|
+
```jsx
|
|
1129
|
+
const ref = useRef(null);
|
|
1130
|
+
const measure = useAnchor(ref);
|
|
1131
|
+
|
|
1132
|
+
// screen coordinates for a <popup>, given the size you intend to use
|
|
1133
|
+
const rect = measure({ placement: 'bottom', align: 'center', width, height });
|
|
1134
|
+
// -> { x, y, width, height, placement }
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
| option | |
|
|
1138
|
+
| ----------------- | ---------------------------------------------------------------------------------- |
|
|
1139
|
+
| `placement` | `'bottom'` (default), `'top'`, `'start'`, `'end'`, `'left'`, `'right'`, `'center'` |
|
|
1140
|
+
| `align` | `'start'` (default), `'center'`, `'end'` on the cross axis |
|
|
1141
|
+
| `offset` | gap from the anchor in px (default 2) |
|
|
1142
|
+
| `at` | a rect **inside** the node to anchor to instead — a caret, a cell |
|
|
1143
|
+
| `width`, `height` | size of the popup you are positioning |
|
|
1144
|
+
| `alignTo` | node the alignment reads, when it is not the anchor |
|
|
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.
|
|
1160
|
+
|
|
1161
|
+
### `at` — anchoring to part of a node
|
|
1162
|
+
|
|
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.
|
|
1168
|
+
|
|
1169
|
+
```jsx
|
|
1170
|
+
const measure = useAnchor(editorRef);
|
|
1171
|
+
const rect = measure({
|
|
1172
|
+
at: { x: caretX, y: lineTop, width: 1, height: lineHeight },
|
|
1173
|
+
placement: 'bottom',
|
|
1174
|
+
width: 240,
|
|
1175
|
+
height: rows * 22,
|
|
1176
|
+
});
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
### `anchorScreenRect(app, rect, options)` — a rect with no node behind it
|
|
1180
|
+
|
|
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
|
|
1186
|
+
[`anchor={{ rect }}`](elements.md#a-popover-under-a-tray-item).
|
|
1187
|
+
|
|
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 —
|
|
1191
|
+
[elements.md](elements.md#anchor--a-popup-that-places-itself).
|
|
1192
|
+
|
|
1193
|
+
### `anchorArea(node)`
|
|
1194
|
+
|
|
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.
|
|
1200
|
+
|
|
1201
|
+
## `useDropTarget(options)` / `useDragSource(options)`
|
|
1202
|
+
|
|
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.
|
|
1206
|
+
|
|
1207
|
+
```jsx
|
|
1208
|
+
const { dropProps, isOver, isAccepted } = useDropTarget({
|
|
1209
|
+
accept: ['files'],
|
|
1210
|
+
onDrop: (e) => setFiles(e.files),
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
const { dragProps, isDragging, position } = useDragSource({
|
|
1214
|
+
data: { 'text/plain': label },
|
|
1215
|
+
actions: ['copy', 'move'],
|
|
1216
|
+
});
|
|
1217
|
+
```
|
|
1218
|
+
|
|
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).
|
|
1224
|
+
|
|
1225
|
+
---
|
|
1226
|
+
|
|
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.
|