@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
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSafeContext,
|
|
3
|
+
useDisabledContext,
|
|
4
|
+
DisabledProvider,
|
|
5
|
+
} from '@ledgerhq/lumen-utils-shared';
|
|
6
|
+
import { Fragment, type ReactNode } from 'react';
|
|
7
|
+
import { StyleSheet, View } from 'react-native';
|
|
8
|
+
import { useStyleSheet } from '../../../styles';
|
|
9
|
+
import { Check, ChevronDown } from '../../Symbols';
|
|
10
|
+
import { useControllableState } from '../../utils/useControllableState';
|
|
11
|
+
import { Divider } from '../Divider';
|
|
12
|
+
import { Box, Pressable, Text } from '../Utility';
|
|
13
|
+
import type {
|
|
14
|
+
MetaShape,
|
|
15
|
+
OptionListContextValue,
|
|
16
|
+
OptionListItemData,
|
|
17
|
+
OptionListProps,
|
|
18
|
+
OptionListContentProps,
|
|
19
|
+
OptionListItemProps,
|
|
20
|
+
OptionListItemLeadingProps,
|
|
21
|
+
OptionListItemTextProps,
|
|
22
|
+
OptionListItemDescriptionProps,
|
|
23
|
+
OptionListItemContentProps,
|
|
24
|
+
OptionListItemContentRowProps,
|
|
25
|
+
OptionListEmptyStateProps,
|
|
26
|
+
OptionListTriggerProps,
|
|
27
|
+
OptionListLabelProps,
|
|
28
|
+
} from './types';
|
|
29
|
+
import { useOptionListItems } from './useOptionList/useOptionListItems';
|
|
30
|
+
|
|
31
|
+
const [OptionListProvider, useOptionListContext] =
|
|
32
|
+
createSafeContext<OptionListContextValue>('OptionList');
|
|
33
|
+
|
|
34
|
+
export const OptionList = <TMeta extends MetaShape = MetaShape>({
|
|
35
|
+
items,
|
|
36
|
+
value,
|
|
37
|
+
defaultValue,
|
|
38
|
+
onValueChange,
|
|
39
|
+
disabled: disabledProp,
|
|
40
|
+
children,
|
|
41
|
+
}: OptionListProps<TMeta>) => {
|
|
42
|
+
const disabled = useDisabledContext({
|
|
43
|
+
consumerName: 'OptionList',
|
|
44
|
+
mergeWith: { disabled: disabledProp },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const [selectedValue, setSelectedValue] = useControllableState<string | null>(
|
|
48
|
+
{
|
|
49
|
+
prop: value,
|
|
50
|
+
defaultProp: defaultValue ?? null,
|
|
51
|
+
onChange: onValueChange,
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const { isGrouped, groups, flatItems } = useOptionListItems<TMeta>({ items });
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<DisabledProvider value={{ disabled }}>
|
|
59
|
+
<OptionListProvider
|
|
60
|
+
value={{
|
|
61
|
+
selectedValue,
|
|
62
|
+
onValueChange: setSelectedValue,
|
|
63
|
+
isGrouped,
|
|
64
|
+
groups,
|
|
65
|
+
flatItems,
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
{children}
|
|
69
|
+
</OptionListProvider>
|
|
70
|
+
</DisabledProvider>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const OptionListContent = <TMeta extends MetaShape = MetaShape>({
|
|
75
|
+
renderItem,
|
|
76
|
+
lx,
|
|
77
|
+
style,
|
|
78
|
+
ref,
|
|
79
|
+
...props
|
|
80
|
+
}: OptionListContentProps<TMeta>) => {
|
|
81
|
+
const { selectedValue, isGrouped, groups, flatItems } = useOptionListContext({
|
|
82
|
+
consumerName: 'OptionListContent',
|
|
83
|
+
contextRequired: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const renderItemWithState = (item: OptionListItemData) =>
|
|
87
|
+
renderItem(item as OptionListItemData<TMeta>, selectedValue === item.value);
|
|
88
|
+
|
|
89
|
+
if (isGrouped) {
|
|
90
|
+
return (
|
|
91
|
+
<Box lx={lx} style={style} ref={ref} {...props}>
|
|
92
|
+
{groups.map((group, groupIndex) => (
|
|
93
|
+
<Fragment key={group.label}>
|
|
94
|
+
{groupIndex > 0 && (
|
|
95
|
+
<Divider lx={{ marginVertical: 's4', marginHorizontal: 's8' }} />
|
|
96
|
+
)}
|
|
97
|
+
{group.label && <OptionListLabel>{group.label}</OptionListLabel>}
|
|
98
|
+
{group.items.map((item) => (
|
|
99
|
+
<Fragment key={item.value}>{renderItemWithState(item)}</Fragment>
|
|
100
|
+
))}
|
|
101
|
+
</Fragment>
|
|
102
|
+
))}
|
|
103
|
+
</Box>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Box lx={lx} style={style} ref={ref} {...props}>
|
|
109
|
+
{flatItems.map((item) => (
|
|
110
|
+
<Fragment key={item.value}>{renderItemWithState(item)}</Fragment>
|
|
111
|
+
))}
|
|
112
|
+
</Box>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const useItemStyles = ({
|
|
117
|
+
disabled,
|
|
118
|
+
pressed,
|
|
119
|
+
}: {
|
|
120
|
+
disabled: boolean;
|
|
121
|
+
pressed: boolean;
|
|
122
|
+
}) => {
|
|
123
|
+
return useStyleSheet(
|
|
124
|
+
(t) => ({
|
|
125
|
+
container: {
|
|
126
|
+
flexDirection: 'row',
|
|
127
|
+
alignItems: 'center',
|
|
128
|
+
minHeight: t.sizes.s40,
|
|
129
|
+
padding: t.spacings.s8,
|
|
130
|
+
gap: t.spacings.s12,
|
|
131
|
+
borderRadius: t.borderRadius.md,
|
|
132
|
+
backgroundColor: pressed
|
|
133
|
+
? t.colors.bg.baseTransparentPressed
|
|
134
|
+
: t.colors.bg.baseTransparent,
|
|
135
|
+
opacity: disabled ? 0.5 : 1,
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
[disabled, pressed],
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const OptionListItem = ({
|
|
143
|
+
value,
|
|
144
|
+
disabled: disabledProp = false,
|
|
145
|
+
children,
|
|
146
|
+
lx,
|
|
147
|
+
style,
|
|
148
|
+
ref,
|
|
149
|
+
...props
|
|
150
|
+
}: OptionListItemProps) => {
|
|
151
|
+
const { selectedValue, onValueChange } = useOptionListContext({
|
|
152
|
+
consumerName: 'OptionListItem',
|
|
153
|
+
contextRequired: true,
|
|
154
|
+
});
|
|
155
|
+
const disabled = useDisabledContext({
|
|
156
|
+
consumerName: 'OptionListItem',
|
|
157
|
+
mergeWith: { disabled: disabledProp },
|
|
158
|
+
});
|
|
159
|
+
const selected = selectedValue === value;
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<DisabledProvider value={{ disabled }}>
|
|
163
|
+
<Pressable
|
|
164
|
+
ref={ref}
|
|
165
|
+
lx={lx}
|
|
166
|
+
style={style}
|
|
167
|
+
onPress={() => onValueChange(value)}
|
|
168
|
+
disabled={disabled}
|
|
169
|
+
accessibilityRole='radio'
|
|
170
|
+
accessibilityState={{ disabled, selected }}
|
|
171
|
+
{...props}
|
|
172
|
+
>
|
|
173
|
+
{({ pressed }) => (
|
|
174
|
+
<OptionListItemInner pressed={pressed} selected={selected}>
|
|
175
|
+
{children}
|
|
176
|
+
</OptionListItemInner>
|
|
177
|
+
)}
|
|
178
|
+
</Pressable>
|
|
179
|
+
</DisabledProvider>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const OptionListItemInner = ({
|
|
184
|
+
pressed,
|
|
185
|
+
selected,
|
|
186
|
+
children,
|
|
187
|
+
}: {
|
|
188
|
+
pressed: boolean;
|
|
189
|
+
selected: boolean;
|
|
190
|
+
children: ReactNode;
|
|
191
|
+
}) => {
|
|
192
|
+
const disabled = useDisabledContext({
|
|
193
|
+
consumerName: 'OptionListItemInner',
|
|
194
|
+
contextRequired: false,
|
|
195
|
+
});
|
|
196
|
+
const styles = useItemStyles({ disabled, pressed });
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<View style={styles.container}>
|
|
200
|
+
{children}
|
|
201
|
+
{selected && <Check size={24} color='active' />}
|
|
202
|
+
</View>
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const OptionListItemText = ({
|
|
207
|
+
children,
|
|
208
|
+
lx,
|
|
209
|
+
style,
|
|
210
|
+
ref,
|
|
211
|
+
...props
|
|
212
|
+
}: OptionListItemTextProps) => {
|
|
213
|
+
const disabled = useDisabledContext({
|
|
214
|
+
consumerName: 'OptionListItemText',
|
|
215
|
+
contextRequired: false,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const styles = useStyleSheet(
|
|
219
|
+
(t) => ({
|
|
220
|
+
text: StyleSheet.flatten([
|
|
221
|
+
t.typographies.body2SemiBold,
|
|
222
|
+
{
|
|
223
|
+
color: disabled ? t.colors.text.disabled : t.colors.text.base,
|
|
224
|
+
},
|
|
225
|
+
]),
|
|
226
|
+
}),
|
|
227
|
+
[disabled],
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<Text
|
|
232
|
+
ref={ref}
|
|
233
|
+
lx={lx}
|
|
234
|
+
style={StyleSheet.flatten([styles.text, style])}
|
|
235
|
+
numberOfLines={1}
|
|
236
|
+
{...props}
|
|
237
|
+
>
|
|
238
|
+
{children}
|
|
239
|
+
</Text>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export const OptionListItemDescription = ({
|
|
244
|
+
children,
|
|
245
|
+
lx,
|
|
246
|
+
style,
|
|
247
|
+
ref,
|
|
248
|
+
...props
|
|
249
|
+
}: OptionListItemDescriptionProps) => {
|
|
250
|
+
const disabled = useDisabledContext({
|
|
251
|
+
consumerName: 'OptionListItemDescription',
|
|
252
|
+
contextRequired: false,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const styles = useStyleSheet(
|
|
256
|
+
(t) => ({
|
|
257
|
+
description: StyleSheet.flatten([
|
|
258
|
+
t.typographies.body3,
|
|
259
|
+
{
|
|
260
|
+
color: disabled ? t.colors.text.disabled : t.colors.text.muted,
|
|
261
|
+
},
|
|
262
|
+
]),
|
|
263
|
+
}),
|
|
264
|
+
[disabled],
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<Text
|
|
269
|
+
ref={ref}
|
|
270
|
+
lx={lx}
|
|
271
|
+
style={StyleSheet.flatten([styles.description, style])}
|
|
272
|
+
numberOfLines={1}
|
|
273
|
+
{...props}
|
|
274
|
+
>
|
|
275
|
+
{children}
|
|
276
|
+
</Text>
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export const OptionListItemContent = ({
|
|
281
|
+
children,
|
|
282
|
+
lx,
|
|
283
|
+
style,
|
|
284
|
+
ref,
|
|
285
|
+
...props
|
|
286
|
+
}: OptionListItemContentProps) => {
|
|
287
|
+
const styles = useStyleSheet(
|
|
288
|
+
(t) => ({
|
|
289
|
+
content: {
|
|
290
|
+
flex: 1,
|
|
291
|
+
minWidth: 0,
|
|
292
|
+
gap: t.spacings.s4,
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
[],
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<Box
|
|
300
|
+
ref={ref}
|
|
301
|
+
lx={lx}
|
|
302
|
+
style={StyleSheet.flatten([styles.content, style])}
|
|
303
|
+
{...props}
|
|
304
|
+
>
|
|
305
|
+
{children}
|
|
306
|
+
</Box>
|
|
307
|
+
);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export const OptionListItemContentRow = ({
|
|
311
|
+
children,
|
|
312
|
+
lx,
|
|
313
|
+
style,
|
|
314
|
+
ref,
|
|
315
|
+
...props
|
|
316
|
+
}: OptionListItemContentRowProps) => {
|
|
317
|
+
const styles = useStyleSheet(
|
|
318
|
+
(t) => ({
|
|
319
|
+
row: {
|
|
320
|
+
flexDirection: 'row',
|
|
321
|
+
alignItems: 'center',
|
|
322
|
+
minWidth: 0,
|
|
323
|
+
gap: t.spacings.s8,
|
|
324
|
+
},
|
|
325
|
+
}),
|
|
326
|
+
[],
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<Box
|
|
331
|
+
ref={ref}
|
|
332
|
+
lx={lx}
|
|
333
|
+
style={StyleSheet.flatten([styles.row, style])}
|
|
334
|
+
{...props}
|
|
335
|
+
>
|
|
336
|
+
{children}
|
|
337
|
+
</Box>
|
|
338
|
+
);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
export const OptionListItemLeading = ({
|
|
342
|
+
children,
|
|
343
|
+
lx,
|
|
344
|
+
style,
|
|
345
|
+
ref,
|
|
346
|
+
...props
|
|
347
|
+
}: OptionListItemLeadingProps) => {
|
|
348
|
+
const styles = useStyleSheet(
|
|
349
|
+
() => ({
|
|
350
|
+
leading: {
|
|
351
|
+
flexShrink: 0,
|
|
352
|
+
alignItems: 'center',
|
|
353
|
+
justifyContent: 'center',
|
|
354
|
+
},
|
|
355
|
+
}),
|
|
356
|
+
[],
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<Box
|
|
361
|
+
ref={ref}
|
|
362
|
+
lx={lx}
|
|
363
|
+
style={StyleSheet.flatten([styles.leading, style])}
|
|
364
|
+
{...props}
|
|
365
|
+
>
|
|
366
|
+
{children}
|
|
367
|
+
</Box>
|
|
368
|
+
);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const OptionListLabel = ({ children }: OptionListLabelProps) => (
|
|
372
|
+
<Text
|
|
373
|
+
lx={{
|
|
374
|
+
color: 'muted',
|
|
375
|
+
paddingHorizontal: 's8',
|
|
376
|
+
paddingTop: 's8',
|
|
377
|
+
marginBottom: 's4',
|
|
378
|
+
}}
|
|
379
|
+
>
|
|
380
|
+
{children}
|
|
381
|
+
</Text>
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
export const OptionListEmptyState = ({
|
|
385
|
+
title,
|
|
386
|
+
description,
|
|
387
|
+
lx,
|
|
388
|
+
style,
|
|
389
|
+
ref,
|
|
390
|
+
...props
|
|
391
|
+
}: OptionListEmptyStateProps) => {
|
|
392
|
+
const { flatItems } = useOptionListContext({
|
|
393
|
+
consumerName: 'OptionListEmptyState',
|
|
394
|
+
contextRequired: true,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const styles = useStyleSheet(
|
|
398
|
+
(t) => ({
|
|
399
|
+
container: {
|
|
400
|
+
width: '100%',
|
|
401
|
+
alignItems: 'center',
|
|
402
|
+
gap: t.spacings.s8,
|
|
403
|
+
paddingVertical: t.spacings.s24,
|
|
404
|
+
},
|
|
405
|
+
title: StyleSheet.flatten([
|
|
406
|
+
t.typographies.heading4SemiBold,
|
|
407
|
+
{ color: t.colors.text.base },
|
|
408
|
+
]),
|
|
409
|
+
description: StyleSheet.flatten([
|
|
410
|
+
t.typographies.body2,
|
|
411
|
+
{ color: t.colors.text.muted },
|
|
412
|
+
]),
|
|
413
|
+
}),
|
|
414
|
+
[],
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
if (flatItems.length > 0) return null;
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
<Box
|
|
421
|
+
ref={ref}
|
|
422
|
+
lx={lx}
|
|
423
|
+
style={StyleSheet.flatten([styles.container, style])}
|
|
424
|
+
{...props}
|
|
425
|
+
>
|
|
426
|
+
<Text style={styles.title}>{title}</Text>
|
|
427
|
+
{description && <Text style={styles.description}>{description}</Text>}
|
|
428
|
+
</Box>
|
|
429
|
+
);
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const useTriggerStyles = ({
|
|
433
|
+
disabled,
|
|
434
|
+
hasValue,
|
|
435
|
+
hasLabel,
|
|
436
|
+
}: {
|
|
437
|
+
disabled: boolean;
|
|
438
|
+
hasValue: boolean;
|
|
439
|
+
hasLabel: boolean;
|
|
440
|
+
}) =>
|
|
441
|
+
useStyleSheet(
|
|
442
|
+
(t) => ({
|
|
443
|
+
trigger: StyleSheet.flatten([
|
|
444
|
+
{
|
|
445
|
+
position: 'relative',
|
|
446
|
+
width: t.sizes.full,
|
|
447
|
+
height: t.sizes.s48,
|
|
448
|
+
backgroundColor: t.colors.bg.muted,
|
|
449
|
+
borderRadius: t.borderRadius.sm,
|
|
450
|
+
paddingHorizontal: t.spacings.s16,
|
|
451
|
+
flexDirection: 'row',
|
|
452
|
+
alignItems: 'center',
|
|
453
|
+
justifyContent: 'space-between',
|
|
454
|
+
},
|
|
455
|
+
disabled && { opacity: 0.5 },
|
|
456
|
+
]),
|
|
457
|
+
label: StyleSheet.flatten([
|
|
458
|
+
t.typographies.body2,
|
|
459
|
+
{
|
|
460
|
+
position: 'absolute',
|
|
461
|
+
left: t.spacings.s16,
|
|
462
|
+
color: t.colors.text.muted,
|
|
463
|
+
width: '100%',
|
|
464
|
+
},
|
|
465
|
+
hasValue
|
|
466
|
+
? { top: t.spacings.s6, ...t.typographies.body4 }
|
|
467
|
+
: { top: t.spacings.s14, ...t.typographies.body2 },
|
|
468
|
+
disabled && { color: t.colors.text.disabled },
|
|
469
|
+
]),
|
|
470
|
+
contentWrapper: StyleSheet.flatten([
|
|
471
|
+
{ flex: 1 },
|
|
472
|
+
hasLabel &&
|
|
473
|
+
hasValue && {
|
|
474
|
+
paddingTop: t.spacings.s16,
|
|
475
|
+
paddingBottom: t.spacings.s2,
|
|
476
|
+
},
|
|
477
|
+
hasLabel && !hasValue && { paddingVertical: 0 },
|
|
478
|
+
]),
|
|
479
|
+
chevron: StyleSheet.flatten([
|
|
480
|
+
{
|
|
481
|
+
flexShrink: 0,
|
|
482
|
+
color: t.colors.text.muted,
|
|
483
|
+
marginLeft: t.spacings.s8,
|
|
484
|
+
},
|
|
485
|
+
disabled && { color: t.colors.text.disabled },
|
|
486
|
+
]),
|
|
487
|
+
}),
|
|
488
|
+
[disabled, hasValue, hasLabel],
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
export const OptionListTrigger = ({
|
|
492
|
+
label,
|
|
493
|
+
onPress,
|
|
494
|
+
disabled: disabledProp,
|
|
495
|
+
children,
|
|
496
|
+
lx,
|
|
497
|
+
style,
|
|
498
|
+
ref,
|
|
499
|
+
...props
|
|
500
|
+
}: OptionListTriggerProps) => {
|
|
501
|
+
const disabled = useDisabledContext({
|
|
502
|
+
consumerName: 'OptionListTrigger',
|
|
503
|
+
mergeWith: { disabled: disabledProp },
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const hasValue = children != null && children !== false;
|
|
507
|
+
const styles = useTriggerStyles({
|
|
508
|
+
disabled,
|
|
509
|
+
hasValue,
|
|
510
|
+
hasLabel: !!label,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return (
|
|
514
|
+
<Pressable
|
|
515
|
+
ref={ref}
|
|
516
|
+
lx={lx}
|
|
517
|
+
style={[styles.trigger, style]}
|
|
518
|
+
disabled={disabled}
|
|
519
|
+
onPress={onPress}
|
|
520
|
+
accessibilityRole='button'
|
|
521
|
+
{...props}
|
|
522
|
+
>
|
|
523
|
+
{label && (
|
|
524
|
+
<Text style={styles.label} numberOfLines={1}>
|
|
525
|
+
{label}
|
|
526
|
+
</Text>
|
|
527
|
+
)}
|
|
528
|
+
<View style={styles.contentWrapper}>{children}</View>
|
|
529
|
+
<ChevronDown size={20} style={styles.chevron} />
|
|
530
|
+
</Pressable>
|
|
531
|
+
);
|
|
532
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
StyledPressableProps,
|
|
4
|
+
StyledTextProps,
|
|
5
|
+
StyledViewProps,
|
|
6
|
+
} from '../../../styles';
|
|
7
|
+
|
|
8
|
+
export type MetaShape = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
export type OptionListItemData<TMeta extends MetaShape = MetaShape> = {
|
|
11
|
+
/** Unique string identifier for this item, used for selection tracking. */
|
|
12
|
+
value: string;
|
|
13
|
+
/** Display text. */
|
|
14
|
+
label: string;
|
|
15
|
+
/** Secondary text displayed below the label inside the item. */
|
|
16
|
+
description?: string;
|
|
17
|
+
/** When true, the item cannot be selected or focused. */
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Optional group name. Items sharing the same `group` value are grouped together
|
|
21
|
+
* with automatic headers and separators.
|
|
22
|
+
* Groups are ordered by first occurrence in the `items` array.
|
|
23
|
+
*/
|
|
24
|
+
group?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Arbitrary data attached to this item.
|
|
27
|
+
* Use it to carry extra fields (icons, tickers, IDs, etc.)
|
|
28
|
+
* that your render function needs.
|
|
29
|
+
*/
|
|
30
|
+
meta?: TMeta;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Internal type -- used by sub-components to read shared state from context. */
|
|
34
|
+
export type OptionListContextValue = {
|
|
35
|
+
selectedValue: string | null;
|
|
36
|
+
onValueChange: (value: string | null) => void;
|
|
37
|
+
isGrouped: boolean;
|
|
38
|
+
groups: OptionListItemGroup[];
|
|
39
|
+
flatItems: OptionListItemData[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Internal type -- consumers never construct this directly. */
|
|
43
|
+
export type OptionListItemGroup<TMeta extends MetaShape = MetaShape> = {
|
|
44
|
+
label: string;
|
|
45
|
+
items: OptionListItemData<TMeta>[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type OptionListProps<TMeta extends MetaShape = MetaShape> = {
|
|
49
|
+
/** Flat array of items. Use the `group` field on each item for automatic grouping. */
|
|
50
|
+
items: OptionListItemData<TMeta>[];
|
|
51
|
+
/** The controlled selected value. */
|
|
52
|
+
value?: string | null;
|
|
53
|
+
/** The default selected value (uncontrolled). */
|
|
54
|
+
defaultValue?: string | null;
|
|
55
|
+
/** Called when the selected value changes. */
|
|
56
|
+
onValueChange?: (value: string | null) => void;
|
|
57
|
+
/** When true, prevents interaction with the entire list. */
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
children: ReactNode;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type OptionListContentProps<TMeta extends MetaShape = MetaShape> = {
|
|
63
|
+
/** Render function called for each item. Receives the item data and selection/disabled state. */
|
|
64
|
+
renderItem: (item: OptionListItemData<TMeta>, selected: boolean) => ReactNode;
|
|
65
|
+
} & Omit<StyledViewProps, 'children'>;
|
|
66
|
+
|
|
67
|
+
export type OptionListItemProps = {
|
|
68
|
+
/** The value associated with this item, used for selection matching. */
|
|
69
|
+
value: string;
|
|
70
|
+
/** Whether the item is disabled. */
|
|
71
|
+
disabled?: boolean;
|
|
72
|
+
children: ReactNode;
|
|
73
|
+
} & Omit<StyledPressableProps, 'children' | 'disabled'>;
|
|
74
|
+
|
|
75
|
+
export type OptionListItemTextProps = {
|
|
76
|
+
children: ReactNode;
|
|
77
|
+
} & Omit<StyledTextProps, 'children'>;
|
|
78
|
+
|
|
79
|
+
export type OptionListItemDescriptionProps = {
|
|
80
|
+
children: ReactNode;
|
|
81
|
+
} & Omit<StyledTextProps, 'children'>;
|
|
82
|
+
|
|
83
|
+
export type OptionListItemContentRowProps = {
|
|
84
|
+
children: ReactNode;
|
|
85
|
+
} & Omit<StyledViewProps, 'children'>;
|
|
86
|
+
|
|
87
|
+
export type OptionListItemContentProps = {
|
|
88
|
+
children: ReactNode;
|
|
89
|
+
} & Omit<StyledViewProps, 'children'>;
|
|
90
|
+
|
|
91
|
+
export type OptionListItemLeadingProps = {
|
|
92
|
+
children: ReactNode;
|
|
93
|
+
} & Omit<StyledViewProps, 'children'>;
|
|
94
|
+
|
|
95
|
+
export type OptionListLabelProps = {
|
|
96
|
+
children: ReactNode;
|
|
97
|
+
} & Omit<StyledTextProps, 'children'>;
|
|
98
|
+
|
|
99
|
+
export type OptionListEmptyStateProps = {
|
|
100
|
+
/** Heading displayed when the list is empty. */
|
|
101
|
+
title: string;
|
|
102
|
+
/** Optional secondary text displayed below the title. */
|
|
103
|
+
description?: string;
|
|
104
|
+
} & Omit<StyledViewProps, 'children'>;
|
|
105
|
+
|
|
106
|
+
export type OptionListTriggerProps = {
|
|
107
|
+
/** Floating label shown above the selected value. */
|
|
108
|
+
label?: string;
|
|
109
|
+
/** Called when the trigger is pressed. Use to open a BottomSheet or navigate. */
|
|
110
|
+
onPress: () => void;
|
|
111
|
+
/** Content to display as the selected value. */
|
|
112
|
+
children?: ReactNode;
|
|
113
|
+
/** Whether the trigger is disabled. Merges with OptionList disabled context. */
|
|
114
|
+
disabled?: boolean;
|
|
115
|
+
} & Omit<StyledPressableProps, 'children' | 'disabled' | 'onPress'>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { renderHook } from '@testing-library/react-native';
|
|
3
|
+
import type { OptionListItemData } from '../types';
|
|
4
|
+
import { useOptionListItems } from './useOptionListItems';
|
|
5
|
+
|
|
6
|
+
const apple: OptionListItemData = {
|
|
7
|
+
value: 'apple',
|
|
8
|
+
label: 'Apple',
|
|
9
|
+
group: 'Fruits',
|
|
10
|
+
};
|
|
11
|
+
const banana: OptionListItemData = {
|
|
12
|
+
value: 'banana',
|
|
13
|
+
label: 'Banana',
|
|
14
|
+
group: 'Fruits',
|
|
15
|
+
};
|
|
16
|
+
const carrot: OptionListItemData = {
|
|
17
|
+
value: 'carrot',
|
|
18
|
+
label: 'Carrot',
|
|
19
|
+
group: 'Vegetables',
|
|
20
|
+
};
|
|
21
|
+
const spinach: OptionListItemData = {
|
|
22
|
+
value: 'spinach',
|
|
23
|
+
label: 'Spinach',
|
|
24
|
+
group: 'Vegetables',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const btc: OptionListItemData = { value: 'btc', label: 'Bitcoin' };
|
|
28
|
+
const eth: OptionListItemData = { value: 'eth', label: 'Ethereum' };
|
|
29
|
+
|
|
30
|
+
describe('useOptionListItems', () => {
|
|
31
|
+
describe('flat items (no group field)', () => {
|
|
32
|
+
it('returns isGrouped false and flatItems as-is', () => {
|
|
33
|
+
const { result } = renderHook(() =>
|
|
34
|
+
useOptionListItems({ items: [btc, eth] }),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(result.current.isGrouped).toBe(false);
|
|
38
|
+
expect(result.current.flatItems).toEqual([btc, eth]);
|
|
39
|
+
expect(result.current.groups).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns empty flatItems for empty array', () => {
|
|
43
|
+
const { result } = renderHook(() => useOptionListItems({ items: [] }));
|
|
44
|
+
|
|
45
|
+
expect(result.current.isGrouped).toBe(false);
|
|
46
|
+
expect(result.current.flatItems).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('grouped items (group field present)', () => {
|
|
51
|
+
it('detects groups and builds OptionListItemGroup[]', () => {
|
|
52
|
+
const { result } = renderHook(() =>
|
|
53
|
+
useOptionListItems({ items: [apple, banana, carrot, spinach] }),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(result.current.isGrouped).toBe(true);
|
|
57
|
+
expect(result.current.flatItems).toEqual([]);
|
|
58
|
+
expect(result.current.groups).toEqual([
|
|
59
|
+
{ label: 'Fruits', items: [apple, banana] },
|
|
60
|
+
{ label: 'Vegetables', items: [carrot, spinach] },
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('preserves group order by first occurrence', () => {
|
|
65
|
+
const { result } = renderHook(() =>
|
|
66
|
+
useOptionListItems({ items: [carrot, apple, spinach, banana] }),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(result.current.groups[0].label).toBe('Vegetables');
|
|
70
|
+
expect(result.current.groups[1].label).toBe('Fruits');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|