rn-inputforge 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +44 -0
  2. package/lib/commonjs/components/FieldFlowDropdown/FieldFlowDropdown.js +288 -0
  3. package/lib/commonjs/components/FieldFlowDropdown/FieldFlowDropdown.js.map +1 -0
  4. package/lib/commonjs/components/FieldFlowDropdown/index.js +13 -0
  5. package/lib/commonjs/components/FieldFlowDropdown/index.js.map +1 -0
  6. package/lib/commonjs/components/FieldFlowDropdown/styles.js +199 -0
  7. package/lib/commonjs/components/FieldFlowDropdown/styles.js.map +1 -0
  8. package/lib/commonjs/components/FieldFlowDropdown/types.js +6 -0
  9. package/lib/commonjs/components/FieldFlowDropdown/types.js.map +1 -0
  10. package/lib/commonjs/icons/Icon.js +35 -0
  11. package/lib/commonjs/icons/Icon.js.map +1 -1
  12. package/lib/commonjs/index.js +7 -0
  13. package/lib/commonjs/index.js.map +1 -1
  14. package/lib/module/components/FieldFlowDropdown/FieldFlowDropdown.js +283 -0
  15. package/lib/module/components/FieldFlowDropdown/FieldFlowDropdown.js.map +1 -0
  16. package/lib/module/components/FieldFlowDropdown/index.js +4 -0
  17. package/lib/module/components/FieldFlowDropdown/index.js.map +1 -0
  18. package/lib/module/components/FieldFlowDropdown/styles.js +195 -0
  19. package/lib/module/components/FieldFlowDropdown/styles.js.map +1 -0
  20. package/lib/module/components/FieldFlowDropdown/types.js +4 -0
  21. package/lib/module/components/FieldFlowDropdown/types.js.map +1 -0
  22. package/lib/module/icons/Icon.js +35 -0
  23. package/lib/module/icons/Icon.js.map +1 -1
  24. package/lib/module/index.js +1 -0
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/typescript/components/FieldFlowDropdown/FieldFlowDropdown.d.ts +4 -0
  27. package/lib/typescript/components/FieldFlowDropdown/FieldFlowDropdown.d.ts.map +1 -0
  28. package/lib/typescript/components/FieldFlowDropdown/index.d.ts +3 -0
  29. package/lib/typescript/components/FieldFlowDropdown/index.d.ts.map +1 -0
  30. package/lib/typescript/components/FieldFlowDropdown/styles.d.ts +188 -0
  31. package/lib/typescript/components/FieldFlowDropdown/styles.d.ts.map +1 -0
  32. package/lib/typescript/components/FieldFlowDropdown/types.d.ts +51 -0
  33. package/lib/typescript/components/FieldFlowDropdown/types.d.ts.map +1 -0
  34. package/lib/typescript/icons/Icon.d.ts +1 -1
  35. package/lib/typescript/icons/Icon.d.ts.map +1 -1
  36. package/lib/typescript/index.d.ts +3 -1
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/types/index.d.ts +7 -0
  39. package/lib/typescript/types/index.d.ts.map +1 -1
  40. package/package.json +2 -2
  41. package/src/components/FieldFlowDropdown/FieldFlowDropdown.tsx +353 -0
  42. package/src/components/FieldFlowDropdown/index.ts +7 -0
  43. package/src/components/FieldFlowDropdown/styles.ts +193 -0
  44. package/src/components/FieldFlowDropdown/types.ts +57 -0
  45. package/src/icons/Icon.tsx +26 -0
  46. package/src/index.ts +9 -0
  47. package/src/types/index.ts +8 -0
@@ -0,0 +1,353 @@
1
+ import React, { useMemo, useRef, useState } from 'react';
2
+ import {
3
+ FlatList,
4
+ Modal,
5
+ Pressable,
6
+ Text,
7
+ TextInput,
8
+ View,
9
+ ViewStyle,
10
+ Dimensions,
11
+ } from 'react-native';
12
+ import { useFieldFlowTheme, mergeDeep } from '../../theme/ThemeProvider';
13
+ import { defaultTheme } from '../../theme/defaultTheme';
14
+ import { useFieldState } from '../../hooks/useFieldState';
15
+ import { FieldFlowIcon } from '../../icons/Icon';
16
+ import { createDropdownStyles } from './styles';
17
+ import type { FieldFlowDropdownProps } from './types';
18
+ import type { FieldFlowOption } from '../../types';
19
+
20
+ export function FieldFlowDropdown(props: FieldFlowDropdownProps) {
21
+ const {
22
+ options,
23
+ label,
24
+ placeholder = 'Select...',
25
+ helperText,
26
+ errorText,
27
+ hasError,
28
+ disabled,
29
+ required,
30
+ title,
31
+ presentation = 'bottomSheet',
32
+ searchable,
33
+ searchThreshold = 6,
34
+ searchPlaceholder = 'Search...',
35
+ emptyText = 'No options found',
36
+ maxModalHeight,
37
+ clearable,
38
+ containerStyle,
39
+ triggerStyle,
40
+ labelStyle,
41
+ theme: themeOverride,
42
+ testID,
43
+ multiple,
44
+ } = props;
45
+
46
+ const contextTheme = useFieldFlowTheme();
47
+ const theme = useMemo(
48
+ () =>
49
+ themeOverride
50
+ ? mergeDeep(contextTheme ?? defaultTheme, themeOverride)
51
+ : (contextTheme ?? defaultTheme),
52
+ [contextTheme, themeOverride],
53
+ );
54
+
55
+ const [isOpen, setIsOpen] = useState(false);
56
+ const [query, setQuery] = useState('');
57
+ const [draftValues, setDraftValues] = useState<string[]>(
58
+ multiple ? (props.values ?? []) : [],
59
+ );
60
+ const [anchorLayout, setAnchorLayout] = useState<{
61
+ x: number;
62
+ y: number;
63
+ width: number;
64
+ height: number;
65
+ } | null>(null);
66
+ const triggerRef = useRef<View>(null);
67
+
68
+ const resolvedError = errorText;
69
+ const state = useFieldState({
70
+ isFocused: isOpen,
71
+ hasError: Boolean(hasError || resolvedError),
72
+ disabled,
73
+ });
74
+ const styles = useMemo(
75
+ () => createDropdownStyles(theme, state),
76
+ [theme, state],
77
+ );
78
+
79
+ const selectedValues: string[] = multiple
80
+ ? (props.values ?? [])
81
+ : props.value
82
+ ? [props.value]
83
+ : [];
84
+
85
+ const shouldShowSearch = searchable ?? options.length > searchThreshold;
86
+ const filteredOptions = useMemo(() => {
87
+ if (!query.trim()) return options;
88
+ const q = query.trim().toLowerCase();
89
+ return options.filter(o => o.label.toLowerCase().includes(q));
90
+ }, [options, query]);
91
+
92
+ const openDropdown = () => {
93
+ if (disabled) return;
94
+ if (multiple) setDraftValues(props.values ?? []);
95
+ setQuery('');
96
+ if (presentation === 'inline') {
97
+ triggerRef.current?.measureInWindow((x, y, width, height) => {
98
+ setAnchorLayout({ x, y, width, height });
99
+ setIsOpen(true);
100
+ });
101
+ } else {
102
+ setIsOpen(true);
103
+ }
104
+ };
105
+
106
+ const closeDropdown = () => setIsOpen(false);
107
+
108
+ const commitSingle = (value: string) => {
109
+ if (!multiple) {
110
+ props.onChange(value);
111
+ if (props.closeOnSelect ?? true) closeDropdown();
112
+ }
113
+ };
114
+
115
+ const toggleMultiDraft = (value: string) => {
116
+ setDraftValues(prev =>
117
+ prev.includes(value) ? prev.filter(v => v !== value) : [...prev, value],
118
+ );
119
+ if (multiple && (props.closeOnSelect ?? false)) {
120
+ const next = draftValues.includes(value)
121
+ ? draftValues.filter(v => v !== value)
122
+ : [...draftValues, value];
123
+ props.onChangeMulti(next);
124
+ }
125
+ };
126
+
127
+ const confirmMulti = () => {
128
+ if (multiple) {
129
+ props.onChangeMulti(draftValues);
130
+ }
131
+ closeDropdown();
132
+ };
133
+
134
+ const clearSelection = () => {
135
+ if (multiple) {
136
+ props.onChangeMulti([]);
137
+ setDraftValues([]);
138
+ } else {
139
+ props.onChange(null);
140
+ }
141
+ };
142
+
143
+ const triggerLabel = useMemo(() => {
144
+ if (selectedValues.length === 0) return null;
145
+ const labels = options
146
+ .filter(o => selectedValues.includes(o.value))
147
+ .map(o => o.label);
148
+ if (!multiple) return labels[0] ?? null;
149
+ const maxShown = props.maxLabelsShown ?? 2;
150
+ if (labels.length <= maxShown) return labels.join(', ');
151
+ return `${labels.slice(0, maxShown).join(', ')} +${labels.length - maxShown} more`;
152
+ }, [selectedValues, options, multiple, props]);
153
+
154
+ const renderOption = ({ item }: { item: FieldFlowOption }) => {
155
+ const isSelected = multiple
156
+ ? draftValues.includes(item.value)
157
+ : props.value === item.value;
158
+ const handlePress = () => {
159
+ if (item.disabled) return;
160
+ multiple ? toggleMultiDraft(item.value) : commitSingle(item.value);
161
+ };
162
+ return (
163
+ <Pressable
164
+ onPress={handlePress}
165
+ disabled={item.disabled}
166
+ style={styles.optionRow}
167
+ >
168
+ <View style={{ flex: 1 }}>
169
+ <Text
170
+ style={[
171
+ styles.optionLabel,
172
+ isSelected && styles.optionLabelSelected,
173
+ item.disabled && styles.optionLabelDisabled,
174
+ ]}
175
+ >
176
+ {item.label}
177
+ </Text>
178
+ {item.description ? (
179
+ <Text style={styles.optionDescription}>{item.description}</Text>
180
+ ) : null}
181
+ </View>
182
+ {multiple ? (
183
+ <View style={[styles.checkbox, isSelected && styles.checkboxChecked]}>
184
+ {isSelected ? (
185
+ <FieldFlowIcon name="check" size={12} color="#FFFFFF" />
186
+ ) : null}
187
+ </View>
188
+ ) : isSelected ? (
189
+ <FieldFlowIcon
190
+ name="check"
191
+ size={16}
192
+ color={theme.colors.borderFocused}
193
+ />
194
+ ) : null}
195
+ </Pressable>
196
+ );
197
+ };
198
+
199
+ const listContent = (
200
+ <>
201
+ {shouldShowSearch ? (
202
+ <View style={styles.searchWrapper}>
203
+ <TextInput
204
+ value={query}
205
+ onChangeText={setQuery}
206
+ placeholder={searchPlaceholder}
207
+ placeholderTextColor={theme.colors.placeholder}
208
+ style={styles.searchInput}
209
+ />
210
+ </View>
211
+ ) : null}
212
+ <FlatList
213
+ data={filteredOptions}
214
+ keyExtractor={item => item.value}
215
+ renderItem={renderOption}
216
+ keyboardShouldPersistTaps="handled"
217
+ style={{ maxHeight: maxModalHeight ?? 360 }}
218
+ ListEmptyComponent={<Text style={styles.emptyText}>{emptyText}</Text>}
219
+ />
220
+ {multiple ? (
221
+ <Pressable style={styles.confirmButton} onPress={confirmMulti}>
222
+ <Text style={styles.confirmButtonText}>
223
+ {props.confirmLabel ?? 'Done'}
224
+ </Text>
225
+ </Pressable>
226
+ ) : null}
227
+ </>
228
+ );
229
+
230
+ const header = (
231
+ <View style={styles.modalHeader}>
232
+ <Text style={styles.modalTitle}>{title ?? label ?? 'Select'}</Text>
233
+ {clearable && selectedValues.length > 0 ? (
234
+ <Pressable onPress={clearSelection} hitSlop={8}>
235
+ <Text style={styles.modalClearButton}>Clear</Text>
236
+ </Pressable>
237
+ ) : null}
238
+ </View>
239
+ );
240
+
241
+ return (
242
+ <View style={[styles.wrapper, containerStyle]} testID={testID}>
243
+ {label ? (
244
+ <Text style={[styles.label, labelStyle]}>
245
+ {label}
246
+ {required ? ' *' : ''}
247
+ </Text>
248
+ ) : null}
249
+
250
+ <View ref={triggerRef} collapsable={false}>
251
+ <Pressable
252
+ onPress={openDropdown}
253
+ disabled={disabled}
254
+ style={[styles.trigger, triggerStyle]}
255
+ >
256
+ {triggerLabel ? (
257
+ <Text style={styles.triggerText} numberOfLines={1}>
258
+ {triggerLabel}
259
+ </Text>
260
+ ) : (
261
+ <Text style={styles.placeholderText} numberOfLines={1}>
262
+ {placeholder}
263
+ </Text>
264
+ )}
265
+ <FieldFlowIcon
266
+ name="chevronDown"
267
+ size={16}
268
+ color={theme.colors.icon}
269
+ />
270
+ </Pressable>
271
+ </View>
272
+
273
+ {(helperText || resolvedError) && (
274
+ <View style={styles.footerRow}>
275
+ <View style={{ flex: 1 }}>
276
+ {resolvedError ? (
277
+ <Text style={styles.errorText}>{resolvedError}</Text>
278
+ ) : helperText ? (
279
+ <Text style={styles.helperText}>{helperText}</Text>
280
+ ) : null}
281
+ </View>
282
+ </View>
283
+ )}
284
+
285
+ {presentation === 'inline' ? (
286
+ isOpen && anchorLayout ? (
287
+ <Modal
288
+ transparent
289
+ visible
290
+ animationType="fade"
291
+ onRequestClose={closeDropdown}
292
+ >
293
+ <Pressable style={{ flex: 1 }} onPress={closeDropdown}>
294
+ <View
295
+ style={[styles.inlineDropdown, inlinePosition(anchorLayout)]}
296
+ >
297
+ {listContent}
298
+ </View>
299
+ </Pressable>
300
+ </Modal>
301
+ ) : null
302
+ ) : (
303
+ <Modal
304
+ transparent
305
+ visible={isOpen}
306
+ animationType={presentation === 'popup' ? 'fade' : 'slide'}
307
+ onRequestClose={closeDropdown}
308
+ >
309
+ <Pressable
310
+ style={
311
+ presentation === 'popup'
312
+ ? styles.popupWrapper
313
+ : styles.bottomSheetWrapper
314
+ }
315
+ onPress={closeDropdown}
316
+ >
317
+ <Pressable
318
+ style={
319
+ presentation === 'popup' ? styles.popupCard : styles.sheetCard
320
+ }
321
+ onPress={e => e.stopPropagation()}
322
+ >
323
+ {presentation === 'bottomSheet' ? (
324
+ <View style={styles.dragHandle} />
325
+ ) : null}
326
+ {header}
327
+ {listContent}
328
+ </Pressable>
329
+ </Pressable>
330
+ </Modal>
331
+ )}
332
+ </View>
333
+ );
334
+ }
335
+
336
+ function inlinePosition(anchor: {
337
+ x: number;
338
+ y: number;
339
+ width: number;
340
+ height: number;
341
+ }): ViewStyle {
342
+ const screenHeight = Dimensions.get('window').height;
343
+ const spaceBelow = screenHeight - (anchor.y + anchor.height);
344
+ const openUpward = spaceBelow < 260;
345
+ return {
346
+ left: anchor.x,
347
+ width: anchor.width,
348
+ maxHeight: 300,
349
+ ...(openUpward
350
+ ? { bottom: screenHeight - anchor.y + 4 }
351
+ : { top: anchor.y + anchor.height + 4 }),
352
+ };
353
+ }
@@ -0,0 +1,7 @@
1
+ export { FieldFlowDropdown } from './FieldFlowDropdown';
2
+ export type {
3
+ FieldFlowDropdownProps,
4
+ FieldFlowDropdownSingleProps,
5
+ FieldFlowDropdownMultiProps,
6
+ FieldFlowDropdownPresentation,
7
+ } from './types';
@@ -0,0 +1,193 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import type { FieldFlowState, FieldFlowTheme } from '../../types';
3
+ import { borderColorFor, labelColorFor } from '../FieldFlowInput/styles';
4
+
5
+ export function createDropdownStyles(theme: FieldFlowTheme, state: FieldFlowState) {
6
+ return StyleSheet.create({
7
+ wrapper: {
8
+ marginBottom: theme.spacing.gap * 2,
9
+ },
10
+ label: {
11
+ fontSize: theme.typography.labelFontSize,
12
+ marginBottom: theme.spacing.gap / 1.5,
13
+ color: labelColorFor(theme, state),
14
+ fontWeight: '500',
15
+ },
16
+ trigger: {
17
+ flexDirection: 'row',
18
+ alignItems: 'center',
19
+ backgroundColor: state === 'disabled' ? theme.colors.backgroundDisabled : theme.colors.background,
20
+ borderRadius: theme.spacing.borderRadius,
21
+ borderWidth: state === 'focused' ? theme.spacing.borderWidthFocused : theme.spacing.borderWidth,
22
+ borderColor: borderColorFor(theme, state),
23
+ paddingHorizontal: theme.spacing.paddingHorizontal,
24
+ minHeight: theme.spacing.paddingVertical * 2 + theme.typography.fontSize + 6,
25
+ },
26
+ triggerText: {
27
+ flex: 1,
28
+ fontSize: theme.typography.fontSize,
29
+ color: theme.colors.text,
30
+ },
31
+ placeholderText: {
32
+ flex: 1,
33
+ fontSize: theme.typography.fontSize,
34
+ color: theme.colors.placeholder,
35
+ },
36
+ footerRow: {
37
+ flexDirection: 'row',
38
+ justifyContent: 'space-between',
39
+ marginTop: theme.spacing.gap / 1.5,
40
+ },
41
+ errorText: {
42
+ fontSize: theme.typography.helperFontSize,
43
+ color: theme.colors.error,
44
+ flexShrink: 1,
45
+ },
46
+ helperText: {
47
+ fontSize: theme.typography.helperFontSize,
48
+ color: theme.colors.label,
49
+ flexShrink: 1,
50
+ },
51
+
52
+ // Modal
53
+ backdrop: {
54
+ flex: 1,
55
+ backgroundColor: 'rgba(0,0,0,0.4)',
56
+ },
57
+ bottomSheetWrapper: {
58
+ flex: 1,
59
+ justifyContent: 'flex-end',
60
+ },
61
+ popupWrapper: {
62
+ flex: 1,
63
+ justifyContent: 'center',
64
+ alignItems: 'center',
65
+ paddingHorizontal: 24,
66
+ },
67
+ sheetCard: {
68
+ backgroundColor: theme.colors.background,
69
+ borderTopLeftRadius: theme.spacing.borderRadius * 1.6,
70
+ borderTopRightRadius: theme.spacing.borderRadius * 1.6,
71
+ paddingTop: theme.spacing.gap,
72
+ paddingBottom: theme.spacing.paddingVertical,
73
+ },
74
+ popupCard: {
75
+ width: '100%',
76
+ backgroundColor: theme.colors.background,
77
+ borderRadius: theme.spacing.borderRadius * 1.4,
78
+ paddingTop: theme.spacing.gap,
79
+ paddingBottom: theme.spacing.paddingVertical,
80
+ maxHeight: '80%',
81
+ },
82
+ dragHandle: {
83
+ alignSelf: 'center',
84
+ width: 36,
85
+ height: 4,
86
+ borderRadius: 2,
87
+ backgroundColor: theme.colors.border,
88
+ marginBottom: theme.spacing.gap,
89
+ },
90
+ modalHeader: {
91
+ flexDirection: 'row',
92
+ alignItems: 'center',
93
+ justifyContent: 'space-between',
94
+ paddingHorizontal: theme.spacing.paddingHorizontal,
95
+ paddingBottom: theme.spacing.gap,
96
+ borderBottomWidth: theme.spacing.borderWidth,
97
+ borderBottomColor: theme.colors.border,
98
+ marginBottom: theme.spacing.gap,
99
+ },
100
+ modalTitle: {
101
+ fontSize: theme.typography.fontSize + 1,
102
+ fontWeight: '600',
103
+ color: theme.colors.text,
104
+ },
105
+ modalClearButton: {
106
+ fontSize: theme.typography.helperFontSize,
107
+ color: theme.colors.borderFocused,
108
+ },
109
+ searchWrapper: {
110
+ paddingHorizontal: theme.spacing.paddingHorizontal,
111
+ marginBottom: theme.spacing.gap,
112
+ },
113
+ searchInput: {
114
+ backgroundColor: theme.colors.backgroundDisabled,
115
+ borderRadius: theme.spacing.borderRadius,
116
+ paddingHorizontal: theme.spacing.paddingHorizontal,
117
+ paddingVertical: theme.spacing.paddingVertical * 0.6,
118
+ fontSize: theme.typography.fontSize,
119
+ color: theme.colors.text,
120
+ },
121
+ optionRow: {
122
+ flexDirection: 'row',
123
+ alignItems: 'center',
124
+ justifyContent: 'space-between',
125
+ paddingHorizontal: theme.spacing.paddingHorizontal,
126
+ paddingVertical: theme.spacing.paddingVertical * 0.9,
127
+ },
128
+ optionLabel: {
129
+ fontSize: theme.typography.fontSize,
130
+ color: theme.colors.text,
131
+ },
132
+ optionLabelSelected: {
133
+ color: theme.colors.borderFocused,
134
+ fontWeight: '600',
135
+ },
136
+ optionLabelDisabled: {
137
+ color: theme.colors.placeholder,
138
+ },
139
+ optionDescription: {
140
+ fontSize: theme.typography.helperFontSize,
141
+ color: theme.colors.placeholder,
142
+ marginTop: 2,
143
+ },
144
+ checkbox: {
145
+ width: 20,
146
+ height: 20,
147
+ borderRadius: 5,
148
+ borderWidth: theme.spacing.borderWidth,
149
+ borderColor: theme.colors.border,
150
+ alignItems: 'center',
151
+ justifyContent: 'center',
152
+ },
153
+ checkboxChecked: {
154
+ backgroundColor: theme.colors.borderFocused,
155
+ borderColor: theme.colors.borderFocused,
156
+ },
157
+ emptyText: {
158
+ textAlign: 'center',
159
+ paddingVertical: theme.spacing.paddingVertical * 2,
160
+ color: theme.colors.placeholder,
161
+ fontSize: theme.typography.fontSize,
162
+ },
163
+ confirmButton: {
164
+ marginHorizontal: theme.spacing.paddingHorizontal,
165
+ marginTop: theme.spacing.gap,
166
+ backgroundColor: theme.colors.borderFocused,
167
+ borderRadius: theme.spacing.borderRadius,
168
+ paddingVertical: theme.spacing.paddingVertical * 0.8,
169
+ alignItems: 'center',
170
+ },
171
+ confirmButtonText: {
172
+ color: '#FFFFFF',
173
+ fontSize: theme.typography.fontSize,
174
+ fontWeight: '600',
175
+ },
176
+
177
+ // Inline presentation
178
+ inlineDropdown: {
179
+ position: 'absolute',
180
+ backgroundColor: theme.colors.background,
181
+ borderRadius: theme.spacing.borderRadius,
182
+ borderWidth: theme.spacing.borderWidth,
183
+ borderColor: theme.colors.border,
184
+ shadowColor: '#000',
185
+ shadowOffset: { width: 0, height: 4 },
186
+ shadowOpacity: 0.15,
187
+ shadowRadius: 8,
188
+ elevation: 6,
189
+ overflow: 'hidden',
190
+ zIndex: 50,
191
+ },
192
+ });
193
+ }
@@ -0,0 +1,57 @@
1
+ import type { ViewStyle, TextStyle } from 'react-native';
2
+ import type { DeepPartial, FieldFlowOption, FieldFlowTheme } from '../../types';
3
+
4
+ export type FieldFlowDropdownPresentation = 'bottomSheet' | 'popup' | 'inline';
5
+
6
+ interface BaseDropdownProps {
7
+ options: FieldFlowOption[];
8
+ label?: string;
9
+ placeholder?: string;
10
+ helperText?: string;
11
+ errorText?: string;
12
+ hasError?: boolean;
13
+ disabled?: boolean;
14
+ required?: boolean;
15
+
16
+ /** Modal title shown at the top of the bottom sheet / popup. Defaults to `label` if omitted. */
17
+ title?: string;
18
+ /** 'bottomSheet' slides up from the bottom, 'popup' fades in centered, 'inline' drops down under the field. Default: 'bottomSheet'. */
19
+ presentation?: FieldFlowDropdownPresentation;
20
+ /** Shows a search box above the option list once there are more than this many options. Default: 6. Set to false to disable search entirely. */
21
+ searchable?: boolean;
22
+ searchThreshold?: number;
23
+ searchPlaceholder?: string;
24
+ emptyText?: string;
25
+
26
+ maxModalHeight?: number;
27
+ clearable?: boolean;
28
+
29
+ containerStyle?: ViewStyle;
30
+ triggerStyle?: ViewStyle;
31
+ labelStyle?: TextStyle;
32
+ helperStyle?: TextStyle;
33
+ theme?: DeepPartial<FieldFlowTheme>;
34
+ testID?: string;
35
+ }
36
+
37
+ export interface FieldFlowDropdownSingleProps extends BaseDropdownProps {
38
+ multiple?: false;
39
+ value?: string | null;
40
+ onChange: (value: string | null) => void;
41
+ /** Close the sheet as soon as an option is tapped. Default: true. */
42
+ closeOnSelect?: boolean;
43
+ }
44
+
45
+ export interface FieldFlowDropdownMultiProps extends BaseDropdownProps {
46
+ multiple: true;
47
+ values?: string[];
48
+ onChangeMulti: (values: string[]) => void;
49
+ /** Close the sheet as soon as an option is tapped. Default: false (user taps "Done"). */
50
+ closeOnSelect?: boolean;
51
+ /** Label for the confirm button shown in multi-select mode. Default: "Done". */
52
+ confirmLabel?: string;
53
+ /** Max number of individual labels to show on the trigger before collapsing to "N selected". Default: 2. */
54
+ maxLabelsShown?: number;
55
+ }
56
+
57
+ export type FieldFlowDropdownProps = FieldFlowDropdownSingleProps | FieldFlowDropdownMultiProps;
@@ -14,6 +14,7 @@ export type FieldFlowIconName =
14
14
  | 'paperclip'
15
15
  | 'mic'
16
16
  | 'mapPin'
17
+ | 'chevronDown'
17
18
  | 'spinner';
18
19
 
19
20
  export interface FieldFlowIconProps {
@@ -296,6 +297,31 @@ export function FieldFlowIcon({ name, size = 18, color = '#6B7280', style }: Fie
296
297
  </View>
297
298
  </View>
298
299
  );
300
+ case 'chevronDown':
301
+ return (
302
+ <View style={[box, style]}>
303
+ <View
304
+ style={{
305
+ width: size * 0.55,
306
+ height: stroke,
307
+ backgroundColor: color,
308
+ borderRadius: stroke,
309
+ transform: [{ rotate: '45deg' }, { translateX: -size * 0.13 }, { translateY: -size * 0.02 }],
310
+ position: 'absolute',
311
+ }}
312
+ />
313
+ <View
314
+ style={{
315
+ width: size * 0.55,
316
+ height: stroke,
317
+ backgroundColor: color,
318
+ borderRadius: stroke,
319
+ transform: [{ rotate: '-45deg' }, { translateX: size * 0.13 }, { translateY: -size * 0.02 }],
320
+ position: 'absolute',
321
+ }}
322
+ />
323
+ </View>
324
+ );
299
325
  default:
300
326
  return <View style={[box, style]} />;
301
327
  }
package/src/index.ts CHANGED
@@ -11,6 +11,14 @@ export type { ChatInputProps, ChatAttachmentAction, CustomChatAction } from './c
11
11
  export { KeyboardAwareFormWrapper } from './components/KeyboardAwareFormWrapper';
12
12
  export type { KeyboardAwareFormWrapperProps } from './components/KeyboardAwareFormWrapper';
13
13
 
14
+ export { FieldFlowDropdown } from './components/FieldFlowDropdown';
15
+ export type {
16
+ FieldFlowDropdownProps,
17
+ FieldFlowDropdownSingleProps,
18
+ FieldFlowDropdownMultiProps,
19
+ FieldFlowDropdownPresentation,
20
+ } from './components/FieldFlowDropdown';
21
+
14
22
  // Theming
15
23
  export { FieldFlowThemeProvider, useFieldFlowTheme, defaultTheme, darkTheme } from './theme';
16
24
  export type { FieldFlowThemeProviderProps } from './theme';
@@ -52,6 +60,7 @@ export type {
52
60
  FieldFlowFieldType,
53
61
  FieldFlowState,
54
62
  FieldFlowRules,
63
+ FieldFlowOption,
55
64
  ValidationResult,
56
65
  Validator,
57
66
  FieldFlowColors,
@@ -1,6 +1,14 @@
1
1
  import type { KeyboardTypeOptions, TextInputProps, TextStyle } from 'react-native';
2
2
 
3
3
  /** Every field "flavor" the core input understands out of the box. */
4
+ export interface FieldFlowOption {
5
+ label: string;
6
+ value: string;
7
+ disabled?: boolean;
8
+ /** Optional secondary line of text shown under the label in the option list. */
9
+ description?: string;
10
+ }
11
+
4
12
  export type FieldFlowFieldType =
5
13
  | 'text'
6
14
  | 'email'