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.
Files changed (46) hide show
  1. package/README.md +4 -1
  2. package/lib/module/components/candlestick-chart/index.js +1161 -0
  3. package/lib/module/components/candlestick-chart/index.js.map +1 -0
  4. package/lib/module/components/combobox/index.js +73 -7
  5. package/lib/module/components/combobox/index.js.map +1 -1
  6. package/lib/module/components/context-menu/index.js +529 -0
  7. package/lib/module/components/context-menu/index.js.map +1 -0
  8. package/lib/module/components/menu/index.js +18 -10
  9. package/lib/module/components/menu/index.js.map +1 -1
  10. package/lib/module/components/popover/index.js +71 -7
  11. package/lib/module/components/popover/index.js.map +1 -1
  12. package/lib/module/components/sortable/index.js +159 -23
  13. package/lib/module/components/sortable/index.js.map +1 -1
  14. package/lib/module/components/tabs/index.js +56 -15
  15. package/lib/module/components/tabs/index.js.map +1 -1
  16. package/lib/module/components/time-picker/index.js +295 -31
  17. package/lib/module/components/time-picker/index.js.map +1 -1
  18. package/lib/module/index.js +3 -1
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/typescript/src/components/candlestick-chart/index.d.ts +278 -0
  21. package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -0
  22. package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
  23. package/lib/typescript/src/components/context-menu/index.d.ts +270 -0
  24. package/lib/typescript/src/components/context-menu/index.d.ts.map +1 -0
  25. package/lib/typescript/src/components/menu/index.d.ts +20 -20
  26. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  27. package/lib/typescript/src/components/popover/index.d.ts +51 -1
  28. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  29. package/lib/typescript/src/components/sortable/index.d.ts +13 -3
  30. package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
  31. package/lib/typescript/src/components/tabs/index.d.ts +34 -2
  32. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  33. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
  34. package/lib/typescript/src/index.d.ts +3 -1
  35. package/lib/typescript/src/index.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/components/candlestick-chart/index.tsx +1360 -0
  38. package/src/components/combobox/index.tsx +149 -18
  39. package/src/components/context-menu/index.tsx +658 -0
  40. package/src/components/menu/index.tsx +17 -10
  41. package/src/components/popover/index.tsx +94 -6
  42. package/src/components/sortable/index.tsx +278 -36
  43. package/src/components/tabs/index.tsx +82 -16
  44. package/src/components/tag-input/index.tsx +619 -0
  45. package/src/components/time-picker/index.tsx +330 -35
  46. 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(cloneElement(child as ReactElement<ComboboxGroupProps>, {}, inner));
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
- kept.push(child);
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
- useEffect(() => {
554
- if (!open || presentation !== 'overlay') return;
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
- }, [open, presentation, keyboardHeight]);
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={() => setFocused(false)}
919
+ onBlur={() => {
920
+ setFocused(false);
921
+ setMarked(null);
922
+ }}
800
923
  onSubmitEditing={submit}
801
924
  onKeyPress={({ nativeEvent }) => {
802
- // Backspace on an empty field takes the last chip back — the same
803
- // reflex that deletes a character, extended to the thing in front
804
- // of the cursor when there is no character left to delete.
805
- if (
806
- multiple &&
807
- nativeEvent.key === 'Backspace' &&
808
- query.length === 0 &&
809
- values.length > 0
810
- ) {
811
- remove(values[values.length - 1]!);
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