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
|
@@ -296,6 +296,13 @@ function eachOption(children: ReactNode, visit: (option: ComboboxItemProps) => v
|
|
|
296
296
|
* A group is rebuilt around whatever survives inside it and dropped when that
|
|
297
297
|
* is nothing — a heading over no options reads as a section that failed to load
|
|
298
298
|
* rather than one the query emptied.
|
|
299
|
+
*
|
|
300
|
+
* Every kept node is given a key on the way out. This walk builds a plain array
|
|
301
|
+
* rather than going through `Children.map`, which is the one that hands out keys
|
|
302
|
+
* of its own, so options written as literal JSX — the ordinary way to write a
|
|
303
|
+
* short, fixed list — arrive here with none. An option's `value` is already
|
|
304
|
+
* unique within a list, and a group is named by its label, so both key
|
|
305
|
+
* themselves; an explicit key on the element still wins.
|
|
299
306
|
*/
|
|
300
307
|
function filterOptions(
|
|
301
308
|
children: ReactNode,
|
|
@@ -303,20 +310,31 @@ function filterOptions(
|
|
|
303
310
|
): ReactNode[] {
|
|
304
311
|
const kept: ReactNode[] = [];
|
|
305
312
|
|
|
306
|
-
Children.forEach(children, (child) => {
|
|
313
|
+
Children.forEach(children, (child, index) => {
|
|
307
314
|
if (!isValidElement(child)) return;
|
|
308
315
|
|
|
309
316
|
if (child.type === ComboboxGroup) {
|
|
310
317
|
const props = child.props as ComboboxGroupProps;
|
|
311
318
|
const inner = filterOptions(props.children, matches);
|
|
312
319
|
if (inner.length) {
|
|
313
|
-
kept.push(
|
|
320
|
+
kept.push(
|
|
321
|
+
cloneElement(
|
|
322
|
+
child as ReactElement<ComboboxGroupProps>,
|
|
323
|
+
{ key: child.key ?? `group:${props.label ?? index}` },
|
|
324
|
+
inner
|
|
325
|
+
)
|
|
326
|
+
);
|
|
314
327
|
}
|
|
315
328
|
return;
|
|
316
329
|
}
|
|
317
330
|
|
|
318
331
|
if (child.type === ComboboxItem && matches(child.props as ComboboxItemProps)) {
|
|
319
|
-
|
|
332
|
+
const props = child.props as ComboboxItemProps;
|
|
333
|
+
kept.push(
|
|
334
|
+
cloneElement(child as ReactElement<ComboboxItemProps>, {
|
|
335
|
+
key: child.key ?? `option:${props.value}`,
|
|
336
|
+
})
|
|
337
|
+
);
|
|
320
338
|
}
|
|
321
339
|
});
|
|
322
340
|
|
|
@@ -471,6 +489,15 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
471
489
|
const [internalQuery, setInternalQuery] = useState(defaultInputValue);
|
|
472
490
|
const query = inputValue !== undefined ? inputValue : internalQuery;
|
|
473
491
|
|
|
492
|
+
/*
|
|
493
|
+
* Which chip a second backspace would take, in `multiple` mode. A held
|
|
494
|
+
* backspace repeats, and a field that removed on the first one would empty
|
|
495
|
+
* itself in the time it takes to notice — the mark is the beat that lets you
|
|
496
|
+
* stop. It is an index rather than a value because the same label can appear
|
|
497
|
+
* twice once the caller allows a custom value that matches an option.
|
|
498
|
+
*/
|
|
499
|
+
const [marked, setMarked] = useState<number | null>(null);
|
|
500
|
+
|
|
474
501
|
/** The selection as a list, which is the shape everything downstream wants. */
|
|
475
502
|
const values = useMemo(() => {
|
|
476
503
|
if (selection == null) return [];
|
|
@@ -549,13 +576,66 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
549
576
|
* that lifts its content clear of the keyboard moves the field after it was
|
|
550
577
|
* measured, and the list would stay at the old position. Re-measure whenever
|
|
551
578
|
* the keyboard's height changes while the list is open.
|
|
579
|
+
*
|
|
580
|
+
* Except while the list is being dragged. The list dismisses the keyboard on
|
|
581
|
+
* drag, so the first finger movement changes the keyboard's height, which
|
|
582
|
+
* lands a new anchor, which recomputes the panel's `top` *and* its
|
|
583
|
+
* `maxHeight` — the panel resizes and moves under the finger that is
|
|
584
|
+
* scrolling it, which is the stutter. The measurement is only stale once the
|
|
585
|
+
* keyboard has finished leaving, so it is deferred to the end of the drag,
|
|
586
|
+
* which is also the first moment it can be taken correctly.
|
|
552
587
|
*/
|
|
553
|
-
|
|
554
|
-
|
|
588
|
+
const draggingList = useRef(false);
|
|
589
|
+
const anchorStale = useRef(false);
|
|
590
|
+
|
|
591
|
+
const remeasure = useCallback(() => {
|
|
555
592
|
fieldRef.current?.measureInWindow((x, y, width, height) =>
|
|
556
593
|
setAnchor({ x, y, width, height })
|
|
557
594
|
);
|
|
558
|
-
}, [
|
|
595
|
+
}, []);
|
|
596
|
+
|
|
597
|
+
useEffect(() => {
|
|
598
|
+
if (!open || presentation !== 'overlay') return;
|
|
599
|
+
if (draggingList.current) {
|
|
600
|
+
anchorStale.current = true;
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
remeasure();
|
|
604
|
+
}, [open, presentation, keyboardHeight, remeasure]);
|
|
605
|
+
|
|
606
|
+
const settleTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
607
|
+
|
|
608
|
+
const cancelSettle = useCallback(() => {
|
|
609
|
+
if (settleTimer.current === null) return;
|
|
610
|
+
clearTimeout(settleTimer.current);
|
|
611
|
+
settleTimer.current = null;
|
|
612
|
+
}, []);
|
|
613
|
+
|
|
614
|
+
const onListDragStart = useCallback(() => {
|
|
615
|
+
cancelSettle();
|
|
616
|
+
draggingList.current = true;
|
|
617
|
+
}, [cancelSettle]);
|
|
618
|
+
|
|
619
|
+
const onListSettled = useCallback(() => {
|
|
620
|
+
cancelSettle();
|
|
621
|
+
draggingList.current = false;
|
|
622
|
+
if (!anchorStale.current) return;
|
|
623
|
+
anchorStale.current = false;
|
|
624
|
+
remeasure();
|
|
625
|
+
}, [cancelSettle, remeasure]);
|
|
626
|
+
|
|
627
|
+
/*
|
|
628
|
+
* A fling ends the drag and then keeps moving, and momentum begins a frame
|
|
629
|
+
* *after* the drag ends — so the end of the drag cannot tell the two apart by
|
|
630
|
+
* itself. It arms a short timer instead, which momentum starting cancels; a
|
|
631
|
+
* drag that stops dead has no momentum to cancel it and settles on the timer.
|
|
632
|
+
*/
|
|
633
|
+
const onListDragEnd = useCallback(() => {
|
|
634
|
+
cancelSettle();
|
|
635
|
+
settleTimer.current = setTimeout(onListSettled, 80);
|
|
636
|
+
}, [cancelSettle, onListSettled]);
|
|
637
|
+
|
|
638
|
+
useEffect(() => cancelSettle, [cancelSettle]);
|
|
559
639
|
|
|
560
640
|
/*
|
|
561
641
|
* A single-select field shows the chosen option's label when it is not being
|
|
@@ -671,12 +751,38 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
671
751
|
[selection, commit]
|
|
672
752
|
);
|
|
673
753
|
|
|
754
|
+
/**
|
|
755
|
+
* Removal by position, which is what the backspace mark holds. Removing by
|
|
756
|
+
* value would take both of a repeated label rather than the marked one.
|
|
757
|
+
*/
|
|
758
|
+
const removeAt = useCallback(
|
|
759
|
+
(index: number) => {
|
|
760
|
+
const current = Array.isArray(selection) ? selection : [];
|
|
761
|
+
if (index < 0 || index >= current.length) return;
|
|
762
|
+
commit(
|
|
763
|
+
current.filter((_, position) => position !== index) as ComboboxSelection[Mode]
|
|
764
|
+
);
|
|
765
|
+
},
|
|
766
|
+
[selection, commit]
|
|
767
|
+
);
|
|
768
|
+
|
|
674
769
|
const clear = useCallback(() => {
|
|
675
770
|
setQuery('');
|
|
771
|
+
setMarked(null);
|
|
676
772
|
commit((multiple ? [] : undefined) as ComboboxSelection[Mode]);
|
|
677
773
|
inputRef.current?.focus();
|
|
678
774
|
}, [setQuery, commit, multiple]);
|
|
679
775
|
|
|
776
|
+
/*
|
|
777
|
+
* The chips can also change from outside — a form reset, a pick undone in the
|
|
778
|
+
* list — which would leave the mark pointing past the end of them. A stale
|
|
779
|
+
* mark is a chip deleted by a backspace meant for the one that used to be
|
|
780
|
+
* there.
|
|
781
|
+
*/
|
|
782
|
+
useEffect(() => {
|
|
783
|
+
if (marked !== null && marked >= values.length) setMarked(null);
|
|
784
|
+
}, [marked, values.length]);
|
|
785
|
+
|
|
680
786
|
const context = useMemo<ComboboxContextValue>(
|
|
681
787
|
() => ({ values, onSelect: (next) => select(next) }),
|
|
682
788
|
[values, select]
|
|
@@ -751,6 +857,13 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
751
857
|
// option would be swallowed by the keyboard dismissing first.
|
|
752
858
|
keyboardShouldPersistTaps="always"
|
|
753
859
|
keyboardDismissMode="on-drag"
|
|
860
|
+
// Both ends are wired: a slow drag that stops where it started never
|
|
861
|
+
// gets a momentum event, and a fling reports the drag ending long before
|
|
862
|
+
// the list has stopped.
|
|
863
|
+
onScrollBeginDrag={onListDragStart}
|
|
864
|
+
onScrollEndDrag={onListDragEnd}
|
|
865
|
+
onMomentumScrollBegin={cancelSettle}
|
|
866
|
+
onMomentumScrollEnd={onListSettled}
|
|
754
867
|
>
|
|
755
868
|
<View className="gap-1">{body}</View>
|
|
756
869
|
</ScrollView>
|
|
@@ -769,10 +882,14 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
769
882
|
>
|
|
770
883
|
<View className={slots.fieldContent()}>
|
|
771
884
|
{multiple
|
|
772
|
-
? values.map((entry) => (
|
|
885
|
+
? values.map((entry, index) => (
|
|
773
886
|
<Chip
|
|
774
887
|
key={entry}
|
|
775
888
|
size="sm"
|
|
889
|
+
// The marked chip turns destructive rather than growing an
|
|
890
|
+
// outline: it is about to be deleted, and that is the one
|
|
891
|
+
// colour in the theme that already means exactly that.
|
|
892
|
+
variant={marked === index ? 'destructive' : 'default'}
|
|
776
893
|
// A chip is `self-start` by default, which overrides the row's
|
|
777
894
|
// `items-center` and leaves it riding high against the input.
|
|
778
895
|
// Inside a field it is one of several things sharing a line.
|
|
@@ -789,6 +906,9 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
789
906
|
className={slots.input()}
|
|
790
907
|
value={query}
|
|
791
908
|
onChangeText={(next) => {
|
|
909
|
+
// Typing takes the mark off: the backspace that would have removed
|
|
910
|
+
// a chip has been overtaken by a new query.
|
|
911
|
+
setMarked(null);
|
|
792
912
|
setQuery(next);
|
|
793
913
|
if (!open) openList();
|
|
794
914
|
}}
|
|
@@ -796,20 +916,31 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
796
916
|
setFocused(true);
|
|
797
917
|
if (openOnFocus) openList();
|
|
798
918
|
}}
|
|
799
|
-
onBlur={() =>
|
|
919
|
+
onBlur={() => {
|
|
920
|
+
setFocused(false);
|
|
921
|
+
setMarked(null);
|
|
922
|
+
}}
|
|
800
923
|
onSubmitEditing={submit}
|
|
801
924
|
onKeyPress={({ nativeEvent }) => {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
)
|
|
811
|
-
|
|
925
|
+
if (!multiple) return;
|
|
926
|
+
if (nativeEvent.key !== 'Backspace') {
|
|
927
|
+
setMarked(null);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
// There is still a character in front of the cursor: backspace
|
|
931
|
+
// means what it always means, and the chips are none of its
|
|
932
|
+
// business.
|
|
933
|
+
if (query.length > 0 || values.length === 0) return;
|
|
934
|
+
|
|
935
|
+
// Backspace on an empty field reaches the thing in front of the
|
|
936
|
+
// cursor when there is no character left to delete — but it marks
|
|
937
|
+
// that chip first, and only the next one takes it.
|
|
938
|
+
if (marked !== null && marked < values.length) {
|
|
939
|
+
removeAt(marked);
|
|
940
|
+
setMarked(null);
|
|
941
|
+
return;
|
|
812
942
|
}
|
|
943
|
+
setMarked(values.length - 1);
|
|
813
944
|
}}
|
|
814
945
|
editable={!disabled}
|
|
815
946
|
// Android lays a single-line input's text against the top of its box
|