panelui-native 0.105.1 → 0.107.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.
@@ -53,6 +53,7 @@ import {
53
53
  View,
54
54
  type LayoutChangeEvent,
55
55
  } from 'react-native';
56
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
56
57
  import Animated, {
57
58
  FadeIn,
58
59
  FadeOut,
@@ -68,7 +69,7 @@ import { Portal } from '../../primitives/portal';
68
69
  import { Text, textChildren } from '../../primitives/text';
69
70
  import { useBackHandler } from '../../hooks/use-back-handler';
70
71
  import { cn } from '../../utils/cn';
71
- import { BottomSheet } from '../bottom-sheet';
72
+ import { BottomSheet, bottomSheetDetentHeight } from '../bottom-sheet';
72
73
  import { InputGroup } from '../input-group';
73
74
  import { nativeSelectSupportsOptions } from './native-select-contract';
74
75
 
@@ -321,6 +322,50 @@ function filterOptions(children: ReactNode, needle: string): FilterResult {
321
322
  return { kept, seen };
322
323
  }
323
324
 
325
+ /**
326
+ * How the sheet is drawn in the `sheet` presentation. The other two
327
+ * presentations ignore it, and so does the `native` picker, which replaces the
328
+ * sheet altogether.
329
+ */
330
+ export interface SelectSheetProps {
331
+ /**
332
+ * Present the platform's own sheet — SwiftUI on iOS, Jetpack Compose on
333
+ * Android — instead of the styled one. Requires the optional `@expo/ui`
334
+ * package; without it the styled sheet is shown.
335
+ *
336
+ * Only the container is the platform's. The options inside are still the
337
+ * select's own, so groups, disabled options and `searchable` all work.
338
+ */
339
+ native?: boolean;
340
+ /**
341
+ * Heights the native sheet can rest at. Omit to size to the options. The
342
+ * list fills the first one and scrolls inside it. `{ fraction }` and
343
+ * `{ height }` are iOS-only. Native sheet only.
344
+ */
345
+ snapPoints?: ('half' | 'full' | { fraction: number } | { height: number })[];
346
+ /**
347
+ * Paint the native sheet a solid colour instead of the platform's
348
+ * translucent material. `true` uses the theme's popover surface; a string
349
+ * paints that colour. Native sheet only.
350
+ */
351
+ nativeBackground?: boolean | string;
352
+ /**
353
+ * How tall the sheet opens. `auto`, the default, sizes to the options and
354
+ * scrolls once they pass 384 points; `half` and `full` fix the height and
355
+ * the list fills it. On the native sheet these map onto the platform's
356
+ * detents, and `snapPoints` wins over them.
357
+ */
358
+ size?: 'auto' | 'half' | 'full';
359
+ }
360
+
361
+ /**
362
+ * The native sheet's own top padding, which clears the platform's grabber —
363
+ * `pt-5` on `BottomSheet.Content`. A list that fills a detent has to be given
364
+ * the height left inside that padding, because the platform hosts the content
365
+ * at whatever size it reports.
366
+ */
367
+ const NATIVE_SHEET_TOP_PADDING = 20;
368
+
324
369
  /** Trigger frame in window coordinates, measured when the list opens. */
325
370
  interface Anchor {
326
371
  x: number;
@@ -384,6 +429,11 @@ export interface SelectProps {
384
429
  presentation?: SelectPresentation;
385
430
  /** Sheet title shown above the options. `sheet` presentation only. */
386
431
  title?: string;
432
+ /**
433
+ * How the sheet is drawn — the platform's own sheet, its detents and
434
+ * surface, or a fixed height. `sheet` presentation only.
435
+ */
436
+ sheetProps?: SelectSheetProps;
387
437
  /**
388
438
  * Width of the floating list. `trigger` matches the trigger, `content` sizes
389
439
  * to the longest option, or pass a pixel value. `overlay` only.
@@ -441,6 +491,7 @@ function SelectRoot({
441
491
  emptyClassName,
442
492
  presentation = 'sheet',
443
493
  title,
494
+ sheetProps,
444
495
  contentWidth = 'trigger',
445
496
  offset = 8,
446
497
  onOpenChange,
@@ -458,6 +509,7 @@ function SelectRoot({
458
509
  const triggerRef = useRef<View>(null);
459
510
  const chevron = useSharedValue(0);
460
511
  const { height: screenHeight } = useWindowDimensions();
512
+ const insets = useSafeAreaInsets();
461
513
 
462
514
  const options = useMemo(() => {
463
515
  const collected: SelectItemProps[] = [];
@@ -653,6 +705,58 @@ function SelectRoot({
653
705
  );
654
706
 
655
707
  if (presentation === 'sheet') {
708
+ const size = sheetProps?.size ?? 'auto';
709
+ const nativeSheet = !!sheetProps?.native && !!getNativeUI();
710
+ // The native sheet's resting heights — `snapPoints`, or `size` mapped onto
711
+ // the platform's detents. Undefined means it sizes to the options.
712
+ const detents = nativeSheet
713
+ ? sheetProps?.snapPoints ?? (size === 'auto' ? undefined : [size])
714
+ : undefined;
715
+ const sized = nativeSheet ? detents !== undefined : size !== 'auto';
716
+
717
+ /*
718
+ * A list asked to fill a native sheet needs a definite height above it.
719
+ * The platform hosts the content at the size it reports, so a list left to
720
+ * fill reports every row and runs off the bottom of the detent.
721
+ */
722
+ const detentHeight = bottomSheetDetentHeight(detents, screenHeight);
723
+ const nativeHeight =
724
+ detentHeight === undefined
725
+ ? undefined
726
+ : detentHeight - NATIVE_SHEET_TOP_PADDING - Math.max(insets.bottom, 16);
727
+
728
+ const sheetBody = (
729
+ <>
730
+ {title ? (
731
+ <Text size="lg" weight="semibold" className="mb-2 px-3">
732
+ {title}
733
+ </Text>
734
+ ) : null}
735
+ {search ? <View className="px-1">{search}</View> : null}
736
+ {/*
737
+ * The sheet's own scroller rather than a plain one, because the sheet
738
+ * and the list both want a vertical drag. Unrelated, whichever takes
739
+ * the touch first keeps it — on Android that was usually the sheet,
740
+ * so the options dragged the sheet instead of scrolling. This one
741
+ * scrolls until the list reaches its top and hands the drag over
742
+ * there.
743
+ */}
744
+ <BottomSheet.Body
745
+ bounces={false}
746
+ // A sized sheet gives the list its height; an `auto` one stops it at
747
+ // 384 points so a long list scrolls instead of filling the screen.
748
+ className={sized ? undefined : 'max-h-96'}
749
+ // The filter is a text field above a scroller: dismissing the
750
+ // keyboard on a drag is what lets you look at what you filtered
751
+ // to without first tapping somewhere neutral.
752
+ keyboardDismissMode="on-drag"
753
+ keyboardShouldPersistTaps="handled"
754
+ >
755
+ <View className={slots.options({ className: listClassName })}>{optionList}</View>
756
+ </BottomSheet.Body>
757
+ </>
758
+ );
759
+
656
760
  return (
657
761
  <SelectContext.Provider value={context}>
658
762
  <View className={className}>{trigger}</View>
@@ -663,28 +767,19 @@ function SelectRoot({
663
767
  onOpenChange={(next) => {
664
768
  if (!next) close();
665
769
  }}
770
+ native={nativeSheet}
771
+ snapPoints={sheetProps?.snapPoints}
772
+ nativeBackground={sheetProps?.nativeBackground}
666
773
  >
667
- <BottomSheet.Content>
774
+ <BottomSheet.Content size={size}>
668
775
  {/* BottomSheet.Content portals its children out of this subtree —
669
776
  re-provide the select context so Select.Item keeps working. */}
670
777
  <SelectContext.Provider value={context}>
671
- {title ? (
672
- <Text size="lg" weight="semibold" className="mb-2 px-3">
673
- {title}
674
- </Text>
675
- ) : null}
676
- {search ? <View className="px-1">{search}</View> : null}
677
- <ScrollView
678
- bounces={false}
679
- className="max-h-96"
680
- // The filter is a text field above a scroller: dismissing the
681
- // keyboard on a drag is what lets you look at what you filtered
682
- // to without first tapping somewhere neutral.
683
- keyboardDismissMode="on-drag"
684
- keyboardShouldPersistTaps="handled"
685
- >
686
- <View className={slots.options({ className: listClassName })}>{optionList}</View>
687
- </ScrollView>
778
+ {nativeHeight === undefined ? (
779
+ sheetBody
780
+ ) : (
781
+ <View style={{ height: nativeHeight }}>{sheetBody}</View>
782
+ )}
688
783
  </SelectContext.Provider>
689
784
  </BottomSheet.Content>
690
785
  </BottomSheet>
package/src/index.ts CHANGED
@@ -380,6 +380,7 @@ export {
380
380
  type SelectItemProps,
381
381
  type SelectGroupProps,
382
382
  type SelectPresentation,
383
+ type SelectSheetProps,
383
384
  } from './components/select';
384
385
  export {
385
386
  SelectionMode,