panelui-native 0.49.0 → 0.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 +9 -1
- package/lib/module/components/candlestick-chart/index.js +1161 -0
- package/lib/module/components/candlestick-chart/index.js.map +1 -0
- package/lib/module/components/combobox/index.js +73 -7
- package/lib/module/components/combobox/index.js.map +1 -1
- package/lib/module/components/context-menu/index.js +529 -0
- package/lib/module/components/context-menu/index.js.map +1 -0
- package/lib/module/components/menu/index.js +18 -10
- package/lib/module/components/menu/index.js.map +1 -1
- package/lib/module/components/popover/index.js +71 -7
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/sortable/index.js +942 -0
- package/lib/module/components/sortable/index.js.map +1 -0
- package/lib/module/components/swipe/index.js +140 -5
- package/lib/module/components/swipe/index.js.map +1 -1
- package/lib/module/components/tabs/index.js +56 -15
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/time-picker/index.js +295 -31
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/icons/index.js +29 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +5 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/haptics.js +19 -0
- package/lib/module/utils/haptics.js.map +1 -1
- package/lib/typescript/src/components/candlestick-chart/index.d.ts +278 -0
- package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/context-menu/index.d.ts +270 -0
- package/lib/typescript/src/components/context-menu/index.d.ts.map +1 -0
- package/lib/typescript/src/components/menu/index.d.ts +20 -20
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/popover/index.d.ts +51 -1
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sortable/index.d.ts +248 -0
- package/lib/typescript/src/components/sortable/index.d.ts.map +1 -0
- package/lib/typescript/src/components/swipe/index.d.ts +35 -0
- package/lib/typescript/src/components/swipe/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tabs/index.d.ts +34 -2
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +8 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/haptics.d.ts +14 -0
- package/lib/typescript/src/utils/haptics.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/candlestick-chart/index.tsx +1360 -0
- package/src/components/combobox/index.tsx +84 -6
- package/src/components/context-menu/index.tsx +658 -0
- package/src/components/menu/index.tsx +17 -10
- package/src/components/popover/index.tsx +94 -6
- package/src/components/sortable/index.tsx +1266 -0
- package/src/components/swipe/index.tsx +165 -3
- package/src/components/tabs/index.tsx +82 -16
- package/src/components/time-picker/index.tsx +330 -35
- package/src/icons/index.tsx +22 -0
- package/src/index.ts +39 -0
- package/src/utils/haptics.ts +21 -0
|
@@ -153,6 +153,58 @@ const timePickerVariants = tv({
|
|
|
153
153
|
},
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* The times a face may offer, and where a value sits among them.
|
|
158
|
+
*
|
|
159
|
+
* A scale or a list that runs the whole day while the picker only accepts part
|
|
160
|
+
* of it is a control that invites a choice in order to refuse it: the finger
|
|
161
|
+
* reaches the end, the root clamps, and the row springs back to where it
|
|
162
|
+
* started having said nothing about why. Offering only what will be accepted
|
|
163
|
+
* means the end of the scale *is* the last bookable time, and dragging to it
|
|
164
|
+
* picks it.
|
|
165
|
+
*
|
|
166
|
+
* The position is the nearest row rather than a division, because a filtered
|
|
167
|
+
* list no longer starts at midnight and its indices are its own.
|
|
168
|
+
*/
|
|
169
|
+
function useBoundedTimes(
|
|
170
|
+
minuteStep: number,
|
|
171
|
+
minTime: TimeValue | undefined,
|
|
172
|
+
maxTime: TimeValue | undefined,
|
|
173
|
+
value: TimeValue
|
|
174
|
+
) {
|
|
175
|
+
const times = useMemo(() => {
|
|
176
|
+
const all = timesOfDay(minuteStep);
|
|
177
|
+
if (!minTime && !maxTime) return all;
|
|
178
|
+
const low = minTime ? timeToMinutes(minTime) : Number.NEGATIVE_INFINITY;
|
|
179
|
+
const high = maxTime ? timeToMinutes(maxTime) : Number.POSITIVE_INFINITY;
|
|
180
|
+
const kept = all.filter((time) => {
|
|
181
|
+
const at = timeToMinutes(time);
|
|
182
|
+
return at >= low && at <= high;
|
|
183
|
+
});
|
|
184
|
+
// A span too narrow to contain a single step would leave nothing to scroll.
|
|
185
|
+
// The bounds are the caller's mistake there, and an empty face is worse.
|
|
186
|
+
return kept.length > 0 ? kept : all;
|
|
187
|
+
}, [minuteStep, minTime, maxTime]);
|
|
188
|
+
|
|
189
|
+
const index = useMemo(() => {
|
|
190
|
+
const target = timeToMinutes(value);
|
|
191
|
+
let nearest = 0;
|
|
192
|
+
let smallest = Number.POSITIVE_INFINITY;
|
|
193
|
+
for (let i = 0; i < times.length; i += 1) {
|
|
194
|
+
const time = times[i];
|
|
195
|
+
if (!time) continue;
|
|
196
|
+
const away = Math.abs(timeToMinutes(time) - target);
|
|
197
|
+
if (away < smallest) {
|
|
198
|
+
smallest = away;
|
|
199
|
+
nearest = i;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return nearest;
|
|
203
|
+
}, [times, value]);
|
|
204
|
+
|
|
205
|
+
return { times, index };
|
|
206
|
+
}
|
|
207
|
+
|
|
156
208
|
/* -------------------------------------------------------------------------- */
|
|
157
209
|
/* One snapping column */
|
|
158
210
|
/* -------------------------------------------------------------------------- */
|
|
@@ -161,6 +213,12 @@ interface ColumnProps<T> {
|
|
|
161
213
|
items: readonly T[];
|
|
162
214
|
/** Index of the selected item. Drives the scroll position. */
|
|
163
215
|
index: number;
|
|
216
|
+
/**
|
|
217
|
+
* Bumped every time the root has looked at a reported time, whether or not
|
|
218
|
+
* it accepted one. See `Column`'s resync effect for why the index alone is
|
|
219
|
+
* not enough to know a column is resting somewhere it should not be.
|
|
220
|
+
*/
|
|
221
|
+
syncToken: number;
|
|
164
222
|
onIndexChange: (index: number) => void;
|
|
165
223
|
label: (item: T) => string;
|
|
166
224
|
disabled?: boolean;
|
|
@@ -183,6 +241,7 @@ interface ColumnProps<T> {
|
|
|
183
241
|
function Column<T>({
|
|
184
242
|
items,
|
|
185
243
|
index,
|
|
244
|
+
syncToken,
|
|
186
245
|
onIndexChange,
|
|
187
246
|
label,
|
|
188
247
|
disabled,
|
|
@@ -208,24 +267,135 @@ function Column<T>({
|
|
|
208
267
|
offset.value = event.contentOffset.y;
|
|
209
268
|
});
|
|
210
269
|
|
|
270
|
+
/*
|
|
271
|
+
* Whether the list is still moving, and whether it owes itself a correction
|
|
272
|
+
* once it stops.
|
|
273
|
+
*
|
|
274
|
+
* Nothing may scroll the column programmatically while it is gliding. A
|
|
275
|
+
* `scrollTo` issued against a running deceleration fights it, and the list
|
|
276
|
+
* stops dead somewhere between two rows and takes no further touches — a
|
|
277
|
+
* flick reads as the wheel freezing. So a correction that arrives mid-flight
|
|
278
|
+
* is remembered rather than performed, and applied at the moment the list
|
|
279
|
+
* comes to rest.
|
|
280
|
+
*/
|
|
281
|
+
const moving = useRef(false);
|
|
282
|
+
const owedResync = useRef(false);
|
|
283
|
+
const settleTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
284
|
+
|
|
285
|
+
// The effect below closes over `index`; `atRest` runs from a scroll handler
|
|
286
|
+
// long after that render, so it needs the current one rather than the one it
|
|
287
|
+
// captured.
|
|
288
|
+
const latestIndex = useRef(index);
|
|
289
|
+
latestIndex.current = index;
|
|
290
|
+
|
|
291
|
+
const cancelTimer = useCallback(() => {
|
|
292
|
+
if (settleTimer.current === null) return;
|
|
293
|
+
clearTimeout(settleTimer.current);
|
|
294
|
+
settleTimer.current = null;
|
|
295
|
+
}, []);
|
|
296
|
+
|
|
297
|
+
/*
|
|
298
|
+
* Unanimated, and that is load-bearing rather than a matter of taste.
|
|
299
|
+
*
|
|
300
|
+
* An animated programmatic scroll raises the same begin and end momentum
|
|
301
|
+
* events a finger does, so the correction reports itself as a gesture, which
|
|
302
|
+
* commits, which corrects again — the column rides up and down and never
|
|
303
|
+
* settles. Placing it outright raises nothing, so the correction ends where
|
|
304
|
+
* it starts. It is also the honest movement: this is the row that was always
|
|
305
|
+
* holding, not a journey to it.
|
|
306
|
+
*/
|
|
307
|
+
const snapTo = useCallback((to: number) => {
|
|
308
|
+
resting.current = to;
|
|
309
|
+
ref.current?.scrollTo({ y: to * ROW_HEIGHT, animated: false });
|
|
310
|
+
}, []);
|
|
311
|
+
|
|
312
|
+
/*
|
|
313
|
+
* Runs on the token as well as the index, and that is the whole point.
|
|
314
|
+
*
|
|
315
|
+
* A reported row can be refused — by `minTime`, by `maxTime`, or by a step
|
|
316
|
+
* the root rounds to. When it is, the value does not change, so `index` does
|
|
317
|
+
* not change either, and an effect watching the index alone never learns that
|
|
318
|
+
* anything happened. The column was left resting on a row the picker had not
|
|
319
|
+
* accepted, showing one time while reporting another, and no later drag could
|
|
320
|
+
* put it right: it had already recorded that row as where it sits.
|
|
321
|
+
*
|
|
322
|
+
* The token changes on every reported time, refused or not, so the column is
|
|
323
|
+
* always told to check itself, and the check is the same one it always was.
|
|
324
|
+
*/
|
|
211
325
|
useEffect(() => {
|
|
212
326
|
if (index === resting.current) return;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
327
|
+
if (moving.current) {
|
|
328
|
+
owedResync.current = true;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
snapTo(index);
|
|
332
|
+
}, [index, syncToken, snapTo]);
|
|
333
|
+
|
|
334
|
+
/** The list has genuinely stopped. Pay off a correction if one is owed. */
|
|
335
|
+
const atRest = useCallback(() => {
|
|
336
|
+
cancelTimer();
|
|
337
|
+
moving.current = false;
|
|
338
|
+
if (!owedResync.current) return;
|
|
339
|
+
owedResync.current = false;
|
|
340
|
+
if (latestIndex.current !== resting.current) snapTo(latestIndex.current);
|
|
341
|
+
}, [cancelTimer, snapTo]);
|
|
342
|
+
|
|
343
|
+
const startMoving = useCallback(() => {
|
|
344
|
+
cancelTimer();
|
|
345
|
+
moving.current = true;
|
|
346
|
+
}, [cancelTimer]);
|
|
347
|
+
|
|
348
|
+
const settleAt = useCallback(
|
|
349
|
+
(y: number) => {
|
|
350
|
+
const next = Math.round(y / ROW_HEIGHT);
|
|
220
351
|
const clamped = Math.min(Math.max(next, 0), items.length - 1);
|
|
221
352
|
if (clamped === resting.current) return;
|
|
222
353
|
resting.current = clamped;
|
|
354
|
+
/*
|
|
355
|
+
* This report supersedes any correction that was owed. The correction was
|
|
356
|
+
* worked out against a row the column has now left, and paying it off
|
|
357
|
+
* afterwards would drag the column back to a row nobody chose.
|
|
358
|
+
*/
|
|
359
|
+
owedResync.current = false;
|
|
223
360
|
selectionTick();
|
|
224
361
|
onIndexChange(clamped);
|
|
225
362
|
},
|
|
226
363
|
[items.length, onIndexChange]
|
|
227
364
|
);
|
|
228
365
|
|
|
366
|
+
/*
|
|
367
|
+
* A flick reports once, when it stops — not twice.
|
|
368
|
+
*
|
|
369
|
+
* Momentum begins a frame *after* the drag ends, so the end of a drag cannot
|
|
370
|
+
* tell a flick from a release by itself. It arms a short timer, and momentum
|
|
371
|
+
* starting cancels it; a drag that stops dead has no momentum to cancel it
|
|
372
|
+
* and settles on the timer. Reporting the release offset directly, as this
|
|
373
|
+
* used to, committed a row the finger was in the middle of flying past, and
|
|
374
|
+
* the picker then had two answers for one gesture.
|
|
375
|
+
*/
|
|
376
|
+
const onDragEnd = useCallback(
|
|
377
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
378
|
+
const y = event.nativeEvent.contentOffset.y;
|
|
379
|
+
cancelTimer();
|
|
380
|
+
settleTimer.current = setTimeout(() => {
|
|
381
|
+
settleAt(y);
|
|
382
|
+
atRest();
|
|
383
|
+
}, 80);
|
|
384
|
+
},
|
|
385
|
+
[atRest, cancelTimer, settleAt]
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const onMomentumEnd = useCallback(
|
|
389
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
390
|
+
cancelTimer();
|
|
391
|
+
settleAt(event.nativeEvent.contentOffset.y);
|
|
392
|
+
atRest();
|
|
393
|
+
},
|
|
394
|
+
[atRest, cancelTimer, settleAt]
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
useEffect(() => cancelTimer, [cancelTimer]);
|
|
398
|
+
|
|
229
399
|
const selectedItem = items[index];
|
|
230
400
|
|
|
231
401
|
return (
|
|
@@ -242,11 +412,14 @@ function Column<T>({
|
|
|
242
412
|
snapToOffsets={snapToOffsets}
|
|
243
413
|
disableIntervalMomentum
|
|
244
414
|
decelerationRate="fast"
|
|
245
|
-
/* Both, because only one of them fires: a flick ends in
|
|
246
|
-
slow drag released on a snap point ends without any.
|
|
247
|
-
case leaves a column that looks settled and has
|
|
248
|
-
|
|
249
|
-
|
|
415
|
+
/* Both ends are wired, because only one of them fires: a flick ends in
|
|
416
|
+
momentum, and a slow drag released on a snap point ends without any.
|
|
417
|
+
Missing the drag case leaves a column that looks settled and has
|
|
418
|
+
reported nothing. */
|
|
419
|
+
onScrollBeginDrag={startMoving}
|
|
420
|
+
onScrollEndDrag={onDragEnd}
|
|
421
|
+
onMomentumScrollBegin={startMoving}
|
|
422
|
+
onMomentumScrollEnd={onMomentumEnd}
|
|
250
423
|
/* The rows fade rather than unmount, so a recycled row would pop in at
|
|
251
424
|
full opacity mid-scroll. A day of minutes is 60 views; keeping them
|
|
252
425
|
all is cheaper than the flicker. */
|
|
@@ -284,11 +457,19 @@ function ColumnRow({
|
|
|
284
457
|
}) {
|
|
285
458
|
const slots = timePickerVariants();
|
|
286
459
|
|
|
460
|
+
/*
|
|
461
|
+
* The row either side of the centre stays a number you can read. It used to
|
|
462
|
+
* drop to just over half opacity and shrink by an eighth at one row out,
|
|
463
|
+
* which on a settling column reads as the digits smearing rather than as
|
|
464
|
+
* depth — and the neighbours are exactly the rows being compared against
|
|
465
|
+
* while choosing. The far rows still fall away, since that fall is what makes
|
|
466
|
+
* the column look like a wheel instead of a list.
|
|
467
|
+
*/
|
|
287
468
|
const style = useAnimatedStyle(() => {
|
|
288
469
|
const distance = Math.abs(offset.value / ROW_HEIGHT - index);
|
|
289
470
|
return {
|
|
290
|
-
opacity: interpolate(distance, [0, 1, 2], [1, 0.
|
|
291
|
-
transform: [{ scale: interpolate(distance, [0, 1], [1, 0.
|
|
471
|
+
opacity: interpolate(distance, [0, 1, 2], [1, 0.75, 0.25], 'clamp'),
|
|
472
|
+
transform: [{ scale: interpolate(distance, [0, 1], [1, 0.94], 'clamp') }],
|
|
292
473
|
};
|
|
293
474
|
});
|
|
294
475
|
|
|
@@ -311,8 +492,22 @@ function ColumnRow({
|
|
|
311
492
|
interface FaceProps {
|
|
312
493
|
value: TimeValue;
|
|
313
494
|
onValueChange: (value: TimeValue) => void;
|
|
495
|
+
/**
|
|
496
|
+
* Bumped every time the root has looked at a reported time, so a face can
|
|
497
|
+
* tell "nothing changed because nothing was reported" from "nothing changed
|
|
498
|
+
* because what I reported was refused". Only the scrolling faces use it.
|
|
499
|
+
*/
|
|
500
|
+
syncToken: number;
|
|
314
501
|
hourCycle: HourCycle;
|
|
315
502
|
minuteStep: number;
|
|
503
|
+
/**
|
|
504
|
+
* The span the picker will accept, so a face that lists whole times can
|
|
505
|
+
* offer only those. A face that does not list whole times — the wheel, whose
|
|
506
|
+
* columns each hold one part of one — ignores these and is corrected by the
|
|
507
|
+
* root instead.
|
|
508
|
+
*/
|
|
509
|
+
minTime?: TimeValue;
|
|
510
|
+
maxTime?: TimeValue;
|
|
316
511
|
locale?: string;
|
|
317
512
|
disabled?: boolean;
|
|
318
513
|
/** Ruler face only — the other two never draw a number of their own. */
|
|
@@ -322,6 +517,7 @@ interface FaceProps {
|
|
|
322
517
|
function WheelFace({
|
|
323
518
|
value,
|
|
324
519
|
onValueChange,
|
|
520
|
+
syncToken,
|
|
325
521
|
hourCycle,
|
|
326
522
|
minuteStep,
|
|
327
523
|
locale,
|
|
@@ -376,7 +572,17 @@ function WheelFace({
|
|
|
376
572
|
const setMeridiem = useCallback(
|
|
377
573
|
(index: number) => {
|
|
378
574
|
const next = index === 0 ? 'am' : 'pm';
|
|
379
|
-
|
|
575
|
+
/*
|
|
576
|
+
* Reported even when the column came back to the half it started on.
|
|
577
|
+
* Staying silent there left the root with nothing to answer, so a column
|
|
578
|
+
* dragged away and released short of a swap had no way to be told to sit
|
|
579
|
+
* back down — the other two columns had the same hole, and this is the
|
|
580
|
+
* same fix.
|
|
581
|
+
*/
|
|
582
|
+
if (next === meridiemOf(value.hour)) {
|
|
583
|
+
onValueChange({ ...value });
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
380
586
|
onValueChange({
|
|
381
587
|
...value,
|
|
382
588
|
hour: next === 'pm' ? value.hour + 12 : value.hour - 12,
|
|
@@ -397,6 +603,7 @@ function WheelFace({
|
|
|
397
603
|
<Column
|
|
398
604
|
items={hours}
|
|
399
605
|
index={hourIndex}
|
|
606
|
+
syncToken={syncToken}
|
|
400
607
|
onIndexChange={setHour}
|
|
401
608
|
label={(hour) => (hourCycle === 24 ? padTwo(hour) : String(hour))}
|
|
402
609
|
disabled={disabled}
|
|
@@ -406,6 +613,7 @@ function WheelFace({
|
|
|
406
613
|
<Column
|
|
407
614
|
items={minutes}
|
|
408
615
|
index={minuteIndex}
|
|
616
|
+
syncToken={syncToken}
|
|
409
617
|
onIndexChange={setMinute}
|
|
410
618
|
label={padTwo}
|
|
411
619
|
disabled={disabled}
|
|
@@ -416,6 +624,7 @@ function WheelFace({
|
|
|
416
624
|
<Column
|
|
417
625
|
items={meridiems}
|
|
418
626
|
index={meridiemIndex}
|
|
627
|
+
syncToken={syncToken}
|
|
419
628
|
onIndexChange={setMeridiem}
|
|
420
629
|
label={(entry) => entry}
|
|
421
630
|
disabled={disabled}
|
|
@@ -535,17 +744,16 @@ function handEnd(turn: number, length: number) {
|
|
|
535
744
|
function ClockFaceLayout({
|
|
536
745
|
value,
|
|
537
746
|
onValueChange,
|
|
747
|
+
syncToken,
|
|
538
748
|
hourCycle,
|
|
539
749
|
minuteStep,
|
|
750
|
+
minTime,
|
|
751
|
+
maxTime,
|
|
540
752
|
locale,
|
|
541
753
|
disabled,
|
|
542
754
|
}: FaceProps) {
|
|
543
755
|
const slots = timePickerVariants();
|
|
544
|
-
const times
|
|
545
|
-
const index = Math.min(
|
|
546
|
-
times.length - 1,
|
|
547
|
-
Math.round(timeToMinutes(value) / minuteStep)
|
|
548
|
-
);
|
|
756
|
+
const { times, index } = useBoundedTimes(minuteStep, minTime, maxTime, value);
|
|
549
757
|
|
|
550
758
|
const setIndex = useCallback(
|
|
551
759
|
(next: number) => {
|
|
@@ -561,6 +769,7 @@ function ClockFaceLayout({
|
|
|
561
769
|
<Column
|
|
562
770
|
items={times}
|
|
563
771
|
index={index}
|
|
772
|
+
syncToken={syncToken}
|
|
564
773
|
onIndexChange={setIndex}
|
|
565
774
|
label={(time) => formatTime(time, { hourCycle, locale })}
|
|
566
775
|
disabled={disabled}
|
|
@@ -590,19 +799,18 @@ function ClockFaceLayout({
|
|
|
590
799
|
function RulerFace({
|
|
591
800
|
value,
|
|
592
801
|
onValueChange,
|
|
802
|
+
syncToken,
|
|
593
803
|
hourCycle,
|
|
594
804
|
minuteStep,
|
|
805
|
+
minTime,
|
|
806
|
+
maxTime,
|
|
595
807
|
locale,
|
|
596
808
|
disabled,
|
|
597
809
|
readout = 'default',
|
|
598
810
|
}: FaceProps) {
|
|
599
811
|
const slots = timePickerVariants({ readout });
|
|
600
812
|
const ref = useRef<ScrollView>(null);
|
|
601
|
-
const times
|
|
602
|
-
const index = Math.min(
|
|
603
|
-
times.length - 1,
|
|
604
|
-
Math.round(timeToMinutes(value) / minuteStep)
|
|
605
|
-
);
|
|
813
|
+
const { times, index } = useBoundedTimes(minuteStep, minTime, maxTime, value);
|
|
606
814
|
const resting = useRef(index);
|
|
607
815
|
/*
|
|
608
816
|
* Measured, not assumed: the scroller is narrower than the panel by whatever
|
|
@@ -622,18 +830,58 @@ function RulerFace({
|
|
|
622
830
|
[times]
|
|
623
831
|
);
|
|
624
832
|
|
|
833
|
+
// The scale runs the same rest-and-resync machinery the wheel's columns do,
|
|
834
|
+
// on the other axis and for the same two reasons: a refused tick moves
|
|
835
|
+
// nothing and has to be corrected, and a correction must never be scrolled
|
|
836
|
+
// into a scale that is still gliding.
|
|
837
|
+
const moving = useRef(false);
|
|
838
|
+
const owedResync = useRef(false);
|
|
839
|
+
const settleTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
840
|
+
const latestIndex = useRef(index);
|
|
841
|
+
latestIndex.current = index;
|
|
842
|
+
|
|
843
|
+
const cancelTimer = useCallback(() => {
|
|
844
|
+
if (settleTimer.current === null) return;
|
|
845
|
+
clearTimeout(settleTimer.current);
|
|
846
|
+
settleTimer.current = null;
|
|
847
|
+
}, []);
|
|
848
|
+
|
|
849
|
+
// Unanimated, for the reason given on the wheel's columns: an animated
|
|
850
|
+
// programmatic scroll reports itself as a gesture and the correction loops.
|
|
851
|
+
const snapTo = useCallback((to: number) => {
|
|
852
|
+
resting.current = to;
|
|
853
|
+
ref.current?.scrollTo({ x: to * TICK_SPACING, animated: false });
|
|
854
|
+
}, []);
|
|
855
|
+
|
|
625
856
|
useEffect(() => {
|
|
626
857
|
if (index === resting.current) return;
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
858
|
+
if (moving.current) {
|
|
859
|
+
owedResync.current = true;
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
snapTo(index);
|
|
863
|
+
}, [index, syncToken, snapTo]);
|
|
864
|
+
|
|
865
|
+
const atRest = useCallback(() => {
|
|
866
|
+
cancelTimer();
|
|
867
|
+
moving.current = false;
|
|
868
|
+
if (!owedResync.current) return;
|
|
869
|
+
owedResync.current = false;
|
|
870
|
+
if (latestIndex.current !== resting.current) snapTo(latestIndex.current);
|
|
871
|
+
}, [cancelTimer, snapTo]);
|
|
872
|
+
|
|
873
|
+
const startMoving = useCallback(() => {
|
|
874
|
+
cancelTimer();
|
|
875
|
+
moving.current = true;
|
|
876
|
+
}, [cancelTimer]);
|
|
877
|
+
|
|
878
|
+
const settleAt = useCallback(
|
|
879
|
+
(x: number) => {
|
|
880
|
+
const next = Math.round(x / TICK_SPACING);
|
|
634
881
|
const clamped = Math.min(Math.max(next, 0), times.length - 1);
|
|
635
882
|
if (clamped === resting.current) return;
|
|
636
883
|
resting.current = clamped;
|
|
884
|
+
owedResync.current = false;
|
|
637
885
|
selectionTick();
|
|
638
886
|
const time = times[clamped];
|
|
639
887
|
if (time) onValueChange(time);
|
|
@@ -641,6 +889,29 @@ function RulerFace({
|
|
|
641
889
|
[onValueChange, times]
|
|
642
890
|
);
|
|
643
891
|
|
|
892
|
+
const onDragEnd = useCallback(
|
|
893
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
894
|
+
const x = event.nativeEvent.contentOffset.x;
|
|
895
|
+
cancelTimer();
|
|
896
|
+
settleTimer.current = setTimeout(() => {
|
|
897
|
+
settleAt(x);
|
|
898
|
+
atRest();
|
|
899
|
+
}, 80);
|
|
900
|
+
},
|
|
901
|
+
[atRest, cancelTimer, settleAt]
|
|
902
|
+
);
|
|
903
|
+
|
|
904
|
+
const onMomentumEnd = useCallback(
|
|
905
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
906
|
+
cancelTimer();
|
|
907
|
+
settleAt(event.nativeEvent.contentOffset.x);
|
|
908
|
+
atRest();
|
|
909
|
+
},
|
|
910
|
+
[atRest, cancelTimer, settleAt]
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
useEffect(() => cancelTimer, [cancelTimer]);
|
|
914
|
+
|
|
644
915
|
const stepsPerHour = Math.max(1, Math.round(60 / minuteStep));
|
|
645
916
|
|
|
646
917
|
/*
|
|
@@ -678,8 +949,10 @@ function RulerFace({
|
|
|
678
949
|
snapToOffsets={snapToOffsets}
|
|
679
950
|
disableIntervalMomentum
|
|
680
951
|
decelerationRate="fast"
|
|
681
|
-
|
|
682
|
-
onScrollEndDrag={
|
|
952
|
+
onScrollBeginDrag={startMoving}
|
|
953
|
+
onScrollEndDrag={onDragEnd}
|
|
954
|
+
onMomentumScrollBegin={startMoving}
|
|
955
|
+
onMomentumScrollEnd={onMomentumEnd}
|
|
683
956
|
accessibilityRole="adjustable"
|
|
684
957
|
accessibilityLabel="Time"
|
|
685
958
|
accessibilityValue={{ text: formatTime(value, { hourCycle, locale }) }}
|
|
@@ -879,10 +1152,29 @@ function TimePickerRoot({
|
|
|
879
1152
|
* itself using — putting both here means every layout is bounded by the same
|
|
880
1153
|
* rule and none of them has to carry it.
|
|
881
1154
|
*/
|
|
1155
|
+
/*
|
|
1156
|
+
* Counts refusals, not reports.
|
|
1157
|
+
*
|
|
1158
|
+
* A face has no way of knowing that the row it landed on was bounded away by
|
|
1159
|
+
* `minTime` or `maxTime`, or rounded to a different one by the step — from
|
|
1160
|
+
* where it stands, a refusal and a value that simply did not move are the
|
|
1161
|
+
* same silence. This changes when a report was refused, which is the one
|
|
1162
|
+
* thing the faces were missing, and is what lets a column put itself back on
|
|
1163
|
+
* the row that actually holds.
|
|
1164
|
+
*
|
|
1165
|
+
* Only on refusal, though. Bumping it on every report re-rendered all three
|
|
1166
|
+
* columns each time any one of them settled, for no answer any of them
|
|
1167
|
+
* needed.
|
|
1168
|
+
*/
|
|
1169
|
+
const [refusals, setRefusals] = useState(0);
|
|
1170
|
+
|
|
882
1171
|
const commit = useCallback(
|
|
883
1172
|
(next: TimeValue) => {
|
|
884
1173
|
const bounded = clampTime(roundToStep(next, step), minTime, maxTime);
|
|
885
|
-
if (isSameTime(bounded, selected))
|
|
1174
|
+
if (isSameTime(bounded, selected)) {
|
|
1175
|
+
setRefusals((count) => count + 1);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
886
1178
|
if (!isValueControlled) setInternalValue(bounded);
|
|
887
1179
|
onValueChange?.(bounded);
|
|
888
1180
|
},
|
|
@@ -898,7 +1190,10 @@ function TimePickerRoot({
|
|
|
898
1190
|
const face: FaceProps = {
|
|
899
1191
|
value: draft,
|
|
900
1192
|
onValueChange: commit,
|
|
1193
|
+
syncToken: refusals,
|
|
901
1194
|
hourCycle,
|
|
1195
|
+
minTime,
|
|
1196
|
+
maxTime,
|
|
902
1197
|
minuteStep: step,
|
|
903
1198
|
locale,
|
|
904
1199
|
disabled,
|
package/src/icons/index.tsx
CHANGED
|
@@ -991,6 +991,28 @@ export function TrashIcon({ size = 16, color, ...props }: IconProps) {
|
|
|
991
991
|
);
|
|
992
992
|
}
|
|
993
993
|
|
|
994
|
+
/**
|
|
995
|
+
* Two columns of dots — the grip on a row that can be dragged somewhere else.
|
|
996
|
+
*
|
|
997
|
+
* Dots rather than the stacked rules of a menu glyph, and two columns rather
|
|
998
|
+
* than one: the pattern has to read as a surface to take hold of from either
|
|
999
|
+
* side, and a single column reads as a divider instead.
|
|
1000
|
+
*/
|
|
1001
|
+
export function GripVerticalIcon({ size = 16, color, ...props }: IconProps) {
|
|
1002
|
+
const resolved = useResolvedColor(color, '#737373');
|
|
1003
|
+
return (
|
|
1004
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
1005
|
+
<Path
|
|
1006
|
+
d="M9 5h.01M9 12h.01M9 19h.01M15 5h.01M15 12h.01M15 19h.01"
|
|
1007
|
+
stroke={resolved}
|
|
1008
|
+
strokeWidth={2.5}
|
|
1009
|
+
strokeLinecap="round"
|
|
1010
|
+
strokeLinejoin="round"
|
|
1011
|
+
/>
|
|
1012
|
+
</Svg>
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
994
1016
|
/** An arrow into a tray — save the thing to a file. */
|
|
995
1017
|
export function DownloadIcon({ size = 16, color, ...props }: IconProps) {
|
|
996
1018
|
const resolved = useResolvedColor(color, '#737373');
|
package/src/index.ts
CHANGED
|
@@ -106,6 +106,21 @@ export {
|
|
|
106
106
|
type BarChartStatus,
|
|
107
107
|
type BarChartOrientation,
|
|
108
108
|
} from './components/bar-chart';
|
|
109
|
+
export {
|
|
110
|
+
CandlestickChart,
|
|
111
|
+
useCandlestickChart,
|
|
112
|
+
type CandlestickChartProps,
|
|
113
|
+
type CandlestickChartHandle,
|
|
114
|
+
type CandlestickChartHeaderProps,
|
|
115
|
+
type CandlestickChartGridProps,
|
|
116
|
+
type CandlestickChartCandlesProps,
|
|
117
|
+
type CandlestickChartXAxisProps,
|
|
118
|
+
type CandlestickChartYAxisProps,
|
|
119
|
+
type CandlestickChartTooltipProps,
|
|
120
|
+
type CandlestickChartLegendProps,
|
|
121
|
+
type CandlestickChartDatum,
|
|
122
|
+
type CandlestickChartStatus,
|
|
123
|
+
} from './components/candlestick-chart';
|
|
109
124
|
export {
|
|
110
125
|
BottomSheet,
|
|
111
126
|
type BottomSheetProps,
|
|
@@ -382,6 +397,15 @@ export {
|
|
|
382
397
|
type ComboboxSelection,
|
|
383
398
|
type ComboboxPresentation,
|
|
384
399
|
} from './components/combobox';
|
|
400
|
+
export {
|
|
401
|
+
ContextMenu,
|
|
402
|
+
type ContextMenuProps,
|
|
403
|
+
type ContextMenuTriggerProps,
|
|
404
|
+
type ContextMenuContentProps,
|
|
405
|
+
type ContextMenuItemProps,
|
|
406
|
+
type ContextMenuPreviewProps,
|
|
407
|
+
type ContextMenuAnchor,
|
|
408
|
+
} from './components/context-menu';
|
|
385
409
|
export {
|
|
386
410
|
Drawer,
|
|
387
411
|
type DrawerProps,
|
|
@@ -608,6 +632,7 @@ export {
|
|
|
608
632
|
} from './components/message';
|
|
609
633
|
export {
|
|
610
634
|
Popover,
|
|
635
|
+
usePopoverAnchor,
|
|
611
636
|
type PopoverProps,
|
|
612
637
|
type PopoverTriggerProps,
|
|
613
638
|
type PopoverContentProps,
|
|
@@ -616,6 +641,8 @@ export {
|
|
|
616
641
|
type PopoverPlacement,
|
|
617
642
|
type PopoverAlign,
|
|
618
643
|
type PopoverPresentation,
|
|
644
|
+
type PopoverAnchorRect,
|
|
645
|
+
type PopoverAnchorControls,
|
|
619
646
|
} from './components/popover';
|
|
620
647
|
export {
|
|
621
648
|
Post,
|
|
@@ -796,6 +823,16 @@ export {
|
|
|
796
823
|
} from './components/thinking-orb';
|
|
797
824
|
export { Skeleton, type SkeletonProps } from './components/skeleton';
|
|
798
825
|
export { Slider, type SliderProps } from './components/slider';
|
|
826
|
+
export {
|
|
827
|
+
Sortable,
|
|
828
|
+
reorderItems,
|
|
829
|
+
useSortableItem,
|
|
830
|
+
type SortableProps,
|
|
831
|
+
type SortableItemProps,
|
|
832
|
+
type SortableHandleProps,
|
|
833
|
+
type SortableActivation,
|
|
834
|
+
type SortableReorderDetails,
|
|
835
|
+
} from './components/sortable';
|
|
799
836
|
export { Spinner, type SpinnerProps } from './components/spinner';
|
|
800
837
|
export {
|
|
801
838
|
Steps,
|
|
@@ -810,7 +847,9 @@ export {
|
|
|
810
847
|
export { Surface, type SurfaceProps } from './components/surface';
|
|
811
848
|
export {
|
|
812
849
|
Swipe,
|
|
850
|
+
useSwipeGroup,
|
|
813
851
|
type SwipeProps,
|
|
852
|
+
type SwipeGroupProps,
|
|
814
853
|
type SwipePanelProps,
|
|
815
854
|
type SwipeActionProps,
|
|
816
855
|
type SwipeActionColor,
|
package/src/utils/haptics.ts
CHANGED
|
@@ -61,3 +61,24 @@ export function hasHaptics(): boolean {
|
|
|
61
61
|
export function selectionTick(): void {
|
|
62
62
|
void getHaptics()?.selectionAsync().catch(() => {});
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The knock for a thing coming loose or landing — a row lifted out of a list,
|
|
67
|
+
* a card dropped into place.
|
|
68
|
+
*
|
|
69
|
+
* An impact rather than a selection because it marks a change of state rather
|
|
70
|
+
* than a change of value: the finger is now holding something it was not
|
|
71
|
+
* holding a moment ago, and the lightest feedback is not enough to say so.
|
|
72
|
+
* `medium` is the one that reads as picking something up without reading as an
|
|
73
|
+
* error.
|
|
74
|
+
*
|
|
75
|
+
* Falls back to a selection tick where the style constants are missing, and to
|
|
76
|
+
* nothing at all where the package is.
|
|
77
|
+
*/
|
|
78
|
+
export function impactKnock(): void {
|
|
79
|
+
const haptics = getHaptics();
|
|
80
|
+
if (!haptics) return;
|
|
81
|
+
void haptics
|
|
82
|
+
.impactAsync(haptics.ImpactFeedbackStyle?.Medium)
|
|
83
|
+
.catch(() => {});
|
|
84
|
+
}
|