@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.
- package/dist/module/lib/Components/ListItem/ListItem.js +57 -27
- package/dist/module/lib/Components/ListItem/ListItem.js.map +1 -1
- package/dist/module/lib/Components/ListItem/ListItem.mdx +15 -7
- package/dist/module/lib/Components/ListItem/ListItem.stories.js +497 -283
- package/dist/module/lib/Components/ListItem/ListItem.stories.js.map +1 -1
- package/dist/module/lib/Components/ListItem/ListItem.test.js +153 -0
- package/dist/module/lib/Components/ListItem/ListItem.test.js.map +1 -0
- package/dist/module/lib/Components/{TriggerButton/TriggerButton.js → MediaButton/MediaButton.js} +13 -10
- package/dist/module/lib/Components/MediaButton/MediaButton.js.map +1 -0
- package/{src/lib/Components/TriggerButton/TriggerButton.mdx → dist/module/lib/Components/MediaButton/MediaButton.mdx} +10 -10
- package/dist/module/lib/Components/{TriggerButton/TriggerButton.stories.js → MediaButton/MediaButton.stories.js} +18 -18
- package/dist/module/lib/Components/MediaButton/MediaButton.stories.js.map +1 -0
- package/dist/module/lib/Components/{TriggerButton/TriggerButton.test.js → MediaButton/MediaButton.test.js} +14 -14
- package/dist/module/lib/Components/MediaButton/MediaButton.test.js.map +1 -0
- package/dist/module/lib/Components/MediaButton/index.js +5 -0
- package/dist/module/lib/Components/MediaButton/index.js.map +1 -0
- package/dist/module/lib/Components/MediaButton/types.js.map +1 -0
- package/dist/module/lib/Components/NavBar/NavBar.js +0 -2
- package/dist/module/lib/Components/NavBar/NavBar.js.map +1 -1
- package/dist/module/lib/Components/OptionList/OptionList.figma.js +28 -0
- package/dist/module/lib/Components/OptionList/OptionList.figma.js.map +1 -0
- package/dist/module/lib/Components/OptionList/OptionList.js +452 -0
- package/dist/module/lib/Components/OptionList/OptionList.js.map +1 -0
- package/dist/module/lib/Components/OptionList/OptionList.mdx +304 -0
- package/dist/module/lib/Components/OptionList/OptionList.stories.js +735 -0
- package/dist/module/lib/Components/OptionList/OptionList.stories.js.map +1 -0
- package/dist/module/lib/Components/OptionList/OptionList.test.js +443 -0
- package/dist/module/lib/Components/OptionList/OptionList.test.js.map +1 -0
- package/dist/module/lib/Components/OptionList/index.js +5 -0
- package/dist/module/lib/Components/OptionList/index.js.map +1 -0
- package/dist/module/lib/Components/OptionList/types.js +4 -0
- package/dist/module/lib/Components/OptionList/types.js.map +1 -0
- package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.js +36 -0
- package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.js.map +1 -0
- package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.test.js +84 -0
- package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.test.js.map +1 -0
- package/dist/module/lib/Components/index.js +2 -1
- package/dist/module/lib/Components/index.js.map +1 -1
- package/dist/typescript/src/lib/Components/ListItem/ListItem.d.ts +8 -8
- package/dist/typescript/src/lib/Components/ListItem/ListItem.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/ListItem/types.d.ts +11 -7
- package/dist/typescript/src/lib/Components/ListItem/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/MediaButton/MediaButton.d.ts +23 -0
- package/dist/typescript/src/lib/Components/MediaButton/MediaButton.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/MediaButton/index.d.ts +3 -0
- package/dist/typescript/src/lib/Components/MediaButton/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/{TriggerButton → MediaButton}/types.d.ts +10 -5
- package/dist/typescript/src/lib/Components/MediaButton/types.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/OptionList/OptionList.d.ts +12 -0
- package/dist/typescript/src/lib/Components/OptionList/OptionList.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/OptionList/OptionList.figma.d.ts +2 -0
- package/dist/typescript/src/lib/Components/OptionList/OptionList.figma.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/OptionList/index.d.ts +3 -0
- package/dist/typescript/src/lib/Components/OptionList/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/OptionList/types.d.ts +97 -0
- package/dist/typescript/src/lib/Components/OptionList/types.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/OptionList/useOptionList/useOptionListItems.d.ts +12 -0
- package/dist/typescript/src/lib/Components/OptionList/useOptionList/useOptionListItems.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/index.d.ts +2 -1
- package/dist/typescript/src/lib/Components/index.d.ts.map +1 -1
- package/dist/typescript/src/styles/types/theme.types.d.ts +7 -6
- package/dist/typescript/src/styles/types/theme.types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/lib/Components/ListItem/ListItem.mdx +15 -7
- package/src/lib/Components/ListItem/ListItem.stories.tsx +354 -220
- package/src/lib/Components/ListItem/ListItem.test.tsx +152 -0
- package/src/lib/Components/ListItem/ListItem.tsx +63 -28
- package/src/lib/Components/ListItem/types.ts +11 -8
- package/{dist/module/lib/Components/TriggerButton/TriggerButton.mdx → src/lib/Components/MediaButton/MediaButton.mdx} +10 -10
- package/src/lib/Components/{TriggerButton/TriggerButton.stories.tsx → MediaButton/MediaButton.stories.tsx} +28 -28
- package/src/lib/Components/{TriggerButton/TriggerButton.test.tsx → MediaButton/MediaButton.test.tsx} +22 -22
- package/src/lib/Components/{TriggerButton/TriggerButton.tsx → MediaButton/MediaButton.tsx} +27 -21
- package/src/lib/Components/MediaButton/index.ts +2 -0
- package/src/lib/Components/{TriggerButton → MediaButton}/types.ts +10 -5
- package/src/lib/Components/NavBar/NavBar.tsx +0 -3
- package/src/lib/Components/OptionList/OptionList.figma.tsx +37 -0
- package/src/lib/Components/OptionList/OptionList.mdx +304 -0
- package/src/lib/Components/OptionList/OptionList.stories.tsx +755 -0
- package/src/lib/Components/OptionList/OptionList.test.tsx +412 -0
- package/src/lib/Components/OptionList/OptionList.tsx +532 -0
- package/src/lib/Components/OptionList/index.ts +2 -0
- package/src/lib/Components/OptionList/types.ts +115 -0
- package/src/lib/Components/OptionList/useOptionList/useOptionListItems.test.ts +73 -0
- package/src/lib/Components/OptionList/useOptionList/useOptionListItems.ts +49 -0
- package/src/lib/Components/index.ts +2 -1
- package/src/styles/types/theme.types.ts +8 -6
- package/dist/module/lib/Components/TriggerButton/TriggerButton.js.map +0 -1
- package/dist/module/lib/Components/TriggerButton/TriggerButton.stories.js.map +0 -1
- package/dist/module/lib/Components/TriggerButton/TriggerButton.test.js.map +0 -1
- package/dist/module/lib/Components/TriggerButton/index.js +0 -5
- package/dist/module/lib/Components/TriggerButton/index.js.map +0 -1
- package/dist/module/lib/Components/TriggerButton/types.js.map +0 -1
- package/dist/typescript/src/lib/Components/TriggerButton/TriggerButton.d.ts +0 -23
- package/dist/typescript/src/lib/Components/TriggerButton/TriggerButton.d.ts.map +0 -1
- package/dist/typescript/src/lib/Components/TriggerButton/index.d.ts +0 -3
- package/dist/typescript/src/lib/Components/TriggerButton/index.d.ts.map +0 -1
- package/dist/typescript/src/lib/Components/TriggerButton/types.d.ts.map +0 -1
- package/src/lib/Components/TriggerButton/index.ts +0 -2
- /package/dist/module/lib/Components/{TriggerButton → MediaButton}/types.js +0 -0
package/src/lib/Components/{TriggerButton/TriggerButton.test.tsx → MediaButton/MediaButton.test.tsx}
RENAMED
|
@@ -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 {
|
|
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('
|
|
19
|
+
describe('MediaButton', () => {
|
|
20
20
|
describe('Rendering', () => {
|
|
21
21
|
it('should render with label text and correct accessibility role', () => {
|
|
22
22
|
renderWithProvider(
|
|
23
|
-
<
|
|
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(<
|
|
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
|
-
<
|
|
40
|
+
<MediaButton testID='trigger' appearance={appearance}>
|
|
41
41
|
Label
|
|
42
|
-
</
|
|
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
|
-
<
|
|
52
|
+
<MediaButton testID='trigger' size={size}>
|
|
53
53
|
Label
|
|
54
|
-
</
|
|
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
|
-
<
|
|
64
|
+
<MediaButton
|
|
65
65
|
testID='trigger'
|
|
66
66
|
icon={<Settings size={20} testID='icon' />}
|
|
67
67
|
iconType='flat'
|
|
68
68
|
>
|
|
69
69
|
Network
|
|
70
|
-
</
|
|
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
|
-
<
|
|
78
|
+
<MediaButton
|
|
79
79
|
testID='trigger'
|
|
80
80
|
icon={<View testID='crypto-icon' />}
|
|
81
81
|
iconType='rounded'
|
|
82
82
|
>
|
|
83
83
|
Bitcoin
|
|
84
|
-
</
|
|
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
|
-
<
|
|
94
|
+
<MediaButton testID='trigger' disabled>
|
|
95
95
|
Label
|
|
96
|
-
</
|
|
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
|
-
<
|
|
107
|
+
<MediaButton testID='trigger' onPress={handlePress}>
|
|
108
108
|
Press me
|
|
109
|
-
</
|
|
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
|
-
<
|
|
119
|
+
<MediaButton testID='trigger' onPress={handlePress} disabled>
|
|
120
120
|
Disabled
|
|
121
|
-
</
|
|
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
|
-
<
|
|
133
|
+
<MediaButton ref={ref} testID='trigger'>
|
|
134
134
|
Label
|
|
135
|
-
</
|
|
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
|
-
<
|
|
145
|
+
<MediaButton
|
|
146
146
|
testID='trigger'
|
|
147
147
|
style={customStyle}
|
|
148
148
|
lx={{ padding: 's8' }}
|
|
149
149
|
>
|
|
150
150
|
Styled
|
|
151
|
-
</
|
|
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 {
|
|
6
|
+
import type { MediaButtonProps } from './types';
|
|
7
7
|
|
|
8
|
-
type Appearance = NonNullable<
|
|
9
|
-
type Size = NonNullable<
|
|
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
|
-
*
|
|
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 {
|
|
142
|
+
* import { MediaButton } from '@ledgerhq/lumen-ui-rnative';
|
|
143
143
|
* import { Settings } from '@ledgerhq/lumen-ui-rnative/symbols';
|
|
144
144
|
*
|
|
145
|
-
* <
|
|
145
|
+
* <MediaButton icon={<Settings size={20} />} iconType="flat">
|
|
146
146
|
* Network
|
|
147
|
-
* </
|
|
147
|
+
* </MediaButton>
|
|
148
148
|
*
|
|
149
|
-
* <
|
|
149
|
+
* <MediaButton>All accounts</MediaButton>
|
|
150
150
|
*/
|
|
151
|
-
export const
|
|
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
|
-
}:
|
|
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
|
-
<
|
|
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
|
-
</
|
|
187
|
+
</MediaButtonContent>
|
|
186
188
|
)}
|
|
187
189
|
</Pressable>
|
|
188
190
|
);
|
|
189
191
|
};
|
|
190
192
|
|
|
191
|
-
type
|
|
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?:
|
|
199
|
+
icon?: MediaButtonProps['icon'];
|
|
200
|
+
hideChevron: boolean;
|
|
198
201
|
}>;
|
|
199
202
|
|
|
200
|
-
const
|
|
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
|
-
}:
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
import { StyledPressableProps } from '../../../styles';
|
|
3
3
|
|
|
4
|
-
export type
|
|
4
|
+
export type MediaButtonProps = {
|
|
5
5
|
/**
|
|
6
|
-
* The visual style of the
|
|
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
|
|
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
|
-
*
|
|
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
|
|
40
|
+
* The label content of the media button.
|
|
36
41
|
*/
|
|
37
42
|
children: ReactNode;
|
|
38
43
|
} & Omit<StyledPressableProps, 'children'>;
|
|
@@ -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>
|