react-x11 1.2.0 → 2.0.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 +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
package/src/components/Slider.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { useRef, useState } from 'react';
|
|
6
6
|
import { useTheme } from './theme.js';
|
|
7
|
+
import { changeEvent } from './change.js';
|
|
7
8
|
import {
|
|
8
9
|
XK_DOWN,
|
|
9
10
|
XK_END,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
const h = React.createElement;
|
|
19
20
|
|
|
20
21
|
const SLIDER_THUMB = 16;
|
|
22
|
+
const SLIDER_SLOP = (24 - SLIDER_THUMB) / 2;
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* <Slider value min max step onChange disabled …boxProps> — draggable
|
|
@@ -37,9 +39,10 @@ export function Slider({
|
|
|
37
39
|
max = 100,
|
|
38
40
|
step = 1,
|
|
39
41
|
onChange,
|
|
42
|
+
name,
|
|
40
43
|
disabled = false,
|
|
41
|
-
width,
|
|
42
44
|
height = 4,
|
|
45
|
+
style,
|
|
43
46
|
...boxProps
|
|
44
47
|
}) {
|
|
45
48
|
const theme = useTheme();
|
|
@@ -60,25 +63,45 @@ export function Slider({
|
|
|
60
63
|
const fraction = (clamp(value) - min) / span;
|
|
61
64
|
|
|
62
65
|
const emit = (next) => {
|
|
63
|
-
if (next !== value) onChange?.(next);
|
|
66
|
+
if (next !== value) onChange?.(changeEvent('range', name, next));
|
|
64
67
|
};
|
|
65
68
|
|
|
66
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Pointer x -> value, using the track's laid-out rect.
|
|
71
|
+
*
|
|
72
|
+
* The direction is read off the **node** rather than the theme: it is the
|
|
73
|
+
* one the track was actually laid out in, which is what the pointer
|
|
74
|
+
* coordinate has to be measured against — a slider inside a `<box
|
|
75
|
+
* style={{ direction: 'rtl' }}>` mirrors without a provider anywhere.
|
|
76
|
+
*/
|
|
67
77
|
const valueAt = (ev) => {
|
|
68
78
|
const node = trackRef.current;
|
|
69
|
-
|
|
79
|
+
// getClientRects rather than raw `abs`: the event, the thumb constant
|
|
80
|
+
// and this rect must share a unit, and the public rect is logical like
|
|
81
|
+
// they are — `abs` is device (src/scale.js)
|
|
82
|
+
const rect = node?.getClientRects?.()[0];
|
|
83
|
+
if (!rect?.width) return value;
|
|
70
84
|
// the thumb is centred on the value, so the usable travel is the track
|
|
71
85
|
// minus one thumb width — otherwise min/max are unreachable at the ends
|
|
72
|
-
const travel = Math.max(1,
|
|
73
|
-
const x =
|
|
86
|
+
const travel = Math.max(1, rect.width - SLIDER_THUMB);
|
|
87
|
+
const x =
|
|
88
|
+
node.direction === 'rtl'
|
|
89
|
+
? rect.x + rect.width - ev.x - SLIDER_THUMB / 2
|
|
90
|
+
: ev.x - rect.x - SLIDER_THUMB / 2;
|
|
74
91
|
return quantize(min + (Math.min(travel, Math.max(0, x)) / travel) * span);
|
|
75
92
|
};
|
|
76
93
|
|
|
77
94
|
const controlProps = disabled
|
|
78
|
-
? {}
|
|
95
|
+
? { disabled: true }
|
|
79
96
|
: {
|
|
80
97
|
focusable: true,
|
|
81
|
-
|
|
98
|
+
// the AT-SPI Value interface writes land here (Orca adjusting the
|
|
99
|
+
// slider): same clamp/quantize/emit as every other input route
|
|
100
|
+
onAccessibilityAction: ({ action, value: next }) => {
|
|
101
|
+
if (action === 'setValue' && typeof next === 'number') {
|
|
102
|
+
emit(quantize(clamp(next)));
|
|
103
|
+
}
|
|
104
|
+
},
|
|
82
105
|
onFocus: () => setFocused(true),
|
|
83
106
|
onBlur: () => setFocused(false),
|
|
84
107
|
onMouseDown: (ev) => {
|
|
@@ -92,12 +115,22 @@ export function Slider({
|
|
|
92
115
|
onMouseUp: () => setDragging(false),
|
|
93
116
|
onKeyDown: (ev) => {
|
|
94
117
|
const big = (step || 1) * 10;
|
|
118
|
+
// Left and Right name what the *thumb* should do on the screen, so
|
|
119
|
+
// in a mirrored slider Left is the one that raises the value —
|
|
120
|
+
// WAI-ARIA's rule for a horizontal slider, and the only one that
|
|
121
|
+
// matches what the hand sees. Up and Down never mirror, which is
|
|
122
|
+
// why they are not folded in with them.
|
|
123
|
+
const along = trackRef.current?.direction === 'rtl' ? -1 : 1;
|
|
95
124
|
switch (ev.keysym) {
|
|
96
125
|
case XK_LEFT:
|
|
126
|
+
emit(quantize(clamp(value - along * (step || 1))));
|
|
127
|
+
return;
|
|
128
|
+
case XK_RIGHT:
|
|
129
|
+
emit(quantize(clamp(value + along * (step || 1))));
|
|
130
|
+
return;
|
|
97
131
|
case XK_DOWN:
|
|
98
132
|
emit(quantize(clamp(value - (step || 1))));
|
|
99
133
|
return;
|
|
100
|
-
case XK_RIGHT:
|
|
101
134
|
case XK_UP:
|
|
102
135
|
emit(quantize(clamp(value + (step || 1))));
|
|
103
136
|
return;
|
|
@@ -122,24 +155,43 @@ export function Slider({
|
|
|
122
155
|
return h(
|
|
123
156
|
'box',
|
|
124
157
|
{
|
|
158
|
+
theme,
|
|
159
|
+
role: 'slider',
|
|
160
|
+
'aria-valuenow': clamp(value),
|
|
161
|
+
'aria-valuemin': min,
|
|
162
|
+
'aria-valuemax': max,
|
|
163
|
+
'aria-orientation': 'horizontal',
|
|
125
164
|
ref: trackRef,
|
|
126
|
-
width,
|
|
127
|
-
height: SLIDER_THUMB,
|
|
128
|
-
minWidth: 0,
|
|
129
|
-
justifyContent: 'center',
|
|
130
165
|
...controlProps,
|
|
131
166
|
...boxProps,
|
|
167
|
+
style: [
|
|
168
|
+
disabled || { cursor: 'pointer' },
|
|
169
|
+
{
|
|
170
|
+
height: SLIDER_THUMB,
|
|
171
|
+
minWidth: 0,
|
|
172
|
+
justifyContent: 'center',
|
|
173
|
+
// the control is as tall as its thumb and the track inside it sets
|
|
174
|
+
// `pointerEvents: 'none'`, so 16px was the whole target. Grown to
|
|
175
|
+
// WCAG 2.2 SC 2.5.8's 24 on the axis that is short, without moving
|
|
176
|
+
// a pixel of the drawing — a taller slider would misalign every row
|
|
177
|
+
// it sits in.
|
|
178
|
+
hitSlop: { top: SLIDER_SLOP, bottom: SLIDER_SLOP },
|
|
179
|
+
},
|
|
180
|
+
style,
|
|
181
|
+
],
|
|
132
182
|
},
|
|
133
183
|
// track
|
|
134
184
|
h(
|
|
135
185
|
'box',
|
|
136
186
|
{
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
187
|
+
style: {
|
|
188
|
+
height: height,
|
|
189
|
+
borderRadius: height / 2,
|
|
190
|
+
backgroundColor: theme.track,
|
|
191
|
+
flexDirection: 'row',
|
|
192
|
+
alignItems: 'center',
|
|
193
|
+
pointerEvents: 'none',
|
|
194
|
+
},
|
|
143
195
|
},
|
|
144
196
|
// flex ratios, not a percentage width: a percentage child resolves
|
|
145
197
|
// against the space available while the track is still being
|
|
@@ -147,31 +199,40 @@ export function Slider({
|
|
|
147
199
|
// at value = max the control grew, which moved the handle, which
|
|
148
200
|
// changed the value, and a drag turned into an oscillation
|
|
149
201
|
h('box', {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
202
|
+
style: {
|
|
203
|
+
flexGrow: fraction,
|
|
204
|
+
flexShrink: 0,
|
|
205
|
+
flexBasis: 0,
|
|
206
|
+
height: height,
|
|
207
|
+
borderRadius: height / 2,
|
|
208
|
+
backgroundColor: disabled ? theme.textMuted : theme.accent,
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
h('box', {
|
|
212
|
+
style: { flexGrow: 1 - fraction, flexShrink: 0, flexBasis: 0 },
|
|
156
213
|
}),
|
|
157
|
-
h('box', { flexGrow: 1 - fraction, flexShrink: 0, flexBasis: 0 }),
|
|
158
214
|
),
|
|
159
|
-
// thumb, centred on the value within the same travel the math uses
|
|
215
|
+
// thumb, centred on the value within the same travel the math uses.
|
|
216
|
+
// Logical inset and logical margin: the track's two flex ratios already
|
|
217
|
+
// mirror on their own — a `row` runs the other way under `direction:
|
|
218
|
+
// 'rtl'` — and these are what keep the thumb on top of the join.
|
|
160
219
|
h('box', {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
220
|
+
style: {
|
|
221
|
+
position: 'absolute',
|
|
222
|
+
start: `${fraction * 100}%`,
|
|
223
|
+
marginStart: -SLIDER_THUMB * fraction,
|
|
224
|
+
width: SLIDER_THUMB,
|
|
225
|
+
height: SLIDER_THUMB,
|
|
226
|
+
borderRadius: SLIDER_THUMB / 2,
|
|
227
|
+
borderWidth: 1,
|
|
228
|
+
borderColor: disabled
|
|
229
|
+
? theme.border
|
|
230
|
+
: focused || dragging
|
|
231
|
+
? theme.accentHover
|
|
232
|
+
: theme.border,
|
|
233
|
+
backgroundColor: disabled ? theme.surfaceHover : theme.surface,
|
|
234
|
+
pointerEvents: 'none',
|
|
235
|
+
},
|
|
175
236
|
}),
|
|
176
237
|
);
|
|
177
238
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Widget components built purely on the host primitives — no reconciler
|
|
2
|
+
// support needed. Plain createElement (no JSX) so the library stays
|
|
3
|
+
// build-step-free for consumers.
|
|
4
|
+
|
|
5
|
+
import React, { useRef, useState } from 'react';
|
|
6
|
+
import { createStyles } from '../styles.js';
|
|
7
|
+
import { useDirection, useTheme } from './theme.js';
|
|
8
|
+
import { XK_DOWN, XK_END, XK_HOME, XK_LEFT, XK_RIGHT, XK_UP } from './keys.js';
|
|
9
|
+
|
|
10
|
+
const h = React.createElement;
|
|
11
|
+
|
|
12
|
+
const DIVIDER = 6;
|
|
13
|
+
const KEY_STEP = 16;
|
|
14
|
+
|
|
15
|
+
const s = createStyles({
|
|
16
|
+
root: { flexGrow: 1, minHeight: 0, minWidth: 0 },
|
|
17
|
+
pane: { minHeight: 0, minWidth: 0 },
|
|
18
|
+
// `flex: 1` is load-bearing, not tidying: the zero basis in it is what
|
|
19
|
+
// makes this pane "whatever is left" rather than "as much as my content
|
|
20
|
+
// wants, shrunk as far as it will go". With a content basis the pane still
|
|
21
|
+
// could not go below the content's own floor, so a second pane holding a
|
|
22
|
+
// wrapping row would lay out at its unwrapped width and overflow. The
|
|
23
|
+
// `min-*: 0` on top says this one may go below even that.
|
|
24
|
+
grow: { flex: 1, minHeight: 0, minWidth: 0 },
|
|
25
|
+
divider: {
|
|
26
|
+
flexShrink: 0,
|
|
27
|
+
alignItems: 'center',
|
|
28
|
+
justifyContent: 'center',
|
|
29
|
+
transition: { backgroundColor: 100 },
|
|
30
|
+
},
|
|
31
|
+
// the grab handle is a hairline; the *target* is the whole divider
|
|
32
|
+
grip: { borderRadius: 1 },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* <SplitPane> — two panes with a divider you can drag.
|
|
37
|
+
*
|
|
38
|
+
* <SplitPane direction="row" defaultSize={220}>
|
|
39
|
+
* <Sidebar />
|
|
40
|
+
* <Editor />
|
|
41
|
+
* </SplitPane>
|
|
42
|
+
*
|
|
43
|
+
* `size` is the first pane's width (or height, when `direction="column"`)
|
|
44
|
+
* in pixels: controlled with `size` + `onResize`, uncontrolled with
|
|
45
|
+
* `defaultSize`. The second pane takes what is left, so only one number is
|
|
46
|
+
* ever stored and a window resize cannot leave a gap.
|
|
47
|
+
*
|
|
48
|
+
* `min` and `minSecond` keep either pane from collapsing; the drag clamps
|
|
49
|
+
* against the container's laid-out size, so it stays honest when the window
|
|
50
|
+
* changes underneath it.
|
|
51
|
+
*
|
|
52
|
+
* The divider is focusable: arrows move it by 16px, Home/End drive it to
|
|
53
|
+
* either limit. Dragging captures the pointer, so it keeps tracking after
|
|
54
|
+
* the pointer leaves the six pixels it started on.
|
|
55
|
+
*/
|
|
56
|
+
export function SplitPane({
|
|
57
|
+
direction = 'row',
|
|
58
|
+
size,
|
|
59
|
+
defaultSize = 200,
|
|
60
|
+
min = 40,
|
|
61
|
+
minSecond = 40,
|
|
62
|
+
onResize,
|
|
63
|
+
children,
|
|
64
|
+
style,
|
|
65
|
+
...boxProps
|
|
66
|
+
}) {
|
|
67
|
+
const theme = useTheme();
|
|
68
|
+
const rtl = useDirection() === 'rtl';
|
|
69
|
+
const [first, second] = React.Children.toArray(children);
|
|
70
|
+
const [uncontrolled, setUncontrolled] = useState(defaultSize);
|
|
71
|
+
const [dragging, setDragging] = useState(false);
|
|
72
|
+
const rootRef = useRef(null);
|
|
73
|
+
const grab = useRef(0);
|
|
74
|
+
const vertical = direction === 'column';
|
|
75
|
+
const current = size ?? uncontrolled;
|
|
76
|
+
|
|
77
|
+
// the live size and drag flag, mirrored in refs: a press and the moves
|
|
78
|
+
// that follow can all land before React re-renders, and a handler closing
|
|
79
|
+
// over the last rendered value would drop everything after the first
|
|
80
|
+
const sizeRef = useRef(current);
|
|
81
|
+
sizeRef.current = current;
|
|
82
|
+
const draggingRef = useRef(false);
|
|
83
|
+
|
|
84
|
+
/** Clamp against the container as laid out right now. */
|
|
85
|
+
const commit = (next) => {
|
|
86
|
+
// logical throughout: `next` becomes a style width/height, and the
|
|
87
|
+
// container rect has to be in the same unit (`abs` is device)
|
|
88
|
+
const box = rootRef.current?.getClientRects?.()[0];
|
|
89
|
+
const total = (vertical ? box?.height : box?.width) ?? 0;
|
|
90
|
+
const room = Math.max(min, total - DIVIDER - minSecond);
|
|
91
|
+
const clamped = Math.min(Math.max(min, next), total > 0 ? room : next);
|
|
92
|
+
if (clamped === sizeRef.current) return;
|
|
93
|
+
sizeRef.current = clamped;
|
|
94
|
+
if (size === undefined) setUncontrolled(clamped);
|
|
95
|
+
onResize?.(clamped);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* How far along the split the pointer is, measured from the first pane's
|
|
100
|
+
* own edge — the top in a column, and in a row the side the panes start
|
|
101
|
+
* at, which is the right-hand one when the layout is mirrored. Everything
|
|
102
|
+
* else here is already a distance rather than a coordinate, so this is the
|
|
103
|
+
* only place the direction gets read.
|
|
104
|
+
*/
|
|
105
|
+
const alongSplit = (ev, box) => {
|
|
106
|
+
if (vertical) return ev.y - box.y;
|
|
107
|
+
return rtl ? box.x + box.width - ev.x : ev.x - box.x;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const dividerProps = {
|
|
111
|
+
focusable: true,
|
|
112
|
+
onMouseDown: (ev) => {
|
|
113
|
+
const box = rootRef.current?.getClientRects?.()[0];
|
|
114
|
+
if (!box) return;
|
|
115
|
+
// where in the divider it was taken, so it does not jump on press
|
|
116
|
+
grab.current = alongSplit(ev, box) - sizeRef.current;
|
|
117
|
+
ev.capturePointer();
|
|
118
|
+
draggingRef.current = true;
|
|
119
|
+
setDragging(true);
|
|
120
|
+
},
|
|
121
|
+
onMouseMove: (ev) => {
|
|
122
|
+
if (!draggingRef.current) return;
|
|
123
|
+
const box = rootRef.current?.getClientRects?.()[0];
|
|
124
|
+
if (!box) return;
|
|
125
|
+
commit(alongSplit(ev, box) - grab.current);
|
|
126
|
+
},
|
|
127
|
+
onMouseUp: () => {
|
|
128
|
+
draggingRef.current = false;
|
|
129
|
+
setDragging(false);
|
|
130
|
+
},
|
|
131
|
+
onKeyDown: (ev) => {
|
|
132
|
+
// the key that *shrinks* the first pane is the one pointing back at
|
|
133
|
+
// its own edge, which swaps with the layout
|
|
134
|
+
const swap = !vertical && rtl;
|
|
135
|
+
const back = vertical ? XK_UP : swap ? XK_RIGHT : XK_LEFT;
|
|
136
|
+
const forward = vertical ? XK_DOWN : swap ? XK_LEFT : XK_RIGHT;
|
|
137
|
+
switch (ev.keysym) {
|
|
138
|
+
case back:
|
|
139
|
+
commit(sizeRef.current - KEY_STEP);
|
|
140
|
+
return;
|
|
141
|
+
case forward:
|
|
142
|
+
commit(sizeRef.current + KEY_STEP);
|
|
143
|
+
return;
|
|
144
|
+
case XK_HOME:
|
|
145
|
+
commit(min);
|
|
146
|
+
return;
|
|
147
|
+
case XK_END:
|
|
148
|
+
commit(Number.MAX_SAFE_INTEGER);
|
|
149
|
+
return;
|
|
150
|
+
default:
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return h(
|
|
156
|
+
'box',
|
|
157
|
+
{
|
|
158
|
+
theme,
|
|
159
|
+
ref: rootRef,
|
|
160
|
+
...boxProps,
|
|
161
|
+
style: [s.root, { flexDirection: vertical ? 'column' : 'row' }, style],
|
|
162
|
+
},
|
|
163
|
+
h(
|
|
164
|
+
'box',
|
|
165
|
+
{
|
|
166
|
+
style: [
|
|
167
|
+
s.pane,
|
|
168
|
+
vertical
|
|
169
|
+
? { height: current, flexShrink: 0 }
|
|
170
|
+
: { width: current, flexShrink: 0 },
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
first ?? null,
|
|
174
|
+
),
|
|
175
|
+
h(
|
|
176
|
+
'box',
|
|
177
|
+
{
|
|
178
|
+
// the ARIA window-splitter pattern: a separator whose value is the
|
|
179
|
+
// primary pane's size
|
|
180
|
+
role: 'separator',
|
|
181
|
+
'aria-orientation': vertical ? 'vertical' : 'horizontal',
|
|
182
|
+
'aria-valuenow': current,
|
|
183
|
+
'aria-valuemin': min,
|
|
184
|
+
...dividerProps,
|
|
185
|
+
style: [
|
|
186
|
+
s.divider,
|
|
187
|
+
vertical
|
|
188
|
+
? { height: DIVIDER, cursor: 'row-resize' }
|
|
189
|
+
: { width: DIVIDER, cursor: 'col-resize' },
|
|
190
|
+
{ backgroundColor: dragging ? theme.surfaceHover : 'transparent' },
|
|
191
|
+
{ ':hover': { backgroundColor: theme.surfaceHover } },
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
h('box', {
|
|
195
|
+
style: [
|
|
196
|
+
s.grip,
|
|
197
|
+
vertical
|
|
198
|
+
? { height: 2, alignSelf: 'stretch' }
|
|
199
|
+
: { width: 2, alignSelf: 'stretch' },
|
|
200
|
+
{ backgroundColor: theme.border },
|
|
201
|
+
],
|
|
202
|
+
}),
|
|
203
|
+
),
|
|
204
|
+
h('box', { style: s.grow }, second ?? null),
|
|
205
|
+
);
|
|
206
|
+
}
|
package/src/components/Switch.js
CHANGED
|
@@ -3,47 +3,125 @@
|
|
|
3
3
|
// build-step-free for consumers.
|
|
4
4
|
|
|
5
5
|
import React from 'react';
|
|
6
|
+
import { createStyles } from '../styles.js';
|
|
7
|
+
import { changeEvent } from './change.js';
|
|
6
8
|
import { useControl, useTheme } from './theme.js';
|
|
7
9
|
|
|
8
10
|
const h = React.createElement;
|
|
9
11
|
|
|
12
|
+
const TRACK_WIDTH = 36;
|
|
13
|
+
const THUMB = 16;
|
|
14
|
+
const INSET = 2;
|
|
15
|
+
|
|
16
|
+
const TRACK_HEIGHT = 20;
|
|
17
|
+
// WCAG 2.2 SC 2.5.8 wants 24px in both directions and the pill is 20 tall.
|
|
18
|
+
// Growing the pill would be a redesign of a control whose proportions are
|
|
19
|
+
// the point, so the *target* grows instead: `hitSlop` is hit testing only,
|
|
20
|
+
// so nothing about the drawing or the layout moves.
|
|
21
|
+
const TRACK_SLOP = (24 - TRACK_HEIGHT) / 2;
|
|
22
|
+
|
|
23
|
+
const s = createStyles({
|
|
24
|
+
track: {
|
|
25
|
+
width: TRACK_WIDTH,
|
|
26
|
+
height: TRACK_HEIGHT,
|
|
27
|
+
borderRadius: 10,
|
|
28
|
+
justifyContent: 'center',
|
|
29
|
+
hitSlop: TRACK_SLOP,
|
|
30
|
+
transition: { backgroundColor: 120 },
|
|
31
|
+
},
|
|
32
|
+
// absolutely positioned so the thumb slides on `start`: `justifyContent`
|
|
33
|
+
// would flip it between the ends with nothing in between to animate. The
|
|
34
|
+
// *logical* inset, so an off switch has its thumb on the side its label
|
|
35
|
+
// starts at and the slide runs the way the text does.
|
|
36
|
+
thumb: {
|
|
37
|
+
position: 'absolute',
|
|
38
|
+
width: THUMB,
|
|
39
|
+
height: THUMB,
|
|
40
|
+
borderRadius: THUMB / 2,
|
|
41
|
+
transition: { start: 120 },
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const THUMB_OFF = INSET;
|
|
46
|
+
const THUMB_ON = TRACK_WIDTH - THUMB - INSET;
|
|
47
|
+
|
|
10
48
|
/**
|
|
11
|
-
* <Switch checked onChange disabled/> — Checkbox semantics in a
|
|
12
|
-
* pill; the thumb
|
|
49
|
+
* <Switch checked onChange disabled style/> — Checkbox semantics in a
|
|
50
|
+
* sliding pill; the thumb slides to the end matching the state.
|
|
51
|
+
* `onChange(ev)`, as `Checkbox`.
|
|
52
|
+
*
|
|
53
|
+
* The hover, press and focus tints are `:hover`/`:active`/`:focus` blocks
|
|
54
|
+
* rather than React state: the renderer already knows which node the pointer
|
|
55
|
+
* is over, which one is held and which one has focus, so lighting the track
|
|
56
|
+
* up is a repaint of one node instead of a re-render of this component and
|
|
57
|
+
* everything under it.
|
|
58
|
+
*
|
|
59
|
+
* The thumb only slides once the switch has actually flipped, which is on
|
|
60
|
+
* the release — so the press is the whole of the feedback in between, and a
|
|
61
|
+
* click held for half a second is half a second of a control that has not
|
|
62
|
+
* answered without it. `:active` marks the press chain rather than the one
|
|
63
|
+
* node under the pointer, which is what lets the track answer a press that
|
|
64
|
+
* landed on the thumb.
|
|
65
|
+
*
|
|
66
|
+
* The slide is a `transition` on `start`, so the animation is the renderer's
|
|
67
|
+
* frame clock rather than a requestAnimationFrame loop written here — and
|
|
68
|
+
* the same declaration animates the track colour with it. It runs on the
|
|
69
|
+
* press tint too: 120ms of fade that *starts* on the press frame, which is
|
|
70
|
+
* the part perception is actually measuring.
|
|
13
71
|
*/
|
|
14
72
|
export function Switch({
|
|
15
73
|
checked = false,
|
|
16
74
|
onChange,
|
|
75
|
+
name,
|
|
17
76
|
disabled = false,
|
|
77
|
+
style,
|
|
18
78
|
...boxProps
|
|
19
79
|
}) {
|
|
20
80
|
const theme = useTheme();
|
|
21
|
-
const
|
|
81
|
+
const control = useControl(
|
|
82
|
+
disabled,
|
|
83
|
+
() => onChange?.(changeEvent('checkbox', name, !checked)),
|
|
84
|
+
{ styled: true },
|
|
85
|
+
);
|
|
22
86
|
return h(
|
|
23
87
|
'box',
|
|
24
88
|
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
flexDirection: 'row',
|
|
30
|
-
alignItems: 'center',
|
|
31
|
-
justifyContent: checked ? 'flex-end' : 'flex-start',
|
|
32
|
-
backgroundColor: disabled
|
|
33
|
-
? theme.track
|
|
34
|
-
: checked
|
|
35
|
-
? theme.accent
|
|
36
|
-
: focused
|
|
37
|
-
? theme.dim
|
|
38
|
-
: theme.border,
|
|
39
|
-
...props,
|
|
89
|
+
theme,
|
|
90
|
+
role: 'switch',
|
|
91
|
+
'aria-checked': checked,
|
|
92
|
+
...control.props,
|
|
40
93
|
...boxProps,
|
|
94
|
+
style: [
|
|
95
|
+
control.style,
|
|
96
|
+
s.track,
|
|
97
|
+
{
|
|
98
|
+
backgroundColor: disabled
|
|
99
|
+
? theme.track
|
|
100
|
+
: checked
|
|
101
|
+
? theme.accent
|
|
102
|
+
: theme.border,
|
|
103
|
+
},
|
|
104
|
+
!disabled && {
|
|
105
|
+
':hover': {
|
|
106
|
+
backgroundColor: checked ? theme.accentHover : theme.textMuted,
|
|
107
|
+
},
|
|
108
|
+
':active': {
|
|
109
|
+
backgroundColor: checked
|
|
110
|
+
? theme.accentActive
|
|
111
|
+
: theme.textMutedActive,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
style,
|
|
115
|
+
],
|
|
41
116
|
},
|
|
42
117
|
h('box', {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
118
|
+
style: [
|
|
119
|
+
s.thumb,
|
|
120
|
+
{
|
|
121
|
+
start: checked ? THUMB_ON : THUMB_OFF,
|
|
122
|
+
backgroundColor: theme.surface,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
47
125
|
}),
|
|
48
126
|
);
|
|
49
127
|
}
|