react-native-molecules 0.5.0-beta.20 → 0.5.0-beta.22
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/components/Card/Card.tsx +1 -1
- package/components/Checkbox/CheckboxBase.ios.tsx +9 -16
- package/components/Checkbox/CheckboxBase.tsx +11 -18
- package/components/DateField/DateField.tsx +4 -3
- package/components/DatePicker/DateCalendar.tsx +4 -4
- package/components/DatePicker/DatePickerModal.tsx +35 -23
- package/components/DatePicker/DatePickerProvider.tsx +8 -2
- package/components/DatePicker/context.tsx +2 -1
- package/components/DatePicker/index.tsx +1 -0
- package/components/DatePickerInline/DatePickerDockedHeader.tsx +11 -7
- package/components/DatePickerInline/DatePickerInline.tsx +1 -1
- package/components/DatePickerInline/DatePickerInlineBase.tsx +3 -3
- package/components/DatePickerInline/DatePickerInlineHeader.tsx +50 -20
- package/components/DatePickerInline/DayNames.tsx +13 -10
- package/components/DatePickerInline/HeaderItem.tsx +2 -2
- package/components/DatePickerInline/Month.tsx +4 -3
- package/components/DatePickerInline/MonthPicker.tsx +74 -54
- package/components/DatePickerInline/Swiper.native.tsx +2 -2
- package/components/DatePickerInline/Swiper.tsx +3 -3
- package/components/DatePickerInline/YearPicker.tsx +136 -112
- package/components/DatePickerInline/{DatePickerContext.tsx → store.tsx} +7 -3
- package/components/DatePickerInline/types.ts +4 -3
- package/components/Divider/Divider.tsx +192 -0
- package/components/Divider/index.tsx +11 -0
- package/components/Drawer/DrawerItemGroup.tsx +3 -7
- package/components/IconButton/IconButton.tsx +2 -12
- package/components/List/List.tsx +507 -0
- package/components/List/context.tsx +28 -0
- package/components/List/index.ts +9 -0
- package/components/List/types.ts +149 -0
- package/components/{ListItem → List}/utils.ts +47 -50
- package/components/Menu/Menu.tsx +156 -12
- package/components/Menu/index.tsx +11 -7
- package/components/Menu/utils.ts +21 -70
- package/components/RadioButton/RadioButtonAndroid.tsx +38 -54
- package/components/RadioButton/RadioButtonIOS.tsx +2 -16
- package/components/Select/Select.tsx +139 -497
- package/components/Select/context.tsx +14 -32
- package/components/Select/types.ts +44 -53
- package/components/Select/utils.ts +15 -47
- package/components/Text/textFactory.tsx +17 -5
- package/components/TimeField/TimeField.tsx +1 -1
- package/components/TimePicker/TimeInput.tsx +2 -7
- package/components/TimePicker/TimePickerModal.tsx +15 -15
- package/components/TimePicker/utils.ts +0 -4
- package/components/TouchableRipple/TouchableRipple.native.tsx +36 -5
- package/components/TouchableRipple/TouchableRipple.tsx +53 -19
- package/components/TouchableRipple/rippleFromForegroundColor.ts +21 -0
- package/package.json +4 -2
- package/components/HorizontalDivider/HorizontalDivider.tsx +0 -103
- package/components/HorizontalDivider/index.tsx +0 -9
- package/components/ListItem/ListItem.tsx +0 -138
- package/components/ListItem/ListItemDescription.tsx +0 -25
- package/components/ListItem/ListItemTitle.tsx +0 -25
- package/components/ListItem/index.tsx +0 -14
- package/components/Menu/MenuDivider.tsx +0 -13
- package/components/Menu/MenuItem.tsx +0 -128
- package/components/VerticalDivider/VerticalDivider.tsx +0 -100
- package/components/VerticalDivider/index.tsx +0 -9
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComponentType,
|
|
3
|
+
memo,
|
|
4
|
+
type RefObject,
|
|
5
|
+
useCallback,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { ScrollView, type StyleProp, View, type ViewStyle } from 'react-native';
|
|
11
|
+
|
|
12
|
+
import { typedMemo } from '../../hocs';
|
|
13
|
+
import { useActionState, useControlledValue, useLatest } from '../../hooks';
|
|
14
|
+
import type { WithElements } from '../../types';
|
|
15
|
+
import { resolveStateVariant } from '../../utils';
|
|
16
|
+
import { Divider } from '../Divider';
|
|
17
|
+
import { Icon } from '../Icon';
|
|
18
|
+
import { IconButton } from '../IconButton';
|
|
19
|
+
import { StateLayer } from '../StateLayer';
|
|
20
|
+
import { Text } from '../Text';
|
|
21
|
+
import { TextInput, type TextInputHandles, type TextInputProps } from '../TextInput';
|
|
22
|
+
import { TouchableRipple, type TouchableRippleProps } from '../TouchableRipple';
|
|
23
|
+
import { ListContextProvider, useListContextValue } from './context';
|
|
24
|
+
import type {
|
|
25
|
+
DefaultListItemT,
|
|
26
|
+
ListContentProps,
|
|
27
|
+
ListContextValue,
|
|
28
|
+
ListGroupProps,
|
|
29
|
+
ListItemOptionProps,
|
|
30
|
+
ListProps,
|
|
31
|
+
ListSearchInputProps,
|
|
32
|
+
} from './types';
|
|
33
|
+
import { listItemStyles, listStyles } from './utils';
|
|
34
|
+
|
|
35
|
+
const emptyArr: unknown[] = [];
|
|
36
|
+
|
|
37
|
+
type InternalListItemProps = Omit<TouchableRippleProps, 'children'> &
|
|
38
|
+
WithElements<React.ReactNode | ((renderArgs: { hovered: boolean }) => React.ReactNode)> & {
|
|
39
|
+
ref?: RefObject<any>;
|
|
40
|
+
hovered?: boolean;
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
style?: StyleProp<ViewStyle>;
|
|
43
|
+
divider?: boolean;
|
|
44
|
+
variant?: 'default' | 'menuItem';
|
|
45
|
+
selected?: boolean;
|
|
46
|
+
hoverable?: boolean;
|
|
47
|
+
contentStyle?: StyleProp<ViewStyle>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const _InternalListItem = ({
|
|
51
|
+
ref,
|
|
52
|
+
left,
|
|
53
|
+
right,
|
|
54
|
+
children,
|
|
55
|
+
style: styleProp,
|
|
56
|
+
disabled = false,
|
|
57
|
+
divider = false,
|
|
58
|
+
variant = 'default',
|
|
59
|
+
selected = false,
|
|
60
|
+
onPress,
|
|
61
|
+
hoverable: hoverableProp = false,
|
|
62
|
+
hovered: hoveredProp = false,
|
|
63
|
+
contentStyle: contentStyleProp,
|
|
64
|
+
...props
|
|
65
|
+
}: InternalListItemProps) => {
|
|
66
|
+
const {
|
|
67
|
+
hovered: _hovered,
|
|
68
|
+
focused,
|
|
69
|
+
actionsRef,
|
|
70
|
+
} = useActionState({ ref, actionsToListen: ['hover', 'focus'] });
|
|
71
|
+
const hoverable = hoverableProp || !!onPress;
|
|
72
|
+
const hovered = hoveredProp || _hovered;
|
|
73
|
+
|
|
74
|
+
const state = resolveStateVariant({
|
|
75
|
+
selectedAndFocused: selected && focused,
|
|
76
|
+
selected,
|
|
77
|
+
disabled,
|
|
78
|
+
hovered: hoverable && hovered,
|
|
79
|
+
focused,
|
|
80
|
+
}) as any;
|
|
81
|
+
|
|
82
|
+
listItemStyles.useVariants({
|
|
83
|
+
state,
|
|
84
|
+
variant: variant as any,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const {
|
|
88
|
+
containerStyles,
|
|
89
|
+
innerContainerStyle,
|
|
90
|
+
contentStyle,
|
|
91
|
+
leftElementStyle,
|
|
92
|
+
rightElementStyle,
|
|
93
|
+
} = useMemo(() => {
|
|
94
|
+
const { innerContainer, content, leftElement, rightElement } = listItemStyles;
|
|
95
|
+
return {
|
|
96
|
+
containerStyles: [listItemStyles.root, styleProp],
|
|
97
|
+
innerContainerStyle: innerContainer,
|
|
98
|
+
contentStyle: content,
|
|
99
|
+
leftElementStyle: leftElement,
|
|
100
|
+
rightElementStyle: rightElement,
|
|
101
|
+
};
|
|
102
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
103
|
+
}, [styleProp, state, variant]);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<TouchableRipple
|
|
107
|
+
{...props}
|
|
108
|
+
style={containerStyles}
|
|
109
|
+
disabled={disabled}
|
|
110
|
+
onPress={onPress}
|
|
111
|
+
ref={actionsRef}>
|
|
112
|
+
<>
|
|
113
|
+
<View style={innerContainerStyle}>
|
|
114
|
+
{left ? (
|
|
115
|
+
<View style={leftElementStyle}>
|
|
116
|
+
{typeof left === 'function' ? left({ hovered }) : left}
|
|
117
|
+
</View>
|
|
118
|
+
) : null}
|
|
119
|
+
<View style={[contentStyle, contentStyleProp]}>{children}</View>
|
|
120
|
+
{right ? (
|
|
121
|
+
<View style={rightElementStyle}>
|
|
122
|
+
{typeof right === 'function' ? right({ hovered }) : right}
|
|
123
|
+
</View>
|
|
124
|
+
) : null}
|
|
125
|
+
</View>
|
|
126
|
+
{divider ? <Divider /> : null}
|
|
127
|
+
<StateLayer style={listItemStyles.stateLayer} />
|
|
128
|
+
</>
|
|
129
|
+
</TouchableRipple>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const ListItem = memo(_InternalListItem);
|
|
134
|
+
|
|
135
|
+
const ListProvider = typedMemo(
|
|
136
|
+
<Option extends DefaultListItemT = DefaultListItemT>({
|
|
137
|
+
children,
|
|
138
|
+
value: valueProp,
|
|
139
|
+
defaultValue,
|
|
140
|
+
onChange,
|
|
141
|
+
multiple = false,
|
|
142
|
+
disabled = false,
|
|
143
|
+
error = false,
|
|
144
|
+
items = emptyArr as Option[],
|
|
145
|
+
searchKey,
|
|
146
|
+
onSearchChange,
|
|
147
|
+
hideSelected: hideSelectedProp,
|
|
148
|
+
}: ListProps<Option>) => {
|
|
149
|
+
type ControlledListValue = Option['id'] | Option['id'][] | null;
|
|
150
|
+
const [value, onValueChange] = useControlledValue<Option['id'] | Option['id'][] | null>({
|
|
151
|
+
value: valueProp,
|
|
152
|
+
defaultValue: defaultValue ?? (multiple ? (emptyArr as Option['id'][]) : null),
|
|
153
|
+
onChange: onChange as
|
|
154
|
+
| ((value: ControlledListValue, item: Option, event?: any) => void)
|
|
155
|
+
| undefined,
|
|
156
|
+
});
|
|
157
|
+
const valueRef = useLatest(value);
|
|
158
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
159
|
+
|
|
160
|
+
const handleSearchQueryChange = useCallback(
|
|
161
|
+
(query: string) => {
|
|
162
|
+
setSearchQuery(query);
|
|
163
|
+
onSearchChange?.(query);
|
|
164
|
+
},
|
|
165
|
+
[onSearchChange],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const hideSelected = hideSelectedProp !== undefined ? hideSelectedProp : multiple;
|
|
169
|
+
|
|
170
|
+
const filteredItems = useMemo(() => {
|
|
171
|
+
let result = items;
|
|
172
|
+
|
|
173
|
+
if (hideSelected) {
|
|
174
|
+
result = result.filter(item => {
|
|
175
|
+
if (multiple) {
|
|
176
|
+
const values = (value as Option['id'][]) || [];
|
|
177
|
+
return !values.some(v => v === item.id);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const singleValue = value as Option['id'] | null;
|
|
181
|
+
return singleValue !== item.id;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (searchQuery) {
|
|
186
|
+
const key = searchKey || 'label';
|
|
187
|
+
const lowerQuery = searchQuery.toLowerCase();
|
|
188
|
+
result = result.filter(item => {
|
|
189
|
+
const itemValue = item[key];
|
|
190
|
+
return String(itemValue || '')
|
|
191
|
+
.toLowerCase()
|
|
192
|
+
.includes(lowerQuery);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result;
|
|
197
|
+
}, [hideSelected, items, multiple, searchKey, searchQuery, value]);
|
|
198
|
+
|
|
199
|
+
const onAdd = useCallback(
|
|
200
|
+
(item: Option) => {
|
|
201
|
+
if (multiple) {
|
|
202
|
+
const currentValue = (valueRef.current as Option['id'][]) || [];
|
|
203
|
+
if (!currentValue.find(v => v === item.id)) {
|
|
204
|
+
onValueChange([...currentValue, item.id] as Option['id'][], item);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
onValueChange(item.id, item);
|
|
210
|
+
},
|
|
211
|
+
[multiple, onValueChange, valueRef],
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const onRemove = useCallback(
|
|
215
|
+
(item: Option) => {
|
|
216
|
+
if (multiple) {
|
|
217
|
+
const currentValue = (valueRef.current as Option['id'][]) || [];
|
|
218
|
+
onValueChange(currentValue.filter(v => v !== item.id) as Option['id'][], item);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
onValueChange(null, item);
|
|
223
|
+
},
|
|
224
|
+
[multiple, onValueChange, valueRef],
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const contextValue = {
|
|
228
|
+
value,
|
|
229
|
+
multiple,
|
|
230
|
+
onAdd: onAdd as (item: DefaultListItemT) => void,
|
|
231
|
+
onRemove: onRemove as (item: DefaultListItemT) => void,
|
|
232
|
+
disabled,
|
|
233
|
+
error,
|
|
234
|
+
items,
|
|
235
|
+
searchQuery,
|
|
236
|
+
setSearchQuery: handleSearchQueryChange,
|
|
237
|
+
filteredItems,
|
|
238
|
+
} as ListContextValue<DefaultListItemT>;
|
|
239
|
+
|
|
240
|
+
return <ListContextProvider value={contextValue}>{children}</ListContextProvider>;
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const ListContent = typedMemo(
|
|
245
|
+
<
|
|
246
|
+
Option extends DefaultListItemT = DefaultListItemT,
|
|
247
|
+
C extends ComponentType<any> = typeof ScrollView,
|
|
248
|
+
>({
|
|
249
|
+
ref,
|
|
250
|
+
children,
|
|
251
|
+
ContainerComponent,
|
|
252
|
+
style,
|
|
253
|
+
emptyState,
|
|
254
|
+
processProps,
|
|
255
|
+
...rest
|
|
256
|
+
}: ListContentProps<Option, C> & { ref?: React.ForwardedRef<any> }) => {
|
|
257
|
+
const { filteredItems, value, multiple, searchQuery, items } = useListContextValue(
|
|
258
|
+
state => ({
|
|
259
|
+
filteredItems: state.filteredItems,
|
|
260
|
+
value: state.value,
|
|
261
|
+
multiple: state.multiple,
|
|
262
|
+
searchQuery: state.searchQuery,
|
|
263
|
+
items: state.items,
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const selectedIds = useMemo(() => {
|
|
268
|
+
const map: Record<string, true> = {};
|
|
269
|
+
const values = multiple ? (value as any[]) ?? [] : value == null ? [] : [value];
|
|
270
|
+
for (const v of values) {
|
|
271
|
+
const id = v?.id ?? v;
|
|
272
|
+
if (id != null) map[id] = true;
|
|
273
|
+
}
|
|
274
|
+
return map;
|
|
275
|
+
}, [multiple, value]);
|
|
276
|
+
|
|
277
|
+
const isSelected = useCallback(
|
|
278
|
+
(item: DefaultListItemT) => selectedIds[item.id as any] === true,
|
|
279
|
+
[selectedIds],
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const content = useMemo(() => {
|
|
283
|
+
if (children === undefined) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
return filteredItems.map(item =>
|
|
287
|
+
children(item as Option, isSelected(item as DefaultListItemT)),
|
|
288
|
+
);
|
|
289
|
+
}, [children, filteredItems, isSelected]);
|
|
290
|
+
|
|
291
|
+
const defaultEmptyState = useMemo(() => {
|
|
292
|
+
const hasSearchQuery = searchQuery && searchQuery.trim().length > 0;
|
|
293
|
+
const hasNoItems = items.length === 0;
|
|
294
|
+
|
|
295
|
+
if (hasNoItems) {
|
|
296
|
+
return (
|
|
297
|
+
<View style={listStyles.emptyState}>
|
|
298
|
+
<Text style={listStyles.emptyStateText}>No items available</Text>
|
|
299
|
+
</View>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (hasSearchQuery) {
|
|
304
|
+
return (
|
|
305
|
+
<View style={listStyles.emptyState}>
|
|
306
|
+
<Text style={listStyles.emptyStateText}>No results found</Text>
|
|
307
|
+
</View>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<View style={listStyles.emptyState}>
|
|
313
|
+
<Text style={listStyles.emptyStateText}>No items</Text>
|
|
314
|
+
</View>
|
|
315
|
+
);
|
|
316
|
+
}, [items.length, searchQuery]);
|
|
317
|
+
|
|
318
|
+
const resolvedEmptyState = emptyState ?? defaultEmptyState;
|
|
319
|
+
const isEmpty = filteredItems.length === 0;
|
|
320
|
+
|
|
321
|
+
const Container = (ContainerComponent ?? ScrollView) as ComponentType<any>;
|
|
322
|
+
|
|
323
|
+
if (processProps) {
|
|
324
|
+
const baseProps = { style, ...rest, accessibilityRole: 'listbox' } as Record<
|
|
325
|
+
string,
|
|
326
|
+
any
|
|
327
|
+
>;
|
|
328
|
+
const processedProps = processProps({
|
|
329
|
+
props: baseProps as any,
|
|
330
|
+
items: filteredItems as Option[],
|
|
331
|
+
isEmpty,
|
|
332
|
+
emptyState: resolvedEmptyState,
|
|
333
|
+
isSelected,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return <Container {...(processedProps as any)} ref={ref} />;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<Container style={style} {...rest} ref={ref} accessibilityRole="listbox">
|
|
341
|
+
{isEmpty ? resolvedEmptyState : content}
|
|
342
|
+
</Container>
|
|
343
|
+
);
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const ListGroup = memo(({ children, label, style, ...rest }: ListGroupProps) => {
|
|
348
|
+
return (
|
|
349
|
+
<View style={style} {...rest}>
|
|
350
|
+
{label ? <Text style={listStyles.groupLabel}>{label}</Text> : null}
|
|
351
|
+
{children}
|
|
352
|
+
</View>
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const ListOption = memo(
|
|
357
|
+
<Option extends DefaultListItemT = DefaultListItemT>({
|
|
358
|
+
value,
|
|
359
|
+
children,
|
|
360
|
+
onPress,
|
|
361
|
+
disabled: itemDisabledProp = false,
|
|
362
|
+
shouldToggleOnPress = true,
|
|
363
|
+
accessibilityRole,
|
|
364
|
+
accessibilityState,
|
|
365
|
+
...rest
|
|
366
|
+
}: ListItemOptionProps<Option>) => {
|
|
367
|
+
const {
|
|
368
|
+
multiple,
|
|
369
|
+
onAdd,
|
|
370
|
+
onRemove,
|
|
371
|
+
disabled: listDisabled,
|
|
372
|
+
items,
|
|
373
|
+
} = useListContextValue(state => ({
|
|
374
|
+
multiple: state.multiple,
|
|
375
|
+
onAdd: state.onAdd,
|
|
376
|
+
onRemove: state.onRemove,
|
|
377
|
+
disabled: state.disabled,
|
|
378
|
+
items: state.items,
|
|
379
|
+
}));
|
|
380
|
+
|
|
381
|
+
const option = useMemo(() => {
|
|
382
|
+
const foundItem = items.find(i => i.id === value);
|
|
383
|
+
if (foundItem) return foundItem as Option;
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
id: value,
|
|
387
|
+
...(itemDisabledProp ? { selectable: false } : {}),
|
|
388
|
+
} as Option;
|
|
389
|
+
}, [itemDisabledProp, items, value]);
|
|
390
|
+
|
|
391
|
+
const isSelected = useListContextValue(state => {
|
|
392
|
+
if (multiple) {
|
|
393
|
+
const values = state.value as any[];
|
|
394
|
+
return values?.some(v => (v?.id ?? v) === option.id) || false;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const singleValue = state.value as any;
|
|
398
|
+
return (singleValue?.id ?? singleValue) === option.id || false;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const isOptionDisabled = Boolean(
|
|
402
|
+
listDisabled || itemDisabledProp || option.selectable === false,
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const handlePress = useCallback(
|
|
406
|
+
(event: any) => {
|
|
407
|
+
if (isOptionDisabled) return;
|
|
408
|
+
onPress?.(option, event);
|
|
409
|
+
|
|
410
|
+
if (shouldToggleOnPress) {
|
|
411
|
+
if (isSelected) {
|
|
412
|
+
onRemove(option);
|
|
413
|
+
} else {
|
|
414
|
+
onAdd(option);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
[isOptionDisabled, isSelected, onAdd, onPress, onRemove, option, shouldToggleOnPress],
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<ListItem
|
|
423
|
+
{...rest}
|
|
424
|
+
selected={isSelected}
|
|
425
|
+
disabled={isOptionDisabled}
|
|
426
|
+
onPress={handlePress}
|
|
427
|
+
variant={rest.variant || 'menuItem'}
|
|
428
|
+
accessibilityRole={accessibilityRole}
|
|
429
|
+
accessibilityState={
|
|
430
|
+
accessibilityState ?? { selected: isSelected, disabled: isOptionDisabled }
|
|
431
|
+
}>
|
|
432
|
+
{children}
|
|
433
|
+
</ListItem>
|
|
434
|
+
);
|
|
435
|
+
},
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const ListSearchInput = memo(({ children, ...textInputProps }: ListSearchInputProps) => {
|
|
439
|
+
const { searchQuery, setSearchQuery } = useListContextValue(state => ({
|
|
440
|
+
searchQuery: state.searchQuery,
|
|
441
|
+
setSearchQuery: state.setSearchQuery,
|
|
442
|
+
}));
|
|
443
|
+
|
|
444
|
+
const textInputRef = useRef<TextInputHandles>(null);
|
|
445
|
+
|
|
446
|
+
const handleChangeText = useCallback(
|
|
447
|
+
(text: string) => {
|
|
448
|
+
setSearchQuery(text);
|
|
449
|
+
},
|
|
450
|
+
[setSearchQuery],
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const inputProps = {
|
|
454
|
+
...textInputProps,
|
|
455
|
+
value: searchQuery,
|
|
456
|
+
onChangeText: handleChangeText,
|
|
457
|
+
placeholder: textInputProps.placeholder || 'Search...',
|
|
458
|
+
inputStyle: listStyles.searchInputInput,
|
|
459
|
+
} as TextInputProps;
|
|
460
|
+
|
|
461
|
+
const onPressLeftIcon = useCallback(() => {
|
|
462
|
+
textInputRef.current?.focus();
|
|
463
|
+
}, []);
|
|
464
|
+
|
|
465
|
+
const onClearSearchQuery = useCallback(() => {
|
|
466
|
+
handleChangeText('');
|
|
467
|
+
}, [handleChangeText]);
|
|
468
|
+
|
|
469
|
+
return (
|
|
470
|
+
<TextInput
|
|
471
|
+
ref={textInputRef}
|
|
472
|
+
style={listStyles.searchInput}
|
|
473
|
+
size="sm"
|
|
474
|
+
variant="outlined"
|
|
475
|
+
{...inputProps}>
|
|
476
|
+
<TextInput.Left>
|
|
477
|
+
<Icon onPress={onPressLeftIcon} name="magnify" size={20} />
|
|
478
|
+
</TextInput.Left>
|
|
479
|
+
{searchQuery ? (
|
|
480
|
+
<TextInput.Right>
|
|
481
|
+
<IconButton name="close" size={20} onPress={onClearSearchQuery} />
|
|
482
|
+
</TextInput.Right>
|
|
483
|
+
) : null}
|
|
484
|
+
{children}
|
|
485
|
+
</TextInput>
|
|
486
|
+
);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const List = Object.assign(ListProvider, {
|
|
490
|
+
Content: ListContent,
|
|
491
|
+
Item: ListOption,
|
|
492
|
+
SearchInput: ListSearchInput,
|
|
493
|
+
Group: ListGroup,
|
|
494
|
+
Row: ListItem,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
export default List;
|
|
498
|
+
|
|
499
|
+
export {
|
|
500
|
+
type InternalListItemProps,
|
|
501
|
+
ListContent,
|
|
502
|
+
ListGroup,
|
|
503
|
+
ListItem,
|
|
504
|
+
ListOption,
|
|
505
|
+
ListProvider,
|
|
506
|
+
ListSearchInput,
|
|
507
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createFastContext } from '../../fast-context';
|
|
2
|
+
import { registerPortalContext } from '../Portal';
|
|
3
|
+
import type { DefaultListItemT, ListContextValue } from './types';
|
|
4
|
+
|
|
5
|
+
const listContextDefaultValue: ListContextValue<DefaultListItemT> = {
|
|
6
|
+
value: null,
|
|
7
|
+
multiple: false,
|
|
8
|
+
onAdd: () => {},
|
|
9
|
+
onRemove: () => {},
|
|
10
|
+
disabled: false,
|
|
11
|
+
error: false,
|
|
12
|
+
items: [],
|
|
13
|
+
searchQuery: '',
|
|
14
|
+
setSearchQuery: () => {},
|
|
15
|
+
filteredItems: [],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
useStoreRef: useListStoreRef,
|
|
20
|
+
Provider: ListContextProvider,
|
|
21
|
+
useContext: useListContext,
|
|
22
|
+
useContextValue: useListContextValue,
|
|
23
|
+
Context: ListContext,
|
|
24
|
+
} = createFastContext<ListContextValue<DefaultListItemT>>(listContextDefaultValue, true);
|
|
25
|
+
|
|
26
|
+
export { ListContext, ListContextProvider, useListContext, useListContextValue, useListStoreRef };
|
|
27
|
+
|
|
28
|
+
registerPortalContext([ListContext]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getRegisteredComponentWithFallback } from '../../core';
|
|
2
|
+
import ListDefault from './List';
|
|
3
|
+
export type { InternalListItemProps as ListItemProps } from './List';
|
|
4
|
+
|
|
5
|
+
export const List = getRegisteredComponentWithFallback('List', ListDefault);
|
|
6
|
+
|
|
7
|
+
export * from './context';
|
|
8
|
+
export type * from './types';
|
|
9
|
+
export * from './utils';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { ComponentProps, ComponentType, ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type GestureResponderEvent,
|
|
4
|
+
ScrollView,
|
|
5
|
+
type TextInputProps,
|
|
6
|
+
type ViewProps,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
|
|
9
|
+
import type { InternalListItemProps as ListItemProps } from './List';
|
|
10
|
+
|
|
11
|
+
export type DefaultListItemT = {
|
|
12
|
+
id: string | number;
|
|
13
|
+
label?: string;
|
|
14
|
+
selectable?: boolean;
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ListValue<
|
|
19
|
+
Option extends DefaultListItemT,
|
|
20
|
+
Multiple extends boolean,
|
|
21
|
+
> = Multiple extends true ? Option['id'][] : Option['id'] | null;
|
|
22
|
+
|
|
23
|
+
export type ListContextValue<Option extends DefaultListItemT = DefaultListItemT> = {
|
|
24
|
+
value: Option['id'] | Option['id'][] | null;
|
|
25
|
+
multiple: boolean;
|
|
26
|
+
onAdd: (item: Option) => void;
|
|
27
|
+
onRemove: (item: Option) => void;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
error: boolean;
|
|
30
|
+
items: Option[];
|
|
31
|
+
searchQuery: string;
|
|
32
|
+
setSearchQuery: (query: string) => void;
|
|
33
|
+
filteredItems: Option[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type ListPropsBase<Option extends DefaultListItemT = DefaultListItemT> = {
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
items: Option[];
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
error?: boolean;
|
|
41
|
+
searchKey?: string;
|
|
42
|
+
onSearchChange?: (query: string) => void;
|
|
43
|
+
hideSelected?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type SingleListProps<Option extends DefaultListItemT = DefaultListItemT> = {
|
|
47
|
+
multiple?: false | undefined;
|
|
48
|
+
value?: ListValue<Option, false>;
|
|
49
|
+
defaultValue?: ListValue<Option, false>;
|
|
50
|
+
onChange?: (
|
|
51
|
+
value: ListValue<Option, false>,
|
|
52
|
+
item: Option,
|
|
53
|
+
event?: GestureResponderEvent,
|
|
54
|
+
) => void;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type MultipleListProps<Option extends DefaultListItemT = DefaultListItemT> = {
|
|
58
|
+
multiple: true;
|
|
59
|
+
value?: ListValue<Option, true>;
|
|
60
|
+
defaultValue?: ListValue<Option, true>;
|
|
61
|
+
onChange?: (
|
|
62
|
+
value: ListValue<Option, true>,
|
|
63
|
+
item: Option,
|
|
64
|
+
event?: GestureResponderEvent,
|
|
65
|
+
) => void;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type ListProps<Option extends DefaultListItemT = DefaultListItemT> = ListPropsBase<Option> &
|
|
69
|
+
(SingleListProps<Option> | MultipleListProps<Option>);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Arguments passed to the `processProps` callback on `<List.Content>`.
|
|
73
|
+
*
|
|
74
|
+
* Use these to adapt `List` context state into the prop contract required by
|
|
75
|
+
* a custom container (for example `FlatList`'s `data`/`renderItem` or
|
|
76
|
+
* `SectionList`'s `sections`/`renderItem`).
|
|
77
|
+
*/
|
|
78
|
+
export type ListContentProcessPropsArgs<
|
|
79
|
+
Option extends DefaultListItemT = DefaultListItemT,
|
|
80
|
+
ContainerProps extends Record<string, any> = Record<string, any>,
|
|
81
|
+
> = {
|
|
82
|
+
/** The user-provided props on `<List.Content>` minus `children`/`ref`. */
|
|
83
|
+
props: ContainerProps;
|
|
84
|
+
/** The current `filteredItems` from the List context. */
|
|
85
|
+
items: Option[];
|
|
86
|
+
/** True when there are no items to render after filtering. */
|
|
87
|
+
isEmpty: boolean;
|
|
88
|
+
/** Resolved empty state node (caller-provided or the default). */
|
|
89
|
+
emptyState: ReactNode;
|
|
90
|
+
/** Returns whether the given item is currently selected. */
|
|
91
|
+
isSelected: (item: Option) => boolean;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type ListContentPropsShared<C extends ComponentType<any> = typeof ScrollView> = Partial<
|
|
95
|
+
Omit<ComponentProps<C>, 'children' | 'ref'>
|
|
96
|
+
> & {
|
|
97
|
+
/**
|
|
98
|
+
* The component used to render the scrollable container. Defaults to ScrollView.
|
|
99
|
+
* The rest of the props on `<List.Content>` are inferred from this component's props.
|
|
100
|
+
*
|
|
101
|
+
* Required props (e.g. `FlatList`'s `data`/`renderItem`) can be supplied
|
|
102
|
+
* either directly or via `processProps`.
|
|
103
|
+
*/
|
|
104
|
+
ContainerComponent?: C;
|
|
105
|
+
emptyState?: ReactNode;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type ListContentProps<
|
|
109
|
+
Option extends DefaultListItemT = DefaultListItemT,
|
|
110
|
+
C extends ComponentType<any> = typeof ScrollView,
|
|
111
|
+
> = ListContentPropsShared<C> &
|
|
112
|
+
(
|
|
113
|
+
| {
|
|
114
|
+
/**
|
|
115
|
+
* Optional when `processProps` renders rows/items itself (e.g. chunked grid rows).
|
|
116
|
+
*/
|
|
117
|
+
processProps: (
|
|
118
|
+
args: ListContentProcessPropsArgs<
|
|
119
|
+
Option,
|
|
120
|
+
Omit<ComponentProps<C>, 'children' | 'ref'>
|
|
121
|
+
>,
|
|
122
|
+
) => ComponentProps<C>;
|
|
123
|
+
children?: (item: Option, isSelected: boolean) => ReactNode;
|
|
124
|
+
}
|
|
125
|
+
| {
|
|
126
|
+
processProps?: undefined;
|
|
127
|
+
children: (item: Option, isSelected: boolean) => ReactNode;
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
export type ListGroupProps = ViewProps & {
|
|
132
|
+
children: ReactNode;
|
|
133
|
+
label?: string;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type ListItemOptionProps<Option extends DefaultListItemT = DefaultListItemT> = Omit<
|
|
137
|
+
ListItemProps,
|
|
138
|
+
'children' | 'selected' | 'disabled' | 'onPress'
|
|
139
|
+
> & {
|
|
140
|
+
value: Option['id'];
|
|
141
|
+
children: ReactNode;
|
|
142
|
+
onPress?: (item: Option, event: GestureResponderEvent) => void;
|
|
143
|
+
disabled?: boolean;
|
|
144
|
+
shouldToggleOnPress?: boolean;
|
|
145
|
+
accessibilityRole?: any;
|
|
146
|
+
accessibilityState?: Record<string, unknown>;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export type ListSearchInputProps = Omit<TextInputProps, 'value' | 'onChangeText'>;
|