@ledgerhq/lumen-ui-rnative 0.1.34 → 0.1.35

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 (89) hide show
  1. package/dist/module/index.js +1 -0
  2. package/dist/module/index.js.map +1 -1
  3. package/dist/module/lib/Animations/Pulse/Pulse.js +17 -7
  4. package/dist/module/lib/Animations/Pulse/Pulse.js.map +1 -1
  5. package/dist/module/lib/Components/BottomSheet/BottomSheet.js +12 -7
  6. package/dist/module/lib/Components/BottomSheet/BottomSheet.js.map +1 -1
  7. package/dist/module/lib/Components/BottomSheet/BottomSheet.stories.js +220 -1
  8. package/dist/module/lib/Components/BottomSheet/BottomSheet.stories.js.map +1 -1
  9. package/dist/module/lib/Components/BottomSheet/BottomSheet.test.js +73 -0
  10. package/dist/module/lib/Components/BottomSheet/BottomSheet.test.js.map +1 -1
  11. package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.js +1 -1
  12. package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.js.map +1 -1
  13. package/dist/module/lib/Components/BottomSheet/CustomHandle.js +15 -2
  14. package/dist/module/lib/Components/BottomSheet/CustomHandle.js.map +1 -1
  15. package/dist/module/lib/Components/Card/Card.js.map +1 -1
  16. package/dist/module/lib/Components/ListItem/ListItem.js.map +1 -1
  17. package/dist/module/lib/Components/MediaImage/MediaImage.js +5 -1
  18. package/dist/module/lib/Components/MediaImage/MediaImage.js.map +1 -1
  19. package/dist/module/lib/Components/OptionList/OptionList.js +45 -4
  20. package/dist/module/lib/Components/OptionList/OptionList.js.map +1 -1
  21. package/dist/module/lib/Components/OptionList/OptionList.mdx +19 -0
  22. package/dist/module/lib/Components/OptionList/OptionList.stories.js +254 -1
  23. package/dist/module/lib/Components/OptionList/OptionList.stories.js.map +1 -1
  24. package/dist/module/lib/Components/OptionList/OptionList.test.js +136 -1
  25. package/dist/module/lib/Components/OptionList/OptionList.test.js.map +1 -1
  26. package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.js +39 -13
  27. package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.js.map +1 -1
  28. package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.test.js +117 -2
  29. package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.test.js.map +1 -1
  30. package/dist/module/lib/Components/PageIndicator/PageIndicator.test.js.map +1 -1
  31. package/dist/module/lib/Components/Skeleton/Skeleton.js +10 -3
  32. package/dist/module/lib/Components/Skeleton/Skeleton.js.map +1 -1
  33. package/dist/module/lib/Components/TabBar/TabBar.js +7 -6
  34. package/dist/module/lib/Components/TabBar/TabBar.js.map +1 -1
  35. package/dist/module/styles/lx/resolveStyle.js.map +1 -1
  36. package/dist/typescript/src/index.d.ts +1 -0
  37. package/dist/typescript/src/index.d.ts.map +1 -1
  38. package/dist/typescript/src/lib/Animations/Pulse/Pulse.d.ts +1 -1
  39. package/dist/typescript/src/lib/Animations/Pulse/Pulse.d.ts.map +1 -1
  40. package/dist/typescript/src/lib/Animations/Pulse/types.d.ts +2 -1
  41. package/dist/typescript/src/lib/Animations/Pulse/types.d.ts.map +1 -1
  42. package/dist/typescript/src/lib/Components/BottomSheet/BottomSheet.d.ts +1 -1
  43. package/dist/typescript/src/lib/Components/BottomSheet/BottomSheet.d.ts.map +1 -1
  44. package/dist/typescript/src/lib/Components/BottomSheet/CustomHandle.d.ts +5 -2
  45. package/dist/typescript/src/lib/Components/BottomSheet/CustomHandle.d.ts.map +1 -1
  46. package/dist/typescript/src/lib/Components/BottomSheet/types.d.ts +16 -3
  47. package/dist/typescript/src/lib/Components/BottomSheet/types.d.ts.map +1 -1
  48. package/dist/typescript/src/lib/Components/Card/Card.d.ts.map +1 -1
  49. package/dist/typescript/src/lib/Components/ListItem/ListItem.d.ts +3 -3
  50. package/dist/typescript/src/lib/Components/ListItem/ListItem.d.ts.map +1 -1
  51. package/dist/typescript/src/lib/Components/MediaImage/MediaImage.d.ts.map +1 -1
  52. package/dist/typescript/src/lib/Components/OptionList/OptionList.d.ts +3 -2
  53. package/dist/typescript/src/lib/Components/OptionList/OptionList.d.ts.map +1 -1
  54. package/dist/typescript/src/lib/Components/OptionList/types.d.ts +42 -5
  55. package/dist/typescript/src/lib/Components/OptionList/types.d.ts.map +1 -1
  56. package/dist/typescript/src/lib/Components/OptionList/useOptionList/useOptionListItems.d.ts +9 -1
  57. package/dist/typescript/src/lib/Components/OptionList/useOptionList/useOptionListItems.d.ts.map +1 -1
  58. package/dist/typescript/src/lib/Components/Skeleton/Skeleton.d.ts +1 -1
  59. package/dist/typescript/src/lib/Components/Skeleton/Skeleton.d.ts.map +1 -1
  60. package/dist/typescript/src/lib/Components/TabBar/TabBar.d.ts.map +1 -1
  61. package/dist/typescript/src/lib/types/index.d.ts +3 -3
  62. package/dist/typescript/src/lib/types/index.d.ts.map +1 -1
  63. package/dist/typescript/src/styles/lx/resolveStyle.d.ts +3 -3
  64. package/dist/typescript/src/styles/lx/resolveStyle.d.ts.map +1 -1
  65. package/package.json +1 -1
  66. package/src/index.ts +1 -0
  67. package/src/lib/Animations/Pulse/Pulse.tsx +34 -29
  68. package/src/lib/Animations/Pulse/types.ts +2 -1
  69. package/src/lib/Components/BottomSheet/BottomSheet.stories.tsx +174 -1
  70. package/src/lib/Components/BottomSheet/BottomSheet.test.tsx +59 -0
  71. package/src/lib/Components/BottomSheet/BottomSheet.tsx +19 -7
  72. package/src/lib/Components/BottomSheet/BottomSheetHeader.tsx +1 -1
  73. package/src/lib/Components/BottomSheet/CustomHandle.tsx +26 -5
  74. package/src/lib/Components/BottomSheet/types.ts +24 -3
  75. package/src/lib/Components/Card/Card.tsx +3 -3
  76. package/src/lib/Components/ListItem/ListItem.tsx +3 -3
  77. package/src/lib/Components/MediaImage/MediaImage.tsx +5 -1
  78. package/src/lib/Components/OptionList/OptionList.mdx +19 -0
  79. package/src/lib/Components/OptionList/OptionList.stories.tsx +254 -0
  80. package/src/lib/Components/OptionList/OptionList.test.tsx +143 -0
  81. package/src/lib/Components/OptionList/OptionList.tsx +49 -3
  82. package/src/lib/Components/OptionList/types.ts +46 -5
  83. package/src/lib/Components/OptionList/useOptionList/useOptionListItems.test.ts +124 -2
  84. package/src/lib/Components/OptionList/useOptionList/useOptionListItems.ts +53 -10
  85. package/src/lib/Components/PageIndicator/PageIndicator.test.tsx +2 -1
  86. package/src/lib/Components/Skeleton/Skeleton.tsx +9 -5
  87. package/src/lib/Components/TabBar/TabBar.tsx +3 -2
  88. package/src/lib/types/index.ts +3 -3
  89. package/src/styles/lx/resolveStyle.ts +4 -3
@@ -6,7 +6,7 @@ import { StyleSheet } from 'react-native';
6
6
  import { useStyleSheet } from '../../../styles';
7
7
  import { RuntimeConstants } from '../../utils';
8
8
  import { CustomBackdrop } from './CustomBackdrop';
9
- import { CustomHandle } from './CustomHandle';
9
+ import { CustomHandle, HiddenHandle } from './CustomHandle';
10
10
  import type { BottomSheetProps } from './types';
11
11
 
12
12
  const OFFSET_TOP = 25;
@@ -25,7 +25,13 @@ const MAX_DYNAMIC_CONTENT_SIZE = {
25
25
  fullWithOffset: FULL_WITH_OFFSET,
26
26
  };
27
27
 
28
- const useStyles = ({ shadow }: { shadow: boolean }) => {
28
+ const useStyles = ({
29
+ shadow,
30
+ hasCustomBackground,
31
+ }: {
32
+ shadow: boolean;
33
+ hasCustomBackground: boolean;
34
+ }) => {
29
35
  return useStyleSheet(
30
36
  (t) => ({
31
37
  root: StyleSheet.flatten([
@@ -35,7 +41,7 @@ const useStyles = ({ shadow }: { shadow: boolean }) => {
35
41
  flex: 1,
36
42
  borderTopLeftRadius: t.borderRadius.xl,
37
43
  borderTopRightRadius: t.borderRadius.xl,
38
- backgroundColor: t.colors.bg.canvasSheet,
44
+ overflow: 'hidden',
39
45
  },
40
46
  shadow && {
41
47
  boxShadow: t.shadows.lg,
@@ -46,7 +52,7 @@ const useStyles = ({ shadow }: { shadow: boolean }) => {
46
52
  backgroundColor: t.colors.bg.canvasSheet,
47
53
  },
48
54
  }),
49
- [shadow],
55
+ [shadow, hasCustomBackground],
50
56
  );
51
57
  };
52
58
 
@@ -74,6 +80,8 @@ export const BottomSheet = ({
74
80
  onBackdropPress,
75
81
  onChange,
76
82
  snapPoints = 'fullWithOffset',
83
+ backgroundComponent,
84
+ hideHandle = false,
77
85
  ref,
78
86
  ...props
79
87
  }: BottomSheetProps) => {
@@ -82,7 +90,10 @@ export const BottomSheet = ({
82
90
  const mergedRefs = useMergedRef<GorhomBottomSheetModal>(ref, innerRef);
83
91
  const [isOpen, setIsOpen] = useState(false);
84
92
 
85
- const styles = useStyles({ shadow: hideBackdrop && isOpen });
93
+ const styles = useStyles({
94
+ shadow: hideBackdrop && isOpen,
95
+ hasCustomBackground: Boolean(backgroundComponent),
96
+ });
86
97
 
87
98
  /**
88
99
  * Match the snap points to the preset or the custom snap points array
@@ -163,7 +174,8 @@ export const BottomSheet = ({
163
174
  {...props}
164
175
  ref={mergedRefs}
165
176
  style={styles.root}
166
- backgroundStyle={styles.background}
177
+ backgroundStyle={backgroundComponent ? undefined : styles.background}
178
+ backgroundComponent={backgroundComponent}
167
179
  onChange={handleChange}
168
180
  onAnimate={handleAnimate}
169
181
  /**
@@ -188,7 +200,7 @@ export const BottomSheet = ({
188
200
  /**
189
201
  * Components
190
202
  */
191
- handleComponent={CustomHandle}
203
+ handleComponent={hideHandle ? HiddenHandle : CustomHandle}
192
204
  backdropComponent={hideBackdrop ? undefined : renderBackdrop}
193
205
  >
194
206
  <BottomSheetProvider value={{ onBack, hideCloseButton }}>
@@ -28,7 +28,7 @@ const useStyles = ({
28
28
  {
29
29
  position: 'relative',
30
30
  zIndex: Z_INDEX_DIALOG_CONTENT,
31
- backgroundColor: t.colors.bg.canvasSheet,
31
+ backgroundColor: 'transparent',
32
32
  paddingBottom: t.spacings.s12,
33
33
  },
34
34
  spacing && {
@@ -1,5 +1,5 @@
1
1
  import type { BottomSheetVariables } from '@gorhom/bottom-sheet/lib/typescript/types';
2
- import type { Ref } from 'react';
2
+ import type { ComponentRef, Ref } from 'react';
3
3
  import { View } from 'react-native';
4
4
  import { useStyleSheet } from '../../../styles';
5
5
 
@@ -12,13 +12,13 @@ const useStyles = () => {
12
12
  alignItems: 'center',
13
13
  justifyContent: 'center',
14
14
  alignSelf: 'center',
15
- backgroundColor: t.colors.bg.canvasSheet,
15
+ backgroundColor: 'transparent',
16
16
  },
17
17
  handle: {
18
18
  height: t.spacings.s4,
19
19
  width: t.sizes.s36,
20
20
  borderRadius: t.borderRadius.full,
21
- backgroundColor: t.colors.bg.mutedPressed,
21
+ backgroundColor: t.colors.bg.mutedTransparentPressed,
22
22
  },
23
23
  }),
24
24
  [],
@@ -28,12 +28,33 @@ const useStyles = () => {
28
28
  export const CustomHandle = ({
29
29
  ref,
30
30
  ...props
31
- }: BottomSheetVariables & { ref?: Ref<React.ElementRef<typeof View>> }) => {
31
+ }: BottomSheetVariables & { ref?: Ref<ComponentRef<typeof View>> }) => {
32
32
  const styles = useStyles();
33
33
 
34
34
  return (
35
- <View {...props} ref={ref} style={styles.container}>
35
+ <View
36
+ {...props}
37
+ ref={ref}
38
+ style={styles.container}
39
+ testID='bottom-sheet-handle'
40
+ >
36
41
  <View style={styles.handle} />
37
42
  </View>
38
43
  );
39
44
  };
45
+
46
+ export const HiddenHandle = ({
47
+ ref,
48
+ ...props
49
+ }: BottomSheetVariables & { ref?: Ref<ComponentRef<typeof View>> }) => {
50
+ const styles = useStyles();
51
+
52
+ return (
53
+ <View
54
+ {...props}
55
+ ref={ref}
56
+ style={styles.container}
57
+ testID='bottom-sheet-handle-hidden'
58
+ />
59
+ );
60
+ };
@@ -5,16 +5,23 @@ import type {
5
5
  BottomSheetSectionList as GorhomBottomSheetSectionList,
6
6
  BottomSheetScrollView as GorhomBottomSheetScrollView,
7
7
  BottomSheetVirtualizedList as GorhomBottomSheetVirtualizedList,
8
+ BottomSheetBackgroundProps,
8
9
  } from '@gorhom/bottom-sheet';
9
-
10
10
  import type { Density } from '@ledgerhq/lumen-utils-shared';
11
- import type { PropsWithChildren, ReactNode, Ref } from 'react';
11
+ import type {
12
+ ComponentRef,
13
+ FC,
14
+ PropsWithChildren,
15
+ ReactNode,
16
+ Ref,
17
+ } from 'react';
18
+
12
19
  import type { StyledViewProps } from '../../../styles';
13
20
  export type BottomSheetProps = PropsWithChildren & {
14
21
  /**
15
22
  * Ref to the bottom sheet component.
16
23
  */
17
- ref?: Ref<React.ElementRef<typeof GorhomBottomSheetModal>>;
24
+ ref?: Ref<ComponentRef<typeof GorhomBottomSheetModal>>;
18
25
  /**
19
26
  * Used to locate this view in end-to-end tests.
20
27
  */
@@ -131,8 +138,22 @@ export type BottomSheetProps = PropsWithChildren & {
131
138
  * @default true
132
139
  */
133
140
  enableBlurKeyboardOnGesture?: boolean;
141
+ /**
142
+ * Custom background component rendered behind the sheet content and handle.
143
+ * Use to render gradients or other effects that should span the full sheet area,
144
+ * including the handle strip. When provided, the default sheet background is removed.
145
+ * @default undefined
146
+ */
147
+ backgroundComponent?: FC<BottomSheetBackgroundProps> | null;
148
+ /**
149
+ * If true, the drag handle (grabber) at the top of the sheet is hidden.
150
+ * @default false
151
+ */
152
+ hideHandle?: boolean;
134
153
  };
135
154
 
155
+ export type { BottomSheetBackgroundProps };
156
+
136
157
  export type BottomSheetHeaderProps = {
137
158
  /**
138
159
  * The density of the header.
@@ -3,7 +3,7 @@ import {
3
3
  DisabledProvider,
4
4
  isTextChildren,
5
5
  } from '@ledgerhq/lumen-utils-shared';
6
- import type { ReactNode, Ref } from 'react';
6
+ import type { ComponentRef, ReactNode, Ref } from 'react';
7
7
  import { useCallback, useEffect, useMemo } from 'react';
8
8
  import type { LayoutChangeEvent, ViewStyle } from 'react-native';
9
9
  import { StyleSheet, View } from 'react-native';
@@ -486,7 +486,7 @@ export const CardContentTitle = ({
486
486
  if (isTextChildren(children)) {
487
487
  return (
488
488
  <Text
489
- ref={ref as Ref<React.ElementRef<typeof Text>>}
489
+ ref={ref as Ref<ComponentRef<typeof Text>>}
490
490
  lx={lx}
491
491
  style={StyleSheet.flatten([styles.asText, style])}
492
492
  numberOfLines={1}
@@ -553,7 +553,7 @@ export const CardContentDescription = ({
553
553
  if (isTextChildren(children)) {
554
554
  return (
555
555
  <Text
556
- ref={ref as Ref<React.ElementRef<typeof Text>>}
556
+ ref={ref as Ref<ComponentRef<typeof Text>>}
557
557
  lx={lx}
558
558
  style={StyleSheet.flatten([styles.asText, style])}
559
559
  numberOfLines={1}
@@ -4,7 +4,7 @@ import {
4
4
  DisabledProvider,
5
5
  useDisabledContext,
6
6
  } from '@ledgerhq/lumen-utils-shared';
7
- import type { ElementRef, ReactNode, Ref } from 'react';
7
+ import type { ComponentRef, ReactNode, Ref } from 'react';
8
8
  import type { ViewStyle } from 'react-native';
9
9
  import { StyleSheet, View } from 'react-native';
10
10
  import { useStyleSheet } from '../../../styles';
@@ -286,7 +286,7 @@ export const ListItemTitle = ({
286
286
  style,
287
287
  ref,
288
288
  ...props
289
- }: ListItemTitleProps & { ref?: Ref<ElementRef<typeof Text>> }) => {
289
+ }: ListItemTitleProps & { ref?: Ref<ComponentRef<typeof Text>> }) => {
290
290
  const disabled = useDisabledContext({
291
291
  consumerName: 'ListItemTitle',
292
292
  contextRequired: true,
@@ -336,7 +336,7 @@ export const ListItemDescription = ({
336
336
  style,
337
337
  ref,
338
338
  ...props
339
- }: ListItemDescriptionProps & { ref?: Ref<ElementRef<typeof Text>> }) => {
339
+ }: ListItemDescriptionProps & { ref?: Ref<ComponentRef<typeof Text>> }) => {
340
340
  const disabled = useDisabledContext({
341
341
  consumerName: 'ListItemDescription',
342
342
  contextRequired: true,
@@ -66,6 +66,10 @@ const useStyles = ({
66
66
  width: '100%',
67
67
  height: '100%',
68
68
  },
69
+ skeleton: {
70
+ width: '100%',
71
+ height: '100%',
72
+ },
69
73
  };
70
74
  },
71
75
  [size, shape],
@@ -117,7 +121,7 @@ export const MediaImage = ({
117
121
  accessibilityLabel={alt}
118
122
  {...props}
119
123
  >
120
- {loading && <Skeleton style={StyleSheet.absoluteFillObject} />}
124
+ {loading && <Skeleton style={styles.skeleton} />}
121
125
  {!loading && shouldFallback && fallback && (
122
126
  <Text
123
127
  style={{
@@ -36,6 +36,7 @@ It handles **selection state**, **automatic grouping** (via a `group` field on i
36
36
  - **OptionListItemContentRow**: Horizontal row for placing elements side-by-side (e.g. title + tag)
37
37
  - **OptionListItemText**: Styled title text
38
38
  - **OptionListItemDescription**: Styled description text
39
+ - **OptionListSearch**: Search input that filters items by label (default) or via a custom `filter` function
39
40
 
40
41
  ## Properties
41
42
 
@@ -63,6 +64,24 @@ Combining grouping with rich item content:
63
64
 
64
65
  <Canvas of={OptionListStories.GroupedWithContentRow} />
65
66
 
67
+ ### Search
68
+
69
+ Drop `OptionListSearch` inside the list to enable case-insensitive filtering on item labels. Pair with `OptionListEmptyState` to handle no-match scenarios. Pass a custom `filter` to match against meta (or anything else), or `filter={null}` to disable filtering. Use `filteredItems` + `onSearchValueChange` for async/server-side search.
70
+
71
+ <Canvas of={OptionListStories.WithSearch} />
72
+
73
+ Search works within grouped items too. Empty groups are hidden automatically:
74
+
75
+ <Canvas of={OptionListStories.WithSearchAndGroups} />
76
+
77
+ Pass a custom `filter` to match against fields beyond the label. Here, the query also matches each item's ticker via `meta`:
78
+
79
+ <Canvas of={OptionListStories.WithCustomSearchFilter} />
80
+
81
+ Drive the search input from the outside with `searchValue` + `onSearchValueChange`. This is useful for syncing search state with other UI or kicking off async fetches:
82
+
83
+ <Canvas of={OptionListStories.WithControlledSearch} />
84
+
66
85
  ### Trigger showcase
67
86
 
68
87
  OptionList can be opened from any trigger. `MediaButton` supports multiple appearances (`gray`, `transparent`, `no-background`), optional icons (`flat` / `rounded`), and disabled state:
@@ -22,6 +22,7 @@ import {
22
22
  OptionListItemContent,
23
23
  OptionListItemDescription,
24
24
  OptionListItemContentRow,
25
+ OptionListSearch,
25
26
  OptionListTrigger,
26
27
  OptionListItemText,
27
28
  } from './OptionList';
@@ -38,6 +39,7 @@ const meta = {
38
39
  OptionListItemText,
39
40
  OptionListItemDescription,
40
41
  OptionListItemContentRow,
42
+ OptionListSearch,
41
43
  OptionListTrigger,
42
44
  },
43
45
  decorators: [
@@ -477,6 +479,258 @@ export const GroupedWithContentRow: Story = {
477
479
  },
478
480
  };
479
481
 
482
+ export const WithSearch: Story = {
483
+ render: () => {
484
+ const [value, setValue] = useState<string | null>(null);
485
+ const bottomSheetRef = useBottomSheetRef();
486
+ const selected = CURRENCIES.find((c) => c.value === value);
487
+
488
+ return (
489
+ <>
490
+ <OptionListTrigger
491
+ label='Currency'
492
+ onPress={() => bottomSheetRef.current?.present()}
493
+ >
494
+ {selected && <Text lx={{ color: 'base' }}>{selected.label}</Text>}
495
+ </OptionListTrigger>
496
+ <BottomSheet
497
+ ref={bottomSheetRef}
498
+ enableDynamicSizing
499
+ snapPoints={null}
500
+ onClose={() => bottomSheetRef.current?.dismiss()}
501
+ >
502
+ <BottomSheetView>
503
+ <BottomSheetHeader title='Select currency' />
504
+ <OptionList
505
+ items={CURRENCIES}
506
+ value={value}
507
+ onValueChange={(v) => {
508
+ setValue(v);
509
+ bottomSheetRef.current?.dismiss();
510
+ }}
511
+ >
512
+ <OptionListSearch placeholder='Search currencies' />
513
+ <OptionListContent
514
+ renderItem={(item) => {
515
+ const ticker = (item.meta as { ticker: string }).ticker;
516
+ return (
517
+ <OptionListItem value={item.value}>
518
+ <OptionListItemLeading>
519
+ <CryptoIcon
520
+ ledgerId={(item.meta?.ledgerId as string) ?? ''}
521
+ ticker={ticker}
522
+ size={32}
523
+ />
524
+ </OptionListItemLeading>
525
+ <OptionListItemContent>
526
+ <OptionListItemText>{item.label}</OptionListItemText>
527
+ <OptionListItemDescription>
528
+ {ticker}
529
+ </OptionListItemDescription>
530
+ </OptionListItemContent>
531
+ </OptionListItem>
532
+ );
533
+ }}
534
+ />
535
+ <OptionListEmptyState
536
+ title='No currencies found'
537
+ description='Try a different search term'
538
+ />
539
+ </OptionList>
540
+ </BottomSheetView>
541
+ </BottomSheet>
542
+ </>
543
+ );
544
+ },
545
+ };
546
+
547
+ export const WithSearchAndGroups: Story = {
548
+ render: () => {
549
+ const [value, setValue] = useState<string | null>(null);
550
+ const bottomSheetRef = useBottomSheetRef();
551
+ const selected = GROUPED_NETWORKS.find((n) => n.value === value);
552
+
553
+ return (
554
+ <>
555
+ <OptionListTrigger
556
+ label='Network'
557
+ onPress={() => bottomSheetRef.current?.present()}
558
+ >
559
+ {selected && <Text lx={{ color: 'base' }}>{selected.label}</Text>}
560
+ </OptionListTrigger>
561
+ <BottomSheet
562
+ ref={bottomSheetRef}
563
+ enableDynamicSizing
564
+ snapPoints={null}
565
+ onClose={() => bottomSheetRef.current?.dismiss()}
566
+ >
567
+ <BottomSheetScrollView>
568
+ <BottomSheetHeader title='Select network' />
569
+ <OptionList
570
+ items={GROUPED_NETWORKS}
571
+ value={value}
572
+ onValueChange={(v) => {
573
+ setValue(v);
574
+ bottomSheetRef.current?.dismiss();
575
+ }}
576
+ >
577
+ <OptionListSearch placeholder='Search networks' />
578
+ <OptionListContent
579
+ renderItem={(item) => (
580
+ <OptionListItem value={item.value}>
581
+ <OptionListItemContent>
582
+ <OptionListItemText>{item.label}</OptionListItemText>
583
+ </OptionListItemContent>
584
+ </OptionListItem>
585
+ )}
586
+ />
587
+ <OptionListEmptyState title='No networks found' />
588
+ </OptionList>
589
+ </BottomSheetScrollView>
590
+ </BottomSheet>
591
+ </>
592
+ );
593
+ },
594
+ };
595
+
596
+ export const WithCustomSearchFilter: Story = {
597
+ render: () => {
598
+ const [value, setValue] = useState<string | null>(null);
599
+ const bottomSheetRef = useBottomSheetRef();
600
+ const selected = CURRENCIES.find((c) => c.value === value);
601
+
602
+ return (
603
+ <>
604
+ <OptionListTrigger
605
+ label='Currency'
606
+ onPress={() => bottomSheetRef.current?.present()}
607
+ >
608
+ {selected && <Text lx={{ color: 'base' }}>{selected.label}</Text>}
609
+ </OptionListTrigger>
610
+ <BottomSheet
611
+ ref={bottomSheetRef}
612
+ enableDynamicSizing
613
+ snapPoints={null}
614
+ onClose={() => bottomSheetRef.current?.dismiss()}
615
+ >
616
+ <BottomSheetView>
617
+ <BottomSheetHeader title='Select currency' />
618
+ <OptionList
619
+ items={CURRENCIES}
620
+ value={value}
621
+ onValueChange={(v) => {
622
+ setValue(v);
623
+ bottomSheetRef.current?.dismiss();
624
+ }}
625
+ filter={(item, query) => {
626
+ const q = query.toLowerCase();
627
+ const ticker = (item.meta as { ticker: string }).ticker;
628
+ return (
629
+ item.label.toLowerCase().includes(q) ||
630
+ ticker.toLowerCase().includes(q)
631
+ );
632
+ }}
633
+ >
634
+ <OptionListSearch placeholder='Search by name or ticker' />
635
+ <OptionListContent
636
+ renderItem={(item) => {
637
+ const ticker = (item.meta as { ticker: string }).ticker;
638
+ return (
639
+ <OptionListItem value={item.value}>
640
+ <OptionListItemLeading>
641
+ <CryptoIcon
642
+ ledgerId={(item.meta?.ledgerId as string) ?? ''}
643
+ ticker={ticker}
644
+ size={32}
645
+ />
646
+ </OptionListItemLeading>
647
+ <OptionListItemContent>
648
+ <OptionListItemText>{item.label}</OptionListItemText>
649
+ <OptionListItemDescription>
650
+ {ticker}
651
+ </OptionListItemDescription>
652
+ </OptionListItemContent>
653
+ </OptionListItem>
654
+ );
655
+ }}
656
+ />
657
+ <OptionListEmptyState title='No currencies found' />
658
+ </OptionList>
659
+ </BottomSheetView>
660
+ </BottomSheet>
661
+ </>
662
+ );
663
+ },
664
+ };
665
+
666
+ export const WithControlledSearch: Story = {
667
+ render: () => {
668
+ const [value, setValue] = useState<string | null>(null);
669
+ const [searchValue, setSearchValue] = useState('');
670
+ const bottomSheetRef = useBottomSheetRef();
671
+ const selected = CURRENCIES.find((c) => c.value === value);
672
+
673
+ return (
674
+ <>
675
+ <OptionListTrigger
676
+ label='Currency'
677
+ onPress={() => bottomSheetRef.current?.present()}
678
+ >
679
+ {selected && <Text lx={{ color: 'base' }}>{selected.label}</Text>}
680
+ </OptionListTrigger>
681
+ <BottomSheet
682
+ ref={bottomSheetRef}
683
+ enableDynamicSizing
684
+ snapPoints={null}
685
+ onClose={() => bottomSheetRef.current?.dismiss()}
686
+ >
687
+ <BottomSheetView>
688
+ <BottomSheetHeader title='Select currency' />
689
+ <Box lx={{ padding: 's8' }}>
690
+ <Text lx={{ color: 'muted' }}>Search: "{searchValue}"</Text>
691
+ </Box>
692
+ <OptionList
693
+ items={CURRENCIES}
694
+ value={value}
695
+ onValueChange={(v) => {
696
+ setValue(v);
697
+ bottomSheetRef.current?.dismiss();
698
+ }}
699
+ searchValue={searchValue}
700
+ onSearchValueChange={setSearchValue}
701
+ >
702
+ <OptionListSearch placeholder='Search currencies' />
703
+ <OptionListContent
704
+ renderItem={(item) => {
705
+ const ticker = (item.meta as { ticker: string }).ticker;
706
+ return (
707
+ <OptionListItem value={item.value}>
708
+ <OptionListItemLeading>
709
+ <CryptoIcon
710
+ ledgerId={(item.meta?.ledgerId as string) ?? ''}
711
+ ticker={ticker}
712
+ size={32}
713
+ />
714
+ </OptionListItemLeading>
715
+ <OptionListItemContent>
716
+ <OptionListItemText>{item.label}</OptionListItemText>
717
+ <OptionListItemDescription>
718
+ {ticker}
719
+ </OptionListItemDescription>
720
+ </OptionListItemContent>
721
+ </OptionListItem>
722
+ );
723
+ }}
724
+ />
725
+ <OptionListEmptyState title='No currencies found' />
726
+ </OptionList>
727
+ </BottomSheetView>
728
+ </BottomSheet>
729
+ </>
730
+ );
731
+ },
732
+ };
733
+
480
734
  export const EmptyState: Story = {
481
735
  render: () => {
482
736
  const bottomSheetRef = useBottomSheetRef();