@ledgerhq/lumen-ui-rnative 0.1.21 → 0.1.23

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 (99) hide show
  1. package/dist/module/lib/Components/ListItem/ListItem.js +57 -27
  2. package/dist/module/lib/Components/ListItem/ListItem.js.map +1 -1
  3. package/dist/module/lib/Components/ListItem/ListItem.mdx +15 -7
  4. package/dist/module/lib/Components/ListItem/ListItem.stories.js +497 -283
  5. package/dist/module/lib/Components/ListItem/ListItem.stories.js.map +1 -1
  6. package/dist/module/lib/Components/ListItem/ListItem.test.js +153 -0
  7. package/dist/module/lib/Components/ListItem/ListItem.test.js.map +1 -0
  8. package/dist/module/lib/Components/{TriggerButton/TriggerButton.js → MediaButton/MediaButton.js} +13 -10
  9. package/dist/module/lib/Components/MediaButton/MediaButton.js.map +1 -0
  10. package/{src/lib/Components/TriggerButton/TriggerButton.mdx → dist/module/lib/Components/MediaButton/MediaButton.mdx} +10 -10
  11. package/dist/module/lib/Components/{TriggerButton/TriggerButton.stories.js → MediaButton/MediaButton.stories.js} +18 -18
  12. package/dist/module/lib/Components/MediaButton/MediaButton.stories.js.map +1 -0
  13. package/dist/module/lib/Components/{TriggerButton/TriggerButton.test.js → MediaButton/MediaButton.test.js} +14 -14
  14. package/dist/module/lib/Components/MediaButton/MediaButton.test.js.map +1 -0
  15. package/dist/module/lib/Components/MediaButton/index.js +5 -0
  16. package/dist/module/lib/Components/MediaButton/index.js.map +1 -0
  17. package/dist/module/lib/Components/MediaButton/types.js.map +1 -0
  18. package/dist/module/lib/Components/NavBar/NavBar.js +0 -2
  19. package/dist/module/lib/Components/NavBar/NavBar.js.map +1 -1
  20. package/dist/module/lib/Components/OptionList/OptionList.figma.js +28 -0
  21. package/dist/module/lib/Components/OptionList/OptionList.figma.js.map +1 -0
  22. package/dist/module/lib/Components/OptionList/OptionList.js +452 -0
  23. package/dist/module/lib/Components/OptionList/OptionList.js.map +1 -0
  24. package/dist/module/lib/Components/OptionList/OptionList.mdx +304 -0
  25. package/dist/module/lib/Components/OptionList/OptionList.stories.js +735 -0
  26. package/dist/module/lib/Components/OptionList/OptionList.stories.js.map +1 -0
  27. package/dist/module/lib/Components/OptionList/OptionList.test.js +443 -0
  28. package/dist/module/lib/Components/OptionList/OptionList.test.js.map +1 -0
  29. package/dist/module/lib/Components/OptionList/index.js +5 -0
  30. package/dist/module/lib/Components/OptionList/index.js.map +1 -0
  31. package/dist/module/lib/Components/OptionList/types.js +4 -0
  32. package/dist/module/lib/Components/OptionList/types.js.map +1 -0
  33. package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.js +36 -0
  34. package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.js.map +1 -0
  35. package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.test.js +84 -0
  36. package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.test.js.map +1 -0
  37. package/dist/module/lib/Components/index.js +2 -1
  38. package/dist/module/lib/Components/index.js.map +1 -1
  39. package/dist/typescript/src/lib/Components/ListItem/ListItem.d.ts +8 -8
  40. package/dist/typescript/src/lib/Components/ListItem/ListItem.d.ts.map +1 -1
  41. package/dist/typescript/src/lib/Components/ListItem/types.d.ts +11 -7
  42. package/dist/typescript/src/lib/Components/ListItem/types.d.ts.map +1 -1
  43. package/dist/typescript/src/lib/Components/MediaButton/MediaButton.d.ts +23 -0
  44. package/dist/typescript/src/lib/Components/MediaButton/MediaButton.d.ts.map +1 -0
  45. package/dist/typescript/src/lib/Components/MediaButton/index.d.ts +3 -0
  46. package/dist/typescript/src/lib/Components/MediaButton/index.d.ts.map +1 -0
  47. package/dist/typescript/src/lib/Components/{TriggerButton → MediaButton}/types.d.ts +10 -5
  48. package/dist/typescript/src/lib/Components/MediaButton/types.d.ts.map +1 -0
  49. package/dist/typescript/src/lib/Components/OptionList/OptionList.d.ts +12 -0
  50. package/dist/typescript/src/lib/Components/OptionList/OptionList.d.ts.map +1 -0
  51. package/dist/typescript/src/lib/Components/OptionList/OptionList.figma.d.ts +2 -0
  52. package/dist/typescript/src/lib/Components/OptionList/OptionList.figma.d.ts.map +1 -0
  53. package/dist/typescript/src/lib/Components/OptionList/index.d.ts +3 -0
  54. package/dist/typescript/src/lib/Components/OptionList/index.d.ts.map +1 -0
  55. package/dist/typescript/src/lib/Components/OptionList/types.d.ts +97 -0
  56. package/dist/typescript/src/lib/Components/OptionList/types.d.ts.map +1 -0
  57. package/dist/typescript/src/lib/Components/OptionList/useOptionList/useOptionListItems.d.ts +12 -0
  58. package/dist/typescript/src/lib/Components/OptionList/useOptionList/useOptionListItems.d.ts.map +1 -0
  59. package/dist/typescript/src/lib/Components/index.d.ts +2 -1
  60. package/dist/typescript/src/lib/Components/index.d.ts.map +1 -1
  61. package/dist/typescript/src/styles/types/theme.types.d.ts +7 -6
  62. package/dist/typescript/src/styles/types/theme.types.d.ts.map +1 -1
  63. package/package.json +1 -1
  64. package/src/lib/Components/ListItem/ListItem.mdx +15 -7
  65. package/src/lib/Components/ListItem/ListItem.stories.tsx +354 -220
  66. package/src/lib/Components/ListItem/ListItem.test.tsx +152 -0
  67. package/src/lib/Components/ListItem/ListItem.tsx +63 -28
  68. package/src/lib/Components/ListItem/types.ts +11 -8
  69. package/{dist/module/lib/Components/TriggerButton/TriggerButton.mdx → src/lib/Components/MediaButton/MediaButton.mdx} +10 -10
  70. package/src/lib/Components/{TriggerButton/TriggerButton.stories.tsx → MediaButton/MediaButton.stories.tsx} +28 -28
  71. package/src/lib/Components/{TriggerButton/TriggerButton.test.tsx → MediaButton/MediaButton.test.tsx} +22 -22
  72. package/src/lib/Components/{TriggerButton/TriggerButton.tsx → MediaButton/MediaButton.tsx} +27 -21
  73. package/src/lib/Components/MediaButton/index.ts +2 -0
  74. package/src/lib/Components/{TriggerButton → MediaButton}/types.ts +10 -5
  75. package/src/lib/Components/NavBar/NavBar.tsx +0 -3
  76. package/src/lib/Components/OptionList/OptionList.figma.tsx +37 -0
  77. package/src/lib/Components/OptionList/OptionList.mdx +304 -0
  78. package/src/lib/Components/OptionList/OptionList.stories.tsx +755 -0
  79. package/src/lib/Components/OptionList/OptionList.test.tsx +412 -0
  80. package/src/lib/Components/OptionList/OptionList.tsx +532 -0
  81. package/src/lib/Components/OptionList/index.ts +2 -0
  82. package/src/lib/Components/OptionList/types.ts +115 -0
  83. package/src/lib/Components/OptionList/useOptionList/useOptionListItems.test.ts +73 -0
  84. package/src/lib/Components/OptionList/useOptionList/useOptionListItems.ts +49 -0
  85. package/src/lib/Components/index.ts +2 -1
  86. package/src/styles/types/theme.types.ts +8 -6
  87. package/dist/module/lib/Components/TriggerButton/TriggerButton.js.map +0 -1
  88. package/dist/module/lib/Components/TriggerButton/TriggerButton.stories.js.map +0 -1
  89. package/dist/module/lib/Components/TriggerButton/TriggerButton.test.js.map +0 -1
  90. package/dist/module/lib/Components/TriggerButton/index.js +0 -5
  91. package/dist/module/lib/Components/TriggerButton/index.js.map +0 -1
  92. package/dist/module/lib/Components/TriggerButton/types.js.map +0 -1
  93. package/dist/typescript/src/lib/Components/TriggerButton/TriggerButton.d.ts +0 -23
  94. package/dist/typescript/src/lib/Components/TriggerButton/TriggerButton.d.ts.map +0 -1
  95. package/dist/typescript/src/lib/Components/TriggerButton/index.d.ts +0 -3
  96. package/dist/typescript/src/lib/Components/TriggerButton/index.d.ts.map +0 -1
  97. package/dist/typescript/src/lib/Components/TriggerButton/types.d.ts.map +0 -1
  98. package/src/lib/Components/TriggerButton/index.ts +0 -2
  99. /package/dist/module/lib/Components/{TriggerButton → MediaButton}/types.js +0 -0
@@ -6,7 +6,7 @@ import { View, ViewStyle } from 'react-native';
6
6
 
7
7
  import { Settings } from '../../Symbols';
8
8
  import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
9
- import { TriggerButton } from './TriggerButton';
9
+ import { MediaButton } from './MediaButton';
10
10
 
11
11
  const renderWithProvider = (component: React.ReactElement) => {
12
12
  return render(
@@ -16,11 +16,11 @@ const renderWithProvider = (component: React.ReactElement) => {
16
16
  );
17
17
  };
18
18
 
19
- describe('TriggerButton', () => {
19
+ describe('MediaButton', () => {
20
20
  describe('Rendering', () => {
21
21
  it('should render with label text and correct accessibility role', () => {
22
22
  renderWithProvider(
23
- <TriggerButton testID='trigger'>All accounts</TriggerButton>,
23
+ <MediaButton testID='trigger'>All accounts</MediaButton>,
24
24
  );
25
25
  const trigger = screen.getByTestId('trigger');
26
26
  expect(trigger).toBeTruthy();
@@ -29,7 +29,7 @@ describe('TriggerButton', () => {
29
29
  });
30
30
 
31
31
  it('should always render a chevron icon', () => {
32
- renderWithProvider(<TriggerButton testID='trigger'>Label</TriggerButton>);
32
+ renderWithProvider(<MediaButton testID='trigger'>Label</MediaButton>);
33
33
  expect(screen.getByTestId('button-trigger-chevron')).toBeTruthy();
34
34
  });
35
35
 
@@ -37,9 +37,9 @@ describe('TriggerButton', () => {
37
37
  'should render without errors for appearance "%s"',
38
38
  (appearance) => {
39
39
  renderWithProvider(
40
- <TriggerButton testID='trigger' appearance={appearance}>
40
+ <MediaButton testID='trigger' appearance={appearance}>
41
41
  Label
42
- </TriggerButton>,
42
+ </MediaButton>,
43
43
  );
44
44
  expect(screen.getByTestId('trigger')).toBeTruthy();
45
45
  },
@@ -49,9 +49,9 @@ describe('TriggerButton', () => {
49
49
  'should render without errors for size "%s"',
50
50
  (size) => {
51
51
  renderWithProvider(
52
- <TriggerButton testID='trigger' size={size}>
52
+ <MediaButton testID='trigger' size={size}>
53
53
  Label
54
- </TriggerButton>,
54
+ </MediaButton>,
55
55
  );
56
56
  expect(screen.getByTestId('trigger')).toBeTruthy();
57
57
  },
@@ -61,13 +61,13 @@ describe('TriggerButton', () => {
61
61
  describe('Icons', () => {
62
62
  it('should render with a flat interface icon', () => {
63
63
  renderWithProvider(
64
- <TriggerButton
64
+ <MediaButton
65
65
  testID='trigger'
66
66
  icon={<Settings size={20} testID='icon' />}
67
67
  iconType='flat'
68
68
  >
69
69
  Network
70
- </TriggerButton>,
70
+ </MediaButton>,
71
71
  );
72
72
  expect(screen.getByTestId('icon')).toBeTruthy();
73
73
  expect(screen.getByText('Network')).toBeTruthy();
@@ -75,13 +75,13 @@ describe('TriggerButton', () => {
75
75
 
76
76
  it('should render with a rounded icon', () => {
77
77
  renderWithProvider(
78
- <TriggerButton
78
+ <MediaButton
79
79
  testID='trigger'
80
80
  icon={<View testID='crypto-icon' />}
81
81
  iconType='rounded'
82
82
  >
83
83
  Bitcoin
84
- </TriggerButton>,
84
+ </MediaButton>,
85
85
  );
86
86
  expect(screen.getByTestId('crypto-icon')).toBeTruthy();
87
87
  expect(screen.getByText('Bitcoin')).toBeTruthy();
@@ -91,9 +91,9 @@ describe('TriggerButton', () => {
91
91
  describe('States', () => {
92
92
  it('should be disabled when disabled prop is true', () => {
93
93
  renderWithProvider(
94
- <TriggerButton testID='trigger' disabled>
94
+ <MediaButton testID='trigger' disabled>
95
95
  Label
96
- </TriggerButton>,
96
+ </MediaButton>,
97
97
  );
98
98
  const trigger = screen.getByTestId('trigger');
99
99
  expect(trigger.props.accessibilityState.disabled).toBe(true);
@@ -104,9 +104,9 @@ describe('TriggerButton', () => {
104
104
  it('should call onPress when pressed', () => {
105
105
  const handlePress = jest.fn();
106
106
  renderWithProvider(
107
- <TriggerButton testID='trigger' onPress={handlePress}>
107
+ <MediaButton testID='trigger' onPress={handlePress}>
108
108
  Press me
109
- </TriggerButton>,
109
+ </MediaButton>,
110
110
  );
111
111
 
112
112
  fireEvent.press(screen.getByTestId('trigger'));
@@ -116,9 +116,9 @@ describe('TriggerButton', () => {
116
116
  it('should not call onPress when disabled', () => {
117
117
  const handlePress = jest.fn();
118
118
  renderWithProvider(
119
- <TriggerButton testID='trigger' onPress={handlePress} disabled>
119
+ <MediaButton testID='trigger' onPress={handlePress} disabled>
120
120
  Disabled
121
- </TriggerButton>,
121
+ </MediaButton>,
122
122
  );
123
123
 
124
124
  fireEvent.press(screen.getByTestId('trigger'));
@@ -130,9 +130,9 @@ describe('TriggerButton', () => {
130
130
  it('should forward ref', () => {
131
131
  const ref = createRef<View>();
132
132
  renderWithProvider(
133
- <TriggerButton ref={ref} testID='trigger'>
133
+ <MediaButton ref={ref} testID='trigger'>
134
134
  Label
135
- </TriggerButton>,
135
+ </MediaButton>,
136
136
  );
137
137
  expect(ref.current).toBeTruthy();
138
138
  });
@@ -142,13 +142,13 @@ describe('TriggerButton', () => {
142
142
  it('should apply custom style and lx props', () => {
143
143
  const customStyle: ViewStyle = { marginTop: 16 };
144
144
  renderWithProvider(
145
- <TriggerButton
145
+ <MediaButton
146
146
  testID='trigger'
147
147
  style={customStyle}
148
148
  lx={{ padding: 's8' }}
149
149
  >
150
150
  Styled
151
- </TriggerButton>,
151
+ </MediaButton>,
152
152
  );
153
153
  const trigger = screen.getByTestId('trigger');
154
154
  expect(trigger.props.style).toBeDefined();
@@ -3,10 +3,10 @@ import { StyleSheet, Text, View } from 'react-native';
3
3
  import { useStyleSheet } from '../../../styles';
4
4
  import { ChevronDown } from '../../Symbols';
5
5
  import { Pressable } from '../Utility';
6
- import type { TriggerButtonProps } from './types';
6
+ import type { MediaButtonProps } from './types';
7
7
 
8
- type Appearance = NonNullable<TriggerButtonProps['appearance']>;
9
- type Size = NonNullable<TriggerButtonProps['size']>;
8
+ type Appearance = NonNullable<MediaButtonProps['appearance']>;
9
+ type Size = NonNullable<MediaButtonProps['size']>;
10
10
  type IconType = 'flat' | 'rounded' | 'none';
11
11
 
12
12
  const useStyles = ({
@@ -129,7 +129,7 @@ const useStyles = ({
129
129
  };
130
130
 
131
131
  /**
132
- * Trigger button for select/dropdown components. Displays a label with an optional
132
+ * Media button for select/dropdown components. Displays a label with an optional
133
133
  * leading icon and a trailing chevron indicator.
134
134
  *
135
135
  * This component is intended to be used exclusively as the trigger inside a Select or
@@ -139,16 +139,16 @@ const useStyles = ({
139
139
  * @see {@link https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=6389-45680 Figma}
140
140
  *
141
141
  * @example
142
- * import { TriggerButton } from '@ledgerhq/lumen-ui-rnative';
142
+ * import { MediaButton } from '@ledgerhq/lumen-ui-rnative';
143
143
  * import { Settings } from '@ledgerhq/lumen-ui-rnative/symbols';
144
144
  *
145
- * <TriggerButton icon={<Settings size={20} />} iconType="flat">
145
+ * <MediaButton icon={<Settings size={20} />} iconType="flat">
146
146
  * Network
147
- * </TriggerButton>
147
+ * </MediaButton>
148
148
  *
149
- * <TriggerButton>All accounts</TriggerButton>
149
+ * <MediaButton>All accounts</MediaButton>
150
150
  */
151
- export const TriggerButton = ({
151
+ export const MediaButton = ({
152
152
  lx,
153
153
  style,
154
154
  appearance = 'gray',
@@ -156,10 +156,11 @@ export const TriggerButton = ({
156
156
  disabled = false,
157
157
  icon,
158
158
  iconType = 'flat',
159
+ hideChevron = false,
159
160
  children: label,
160
161
  ref,
161
162
  ...props
162
- }: TriggerButtonProps) => {
163
+ }: MediaButtonProps) => {
163
164
  const effectiveIconType: IconType = icon ? iconType : 'none';
164
165
 
165
166
  return (
@@ -173,39 +174,42 @@ export const TriggerButton = ({
173
174
  {...props}
174
175
  >
175
176
  {({ pressed }) => (
176
- <TriggerButtonContent
177
+ <MediaButtonContent
177
178
  appearance={appearance}
178
179
  size={size}
179
180
  disabled={disabled}
180
181
  pressed={pressed}
181
182
  iconType={effectiveIconType}
182
183
  icon={icon}
184
+ hideChevron={hideChevron}
183
185
  >
184
186
  {label}
185
- </TriggerButtonContent>
187
+ </MediaButtonContent>
186
188
  )}
187
189
  </Pressable>
188
190
  );
189
191
  };
190
192
 
191
- type TriggerButtonContentProps = PropsWithChildren<{
193
+ type MediaButtonContentProps = PropsWithChildren<{
192
194
  appearance: Appearance;
193
195
  size: Size;
194
196
  disabled: boolean;
195
197
  pressed: boolean;
196
198
  iconType: IconType;
197
- icon?: TriggerButtonProps['icon'];
199
+ icon?: MediaButtonProps['icon'];
200
+ hideChevron: boolean;
198
201
  }>;
199
202
 
200
- const TriggerButtonContent = ({
203
+ const MediaButtonContent = ({
201
204
  appearance,
202
205
  size,
203
206
  disabled,
204
207
  pressed,
205
208
  iconType,
206
209
  icon,
210
+ hideChevron,
207
211
  children,
208
- }: TriggerButtonContentProps) => {
212
+ }: MediaButtonContentProps) => {
209
213
  const styles = useStyles({ appearance, size, disabled, pressed, iconType });
210
214
 
211
215
  return (
@@ -215,11 +219,13 @@ const TriggerButtonContent = ({
215
219
  <Text style={styles.label} numberOfLines={1} ellipsizeMode='tail'>
216
220
  {children}
217
221
  </Text>
218
- <ChevronDown
219
- size={20}
220
- style={styles.chevron}
221
- testID='button-trigger-chevron'
222
- />
222
+ {!hideChevron && (
223
+ <ChevronDown
224
+ size={20}
225
+ style={styles.chevron}
226
+ testID='button-trigger-chevron'
227
+ />
228
+ )}
223
229
  </View>
224
230
  </View>
225
231
  );
@@ -0,0 +1,2 @@
1
+ export { MediaButton } from './MediaButton';
2
+ export * from './types';
@@ -1,14 +1,14 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { StyledPressableProps } from '../../../styles';
3
3
 
4
- export type TriggerButtonProps = {
4
+ export type MediaButtonProps = {
5
5
  /**
6
- * The visual style of the trigger button.
6
+ * The visual style of the media button.
7
7
  * @default 'gray'
8
8
  */
9
9
  appearance?: 'gray' | 'transparent' | 'no-background';
10
10
  /**
11
- * The size variant of the trigger button.
11
+ * The size variant of the media button.
12
12
  * @default 'md'
13
13
  */
14
14
  size?: 'sm' | 'md';
@@ -27,12 +27,17 @@ export type TriggerButtonProps = {
27
27
  */
28
28
  iconType?: 'flat' | 'rounded';
29
29
  /**
30
- * Whether the trigger button is disabled.
30
+ * When true, hides the trailing chevron indicator.
31
+ * @default false
32
+ */
33
+ hideChevron?: boolean;
34
+ /**
35
+ * Whether the media button is disabled.
31
36
  * @default false
32
37
  */
33
38
  disabled?: boolean;
34
39
  /**
35
- * The label content of the trigger button.
40
+ * The label content of the media button.
36
41
  */
37
42
  children: ReactNode;
38
43
  } & Omit<StyledPressableProps, 'children'>;
@@ -286,9 +286,6 @@ const useStyles = ({ density }: StyleParams) => {
286
286
  },
287
287
  ]),
288
288
  contentContainer: StyleSheet.flatten([
289
- {
290
- flex: 1,
291
- },
292
289
  {
293
290
  ...(density === 'compact' && {
294
291
  paddingHorizontal: t.spacings.s48,
@@ -0,0 +1,37 @@
1
+ import figma from '@figma/code-connect';
2
+ import {
3
+ OptionList,
4
+ OptionListContent,
5
+ OptionListEmptyState,
6
+ OptionListItem,
7
+ OptionListItemContent,
8
+ OptionListItemDescription,
9
+ OptionListItemText,
10
+ } from './OptionList';
11
+
12
+ figma.connect(
13
+ OptionList,
14
+ 'https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7?node-id=15941-6709',
15
+ {
16
+ imports: [
17
+ "import { OptionList, OptionListContent, OptionListItem, OptionListItemContent, OptionListItemText, OptionListItemDescription, OptionListEmptyState } from '@ledgerhq/lumen-ui-rnative'",
18
+ ],
19
+ example: () => (
20
+ <OptionList items={[]} value={''} onValueChange={() => {}}>
21
+ <OptionListContent
22
+ renderItem={(item) => (
23
+ <OptionListItem value={item.value}>
24
+ <OptionListItemContent>
25
+ <OptionListItemText>{item.label}</OptionListItemText>
26
+ <OptionListItemDescription>
27
+ {item.description}
28
+ </OptionListItemDescription>
29
+ </OptionListItemContent>
30
+ </OptionListItem>
31
+ )}
32
+ />
33
+ <OptionListEmptyState title='No options available' />
34
+ </OptionList>
35
+ ),
36
+ },
37
+ );
@@ -0,0 +1,304 @@
1
+ import { Meta, Canvas } from '@storybook/addon-docs/blocks';
2
+ import * as OptionListStories from './OptionList.stories';
3
+ import {
4
+ CustomTabs,
5
+ Tab,
6
+ DoVsDontRow,
7
+ DoBlockItem,
8
+ DontBlockItem,
9
+ } from '../../../../.storybook/components';
10
+
11
+ <Meta title='Selection/OptionList' of={OptionListStories} />
12
+
13
+ # OptionList
14
+
15
+ <CustomTabs>
16
+ <Tab label="Overview">
17
+
18
+ ## Introduction
19
+
20
+ OptionList is a data-driven, composable selection list designed to be embedded inside a `BottomSheet`, a new screen, or any container. Unlike `Select`, it does not manage its own trigger or modal — consumers compose it within their own layout.
21
+
22
+ It handles **selection state**, **automatic grouping** (via a `group` field on items), and exposes a `renderItem` callback for full control over item rendering.
23
+
24
+ > View in [Figma](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=15941-6709&m=dev).
25
+
26
+ ## Anatomy
27
+
28
+ <Canvas of={OptionListStories.Base} />
29
+
30
+ - **OptionList**: Root component managing selection state and item processing
31
+ - **OptionListTrigger**: Input-style trigger with floating label and chevron — displays the selected value and opens a BottomSheet on press
32
+ - **OptionListContent**: Iterates items, renders group labels and separators automatically, calls `renderItem`
33
+ - **OptionListItem**: Pressable item with selection check mark
34
+ - **OptionListItemLeading**: Slot for leading visuals (icons, avatars)
35
+ - **OptionListItemContent**: Flex column for title + description
36
+ - **OptionListItemContentRow**: Horizontal row for placing elements side-by-side (e.g. title + tag)
37
+ - **OptionListItemText**: Styled title text
38
+ - **OptionListItemDescription**: Styled description text
39
+
40
+ ## Properties
41
+
42
+ ### Grouped items
43
+
44
+ Items with a `group` field are automatically grouped with labels and separators:
45
+
46
+ <Canvas of={OptionListStories.WithGroups} />
47
+
48
+ ### Complex item layout
49
+
50
+ Use `OptionListItemContentRow` to place a `Tag` next to the title:
51
+
52
+ <Canvas of={OptionListStories.WithContentRow} />
53
+
54
+ ### Disabled items
55
+
56
+ Individual items can be disabled:
57
+
58
+ <Canvas of={OptionListStories.WithDisabledItems} />
59
+
60
+ ### Groups + complex layout
61
+
62
+ Combining grouping with rich item content:
63
+
64
+ <Canvas of={OptionListStories.GroupedWithContentRow} />
65
+
66
+ ### Trigger showcase
67
+
68
+ OptionList can be opened from any trigger. `MediaButton` supports multiple appearances (`gray`, `transparent`, `no-background`), optional icons (`flat` / `rounded`), and disabled state:
69
+
70
+ <Canvas of={OptionListStories.TriggerShowcase} />
71
+
72
+ </Tab>
73
+ <Tab label="Implementation">
74
+
75
+ ## Setup
76
+
77
+ Install and set up the library with our [Setup Guide →](?path=/docs/getting-started-setup--docs).
78
+
79
+ ## Basic usage
80
+
81
+ ```tsx
82
+ import {
83
+ OptionList,
84
+ OptionListContent,
85
+ OptionListItem,
86
+ OptionListItemContent,
87
+ OptionListItemText,
88
+ } from '@ledgerhq/lumen-ui-rnative';
89
+
90
+ const items = [
91
+ { value: 'a', label: 'Option A' },
92
+ { value: 'b', label: 'Option B' },
93
+ ];
94
+
95
+ function MyList() {
96
+ const [value, setValue] = useState<string | null>(null);
97
+
98
+ return (
99
+ <OptionList items={items} value={value} onValueChange={setValue}>
100
+ <OptionListContent
101
+ renderItem={(item) => (
102
+ <OptionListItem value={item.value}>
103
+ <OptionListItemContent>
104
+ <OptionListItemText>{item.label}</OptionListItemText>
105
+ </OptionListItemContent>
106
+ </OptionListItem>
107
+ )}
108
+ />
109
+ </OptionList>
110
+ );
111
+ }
112
+ ```
113
+
114
+ ## Inside a BottomSheet
115
+
116
+ Use `OptionListTrigger` as the input-style trigger — it provides a floating label that animates up when a value is selected, and a chevron indicator. Pass children only when a value is selected so the label stays centered when empty.
117
+
118
+ ```tsx
119
+ import {
120
+ OptionList, OptionListContent, OptionListItem,
121
+ OptionListItemContent, OptionListItemText, OptionListTrigger,
122
+ BottomSheet, BottomSheetHeader, BottomSheetView,
123
+ useBottomSheetRef, Text,
124
+ } from '@ledgerhq/lumen-ui-rnative';
125
+
126
+ function CurrencyPicker() {
127
+ const [value, setValue] = useState<string | null>(null);
128
+ const ref = useBottomSheetRef();
129
+ const selected = items.find((i) => i.value === value);
130
+
131
+ return (
132
+ <>
133
+ <OptionListTrigger
134
+ label='Currency'
135
+ onPress={() => ref.current?.present()}
136
+ >
137
+ {selected && <Text lx={{ color: 'base' }}>{selected.label}</Text>}
138
+ </OptionListTrigger>
139
+ <BottomSheet
140
+ ref={ref}
141
+ enableDynamicSizing
142
+ snapPoints={null}
143
+ onClose={() => ref.current?.dismiss()}
144
+ >
145
+ <BottomSheetView>
146
+ <BottomSheetHeader title='Pick a currency' />
147
+ <OptionList
148
+ items={items}
149
+ value={value}
150
+ onValueChange={(v) => { setValue(v); ref.current?.dismiss(); }}
151
+ >
152
+ <OptionListContent
153
+ renderItem={(item) => (
154
+ <OptionListItem value={item.value}>
155
+ <OptionListItemContent>
156
+ <OptionListItemText>{item.label}</OptionListItemText>
157
+ </OptionListItemContent>
158
+ </OptionListItem>
159
+ )}
160
+ />
161
+ </OptionList>
162
+ </BottomSheetView>
163
+ </BottomSheet>
164
+ </>
165
+ );
166
+ }
167
+ ```
168
+
169
+ ## Grouping
170
+
171
+ Add a `group` field to items — labels and separators are rendered automatically:
172
+
173
+ ```tsx
174
+ const items = [
175
+ { value: 'btc', label: 'Bitcoin', group: 'Crypto' },
176
+ { value: 'eth', label: 'Ethereum', group: 'Crypto' },
177
+ { value: 'usd', label: 'US Dollar', group: 'Fiat' },
178
+ ];
179
+ ```
180
+
181
+ ## Controlled vs uncontrolled
182
+
183
+ Use `value` + `onValueChange` (controlled) when OptionList is inside a BottomSheet — the sheet unmounts children on dismiss, so internal state is lost. Use `defaultValue` (uncontrolled) only when the component stays mounted (e.g. inline in a screen).
184
+
185
+ ## Custom item layout with meta
186
+
187
+ Use `meta` for arbitrary data, and sub-components for layout:
188
+
189
+ ```tsx
190
+ const items = [
191
+ { value: 'eth', label: 'Ethereum', meta: { ticker: 'ETH', tag: 'ERC-20' } },
192
+ ];
193
+
194
+ <OptionListContent
195
+ renderItem={(item) => {
196
+ const meta = item.meta as { ticker: string; tag: string };
197
+ return (
198
+ <OptionListItem value={item.value}>
199
+ <OptionListItemLeading>
200
+ <Spot appearance='icon' icon={Wallet} />
201
+ </OptionListItemLeading>
202
+ <OptionListItemContent>
203
+ <OptionListItemContentRow>
204
+ <OptionListItemText>{item.label}</OptionListItemText>
205
+ <Tag label={meta.tag} appearance='gray' size='sm' />
206
+ </OptionListItemContentRow>
207
+ <OptionListItemDescription>{meta.ticker}</OptionListItemDescription>
208
+ </OptionListItemContent>
209
+ </OptionListItem>
210
+ );
211
+ }}
212
+ />
213
+ ```
214
+
215
+ ## Do's and Don'ts
216
+
217
+ <div className='flex flex-col gap-24'>
218
+
219
+ <DoVsDontRow>
220
+ <DoBlockItem
221
+ title='Pass items as a flat array'
222
+ description='Use the group field for grouping — the component handles labels and separators automatically.'
223
+ >
224
+
225
+ {/* prettier-ignore */}
226
+ ```tsx
227
+ <OptionList
228
+ items={[
229
+ { value: 'a', label: 'A', group: 'Group 1' },
230
+ { value: 'b', label: 'B', group: 'Group 2' },
231
+ ]}
232
+ value={value}
233
+ onValueChange={setValue}
234
+ >
235
+ <OptionListContent renderItem={...} />
236
+ </OptionList>
237
+ ```
238
+
239
+ </DoBlockItem>
240
+ <DontBlockItem
241
+ title="Don't manually render group labels or separators"
242
+ description='OptionListContent handles group rendering internally based on item data.'
243
+ >
244
+
245
+ {/* prettier-ignore */}
246
+ ```tsx
247
+ <OptionList
248
+ items={items}
249
+ value={value}
250
+ onValueChange={setValue}
251
+ >
252
+ <Text>Group 1</Text>
253
+ <OptionListContent renderItem={...} />
254
+ <Divider />
255
+ <Text>Group 2</Text>
256
+ <OptionListContent renderItem={...} />
257
+ </OptionList>
258
+ ```
259
+
260
+ </DontBlockItem>
261
+ </DoVsDontRow>
262
+
263
+ <DoVsDontRow>
264
+ <DoBlockItem
265
+ title='Compose OptionList inside your own container'
266
+ description='OptionList is display-only — wrap it in a BottomSheet, screen, or any layout.'
267
+ >
268
+
269
+ {/* prettier-ignore */}
270
+ ```tsx
271
+ <BottomSheet ref={ref}>
272
+ <BottomSheetView>
273
+ <OptionList items={items} value={value} onValueChange={setValue}>
274
+ <OptionListContent renderItem={...} />
275
+ </OptionList>
276
+ </BottomSheetView>
277
+ </BottomSheet>
278
+ ```
279
+
280
+ </DoBlockItem>
281
+ <DontBlockItem
282
+ title="Don't expect OptionList to manage the BottomSheet"
283
+ description='Use OptionListTrigger to open the sheet yourself — OptionList only handles selection state, not the container.'
284
+ >
285
+
286
+ {/* prettier-ignore */}
287
+ ```tsx
288
+ <OptionList
289
+ items={items}
290
+ value={value}
291
+ onValueChange={setValue}
292
+ trigger={<Button>Open</Button>}
293
+ >
294
+ <OptionListContent renderItem={...} />
295
+ </OptionList>
296
+ ```
297
+
298
+ </DontBlockItem>
299
+ </DoVsDontRow>
300
+
301
+ </div>
302
+
303
+ </Tab>
304
+ </CustomTabs>