@ledgerhq/lumen-ui-rnative 0.1.47 → 0.1.48
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/AmountInput/AmountInput.js +3 -1
- package/dist/module/lib/Components/AmountInput/AmountInput.js.map +1 -1
- package/dist/module/lib/Components/AmountInput/AmountInput.stories.js +15 -0
- package/dist/module/lib/Components/AmountInput/AmountInput.stories.js.map +1 -1
- package/dist/module/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.js +1 -1
- package/dist/module/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.js.map +1 -1
- package/dist/module/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.test.js +20 -1
- package/dist/module/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.test.js.map +1 -1
- package/dist/module/lib/Components/OptionList/OptionList.js +16 -1
- package/dist/module/lib/Components/OptionList/OptionList.js.map +1 -1
- package/dist/module/lib/Components/OptionList/OptionList.mdx +45 -0
- package/dist/module/lib/Components/OptionList/OptionList.stories.js +61 -1
- package/dist/module/lib/Components/OptionList/OptionList.stories.js.map +1 -1
- package/dist/module/lib/Components/OptionList/OptionList.test.js +43 -1
- package/dist/module/lib/Components/OptionList/OptionList.test.js.map +1 -1
- package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.js.map +1 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.js +7 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.js.map +1 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.mdx +28 -0
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.stories.js +24 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.stories.js.map +1 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.test.js +28 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.test.js.map +1 -1
- package/dist/module/lib/Components/TabBar/TabBar.js +7 -4
- package/dist/module/lib/Components/TabBar/TabBar.js.map +1 -1
- package/dist/module/lib/Components/TabBar/TabBar.mdx +29 -0
- package/dist/module/lib/Components/TabBar/TabBar.stories.js +35 -1
- package/dist/module/lib/Components/TabBar/TabBar.stories.js.map +1 -1
- package/dist/module/lib/Components/TabBar/TabBar.test.js +28 -1
- package/dist/module/lib/Components/TabBar/TabBar.test.js.map +1 -1
- package/dist/module/lib/Components/TabBar/index.js +2 -1
- package/dist/module/lib/Components/TabBar/index.js.map +1 -1
- package/dist/typescript/src/lib/Components/AmountInput/AmountInput.d.ts +1 -1
- package/dist/typescript/src/lib/Components/AmountInput/AmountInput.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/AmountInput/types.d.ts +8 -0
- package/dist/typescript/src/lib/Components/AmountInput/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.d.ts +1 -0
- package/dist/typescript/src/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/OptionList/OptionList.d.ts +18 -4
- package/dist/typescript/src/lib/Components/OptionList/OptionList.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/OptionList/types.d.ts +16 -15
- package/dist/typescript/src/lib/Components/OptionList/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/OptionList/useOptionList/useOptionListItems.d.ts +9 -9
- package/dist/typescript/src/lib/Components/OptionList/useOptionList/useOptionListItems.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/SegmentedControl/SegmentedControl.d.ts +8 -3
- package/dist/typescript/src/lib/Components/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/SegmentedControl/types.d.ts +6 -5
- package/dist/typescript/src/lib/Components/SegmentedControl/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/TabBar/TabBar.d.ts +7 -2
- package/dist/typescript/src/lib/Components/TabBar/TabBar.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/TabBar/index.d.ts +2 -2
- package/dist/typescript/src/lib/Components/TabBar/index.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/TabBar/types.d.ts +17 -16
- package/dist/typescript/src/lib/Components/TabBar/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/lib/Components/AmountInput/AmountInput.stories.tsx +14 -0
- package/src/lib/Components/AmountInput/AmountInput.tsx +2 -0
- package/src/lib/Components/AmountInput/types.ts +8 -0
- package/src/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.test.ts +20 -0
- package/src/lib/Components/AmountInput/useAmountInputFormatting/useAmountInputFormatting.ts +2 -0
- package/src/lib/Components/OptionList/OptionList.mdx +45 -0
- package/src/lib/Components/OptionList/OptionList.stories.tsx +63 -0
- package/src/lib/Components/OptionList/OptionList.test.tsx +40 -0
- package/src/lib/Components/OptionList/OptionList.tsx +57 -10
- package/src/lib/Components/OptionList/types.ts +34 -15
- package/src/lib/Components/OptionList/useOptionList/useOptionListItems.ts +28 -13
- package/src/lib/Components/SegmentedControl/SegmentedControl.mdx +28 -0
- package/src/lib/Components/SegmentedControl/SegmentedControl.stories.tsx +34 -1
- package/src/lib/Components/SegmentedControl/SegmentedControl.test.tsx +32 -1
- package/src/lib/Components/SegmentedControl/SegmentedControl.tsx +22 -5
- package/src/lib/Components/SegmentedControl/types.ts +11 -5
- package/src/lib/Components/TabBar/TabBar.mdx +29 -0
- package/src/lib/Components/TabBar/TabBar.stories.tsx +34 -1
- package/src/lib/Components/TabBar/TabBar.test.tsx +23 -1
- package/src/lib/Components/TabBar/TabBar.tsx +17 -9
- package/src/lib/Components/TabBar/index.ts +2 -2
- package/src/lib/Components/TabBar/types.ts +19 -17
|
@@ -97,6 +97,8 @@ Install and set up the library with our [Setup Guide →](?path=/docs/getting-st
|
|
|
97
97
|
|
|
98
98
|
## Basic usage
|
|
99
99
|
|
|
100
|
+
> **Recommended:** prefer the [type-safe variant](#type-safe-variant) below. It ties `value`, `items[].value`, `OptionListItem.value`, and the `renderItem(item).meta` shape to one literal union plus an optional `TMeta`, dropping the `as Meta` casts in `renderItem`.
|
|
101
|
+
|
|
100
102
|
```tsx
|
|
101
103
|
import {
|
|
102
104
|
OptionList,
|
|
@@ -130,6 +132,49 @@ function MyList() {
|
|
|
130
132
|
}
|
|
131
133
|
```
|
|
132
134
|
|
|
135
|
+
## Type-safe variant
|
|
136
|
+
|
|
137
|
+
`OptionList` and its subcomponents communicate via context, so TypeScript cannot connect `value` / `items[].value` / `OptionListItem.value` on its own. A typo compiles. `createOptionList<T, TMeta>()` ties them together via one literal union (`T`) and an optional meta shape (`TMeta`) that flows into `renderItem(item)`, removing the `as` cast on `item.meta`.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { createOptionList } from '@ledgerhq/lumen-ui-rnative';
|
|
141
|
+
import type { OptionListItemData } from '@ledgerhq/lumen-ui-rnative';
|
|
142
|
+
|
|
143
|
+
type Network = 'eth' | 'sol' | 'btc';
|
|
144
|
+
type NetworkMeta = { ticker: string; ledgerId: string };
|
|
145
|
+
|
|
146
|
+
const NetworkList = createOptionList<Network, NetworkMeta>();
|
|
147
|
+
|
|
148
|
+
const NETWORKS: OptionListItemData<Network, NetworkMeta>[] = [
|
|
149
|
+
{ value: 'eth', label: 'Ethereum', meta: { ticker: 'ETH', ledgerId: 'ethereum' } },
|
|
150
|
+
{ value: 'sol', label: 'Solana', meta: { ticker: 'SOL', ledgerId: 'solana' } },
|
|
151
|
+
{ value: 'btc', label: 'Bitcoin', meta: { ticker: 'BTC', ledgerId: 'bitcoin' } },
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
function Example() {
|
|
155
|
+
const [value, setValue] = React.useState<Network | null>('eth');
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<NetworkList.OptionList items={NETWORKS} value={value} onValueChange={setValue}>
|
|
159
|
+
<NetworkList.OptionListContent
|
|
160
|
+
renderItem={({ value, label, meta }) =>
|
|
161
|
+
meta ? (
|
|
162
|
+
<NetworkList.OptionListItem value={value}>
|
|
163
|
+
<NetworkList.OptionListItemContent>
|
|
164
|
+
<NetworkList.OptionListItemText>{label}</NetworkList.OptionListItemText>
|
|
165
|
+
<NetworkList.OptionListItemDescription>
|
|
166
|
+
{meta.ticker}
|
|
167
|
+
</NetworkList.OptionListItemDescription>
|
|
168
|
+
</NetworkList.OptionListItemContent>
|
|
169
|
+
</NetworkList.OptionListItem>
|
|
170
|
+
) : null
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
</NetworkList.OptionList>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
133
178
|
## Inside a BottomSheet
|
|
134
179
|
|
|
135
180
|
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.
|
|
@@ -14,6 +14,7 @@ import { Spot } from '../Spot';
|
|
|
14
14
|
import { Tag } from '../Tag/Tag';
|
|
15
15
|
import { Box, Text } from '../Utility';
|
|
16
16
|
import {
|
|
17
|
+
createOptionList,
|
|
17
18
|
OptionList,
|
|
18
19
|
OptionListContent,
|
|
19
20
|
OptionListEmptyState,
|
|
@@ -1008,3 +1009,65 @@ export const WithDefaultValue: Story = {
|
|
|
1008
1009
|
</Box>
|
|
1009
1010
|
),
|
|
1010
1011
|
};
|
|
1012
|
+
|
|
1013
|
+
type TypedNetwork = 'eth' | 'sol' | 'btc';
|
|
1014
|
+
type TypedNetworkMeta = { ticker: string; ledgerId: string };
|
|
1015
|
+
const NetworkList = createOptionList<TypedNetwork, TypedNetworkMeta>();
|
|
1016
|
+
|
|
1017
|
+
const TYPED_NETWORKS: OptionListItemData<TypedNetwork, TypedNetworkMeta>[] = [
|
|
1018
|
+
{
|
|
1019
|
+
value: 'eth',
|
|
1020
|
+
label: 'Ethereum',
|
|
1021
|
+
meta: { ticker: 'ETH', ledgerId: 'ethereum' },
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
value: 'sol',
|
|
1025
|
+
label: 'Solana',
|
|
1026
|
+
meta: { ticker: 'SOL', ledgerId: 'solana' },
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
value: 'btc',
|
|
1030
|
+
label: 'Bitcoin',
|
|
1031
|
+
meta: { ticker: 'BTC', ledgerId: 'bitcoin' },
|
|
1032
|
+
},
|
|
1033
|
+
];
|
|
1034
|
+
|
|
1035
|
+
export const TypesafeFactory: Story = {
|
|
1036
|
+
render: () => {
|
|
1037
|
+
const [value, setValue] = useState<TypedNetwork | null>('eth');
|
|
1038
|
+
|
|
1039
|
+
return (
|
|
1040
|
+
<Box lx={{ width: 's320' }}>
|
|
1041
|
+
<NetworkList.OptionList
|
|
1042
|
+
items={TYPED_NETWORKS}
|
|
1043
|
+
value={value}
|
|
1044
|
+
onValueChange={setValue}
|
|
1045
|
+
>
|
|
1046
|
+
<NetworkList.OptionListContent
|
|
1047
|
+
renderItem={({ value, label, meta }) =>
|
|
1048
|
+
meta ? (
|
|
1049
|
+
<NetworkList.OptionListItem value={value}>
|
|
1050
|
+
<NetworkList.OptionListItemLeading>
|
|
1051
|
+
<CryptoIcon
|
|
1052
|
+
ledgerId={meta.ledgerId}
|
|
1053
|
+
ticker={meta.ticker}
|
|
1054
|
+
size={32}
|
|
1055
|
+
/>
|
|
1056
|
+
</NetworkList.OptionListItemLeading>
|
|
1057
|
+
<NetworkList.OptionListItemContent>
|
|
1058
|
+
<NetworkList.OptionListItemText>
|
|
1059
|
+
{label}
|
|
1060
|
+
</NetworkList.OptionListItemText>
|
|
1061
|
+
<NetworkList.OptionListItemDescription>
|
|
1062
|
+
{meta.ticker}
|
|
1063
|
+
</NetworkList.OptionListItemDescription>
|
|
1064
|
+
</NetworkList.OptionListItemContent>
|
|
1065
|
+
</NetworkList.OptionListItem>
|
|
1066
|
+
) : null
|
|
1067
|
+
}
|
|
1068
|
+
/>
|
|
1069
|
+
</NetworkList.OptionList>
|
|
1070
|
+
</Box>
|
|
1071
|
+
);
|
|
1072
|
+
},
|
|
1073
|
+
};
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
OptionListEmptyState,
|
|
16
16
|
OptionListSearch,
|
|
17
17
|
OptionListTrigger,
|
|
18
|
+
createOptionList,
|
|
18
19
|
} from './OptionList';
|
|
19
20
|
import type { OptionListItemData } from './types';
|
|
20
21
|
|
|
@@ -552,4 +553,43 @@ describe('OptionList', () => {
|
|
|
552
553
|
expect(queryByText('Selected value')).toBeTruthy();
|
|
553
554
|
});
|
|
554
555
|
});
|
|
556
|
+
|
|
557
|
+
describe('createOptionList', () => {
|
|
558
|
+
it('returns typed components that render and select', () => {
|
|
559
|
+
type Value = 'a' | 'b' | 'c';
|
|
560
|
+
const typedItems: OptionListItemData<Value>[] = [
|
|
561
|
+
{ value: 'a', label: 'Alpha' },
|
|
562
|
+
{ value: 'b', label: 'Beta' },
|
|
563
|
+
{ value: 'c', label: 'Gamma' },
|
|
564
|
+
];
|
|
565
|
+
const {
|
|
566
|
+
OptionList: TypedList,
|
|
567
|
+
OptionListContent: TypedContent,
|
|
568
|
+
OptionListItem: TypedItem,
|
|
569
|
+
OptionListItemContent: TypedItemContent,
|
|
570
|
+
OptionListItemText: TypedItemText,
|
|
571
|
+
} = createOptionList<Value>();
|
|
572
|
+
const onValueChange = jest.fn();
|
|
573
|
+
|
|
574
|
+
const { getByText } = render(
|
|
575
|
+
<TestWrapper>
|
|
576
|
+
<TypedList items={typedItems} onValueChange={onValueChange}>
|
|
577
|
+
<TypedContent
|
|
578
|
+
renderItem={(item) => (
|
|
579
|
+
<TypedItem value={item.value}>
|
|
580
|
+
<TypedItemContent>
|
|
581
|
+
<TypedItemText>{item.label}</TypedItemText>
|
|
582
|
+
</TypedItemContent>
|
|
583
|
+
</TypedItem>
|
|
584
|
+
)}
|
|
585
|
+
/>
|
|
586
|
+
</TypedList>
|
|
587
|
+
</TestWrapper>,
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
fireEvent.press(getByText('Beta'));
|
|
591
|
+
|
|
592
|
+
expect(onValueChange).toHaveBeenCalledWith('b');
|
|
593
|
+
});
|
|
594
|
+
});
|
|
555
595
|
});
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
useDisabledContext,
|
|
4
4
|
DisabledProvider,
|
|
5
5
|
} from '@ledgerhq/lumen-utils-shared';
|
|
6
|
-
import { Fragment, type ReactNode } from 'react';
|
|
6
|
+
import { Fragment, type ReactElement, type ReactNode } from 'react';
|
|
7
7
|
import { StyleSheet, View } from 'react-native';
|
|
8
8
|
import { useStyleSheet } from '../../../styles';
|
|
9
9
|
import { Check, ChevronDown } from '../../Symbols';
|
|
@@ -27,13 +27,17 @@ import type {
|
|
|
27
27
|
OptionListSearchProps,
|
|
28
28
|
OptionListTriggerProps,
|
|
29
29
|
OptionListLabelProps,
|
|
30
|
+
OptionListValue,
|
|
30
31
|
} from './types';
|
|
31
32
|
import { useOptionListItems } from './useOptionList/useOptionListItems';
|
|
32
33
|
|
|
33
34
|
const [OptionListProvider, useOptionListContext] =
|
|
34
35
|
createSafeContext<OptionListContextValue>('OptionList');
|
|
35
36
|
|
|
36
|
-
export const OptionList = <
|
|
37
|
+
export const OptionList = <
|
|
38
|
+
T extends OptionListValue = OptionListValue,
|
|
39
|
+
TMeta extends MetaShape = MetaShape,
|
|
40
|
+
>({
|
|
37
41
|
items,
|
|
38
42
|
value,
|
|
39
43
|
defaultValue,
|
|
@@ -45,7 +49,7 @@ export const OptionList = <TMeta extends MetaShape = MetaShape>({
|
|
|
45
49
|
defaultSearchValue,
|
|
46
50
|
onSearchValueChange,
|
|
47
51
|
children,
|
|
48
|
-
}: OptionListProps<TMeta>) => {
|
|
52
|
+
}: OptionListProps<T, TMeta>) => {
|
|
49
53
|
const disabled = useDisabledContext({
|
|
50
54
|
consumerName: 'OptionList',
|
|
51
55
|
mergeWith: { disabled: disabledProp },
|
|
@@ -55,7 +59,7 @@ export const OptionList = <TMeta extends MetaShape = MetaShape>({
|
|
|
55
59
|
{
|
|
56
60
|
prop: value,
|
|
57
61
|
defaultProp: defaultValue ?? null,
|
|
58
|
-
onChange: onValueChange,
|
|
62
|
+
onChange: (next: string | null) => onValueChange?.(next as T | null),
|
|
59
63
|
},
|
|
60
64
|
);
|
|
61
65
|
|
|
@@ -65,7 +69,7 @@ export const OptionList = <TMeta extends MetaShape = MetaShape>({
|
|
|
65
69
|
flatItems,
|
|
66
70
|
resolvedSearchValue,
|
|
67
71
|
handleSearchValueChange,
|
|
68
|
-
} = useOptionListItems<TMeta>({
|
|
72
|
+
} = useOptionListItems<T, TMeta>({
|
|
69
73
|
items,
|
|
70
74
|
filter,
|
|
71
75
|
filteredItems,
|
|
@@ -93,20 +97,26 @@ export const OptionList = <TMeta extends MetaShape = MetaShape>({
|
|
|
93
97
|
);
|
|
94
98
|
};
|
|
95
99
|
|
|
96
|
-
export const OptionListContent = <
|
|
100
|
+
export const OptionListContent = <
|
|
101
|
+
T extends OptionListValue = OptionListValue,
|
|
102
|
+
TMeta extends MetaShape = MetaShape,
|
|
103
|
+
>({
|
|
97
104
|
renderItem,
|
|
98
105
|
lx,
|
|
99
106
|
style,
|
|
100
107
|
ref,
|
|
101
108
|
...props
|
|
102
|
-
}: OptionListContentProps<TMeta>) => {
|
|
109
|
+
}: OptionListContentProps<T, TMeta>) => {
|
|
103
110
|
const { selectedValue, isGrouped, groups, flatItems } = useOptionListContext({
|
|
104
111
|
consumerName: 'OptionListContent',
|
|
105
112
|
contextRequired: true,
|
|
106
113
|
});
|
|
107
114
|
|
|
108
115
|
const renderItemWithState = (item: OptionListItemData) =>
|
|
109
|
-
renderItem(
|
|
116
|
+
renderItem(
|
|
117
|
+
item as OptionListItemData<T, TMeta>,
|
|
118
|
+
selectedValue === item.value,
|
|
119
|
+
);
|
|
110
120
|
|
|
111
121
|
if (isGrouped) {
|
|
112
122
|
return (
|
|
@@ -161,7 +171,7 @@ const useItemStyles = ({
|
|
|
161
171
|
);
|
|
162
172
|
};
|
|
163
173
|
|
|
164
|
-
export const OptionListItem = ({
|
|
174
|
+
export const OptionListItem = <T extends OptionListValue = OptionListValue>({
|
|
165
175
|
value,
|
|
166
176
|
disabled: disabledProp = false,
|
|
167
177
|
children,
|
|
@@ -169,7 +179,7 @@ export const OptionListItem = ({
|
|
|
169
179
|
style,
|
|
170
180
|
ref,
|
|
171
181
|
...props
|
|
172
|
-
}: OptionListItemProps) => {
|
|
182
|
+
}: OptionListItemProps<T>) => {
|
|
173
183
|
const { selectedValue, onValueChange } = useOptionListContext({
|
|
174
184
|
consumerName: 'OptionListItem',
|
|
175
185
|
contextRequired: true,
|
|
@@ -576,3 +586,40 @@ export const OptionListTrigger = ({
|
|
|
576
586
|
</Pressable>
|
|
577
587
|
);
|
|
578
588
|
};
|
|
589
|
+
|
|
590
|
+
export function createOptionList<
|
|
591
|
+
T extends OptionListValue = never,
|
|
592
|
+
TMeta extends MetaShape = MetaShape,
|
|
593
|
+
>(): {
|
|
594
|
+
OptionList: (props: OptionListProps<T, TMeta>) => ReactElement;
|
|
595
|
+
OptionListContent: (props: OptionListContentProps<T, TMeta>) => ReactElement;
|
|
596
|
+
OptionListItem: (props: OptionListItemProps<T>) => ReactElement;
|
|
597
|
+
OptionListItemText: (props: OptionListItemTextProps) => ReactElement;
|
|
598
|
+
OptionListItemDescription: (
|
|
599
|
+
props: OptionListItemDescriptionProps,
|
|
600
|
+
) => ReactElement;
|
|
601
|
+
OptionListItemContent: (props: OptionListItemContentProps) => ReactElement;
|
|
602
|
+
OptionListItemContentRow: (
|
|
603
|
+
props: OptionListItemContentRowProps,
|
|
604
|
+
) => ReactElement;
|
|
605
|
+
OptionListItemLeading: (props: OptionListItemLeadingProps) => ReactElement;
|
|
606
|
+
OptionListSearch: (props: OptionListSearchProps) => ReactElement;
|
|
607
|
+
OptionListEmptyState: (
|
|
608
|
+
props: OptionListEmptyStateProps,
|
|
609
|
+
) => ReactElement | null;
|
|
610
|
+
OptionListTrigger: (props: OptionListTriggerProps) => ReactElement;
|
|
611
|
+
} {
|
|
612
|
+
return {
|
|
613
|
+
OptionList,
|
|
614
|
+
OptionListContent,
|
|
615
|
+
OptionListItem,
|
|
616
|
+
OptionListItemText,
|
|
617
|
+
OptionListItemDescription,
|
|
618
|
+
OptionListItemContent,
|
|
619
|
+
OptionListItemContentRow,
|
|
620
|
+
OptionListItemLeading,
|
|
621
|
+
OptionListSearch,
|
|
622
|
+
OptionListEmptyState,
|
|
623
|
+
OptionListTrigger,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
@@ -8,9 +8,14 @@ import type { SearchInputProps } from '../SearchInput';
|
|
|
8
8
|
|
|
9
9
|
export type MetaShape = Record<string, unknown>;
|
|
10
10
|
|
|
11
|
-
export type
|
|
11
|
+
export type OptionListValue = string;
|
|
12
|
+
|
|
13
|
+
export type OptionListItemData<
|
|
14
|
+
T extends OptionListValue = OptionListValue,
|
|
15
|
+
TMeta extends MetaShape = MetaShape,
|
|
16
|
+
> = {
|
|
12
17
|
/** Unique string identifier for this item, used for selection tracking. */
|
|
13
|
-
value:
|
|
18
|
+
value: T;
|
|
14
19
|
/** Display text. */
|
|
15
20
|
label: string;
|
|
16
21
|
/** Secondary text displayed below the label inside the item. */
|
|
@@ -43,29 +48,35 @@ export type OptionListContextValue = {
|
|
|
43
48
|
};
|
|
44
49
|
|
|
45
50
|
/** Internal type -- consumers never construct this directly. */
|
|
46
|
-
export type OptionListItemGroup<
|
|
51
|
+
export type OptionListItemGroup<
|
|
52
|
+
T extends OptionListValue = OptionListValue,
|
|
53
|
+
TMeta extends MetaShape = MetaShape,
|
|
54
|
+
> = {
|
|
47
55
|
label: string;
|
|
48
|
-
items: OptionListItemData<TMeta>[];
|
|
56
|
+
items: OptionListItemData<T, TMeta>[];
|
|
49
57
|
};
|
|
50
58
|
|
|
51
|
-
export type OptionListProps<
|
|
59
|
+
export type OptionListProps<
|
|
60
|
+
T extends OptionListValue = OptionListValue,
|
|
61
|
+
TMeta extends MetaShape = MetaShape,
|
|
62
|
+
> = {
|
|
52
63
|
/**
|
|
53
64
|
* Flat array of items.
|
|
54
65
|
* Use the `group` field on each item for automatic grouping.
|
|
55
66
|
*/
|
|
56
|
-
items: OptionListItemData<TMeta>[];
|
|
67
|
+
items: OptionListItemData<T, TMeta>[];
|
|
57
68
|
/**
|
|
58
69
|
* The controlled selected value.
|
|
59
70
|
*/
|
|
60
|
-
value?:
|
|
71
|
+
value?: T | null;
|
|
61
72
|
/**
|
|
62
73
|
* The default selected value (uncontrolled)
|
|
63
74
|
*/
|
|
64
|
-
defaultValue?:
|
|
75
|
+
defaultValue?: T | null;
|
|
65
76
|
/**
|
|
66
77
|
* Called when the selected value changes.
|
|
67
78
|
*/
|
|
68
|
-
onValueChange?: (value:
|
|
79
|
+
onValueChange?: (value: T | null) => void;
|
|
69
80
|
/**
|
|
70
81
|
* When true, prevents interaction with the entire list.
|
|
71
82
|
*/
|
|
@@ -74,12 +85,14 @@ export type OptionListProps<TMeta extends MetaShape = MetaShape> = {
|
|
|
74
85
|
* Custom item/query matcher.
|
|
75
86
|
* Defaults to case-insensitive label match; `null` disables filtering.
|
|
76
87
|
*/
|
|
77
|
-
filter?:
|
|
88
|
+
filter?:
|
|
89
|
+
| null
|
|
90
|
+
| ((item: OptionListItemData<T, TMeta>, query: string) => boolean);
|
|
78
91
|
/**
|
|
79
92
|
* Pre-filtered items for async/remote search.
|
|
80
93
|
* Bypasses internal filtering.
|
|
81
94
|
*/
|
|
82
|
-
filteredItems?: OptionListItemData<TMeta>[];
|
|
95
|
+
filteredItems?: OptionListItemData<T, TMeta>[];
|
|
83
96
|
/**
|
|
84
97
|
* Controlled search input value.
|
|
85
98
|
*/
|
|
@@ -95,14 +108,20 @@ export type OptionListProps<TMeta extends MetaShape = MetaShape> = {
|
|
|
95
108
|
children: ReactNode;
|
|
96
109
|
};
|
|
97
110
|
|
|
98
|
-
export type OptionListContentProps<
|
|
111
|
+
export type OptionListContentProps<
|
|
112
|
+
T extends OptionListValue = OptionListValue,
|
|
113
|
+
TMeta extends MetaShape = MetaShape,
|
|
114
|
+
> = {
|
|
99
115
|
/** Render function called for each item. Receives the item data and selection/disabled state. */
|
|
100
|
-
renderItem: (
|
|
116
|
+
renderItem: (
|
|
117
|
+
item: OptionListItemData<T, TMeta>,
|
|
118
|
+
selected: boolean,
|
|
119
|
+
) => ReactNode;
|
|
101
120
|
} & Omit<StyledViewProps, 'children'>;
|
|
102
121
|
|
|
103
|
-
export type OptionListItemProps = {
|
|
122
|
+
export type OptionListItemProps<T extends OptionListValue = OptionListValue> = {
|
|
104
123
|
/** The value associated with this item, used for selection matching. */
|
|
105
|
-
value:
|
|
124
|
+
value: T;
|
|
106
125
|
/** Whether the item is disabled. */
|
|
107
126
|
disabled?: boolean;
|
|
108
127
|
children: ReactNode;
|
|
@@ -4,13 +4,17 @@ import type {
|
|
|
4
4
|
MetaShape,
|
|
5
5
|
OptionListItemData,
|
|
6
6
|
OptionListItemGroup,
|
|
7
|
+
OptionListValue,
|
|
7
8
|
} from '../types';
|
|
8
9
|
|
|
9
|
-
const groupByField = <
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const groupByField = <
|
|
11
|
+
T extends OptionListValue = OptionListValue,
|
|
12
|
+
TMeta extends MetaShape = MetaShape,
|
|
13
|
+
>(
|
|
14
|
+
items: OptionListItemData<T, TMeta>[],
|
|
15
|
+
): OptionListItemGroup<T, TMeta>[] => {
|
|
12
16
|
const order: string[] = [];
|
|
13
|
-
const map: Record<string, OptionListItemData<TMeta>[]> = {};
|
|
17
|
+
const map: Record<string, OptionListItemData<T, TMeta>[]> = {};
|
|
14
18
|
for (const item of items) {
|
|
15
19
|
const key = item.group ?? '';
|
|
16
20
|
if (!map[key]) {
|
|
@@ -30,31 +34,42 @@ export const defaultLabelFilter = (
|
|
|
30
34
|
query: string,
|
|
31
35
|
): boolean => item.label.toLowerCase().includes(query.toLowerCase());
|
|
32
36
|
|
|
33
|
-
type UseOptionListItemsParams<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
type UseOptionListItemsParams<
|
|
38
|
+
T extends OptionListValue = OptionListValue,
|
|
39
|
+
TMeta extends MetaShape = MetaShape,
|
|
40
|
+
> = {
|
|
41
|
+
items: OptionListItemData<T, TMeta>[];
|
|
42
|
+
filter?:
|
|
43
|
+
| null
|
|
44
|
+
| ((item: OptionListItemData<T, TMeta>, query: string) => boolean);
|
|
45
|
+
filteredItems?: OptionListItemData<T, TMeta>[];
|
|
37
46
|
searchValue?: string;
|
|
38
47
|
defaultSearchValue?: string;
|
|
39
48
|
onSearchValueChange?: (value: string) => void;
|
|
40
49
|
};
|
|
41
50
|
|
|
42
|
-
type UseOptionListItemsResult<
|
|
51
|
+
type UseOptionListItemsResult<
|
|
52
|
+
T extends OptionListValue = OptionListValue,
|
|
53
|
+
TMeta extends MetaShape = MetaShape,
|
|
54
|
+
> = {
|
|
43
55
|
isGrouped: boolean;
|
|
44
|
-
groups: OptionListItemGroup<TMeta>[];
|
|
45
|
-
flatItems: OptionListItemData<TMeta>[];
|
|
56
|
+
groups: OptionListItemGroup<T, TMeta>[];
|
|
57
|
+
flatItems: OptionListItemData<T, TMeta>[];
|
|
46
58
|
resolvedSearchValue: string;
|
|
47
59
|
handleSearchValueChange: (val: string) => void;
|
|
48
60
|
};
|
|
49
61
|
|
|
50
|
-
export const useOptionListItems = <
|
|
62
|
+
export const useOptionListItems = <
|
|
63
|
+
T extends OptionListValue = OptionListValue,
|
|
64
|
+
TMeta extends MetaShape = MetaShape,
|
|
65
|
+
>({
|
|
51
66
|
items,
|
|
52
67
|
filter,
|
|
53
68
|
filteredItems,
|
|
54
69
|
searchValue: searchValueProp,
|
|
55
70
|
defaultSearchValue,
|
|
56
71
|
onSearchValueChange,
|
|
57
|
-
}: UseOptionListItemsParams<TMeta>): UseOptionListItemsResult<TMeta> => {
|
|
72
|
+
}: UseOptionListItemsParams<T, TMeta>): UseOptionListItemsResult<T, TMeta> => {
|
|
58
73
|
const [searchValue, handleSearchValueChange] = useControllableState<string>({
|
|
59
74
|
prop: searchValueProp,
|
|
60
75
|
defaultProp: defaultSearchValue ?? '',
|
|
@@ -67,6 +67,8 @@ Use the library as part of `@ledgerhq/lumen-ui-rnative`. See the [Setup Guide
|
|
|
67
67
|
|
|
68
68
|
Control selected value in state at the top level and pass it to SegmentedControl. Buttons use value to identify themselves; selected state is derived from context.
|
|
69
69
|
|
|
70
|
+
> **Recommended:** prefer the [type-safe variant](#type-safe-variant) below. It shares a single literal union across the root and every button.
|
|
71
|
+
|
|
70
72
|
```tsx
|
|
71
73
|
import { SegmentedControl, SegmentedControlButton } from '@ledgerhq/lumen-ui-rnative';
|
|
72
74
|
|
|
@@ -87,6 +89,32 @@ export default function Example() {
|
|
|
87
89
|
}
|
|
88
90
|
```
|
|
89
91
|
|
|
92
|
+
## Type-safe variant
|
|
93
|
+
|
|
94
|
+
`SegmentedControl` and `SegmentedControlButton` communicate via context, so TypeScript cannot connect the root's `selectedValue` to each button's `value` on its own. A typo compiles, and the pill silently never moves to that segment.
|
|
95
|
+
|
|
96
|
+
`createSegmentedControl<T>()` ties both components to one literal union. Call it once at module scope; the returned pair are typed against your `T`.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { createSegmentedControl } from '@ledgerhq/lumen-ui-rnative';
|
|
100
|
+
|
|
101
|
+
type View = 'preview' | 'raw' | 'blame';
|
|
102
|
+
|
|
103
|
+
const { SegmentedControl, SegmentedControlButton } = createSegmentedControl<View>();
|
|
104
|
+
|
|
105
|
+
function Example() {
|
|
106
|
+
const [view, setView] = React.useState<View>('preview');
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<SegmentedControl selectedValue={view} onSelectedChange={setView}>
|
|
110
|
+
<SegmentedControlButton value='preview'>Preview</SegmentedControlButton>
|
|
111
|
+
<SegmentedControlButton value='raw'>Raw</SegmentedControlButton>
|
|
112
|
+
<SegmentedControlButton value='blame'>Blame</SegmentedControlButton>
|
|
113
|
+
</SegmentedControl>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
90
118
|
## With trailing content
|
|
91
119
|
|
|
92
120
|
Pass any node via `trailingContent` to render it to the right of the label. Use it for badges like `DotCount` to surface counts per segment.
|
|
@@ -3,7 +3,11 @@ import { useState } from 'react';
|
|
|
3
3
|
import { Coins, Nft, TransferHorizontal } from '../../Symbols';
|
|
4
4
|
import { DotCount } from '../DotCount';
|
|
5
5
|
import { Box } from '../Utility';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createSegmentedControl,
|
|
8
|
+
SegmentedControl,
|
|
9
|
+
SegmentedControlButton,
|
|
10
|
+
} from './SegmentedControl';
|
|
7
11
|
|
|
8
12
|
const meta = {
|
|
9
13
|
id: 'rnative-segmentedcontrol',
|
|
@@ -171,3 +175,32 @@ export const WithTrailingContent: Story = {
|
|
|
171
175
|
);
|
|
172
176
|
},
|
|
173
177
|
};
|
|
178
|
+
|
|
179
|
+
type View = 'preview' | 'raw' | 'blame';
|
|
180
|
+
const TypedSegmentedControl = createSegmentedControl<View>();
|
|
181
|
+
|
|
182
|
+
export const TypesafeFactory: Story = {
|
|
183
|
+
args: {} as React.ComponentProps<typeof SegmentedControl>,
|
|
184
|
+
render: (args) => {
|
|
185
|
+
const [view, setView] = useState<View>('preview');
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<TypedSegmentedControl.SegmentedControl
|
|
189
|
+
{...args}
|
|
190
|
+
selectedValue={view}
|
|
191
|
+
onSelectedChange={setView}
|
|
192
|
+
accessibilityLabel='File view'
|
|
193
|
+
>
|
|
194
|
+
<TypedSegmentedControl.SegmentedControlButton value='preview'>
|
|
195
|
+
Preview
|
|
196
|
+
</TypedSegmentedControl.SegmentedControlButton>
|
|
197
|
+
<TypedSegmentedControl.SegmentedControlButton value='raw'>
|
|
198
|
+
Raw
|
|
199
|
+
</TypedSegmentedControl.SegmentedControlButton>
|
|
200
|
+
<TypedSegmentedControl.SegmentedControlButton value='blame'>
|
|
201
|
+
Blame
|
|
202
|
+
</TypedSegmentedControl.SegmentedControlButton>
|
|
203
|
+
</TypedSegmentedControl.SegmentedControl>
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
};
|
|
@@ -3,7 +3,11 @@ import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
|
|
|
3
3
|
import { render, fireEvent } from '@testing-library/react-native';
|
|
4
4
|
import { DotCount } from '../DotCount';
|
|
5
5
|
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
SegmentedControl,
|
|
8
|
+
SegmentedControlButton,
|
|
9
|
+
createSegmentedControl,
|
|
10
|
+
} from './SegmentedControl';
|
|
7
11
|
|
|
8
12
|
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
9
13
|
<ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
|
|
@@ -172,4 +176,31 @@ describe('SegmentedControl', () => {
|
|
|
172
176
|
expect(onSelectedChange).toHaveBeenCalledWith('preview');
|
|
173
177
|
});
|
|
174
178
|
});
|
|
179
|
+
|
|
180
|
+
describe('createSegmentedControl', () => {
|
|
181
|
+
it('returns typed components that render and select like the originals', () => {
|
|
182
|
+
const {
|
|
183
|
+
SegmentedControl: TypedControl,
|
|
184
|
+
SegmentedControlButton: TypedButton,
|
|
185
|
+
} = createSegmentedControl<'send' | 'receive'>();
|
|
186
|
+
const onSelectedChange = jest.fn();
|
|
187
|
+
|
|
188
|
+
const { getByText } = render(
|
|
189
|
+
<TestWrapper>
|
|
190
|
+
<TypedControl
|
|
191
|
+
selectedValue='send'
|
|
192
|
+
onSelectedChange={onSelectedChange}
|
|
193
|
+
accessibilityLabel='Transaction type'
|
|
194
|
+
>
|
|
195
|
+
<TypedButton value='send'>Send</TypedButton>
|
|
196
|
+
<TypedButton value='receive'>Receive</TypedButton>
|
|
197
|
+
</TypedControl>
|
|
198
|
+
</TestWrapper>,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
fireEvent.press(getByText('Receive'));
|
|
202
|
+
|
|
203
|
+
expect(onSelectedChange).toHaveBeenCalledWith('receive');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
175
206
|
});
|