@uniflowed/ui 0.0.0-alpha.4 → 0.0.0-alpha.40
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/accordion.js +360 -0
- package/alert-dialog.js +282 -0
- package/alert.js +142 -0
- package/avatar.js +276 -0
- package/breadcrumb.js +138 -0
- package/calendar.js +547 -0
- package/carousel.js +410 -0
- package/checkbox.js +216 -31
- package/collapsible.js +169 -0
- package/combobox.js +209 -40
- package/context-menu.js +206 -0
- package/date-picker.js +346 -0
- package/dialog.js +229 -197
- package/drawer.js +490 -0
- package/field.js +257 -42
- package/hover-card.js +330 -0
- package/index.js +1548 -24
- package/input-otp.js +218 -0
- package/interactions.js +2323 -0
- package/internal/anchor.js +565 -0
- package/internal/date-grid.js +260 -0
- package/internal/disclosure.js +298 -0
- package/internal/focus.js +64 -0
- package/internal/form-value.js +83 -0
- package/internal/hover-intent.js +259 -0
- package/internal/menu-tree.js +228 -0
- package/internal/merge-props.js +206 -7
- package/internal/range.js +147 -0
- package/internal/roving-focus.js +205 -11
- package/menu.js +521 -336
- package/menubar.js +288 -0
- package/navigation-menu.js +251 -0
- package/package.json +8 -12
- package/pagination.js +209 -0
- package/popover.js +344 -0
- package/progress.js +91 -0
- package/radio-group.js +302 -0
- package/resizable.js +447 -0
- package/scroll-area.js +283 -0
- package/select.js +888 -0
- package/separator.js +97 -0
- package/sheet.js +189 -0
- package/sidebar.js +313 -0
- package/skeleton.js +159 -0
- package/slider.js +405 -0
- package/switch.js +43 -34
- package/table.js +520 -0
- package/tabs.js +99 -96
- package/toast.js +592 -0
- package/toggle-group.js +282 -0
- package/toggle.js +105 -0
- package/tooltip.js +400 -0
package/resizable.js
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Two panes and the handle between them — a slider wearing a different role.
|
|
4
|
+
//
|
|
5
|
+
// The APG calls this a **window splitter**, and it defines it as a separator
|
|
6
|
+
// that behaves like a slider: `aria-valuenow`, `aria-valuemin` and
|
|
7
|
+
// `aria-valuemax` say how much of the space the primary pane has, and the
|
|
8
|
+
// arrow keys change it. That is why it is next to `slider.js` and shares
|
|
9
|
+
// `internal/range.js` with it rather than living with the layout components.
|
|
10
|
+
//
|
|
11
|
+
// Almost every resizable panel on the web is pointer-only, which is a WCAG
|
|
12
|
+
// 2.1.1 failure — no keyboard operation at all — and a 2.5.7 one on top of it.
|
|
13
|
+
// If uf ships one the keyboard is the feature, so the keyboard was written
|
|
14
|
+
// first and shipped on its own: a keyboard-only splitter is a working
|
|
15
|
+
// splitter, where a pointer-only one is not.
|
|
16
|
+
//
|
|
17
|
+
// It was not the finished control. A bar between two panes is a bar that
|
|
18
|
+
// approximately everybody takes hold of first, and this one could not be
|
|
19
|
+
// moved that way at all. Both halves are here now, and the key map is exactly
|
|
20
|
+
// what it was — a drag that had quietly replaced it would be the first failure
|
|
21
|
+
// in the other direction.
|
|
22
|
+
//
|
|
23
|
+
// # The drag, and the step it does not use
|
|
24
|
+
//
|
|
25
|
+
// It is `slider.js`'s `Slider.Track` arithmetic measured against the *group's*
|
|
26
|
+
// box rather than a track's. `pointerdown` captures the pointer, so a drag
|
|
27
|
+
// that wanders off a bar four pixels wide — which every drag does — keeps
|
|
28
|
+
// arriving at the handle instead of being lost to whatever it wandered over;
|
|
29
|
+
// `pointermove` turns the position into a percentage from
|
|
30
|
+
// `getBoundingClientRect`; and the percentage goes through `internal/range.js`
|
|
31
|
+
// like every other value here, so a drag cannot leave the pane anywhere an
|
|
32
|
+
// arrow key could not put it, and `aria-valuenow` is true about it while it
|
|
33
|
+
// moves.
|
|
34
|
+
//
|
|
35
|
+
// Pressing the handle does not move it. A press on a *track* means "put the
|
|
36
|
+
// value here", which is what `Slider.Track` does with one and why it is half
|
|
37
|
+
// of WCAG 2.5.7 there; a press on a handle means "take hold of this". A
|
|
38
|
+
// splitter that also jumped by the distance between the pointer and its own
|
|
39
|
+
// centre would move a little every time it was clicked, which is the one thing
|
|
40
|
+
// a person who clicked it did not ask for.
|
|
41
|
+
//
|
|
42
|
+
// `step` stays the keyboard's, and the pointer has one of its own. `step`
|
|
43
|
+
// defaults to 10 because ten presses of an arrow key ought to cross the pane,
|
|
44
|
+
// and 10 is an absurd granularity for a bar being dragged under a pointer that
|
|
45
|
+
// moves smoothly. The pointer's is one percent, and it is a constant rather
|
|
46
|
+
// than a second prop because the value is already a percentage of the group:
|
|
47
|
+
// one is the smallest move that means anything, and a splitter announcing
|
|
48
|
+
// 47.382 would be reading out its arithmetic rather than its size.
|
|
49
|
+
//
|
|
50
|
+
// # The two separators in this package are not the same thing
|
|
51
|
+
//
|
|
52
|
+
// A reader who greps for `separator` finds this and `menu.js`, and they are
|
|
53
|
+
// unrelated:
|
|
54
|
+
//
|
|
55
|
+
// * `Menu.Separator` is a rule between groups of items. It is not focusable,
|
|
56
|
+
// it has no value, and it exists so a reader moving through a menu is told
|
|
57
|
+
// the group changed.
|
|
58
|
+
// * `Resizable.Handle` is a *window splitter*. It is focusable, it carries a
|
|
59
|
+
// value, and operating it changes the layout.
|
|
60
|
+
//
|
|
61
|
+
// ARIA gives both the same role because both are separators; only the second
|
|
62
|
+
// one is a control. The difference is `tabindex` and `aria-valuenow`, which is
|
|
63
|
+
// also how a screen reader tells them apart.
|
|
64
|
+
//
|
|
65
|
+
// # `aria-orientation` is the separator's, not the layout's
|
|
66
|
+
//
|
|
67
|
+
// Two panes side by side are divided by a **vertical** line, so a
|
|
68
|
+
// `PanelGroup` whose `orientation` is `"horizontal"` renders a handle whose
|
|
69
|
+
// `aria-orientation` is `"vertical"`. That inversion is easy to get backwards
|
|
70
|
+
// and worth stating: `aria-orientation` on a separator describes the separator,
|
|
71
|
+
// and ARIA's default for the role is `horizontal` — so a vertical splitter
|
|
72
|
+
// that says nothing is announced as a horizontal rule.
|
|
73
|
+
//
|
|
74
|
+
// The APG's own pattern page does not mention `aria-orientation` at all, which
|
|
75
|
+
// is why implementations differ. This follows the role's definition rather
|
|
76
|
+
// than the pattern's silence.
|
|
77
|
+
//
|
|
78
|
+
// # Two panes, which is the pattern and not a limitation
|
|
79
|
+
//
|
|
80
|
+
// The window splitter is defined between two panes: a primary one whose size
|
|
81
|
+
// is the value, and the rest. A group of five panels is a layout-constraint
|
|
82
|
+
// problem — every handle's range depends on every other panel's minimum — and
|
|
83
|
+
// it is a different component with a different core, not a bigger version of
|
|
84
|
+
// this one. What is here is the pattern, complete.
|
|
85
|
+
//
|
|
86
|
+
// # Drawing it
|
|
87
|
+
//
|
|
88
|
+
// Each pane carries its share as `--uf-resizable-size`, a percentage, and the
|
|
89
|
+
// caller's stylesheet decides whether that is a width, a height, a `flex-basis`
|
|
90
|
+
// or nothing at all. The group is measured rather than drawn — the drag reads
|
|
91
|
+
// its box and never writes to it — so a caller whose panes are flex children,
|
|
92
|
+
// grid tracks or absolutely positioned gets the same splitter.
|
|
93
|
+
|
|
94
|
+
"use client";
|
|
95
|
+
|
|
96
|
+
import * as React from "@uniflowed/react";
|
|
97
|
+
import {
|
|
98
|
+
createContext,
|
|
99
|
+
useContext,
|
|
100
|
+
useEffect,
|
|
101
|
+
useId,
|
|
102
|
+
useMemo,
|
|
103
|
+
useRef,
|
|
104
|
+
useState,
|
|
105
|
+
} from "@uniflowed/react";
|
|
106
|
+
|
|
107
|
+
import type { Rest } from "./internal/merge-props.js";
|
|
108
|
+
import { composeHandlers, composeRefs, withoutComposed } from "./internal/merge-props.js";
|
|
109
|
+
import type { Orientation } from "./internal/roving-focus.js";
|
|
110
|
+
import { clamp, isReversed, snap } from "./internal/range.js";
|
|
111
|
+
import { useControlled } from "./internal/controlled-state.js";
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* How finely a drag may move the splitter, in percentage points.
|
|
115
|
+
*
|
|
116
|
+
* Not `step`, which is the keyboard's and defaults to ten; the module header
|
|
117
|
+
* says why the two cannot be the same number. One is a constant rather than a
|
|
118
|
+
* prop because the value is already a percentage of the group, so one is the
|
|
119
|
+
* smallest move that means anything.
|
|
120
|
+
*/
|
|
121
|
+
const POINTER_STEP = 1;
|
|
122
|
+
|
|
123
|
+
type ResizableState = {|
|
|
124
|
+
readonly base: string,
|
|
125
|
+
/** The primary pane's share of the group, as a percentage. */
|
|
126
|
+
readonly value: number,
|
|
127
|
+
readonly setValue: (value: number) => void,
|
|
128
|
+
readonly min: number,
|
|
129
|
+
readonly max: number,
|
|
130
|
+
readonly step: number,
|
|
131
|
+
/** How the panes are laid out; the handle's own orientation is the other one. */
|
|
132
|
+
readonly orientation: Orientation,
|
|
133
|
+
readonly disabled: boolean,
|
|
134
|
+
readonly hasPrimary: boolean,
|
|
135
|
+
readonly registerPrimary: (present: boolean) => void,
|
|
136
|
+
/**
|
|
137
|
+
* The element a drag is measured against.
|
|
138
|
+
*
|
|
139
|
+
* The group rather than the handle, because the value is the primary pane's
|
|
140
|
+
* share *of the group* — the handle is a few pixels wide and has no idea how
|
|
141
|
+
* much space there is to divide.
|
|
142
|
+
*/
|
|
143
|
+
readonly groupRef: { current: HTMLElement | null },
|
|
144
|
+
|};
|
|
145
|
+
|
|
146
|
+
const ResizableContext: React.Context<ResizableState | null> = createContext(null);
|
|
147
|
+
|
|
148
|
+
hook useResizable(part: string): ResizableState {
|
|
149
|
+
const state = useContext(ResizableContext);
|
|
150
|
+
if (state == null) {
|
|
151
|
+
throw new Error(`${part} must be rendered inside a Resizable.PanelGroup`);
|
|
152
|
+
}
|
|
153
|
+
return state;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The two panes and their handle.
|
|
158
|
+
*
|
|
159
|
+
* `value` is the primary pane's percentage of the group, which is what the
|
|
160
|
+
* handle announces — the APG's "a decimal value representing the current
|
|
161
|
+
* position of the separator", where 0 is collapsed and 100 is as large as it
|
|
162
|
+
* is allowed to be.
|
|
163
|
+
*
|
|
164
|
+
* `min` is the size the primary pane collapses to. It is 0 by default, so
|
|
165
|
+
* `Enter` collapses the pane entirely; a group whose primary pane should never
|
|
166
|
+
* disappear gives it a floor.
|
|
167
|
+
*/
|
|
168
|
+
export component ResizablePanelGroup(
|
|
169
|
+
children: React.Node,
|
|
170
|
+
value?: number,
|
|
171
|
+
defaultValue?: number = 50,
|
|
172
|
+
onValueChange?: (value: number) => void,
|
|
173
|
+
min?: number = 0,
|
|
174
|
+
max?: number = 100,
|
|
175
|
+
step?: number = 10,
|
|
176
|
+
orientation?: Orientation = "horizontal",
|
|
177
|
+
disabled?: boolean = false,
|
|
178
|
+
...rest: Rest
|
|
179
|
+
) {
|
|
180
|
+
const base = useId();
|
|
181
|
+
const [share, setShare] = useControlled(value, defaultValue, onValueChange);
|
|
182
|
+
const [hasPrimary, setHasPrimary] = useState(false);
|
|
183
|
+
const groupRef = useRef<HTMLElement | null>(null);
|
|
184
|
+
|
|
185
|
+
const state = useMemo(
|
|
186
|
+
() => ({
|
|
187
|
+
base,
|
|
188
|
+
value: clamp(share, min, max),
|
|
189
|
+
setValue: setShare,
|
|
190
|
+
min,
|
|
191
|
+
max,
|
|
192
|
+
step,
|
|
193
|
+
orientation,
|
|
194
|
+
disabled,
|
|
195
|
+
hasPrimary,
|
|
196
|
+
registerPrimary: setHasPrimary,
|
|
197
|
+
groupRef,
|
|
198
|
+
}),
|
|
199
|
+
[base, share, setShare, min, max, step, orientation, disabled, hasPrimary],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const passed = withoutComposed(rest, ["ref"]);
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<ResizableContext.Provider value={state}>
|
|
206
|
+
<div
|
|
207
|
+
{...passed}
|
|
208
|
+
ref={composeRefs(rest.ref, (element) => {
|
|
209
|
+
groupRef.current = element;
|
|
210
|
+
})}
|
|
211
|
+
>
|
|
212
|
+
{children}
|
|
213
|
+
</div>
|
|
214
|
+
</ResizableContext.Provider>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* One pane.
|
|
220
|
+
*
|
|
221
|
+
* `primary` marks the one whose size is the value, and the one the handle
|
|
222
|
+
* names with `aria-controls`. Exactly one pane in a group is primary; the
|
|
223
|
+
* other takes what is left. It is a prop rather than "the first one", because
|
|
224
|
+
* the first one in the document is not the first one to mount the moment a
|
|
225
|
+
* caller renders a pane conditionally, and a handle pointing at the wrong pane
|
|
226
|
+
* is a handle that announces someone else's size.
|
|
227
|
+
*/
|
|
228
|
+
export component ResizablePanel(children: React.Node, primary?: boolean = false, ...rest: Rest) {
|
|
229
|
+
const group = useResizable("Resizable.Panel");
|
|
230
|
+
const passed = withoutComposed(rest, ["style"]);
|
|
231
|
+
const register = group.registerPrimary;
|
|
232
|
+
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (!primary) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
register(true);
|
|
238
|
+
return () => register(false);
|
|
239
|
+
}, [primary, register]);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div
|
|
243
|
+
{...passed}
|
|
244
|
+
id={primary ? `${group.base}-primary` : undefined}
|
|
245
|
+
style={{
|
|
246
|
+
...(rest.style as $FlowFixMe),
|
|
247
|
+
"--uf-resizable-size": `${String(primary ? group.value : 100 - group.value)}%`,
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
{children}
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The splitter: a separator that behaves like a slider.
|
|
257
|
+
*
|
|
258
|
+
* `label` is its accessible name and has a default, because a splitter is a
|
|
259
|
+
* bare bar with no text in it every time — and a focusable separator with no
|
|
260
|
+
* name is announced as "separator", which tells a reader there is a control
|
|
261
|
+
* here and nothing about what it does.
|
|
262
|
+
*/
|
|
263
|
+
export component ResizableHandle(label?: string = "Resize", ...rest: Rest) {
|
|
264
|
+
const group = useResizable("Resizable.Handle");
|
|
265
|
+
const passed = withoutComposed(rest, [
|
|
266
|
+
"onKeyDown",
|
|
267
|
+
"onPointerCancel",
|
|
268
|
+
"onPointerDown",
|
|
269
|
+
"onPointerMove",
|
|
270
|
+
"onPointerUp",
|
|
271
|
+
]);
|
|
272
|
+
// Where the pane was before `Enter` collapsed it. A ref because nothing
|
|
273
|
+
// renders it: it is a fact about the last keystroke, not about the layout.
|
|
274
|
+
const restoreTo = useRef<number | null>(null);
|
|
275
|
+
// Whether the pointer is down on this handle. Also a ref, and for the same
|
|
276
|
+
// reason: it changes between renders and no render depends on it.
|
|
277
|
+
const dragging = useRef(false);
|
|
278
|
+
|
|
279
|
+
const moveBy = (amount: number) => {
|
|
280
|
+
restoreTo.current = null;
|
|
281
|
+
group.setValue(clamp(group.value + amount, group.min, group.max));
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/** The primary pane's share at the pointer, or null with no box to read. */
|
|
285
|
+
const shareAt = (event: $FlowFixMe): number | null => {
|
|
286
|
+
const element = group.groupRef.current;
|
|
287
|
+
if (element == null) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const box = element.getBoundingClientRect();
|
|
291
|
+
const vertical = group.orientation === "vertical";
|
|
292
|
+
const size = vertical ? box.height : box.width;
|
|
293
|
+
if (size <= 0) {
|
|
294
|
+
// A group with no box has no percentages in it, and dividing by its
|
|
295
|
+
// width would put `Infinity` into `aria-valuenow`.
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
// From the top for stacked panes, where `Slider.Track` reads from the
|
|
299
|
+
// bottom: a slider's minimum is at the bottom of its track, and a group's
|
|
300
|
+
// primary pane is the one *before* the handle, which is the top one.
|
|
301
|
+
const along = vertical ? event.clientY - box.top : event.clientX - box.left;
|
|
302
|
+
const part = clamp(along / size, 0, 1);
|
|
303
|
+
// In a right-to-left page the pane before the handle is the one on the
|
|
304
|
+
// right, so the reading runs the other way. `isReversed` mirrors nothing on
|
|
305
|
+
// a stacked group, because writing direction does not flip the vertical
|
|
306
|
+
// axis.
|
|
307
|
+
const forward = isReversed(element, group.orientation) ? 1 - part : part;
|
|
308
|
+
// The value *is* the percentage, so the position becomes one directly
|
|
309
|
+
// rather than being mapped across `min`–`max` the way a slider's is: those
|
|
310
|
+
// two are bounds on how far the pane may be dragged, not the ends of a
|
|
311
|
+
// scale. Mapping them would put the handle somewhere the pointer is not.
|
|
312
|
+
return snap(forward * 100, group.min, group.max, POINTER_STEP);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const endDrag = (event: $FlowFixMe) => {
|
|
316
|
+
dragging.current = false;
|
|
317
|
+
event.currentTarget?.releasePointerCapture?.(event.pointerId);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<div
|
|
322
|
+
{...passed}
|
|
323
|
+
aria-label={label}
|
|
324
|
+
// Only while a primary pane is in the document, for the reason every
|
|
325
|
+
// part of this package repeats: an `aria-controls` naming an id nothing
|
|
326
|
+
// has is worse than saying nothing at all.
|
|
327
|
+
aria-controls={group.hasPrimary ? `${group.base}-primary` : undefined}
|
|
328
|
+
aria-disabled={group.disabled ? "true" : undefined}
|
|
329
|
+
// The separator's own orientation, which is the other axis from the one
|
|
330
|
+
// the panes are laid out along. See the module header.
|
|
331
|
+
aria-orientation={group.orientation === "horizontal" ? "vertical" : "horizontal"}
|
|
332
|
+
aria-valuemax={group.max}
|
|
333
|
+
aria-valuemin={group.min}
|
|
334
|
+
aria-valuenow={group.value}
|
|
335
|
+
onKeyDown={composeHandlers(rest.onKeyDown, (event: $FlowFixMe) => {
|
|
336
|
+
if (group.disabled) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (event.key === "Enter") {
|
|
340
|
+
event.preventDefault();
|
|
341
|
+
// Collapse, or put it back where it was. The APG gives `Enter` both
|
|
342
|
+
// jobs, and a collapse with no way back is a pane a keyboard reader
|
|
343
|
+
// has thrown away.
|
|
344
|
+
const previous = restoreTo.current;
|
|
345
|
+
if (previous != null) {
|
|
346
|
+
restoreTo.current = null;
|
|
347
|
+
group.setValue(previous);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (group.value === group.min) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
restoreTo.current = group.value;
|
|
354
|
+
group.setValue(group.min);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (event.key === "Home" || event.key === "End") {
|
|
359
|
+
event.preventDefault();
|
|
360
|
+
restoreTo.current = null;
|
|
361
|
+
group.setValue(event.key === "Home" ? group.min : group.max);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const amount = stepFor(
|
|
366
|
+
event.key,
|
|
367
|
+
group.step,
|
|
368
|
+
group.orientation,
|
|
369
|
+
isReversed(event.currentTarget, group.orientation),
|
|
370
|
+
);
|
|
371
|
+
if (amount != null) {
|
|
372
|
+
event.preventDefault();
|
|
373
|
+
moveBy(amount);
|
|
374
|
+
}
|
|
375
|
+
})}
|
|
376
|
+
onPointerCancel={composeHandlers(rest.onPointerCancel, endDrag)}
|
|
377
|
+
onPointerDown={composeHandlers(rest.onPointerDown, (event: $FlowFixMe) => {
|
|
378
|
+
if (group.disabled) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// Otherwise the press selects the text in the panes either side on the
|
|
382
|
+
// way past, so a drag paints half the page blue.
|
|
383
|
+
event.preventDefault();
|
|
384
|
+
// Which also means the browser will not focus this element, and a
|
|
385
|
+
// reader who has just dragged the splitter is the reader most likely to
|
|
386
|
+
// want an arrow key next.
|
|
387
|
+
event.currentTarget?.focus?.();
|
|
388
|
+
event.currentTarget?.setPointerCapture?.(event.pointerId);
|
|
389
|
+
dragging.current = true;
|
|
390
|
+
// A drag is a move, so the pane `Enter` would put back is no longer
|
|
391
|
+
// where it was. Leaving it would make the next `Enter` restore a size
|
|
392
|
+
// from before the drag.
|
|
393
|
+
restoreTo.current = null;
|
|
394
|
+
// Deliberately no value change: taking hold of the handle is not asking
|
|
395
|
+
// it to move. The module header says what a press does on a track
|
|
396
|
+
// instead, and why the two are not the same gesture.
|
|
397
|
+
})}
|
|
398
|
+
onPointerMove={composeHandlers(rest.onPointerMove, (event: $FlowFixMe) => {
|
|
399
|
+
if (!dragging.current) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const share = shareAt(event);
|
|
403
|
+
if (share != null) {
|
|
404
|
+
group.setValue(share);
|
|
405
|
+
}
|
|
406
|
+
})}
|
|
407
|
+
onPointerUp={composeHandlers(rest.onPointerUp, endDrag)}
|
|
408
|
+
role="separator"
|
|
409
|
+
// A separator that is not in the tab sequence is the WCAG 2.1.1 failure
|
|
410
|
+
// this module exists to avoid.
|
|
411
|
+
tabIndex={group.disabled ? -1 : 0}
|
|
412
|
+
/>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* How far a key moves the splitter, or nothing when the key is not ours.
|
|
418
|
+
*
|
|
419
|
+
* Only the keys along the axis the panes are laid out on: `ArrowUp` in a group
|
|
420
|
+
* of side-by-side panes is the page's, and swallowing it takes a scroll key
|
|
421
|
+
* away from every reader who uses one.
|
|
422
|
+
*
|
|
423
|
+
* The primary pane is the one before the handle, so moving the handle towards
|
|
424
|
+
* the end of the axis makes it larger — and in a right-to-left page the end of
|
|
425
|
+
* a horizontal axis is on the left, so the arrows mirror.
|
|
426
|
+
*/
|
|
427
|
+
function stepFor(
|
|
428
|
+
key: string,
|
|
429
|
+
step: number,
|
|
430
|
+
orientation: Orientation,
|
|
431
|
+
reversed: boolean,
|
|
432
|
+
): number | null {
|
|
433
|
+
const move = step <= 0 ? 1 : step;
|
|
434
|
+
if (orientation === "vertical") {
|
|
435
|
+
return match (key) {
|
|
436
|
+
"ArrowDown" => move,
|
|
437
|
+
"ArrowUp" => -move,
|
|
438
|
+
_ => null,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const forward = reversed ? -1 : 1;
|
|
442
|
+
return match (key) {
|
|
443
|
+
"ArrowRight" => move * forward,
|
|
444
|
+
"ArrowLeft" => -move * forward,
|
|
445
|
+
_ => null,
|
|
446
|
+
};
|
|
447
|
+
}
|