panelui-native 0.51.1 → 0.54.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 +4 -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 +159 -23
- package/lib/module/components/sortable/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/index.js +3 -1
- package/lib/module/index.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 +13 -3
- package/lib/typescript/src/components/sortable/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/index.d.ts +3 -1
- package/lib/typescript/src/index.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 +149 -18
- 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 +278 -36
- package/src/components/tabs/index.tsx +82 -16
- package/src/components/tag-input/index.tsx +619 -0
- package/src/components/time-picker/index.tsx +330 -35
- package/src/index.ts +35 -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/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,
|
|
@@ -490,6 +514,14 @@ export {
|
|
|
490
514
|
type PieDatum,
|
|
491
515
|
} from './components/pie-chart';
|
|
492
516
|
export { Textarea, type TextareaProps } from './components/textarea';
|
|
517
|
+
export {
|
|
518
|
+
TagInput,
|
|
519
|
+
type TagInputProps,
|
|
520
|
+
type TagInputVariant,
|
|
521
|
+
type TagInputSize,
|
|
522
|
+
type TagBlurBehavior,
|
|
523
|
+
type TagRejection,
|
|
524
|
+
} from './components/tag-input';
|
|
493
525
|
export {
|
|
494
526
|
Item,
|
|
495
527
|
type ItemProps,
|
|
@@ -608,6 +640,7 @@ export {
|
|
|
608
640
|
} from './components/message';
|
|
609
641
|
export {
|
|
610
642
|
Popover,
|
|
643
|
+
usePopoverAnchor,
|
|
611
644
|
type PopoverProps,
|
|
612
645
|
type PopoverTriggerProps,
|
|
613
646
|
type PopoverContentProps,
|
|
@@ -616,6 +649,8 @@ export {
|
|
|
616
649
|
type PopoverPlacement,
|
|
617
650
|
type PopoverAlign,
|
|
618
651
|
type PopoverPresentation,
|
|
652
|
+
type PopoverAnchorRect,
|
|
653
|
+
type PopoverAnchorControls,
|
|
619
654
|
} from './components/popover';
|
|
620
655
|
export {
|
|
621
656
|
Post,
|