rn-inputforge 0.3.1 → 0.4.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 (196) hide show
  1. package/README.md +224 -19
  2. package/lib/commonjs/components/FieldFlowCheckbox/FieldFlowCheckbox.js +131 -0
  3. package/lib/commonjs/components/FieldFlowCheckbox/FieldFlowCheckbox.js.map +1 -0
  4. package/lib/commonjs/components/FieldFlowCheckbox/index.js +13 -0
  5. package/lib/commonjs/components/FieldFlowCheckbox/index.js.map +1 -0
  6. package/lib/commonjs/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.js +536 -0
  7. package/lib/commonjs/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.js.map +1 -0
  8. package/lib/commonjs/components/FieldFlowDateTimePicker/index.js +13 -0
  9. package/lib/commonjs/components/FieldFlowDateTimePicker/index.js.map +1 -0
  10. package/lib/commonjs/components/FieldFlowDateTimePicker/types.js +6 -0
  11. package/lib/commonjs/components/FieldFlowDateTimePicker/types.js.map +1 -0
  12. package/lib/commonjs/components/FieldFlowDropdown/FieldFlowDropdown.js +24 -9
  13. package/lib/commonjs/components/FieldFlowDropdown/FieldFlowDropdown.js.map +1 -1
  14. package/lib/commonjs/components/FieldFlowInput/FieldFlowInput.js +55 -24
  15. package/lib/commonjs/components/FieldFlowInput/FieldFlowInput.js.map +1 -1
  16. package/lib/commonjs/components/FieldFlowInput/styles.js +32 -11
  17. package/lib/commonjs/components/FieldFlowInput/styles.js.map +1 -1
  18. package/lib/commonjs/components/FieldFlowMaskedInput/FieldFlowMaskedInput.js +66 -0
  19. package/lib/commonjs/components/FieldFlowMaskedInput/FieldFlowMaskedInput.js.map +1 -0
  20. package/lib/commonjs/components/FieldFlowMaskedInput/index.js +13 -0
  21. package/lib/commonjs/components/FieldFlowMaskedInput/index.js.map +1 -0
  22. package/lib/commonjs/components/FieldFlowMaskedInput/types.js +6 -0
  23. package/lib/commonjs/components/FieldFlowMaskedInput/types.js.map +1 -0
  24. package/lib/commonjs/components/FieldFlowOTP/FieldFlowOTP.js +255 -0
  25. package/lib/commonjs/components/FieldFlowOTP/FieldFlowOTP.js.map +1 -0
  26. package/lib/commonjs/components/FieldFlowOTP/index.js +13 -0
  27. package/lib/commonjs/components/FieldFlowOTP/index.js.map +1 -0
  28. package/lib/commonjs/components/FieldFlowOTP/types.js +6 -0
  29. package/lib/commonjs/components/FieldFlowOTP/types.js.map +1 -0
  30. package/lib/commonjs/components/FieldFlowRadio/FieldFlowRadio.js +164 -0
  31. package/lib/commonjs/components/FieldFlowRadio/FieldFlowRadio.js.map +1 -0
  32. package/lib/commonjs/components/FieldFlowRadio/index.js +13 -0
  33. package/lib/commonjs/components/FieldFlowRadio/index.js.map +1 -0
  34. package/lib/commonjs/components/FieldFlowSwitch/FieldFlowSwitch.js +152 -0
  35. package/lib/commonjs/components/FieldFlowSwitch/FieldFlowSwitch.js.map +1 -0
  36. package/lib/commonjs/components/FieldFlowSwitch/index.js +13 -0
  37. package/lib/commonjs/components/FieldFlowSwitch/index.js.map +1 -0
  38. package/lib/commonjs/hooks/index.js.map +1 -1
  39. package/lib/commonjs/hooks/useFieldFlowForm.js +146 -31
  40. package/lib/commonjs/hooks/useFieldFlowForm.js.map +1 -1
  41. package/lib/commonjs/icons/Icon.js +719 -30
  42. package/lib/commonjs/icons/Icon.js.map +1 -1
  43. package/lib/commonjs/icons/index.js +7 -0
  44. package/lib/commonjs/icons/index.js.map +1 -1
  45. package/lib/commonjs/icons/renderIcon.js +24 -0
  46. package/lib/commonjs/icons/renderIcon.js.map +1 -0
  47. package/lib/commonjs/index.js +127 -0
  48. package/lib/commonjs/index.js.map +1 -1
  49. package/lib/commonjs/utils/masks.js +130 -0
  50. package/lib/commonjs/utils/masks.js.map +1 -0
  51. package/lib/commonjs/validation/index.js +25 -0
  52. package/lib/commonjs/validation/index.js.map +1 -1
  53. package/lib/commonjs/validation/zodAdapter.js +91 -0
  54. package/lib/commonjs/validation/zodAdapter.js.map +1 -0
  55. package/lib/module/components/FieldFlowCheckbox/FieldFlowCheckbox.js +126 -0
  56. package/lib/module/components/FieldFlowCheckbox/FieldFlowCheckbox.js.map +1 -0
  57. package/lib/module/components/FieldFlowCheckbox/index.js +4 -0
  58. package/lib/module/components/FieldFlowCheckbox/index.js.map +1 -0
  59. package/lib/module/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.js +531 -0
  60. package/lib/module/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.js.map +1 -0
  61. package/lib/module/components/FieldFlowDateTimePicker/index.js +4 -0
  62. package/lib/module/components/FieldFlowDateTimePicker/index.js.map +1 -0
  63. package/lib/module/components/FieldFlowDateTimePicker/types.js +4 -0
  64. package/lib/module/components/FieldFlowDateTimePicker/types.js.map +1 -0
  65. package/lib/module/components/FieldFlowDropdown/FieldFlowDropdown.js +24 -9
  66. package/lib/module/components/FieldFlowDropdown/FieldFlowDropdown.js.map +1 -1
  67. package/lib/module/components/FieldFlowInput/FieldFlowInput.js +59 -28
  68. package/lib/module/components/FieldFlowInput/FieldFlowInput.js.map +1 -1
  69. package/lib/module/components/FieldFlowInput/styles.js +32 -11
  70. package/lib/module/components/FieldFlowInput/styles.js.map +1 -1
  71. package/lib/module/components/FieldFlowMaskedInput/FieldFlowMaskedInput.js +61 -0
  72. package/lib/module/components/FieldFlowMaskedInput/FieldFlowMaskedInput.js.map +1 -0
  73. package/lib/module/components/FieldFlowMaskedInput/index.js +4 -0
  74. package/lib/module/components/FieldFlowMaskedInput/index.js.map +1 -0
  75. package/lib/module/components/FieldFlowMaskedInput/types.js +4 -0
  76. package/lib/module/components/FieldFlowMaskedInput/types.js.map +1 -0
  77. package/lib/module/components/FieldFlowOTP/FieldFlowOTP.js +250 -0
  78. package/lib/module/components/FieldFlowOTP/FieldFlowOTP.js.map +1 -0
  79. package/lib/module/components/FieldFlowOTP/index.js +4 -0
  80. package/lib/module/components/FieldFlowOTP/index.js.map +1 -0
  81. package/lib/module/components/FieldFlowOTP/types.js +4 -0
  82. package/lib/module/components/FieldFlowOTP/types.js.map +1 -0
  83. package/lib/module/components/FieldFlowRadio/FieldFlowRadio.js +159 -0
  84. package/lib/module/components/FieldFlowRadio/FieldFlowRadio.js.map +1 -0
  85. package/lib/module/components/FieldFlowRadio/index.js +4 -0
  86. package/lib/module/components/FieldFlowRadio/index.js.map +1 -0
  87. package/lib/module/components/FieldFlowSwitch/FieldFlowSwitch.js +147 -0
  88. package/lib/module/components/FieldFlowSwitch/FieldFlowSwitch.js.map +1 -0
  89. package/lib/module/components/FieldFlowSwitch/index.js +4 -0
  90. package/lib/module/components/FieldFlowSwitch/index.js.map +1 -0
  91. package/lib/module/hooks/index.js.map +1 -1
  92. package/lib/module/hooks/useFieldFlowForm.js +147 -32
  93. package/lib/module/hooks/useFieldFlowForm.js.map +1 -1
  94. package/lib/module/icons/Icon.js +720 -31
  95. package/lib/module/icons/Icon.js.map +1 -1
  96. package/lib/module/icons/index.js +1 -0
  97. package/lib/module/icons/index.js.map +1 -1
  98. package/lib/module/icons/renderIcon.js +19 -0
  99. package/lib/module/icons/renderIcon.js.map +1 -0
  100. package/lib/module/index.js +11 -3
  101. package/lib/module/index.js.map +1 -1
  102. package/lib/module/utils/masks.js +118 -0
  103. package/lib/module/utils/masks.js.map +1 -0
  104. package/lib/module/validation/index.js +1 -0
  105. package/lib/module/validation/index.js.map +1 -1
  106. package/lib/module/validation/zodAdapter.js +84 -0
  107. package/lib/module/validation/zodAdapter.js.map +1 -0
  108. package/lib/typescript/components/FieldFlowCheckbox/FieldFlowCheckbox.d.ts +36 -0
  109. package/lib/typescript/components/FieldFlowCheckbox/FieldFlowCheckbox.d.ts.map +1 -0
  110. package/lib/typescript/components/FieldFlowCheckbox/index.d.ts +3 -0
  111. package/lib/typescript/components/FieldFlowCheckbox/index.d.ts.map +1 -0
  112. package/lib/typescript/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.d.ts +4 -0
  113. package/lib/typescript/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.d.ts.map +1 -0
  114. package/lib/typescript/components/FieldFlowDateTimePicker/index.d.ts +3 -0
  115. package/lib/typescript/components/FieldFlowDateTimePicker/index.d.ts.map +1 -0
  116. package/lib/typescript/components/FieldFlowDateTimePicker/types.d.ts +55 -0
  117. package/lib/typescript/components/FieldFlowDateTimePicker/types.d.ts.map +1 -0
  118. package/lib/typescript/components/FieldFlowDropdown/FieldFlowDropdown.d.ts.map +1 -1
  119. package/lib/typescript/components/FieldFlowDropdown/types.d.ts +7 -1
  120. package/lib/typescript/components/FieldFlowDropdown/types.d.ts.map +1 -1
  121. package/lib/typescript/components/FieldFlowInput/FieldFlowInput.d.ts.map +1 -1
  122. package/lib/typescript/components/FieldFlowInput/styles.d.ts +19 -2
  123. package/lib/typescript/components/FieldFlowInput/styles.d.ts.map +1 -1
  124. package/lib/typescript/components/FieldFlowInput/types.d.ts +24 -5
  125. package/lib/typescript/components/FieldFlowInput/types.d.ts.map +1 -1
  126. package/lib/typescript/components/FieldFlowMaskedInput/FieldFlowMaskedInput.d.ts +5 -0
  127. package/lib/typescript/components/FieldFlowMaskedInput/FieldFlowMaskedInput.d.ts.map +1 -0
  128. package/lib/typescript/components/FieldFlowMaskedInput/index.d.ts +3 -0
  129. package/lib/typescript/components/FieldFlowMaskedInput/index.d.ts.map +1 -0
  130. package/lib/typescript/components/FieldFlowMaskedInput/types.d.ts +20 -0
  131. package/lib/typescript/components/FieldFlowMaskedInput/types.d.ts.map +1 -0
  132. package/lib/typescript/components/FieldFlowOTP/FieldFlowOTP.d.ts +4 -0
  133. package/lib/typescript/components/FieldFlowOTP/FieldFlowOTP.d.ts.map +1 -0
  134. package/lib/typescript/components/FieldFlowOTP/index.d.ts +3 -0
  135. package/lib/typescript/components/FieldFlowOTP/index.d.ts.map +1 -0
  136. package/lib/typescript/components/FieldFlowOTP/types.d.ts +52 -0
  137. package/lib/typescript/components/FieldFlowOTP/types.d.ts.map +1 -0
  138. package/lib/typescript/components/FieldFlowRadio/FieldFlowRadio.d.ts +39 -0
  139. package/lib/typescript/components/FieldFlowRadio/FieldFlowRadio.d.ts.map +1 -0
  140. package/lib/typescript/components/FieldFlowRadio/index.d.ts +3 -0
  141. package/lib/typescript/components/FieldFlowRadio/index.d.ts.map +1 -0
  142. package/lib/typescript/components/FieldFlowSwitch/FieldFlowSwitch.d.ts +31 -0
  143. package/lib/typescript/components/FieldFlowSwitch/FieldFlowSwitch.d.ts.map +1 -0
  144. package/lib/typescript/components/FieldFlowSwitch/index.d.ts +3 -0
  145. package/lib/typescript/components/FieldFlowSwitch/index.d.ts.map +1 -0
  146. package/lib/typescript/hooks/index.d.ts +1 -1
  147. package/lib/typescript/hooks/index.d.ts.map +1 -1
  148. package/lib/typescript/hooks/useFieldFlowForm.d.ts +46 -7
  149. package/lib/typescript/hooks/useFieldFlowForm.d.ts.map +1 -1
  150. package/lib/typescript/icons/Icon.d.ts +2 -2
  151. package/lib/typescript/icons/Icon.d.ts.map +1 -1
  152. package/lib/typescript/icons/index.d.ts +2 -0
  153. package/lib/typescript/icons/index.d.ts.map +1 -1
  154. package/lib/typescript/icons/renderIcon.d.ts +6 -0
  155. package/lib/typescript/icons/renderIcon.d.ts.map +1 -0
  156. package/lib/typescript/index.d.ts +20 -6
  157. package/lib/typescript/index.d.ts.map +1 -1
  158. package/lib/typescript/types/index.d.ts +4 -0
  159. package/lib/typescript/types/index.d.ts.map +1 -1
  160. package/lib/typescript/utils/masks.d.ts +37 -0
  161. package/lib/typescript/utils/masks.d.ts.map +1 -0
  162. package/lib/typescript/validation/index.d.ts +2 -0
  163. package/lib/typescript/validation/index.d.ts.map +1 -1
  164. package/lib/typescript/validation/zodAdapter.d.ts +53 -0
  165. package/lib/typescript/validation/zodAdapter.d.ts.map +1 -0
  166. package/package.json +17 -3
  167. package/src/components/FieldFlowCheckbox/FieldFlowCheckbox.tsx +174 -0
  168. package/src/components/FieldFlowCheckbox/index.ts +2 -0
  169. package/src/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.tsx +585 -0
  170. package/src/components/FieldFlowDateTimePicker/index.ts +2 -0
  171. package/src/components/FieldFlowDateTimePicker/types.ts +57 -0
  172. package/src/components/FieldFlowDropdown/FieldFlowDropdown.tsx +17 -7
  173. package/src/components/FieldFlowDropdown/types.ts +8 -1
  174. package/src/components/FieldFlowInput/FieldFlowInput.tsx +69 -26
  175. package/src/components/FieldFlowInput/styles.ts +41 -6
  176. package/src/components/FieldFlowInput/types.ts +24 -5
  177. package/src/components/FieldFlowMaskedInput/FieldFlowMaskedInput.tsx +76 -0
  178. package/src/components/FieldFlowMaskedInput/index.ts +2 -0
  179. package/src/components/FieldFlowMaskedInput/types.ts +22 -0
  180. package/src/components/FieldFlowOTP/FieldFlowOTP.tsx +305 -0
  181. package/src/components/FieldFlowOTP/index.ts +2 -0
  182. package/src/components/FieldFlowOTP/types.ts +57 -0
  183. package/src/components/FieldFlowRadio/FieldFlowRadio.tsx +215 -0
  184. package/src/components/FieldFlowRadio/index.ts +2 -0
  185. package/src/components/FieldFlowSwitch/FieldFlowSwitch.tsx +172 -0
  186. package/src/components/FieldFlowSwitch/index.ts +2 -0
  187. package/src/hooks/index.ts +3 -0
  188. package/src/hooks/useFieldFlowForm.ts +209 -33
  189. package/src/icons/Icon.tsx +451 -5
  190. package/src/icons/index.ts +2 -0
  191. package/src/icons/renderIcon.tsx +17 -0
  192. package/src/index.ts +47 -4
  193. package/src/types/index.ts +5 -0
  194. package/src/utils/masks.ts +137 -0
  195. package/src/validation/index.ts +7 -0
  196. package/src/validation/zodAdapter.ts +103 -0
@@ -0,0 +1,305 @@
1
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ NativeSyntheticEvent,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ TextInput,
8
+ TextInputKeyPressEventData,
9
+ View,
10
+ } from 'react-native';
11
+ import { useFieldFlowTheme, mergeDeep } from '../../theme/ThemeProvider';
12
+ import { defaultTheme } from '../../theme/defaultTheme';
13
+ import { useFieldState } from '../../hooks/useFieldState';
14
+ import { borderColorFor, labelColorFor } from '../FieldFlowInput/styles';
15
+ import { digitsOnly } from '../../utils/masks';
16
+ import { renderFieldFlowIcon } from '../../icons/renderIcon';
17
+ import type { FieldFlowOTPProps } from './types';
18
+
19
+ export function FieldFlowOTP({
20
+ length = 6,
21
+ value,
22
+ onChangeText,
23
+ onComplete,
24
+ label,
25
+ helperText,
26
+ errorText,
27
+ hasError,
28
+ successText,
29
+ isSuccess,
30
+ disabled,
31
+ required,
32
+ secure = false,
33
+ autoFocus = true,
34
+ leftIcon = 'otp',
35
+ rightIcon,
36
+ iconSize = 18,
37
+ onLeftIconPress,
38
+ onRightIconPress,
39
+ cellGap,
40
+ cellSize,
41
+ containerStyle,
42
+ rowStyle,
43
+ cellStyle,
44
+ cellFocusedStyle,
45
+ cellTextStyle,
46
+ labelStyle,
47
+ helperStyle,
48
+ errorStyle,
49
+ theme: themeOverride,
50
+ testID,
51
+ renderCell,
52
+ }: FieldFlowOTPProps) {
53
+ const contextTheme = useFieldFlowTheme();
54
+ const theme = useMemo(
55
+ () => (themeOverride ? mergeDeep(contextTheme ?? defaultTheme, themeOverride) : contextTheme ?? defaultTheme),
56
+ [contextTheme, themeOverride],
57
+ );
58
+
59
+ const isControlled = value !== undefined;
60
+ const [internal, setInternal] = useState('');
61
+ const code = (isControlled ? value ?? '' : internal).slice(0, length);
62
+ const digits = code.padEnd(length, ' ').split('').map(c => (c === ' ' ? '' : c));
63
+
64
+ const [focusedIndex, setFocusedIndex] = useState<number | null>(autoFocus ? 0 : null);
65
+ const refs = useRef<Array<TextInput | null>>([]);
66
+
67
+ const state = useFieldState({
68
+ isFocused: focusedIndex !== null,
69
+ hasError: Boolean(hasError || errorText),
70
+ isSuccess,
71
+ disabled,
72
+ });
73
+
74
+ const styles = useMemo(
75
+ () => createStyles(theme, state, cellGap ?? theme.spacing.gap, cellSize),
76
+ [theme, state, cellGap, cellSize],
77
+ );
78
+ const iconColor = state === 'focused' ? theme.colors.iconFocused : theme.colors.icon;
79
+
80
+ const commit = useCallback(
81
+ (next: string) => {
82
+ const clipped = digitsOnly(next).slice(0, length);
83
+ if (!isControlled) setInternal(clipped);
84
+ onChangeText?.(clipped);
85
+ if (clipped.length === length) onComplete?.(clipped);
86
+ },
87
+ [isControlled, length, onChangeText, onComplete],
88
+ );
89
+
90
+ const focusAt = useCallback(
91
+ (index: number) => {
92
+ const clamped = Math.max(0, Math.min(length - 1, index));
93
+ refs.current[clamped]?.focus();
94
+ setFocusedIndex(clamped);
95
+ },
96
+ [length],
97
+ );
98
+
99
+ const handleChangeAt = useCallback(
100
+ (index: number, text: string) => {
101
+ if (disabled) return;
102
+ const incoming = digitsOnly(text);
103
+ if (incoming.length > 1) {
104
+ commit(incoming);
105
+ focusAt(Math.min(incoming.length, length) - 1);
106
+ return;
107
+ }
108
+ const next = digits.slice();
109
+ next[index] = incoming.slice(-1);
110
+ commit(next.join(''));
111
+ if (incoming && index < length - 1) focusAt(index + 1);
112
+ },
113
+ [commit, digits, disabled, focusAt, length],
114
+ );
115
+
116
+ const handleKeyPress = useCallback(
117
+ (index: number, e: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
118
+ if (e.nativeEvent.key !== 'Backspace') return;
119
+ if (digits[index]) {
120
+ const next = digits.slice();
121
+ next[index] = '';
122
+ commit(next.join(''));
123
+ return;
124
+ }
125
+ if (index > 0) {
126
+ const next = digits.slice();
127
+ next[index - 1] = '';
128
+ commit(next.join(''));
129
+ focusAt(index - 1);
130
+ }
131
+ },
132
+ [commit, digits, focusAt],
133
+ );
134
+
135
+ return (
136
+ <View
137
+ style={[styles.wrapper, containerStyle]}
138
+ testID={testID}
139
+ accessibilityLabel={label ?? 'One-time code'}
140
+ >
141
+ {(label || leftIcon || rightIcon) && (
142
+ <View style={styles.labelRow}>
143
+ {leftIcon ? (
144
+ <Pressable onPress={onLeftIconPress} disabled={!onLeftIconPress} hitSlop={8} style={styles.labelIcon}>
145
+ {renderFieldFlowIcon(leftIcon, iconColor, iconSize)}
146
+ </Pressable>
147
+ ) : null}
148
+ {label ? (
149
+ <Text style={[styles.label, labelStyle]}>
150
+ {label}
151
+ {required ? ' *' : ''}
152
+ </Text>
153
+ ) : (
154
+ <View style={{ flex: 1 }} />
155
+ )}
156
+ {rightIcon ? (
157
+ <Pressable onPress={onRightIconPress} disabled={!onRightIconPress} hitSlop={8} style={styles.labelIcon}>
158
+ {renderFieldFlowIcon(rightIcon, iconColor, iconSize)}
159
+ </Pressable>
160
+ ) : null}
161
+ </View>
162
+ )}
163
+
164
+ <View style={[styles.row, rowStyle]}>
165
+ {digits.map((digit, index) => {
166
+ const isFocused = focusedIndex === index;
167
+ if (renderCell) {
168
+ return (
169
+ <View key={index}>
170
+ {renderCell({
171
+ index,
172
+ digit,
173
+ isFocused,
174
+ onChangeText: text => handleChangeAt(index, text),
175
+ })}
176
+ </View>
177
+ );
178
+ }
179
+ return (
180
+ <Pressable
181
+ key={index}
182
+ onPress={() => !disabled && focusAt(index)}
183
+ style={[
184
+ styles.cell,
185
+ {
186
+ borderColor: borderColorFor(theme, isFocused ? 'focused' : state),
187
+ borderWidth: isFocused
188
+ ? theme.spacing.borderWidthFocused
189
+ : theme.spacing.borderWidth,
190
+ },
191
+ cellStyle,
192
+ isFocused ? cellFocusedStyle : null,
193
+ ]}
194
+ >
195
+ <TextInput
196
+ ref={r => {
197
+ refs.current[index] = r;
198
+ }}
199
+ value={digit}
200
+ onChangeText={text => handleChangeAt(index, text)}
201
+ onKeyPress={e => handleKeyPress(index, e)}
202
+ onFocus={() => setFocusedIndex(index)}
203
+ onBlur={() => setFocusedIndex(prev => (prev === index ? null : prev))}
204
+ editable={!disabled}
205
+ keyboardType="number-pad"
206
+ textContentType="oneTimeCode"
207
+ autoComplete="sms-otp"
208
+ secureTextEntry={secure}
209
+ maxLength={length}
210
+ selectTextOnFocus
211
+ autoFocus={autoFocus && index === 0}
212
+ style={[styles.cellInput, cellTextStyle]}
213
+ caretHidden
214
+ accessibilityLabel={`Digit ${index + 1} of ${length}`}
215
+ />
216
+ </Pressable>
217
+ );
218
+ })}
219
+ </View>
220
+
221
+ {(helperText || errorText || successText) && (
222
+ <View style={styles.footer}>
223
+ {errorText ? (
224
+ <Text style={[styles.errorText, errorStyle, helperStyle]}>{errorText}</Text>
225
+ ) : successText ? (
226
+ <Text style={[styles.successText, helperStyle]}>{successText}</Text>
227
+ ) : helperText ? (
228
+ <Text style={[styles.helperText, helperStyle]}>{helperText}</Text>
229
+ ) : null}
230
+ </View>
231
+ )}
232
+ </View>
233
+ );
234
+ }
235
+
236
+ function createStyles(
237
+ theme: ReturnType<typeof useFieldFlowTheme>,
238
+ state: ReturnType<typeof useFieldState>,
239
+ gap: number,
240
+ cellSize?: number,
241
+ ) {
242
+ return StyleSheet.create({
243
+ wrapper: {
244
+ marginBottom: theme.spacing.gap * 2,
245
+ },
246
+ labelRow: {
247
+ flexDirection: 'row',
248
+ alignItems: 'center',
249
+ marginBottom: theme.spacing.gap / 1.5,
250
+ gap: theme.spacing.gap,
251
+ },
252
+ labelIcon: {
253
+ alignItems: 'center',
254
+ justifyContent: 'center',
255
+ },
256
+ label: {
257
+ flex: 1,
258
+ fontSize: theme.typography.labelFontSize,
259
+ color: labelColorFor(theme, state),
260
+ fontWeight: '500',
261
+ fontFamily: theme.typography.fontFamily,
262
+ },
263
+ row: {
264
+ flexDirection: 'row',
265
+ justifyContent: 'space-between',
266
+ gap,
267
+ },
268
+ cell: {
269
+ flex: cellSize ? undefined : 1,
270
+ width: cellSize,
271
+ height: cellSize,
272
+ aspectRatio: cellSize ? undefined : 0.9,
273
+ maxWidth: cellSize ?? 56,
274
+ borderRadius: theme.spacing.borderRadius,
275
+ backgroundColor: state === 'disabled' ? theme.colors.backgroundDisabled : theme.colors.background,
276
+ alignItems: 'center',
277
+ justifyContent: 'center',
278
+ },
279
+ cellInput: {
280
+ width: '100%',
281
+ height: '100%',
282
+ textAlign: 'center',
283
+ fontSize: theme.typography.fontSize + 4,
284
+ fontWeight: '600',
285
+ color: theme.colors.text,
286
+ padding: 0,
287
+ fontFamily: theme.typography.fontFamily,
288
+ },
289
+ footer: {
290
+ marginTop: theme.spacing.gap / 1.5,
291
+ },
292
+ helperText: {
293
+ fontSize: theme.typography.helperFontSize,
294
+ color: theme.colors.label,
295
+ },
296
+ errorText: {
297
+ fontSize: theme.typography.helperFontSize,
298
+ color: theme.colors.error,
299
+ },
300
+ successText: {
301
+ fontSize: theme.typography.helperFontSize,
302
+ color: theme.colors.success,
303
+ },
304
+ });
305
+ }
@@ -0,0 +1,2 @@
1
+ export { FieldFlowOTP } from './FieldFlowOTP';
2
+ export type { FieldFlowOTPProps } from './types';
@@ -0,0 +1,57 @@
1
+ import type React from 'react';
2
+ import type { ViewStyle, TextStyle } from 'react-native';
3
+ import type { DeepPartial, FieldFlowTheme } from '../../types';
4
+ import type { FieldFlowIconSource } from '../../icons';
5
+
6
+ export interface FieldFlowOTPProps {
7
+ /** Number of digit cells. Default: 6. */
8
+ length?: number;
9
+ value?: string;
10
+ onChangeText?: (value: string) => void;
11
+ /** Fired once every cell is filled. */
12
+ onComplete?: (value: string) => void;
13
+
14
+ label?: string;
15
+ helperText?: string;
16
+ errorText?: string;
17
+ hasError?: boolean;
18
+ successText?: string;
19
+ isSuccess?: boolean;
20
+ disabled?: boolean;
21
+ required?: boolean;
22
+ /** Mask digits (password-style). Default: false. */
23
+ secure?: boolean;
24
+ /** Auto-focus the first empty cell on mount. Default: true. */
25
+ autoFocus?: boolean;
26
+
27
+ /** Icon shown before the label / row. Built-in name or custom node. */
28
+ leftIcon?: FieldFlowIconSource;
29
+ rightIcon?: FieldFlowIconSource;
30
+ iconSize?: number;
31
+ onLeftIconPress?: () => void;
32
+ onRightIconPress?: () => void;
33
+
34
+ /** Gap between cells. */
35
+ cellGap?: number;
36
+ /** Override cell size (width/height). */
37
+ cellSize?: number;
38
+
39
+ containerStyle?: ViewStyle;
40
+ rowStyle?: ViewStyle;
41
+ cellStyle?: ViewStyle;
42
+ cellFocusedStyle?: ViewStyle;
43
+ cellTextStyle?: TextStyle;
44
+ labelStyle?: TextStyle;
45
+ helperStyle?: TextStyle;
46
+ errorStyle?: TextStyle;
47
+ theme?: DeepPartial<FieldFlowTheme>;
48
+ testID?: string;
49
+
50
+ /** Render a custom cell. */
51
+ renderCell?: (args: {
52
+ index: number;
53
+ digit: string;
54
+ isFocused: boolean;
55
+ onChangeText: (text: string) => void;
56
+ }) => React.ReactNode;
57
+ }
@@ -0,0 +1,215 @@
1
+ import React, { useMemo, type ReactNode } from 'react';
2
+ import { Pressable, StyleSheet, Text, View, ViewStyle, TextStyle } from 'react-native';
3
+ import { useFieldFlowTheme, mergeDeep } from '../../theme/ThemeProvider';
4
+ import { defaultTheme } from '../../theme/defaultTheme';
5
+ import { renderFieldFlowIcon, type FieldFlowIconSource } from '../../icons';
6
+ import type { DeepPartial, FieldFlowOption, FieldFlowTheme } from '../../types';
7
+
8
+ export interface FieldFlowRadioProps {
9
+ options: FieldFlowOption[];
10
+ value?: string | null;
11
+ onChange: (value: string) => void;
12
+ label?: string;
13
+ helperText?: string;
14
+ errorText?: string;
15
+ disabled?: boolean;
16
+ required?: boolean;
17
+ direction?: 'vertical' | 'horizontal';
18
+ /** Leading icon next to the group label. */
19
+ leftIcon?: FieldFlowIconSource;
20
+ /** Override selected indicator (default filled radio). */
21
+ selectedIcon?: FieldFlowIconSource;
22
+ /** Override unselected indicator. */
23
+ unselectedIcon?: FieldFlowIconSource;
24
+ iconSize?: number;
25
+ selectedColor?: string;
26
+ containerStyle?: ViewStyle;
27
+ optionStyle?: ViewStyle;
28
+ labelStyle?: TextStyle;
29
+ helperStyle?: TextStyle;
30
+ errorStyle?: TextStyle;
31
+ theme?: DeepPartial<FieldFlowTheme>;
32
+ testID?: string;
33
+ /** Fully custom option renderer. */
34
+ renderOption?: (args: {
35
+ option: FieldFlowOption;
36
+ selected: boolean;
37
+ disabled: boolean;
38
+ onPress: () => void;
39
+ }) => ReactNode;
40
+ }
41
+
42
+ export function FieldFlowRadio({
43
+ options,
44
+ value,
45
+ onChange,
46
+ label,
47
+ helperText,
48
+ errorText,
49
+ disabled,
50
+ required,
51
+ direction = 'vertical',
52
+ leftIcon,
53
+ selectedIcon = 'radioFilled',
54
+ unselectedIcon = 'radio',
55
+ iconSize = 20,
56
+ selectedColor,
57
+ containerStyle,
58
+ optionStyle,
59
+ labelStyle,
60
+ helperStyle,
61
+ errorStyle,
62
+ theme: themeOverride,
63
+ testID,
64
+ renderOption,
65
+ }: FieldFlowRadioProps) {
66
+ const contextTheme = useFieldFlowTheme();
67
+ const theme = useMemo(
68
+ () => (themeOverride ? mergeDeep(contextTheme ?? defaultTheme, themeOverride) : contextTheme ?? defaultTheme),
69
+ [contextTheme, themeOverride],
70
+ );
71
+ const styles = useMemo(
72
+ () => createStyles(theme, Boolean(errorText), Boolean(disabled), direction),
73
+ [theme, errorText, disabled, direction],
74
+ );
75
+ const active = selectedColor ?? theme.colors.borderFocused;
76
+
77
+ return (
78
+ <View
79
+ style={[styles.wrapper, containerStyle]}
80
+ testID={testID}
81
+ accessibilityRole="radiogroup"
82
+ accessibilityLabel={label}
83
+ >
84
+ {(label || leftIcon) && (
85
+ <View style={styles.labelRow}>
86
+ {leftIcon ? renderFieldFlowIcon(leftIcon, theme.colors.icon, 18) : null}
87
+ {label ? (
88
+ <Text style={[styles.groupLabel, labelStyle]}>
89
+ {label}
90
+ {required ? ' *' : ''}
91
+ </Text>
92
+ ) : null}
93
+ </View>
94
+ )}
95
+
96
+ <View style={styles.options}>
97
+ {options.map(option => {
98
+ const selected = value === option.value;
99
+ const itemDisabled = Boolean(disabled || option.disabled);
100
+ const onPress = () => onChange(option.value);
101
+
102
+ if (renderOption) {
103
+ return (
104
+ <View key={option.value}>
105
+ {renderOption({ option, selected, disabled: itemDisabled, onPress })}
106
+ </View>
107
+ );
108
+ }
109
+
110
+ return (
111
+ <Pressable
112
+ key={option.value}
113
+ onPress={() => !itemDisabled && onPress()}
114
+ disabled={itemDisabled}
115
+ style={[styles.optionRow, itemDisabled && styles.optionDisabled, optionStyle]}
116
+ accessibilityRole="radio"
117
+ accessibilityState={{ selected, disabled: itemDisabled }}
118
+ accessibilityLabel={option.label}
119
+ hitSlop={4}
120
+ >
121
+ {renderFieldFlowIcon(
122
+ selected ? selectedIcon : unselectedIcon,
123
+ selected ? active : errorText ? theme.colors.borderError : theme.colors.border,
124
+ iconSize,
125
+ )}
126
+ {option.icon ? (
127
+ <View style={styles.optionIcon}>
128
+ {renderFieldFlowIcon(option.icon, theme.colors.icon, 18)}
129
+ </View>
130
+ ) : null}
131
+ <View style={styles.optionTextCol}>
132
+ <Text style={styles.optionLabel}>{option.label}</Text>
133
+ {option.description ? (
134
+ <Text style={styles.optionDescription}>{option.description}</Text>
135
+ ) : null}
136
+ </View>
137
+ </Pressable>
138
+ );
139
+ })}
140
+ </View>
141
+
142
+ {errorText ? (
143
+ <Text style={[styles.errorText, errorStyle, helperStyle]}>{errorText}</Text>
144
+ ) : helperText ? (
145
+ <Text style={[styles.helperText, helperStyle]}>{helperText}</Text>
146
+ ) : null}
147
+ </View>
148
+ );
149
+ }
150
+
151
+ function createStyles(
152
+ theme: FieldFlowTheme,
153
+ hasError: boolean,
154
+ disabled: boolean,
155
+ direction: 'vertical' | 'horizontal',
156
+ ) {
157
+ return StyleSheet.create({
158
+ wrapper: {
159
+ marginBottom: theme.spacing.gap * 2,
160
+ opacity: disabled ? 0.6 : 1,
161
+ },
162
+ labelRow: {
163
+ flexDirection: 'row',
164
+ alignItems: 'center',
165
+ gap: theme.spacing.gap,
166
+ marginBottom: theme.spacing.gap,
167
+ },
168
+ groupLabel: {
169
+ fontSize: theme.typography.labelFontSize,
170
+ color: hasError ? theme.colors.error : theme.colors.label,
171
+ fontWeight: '500',
172
+ fontFamily: theme.typography.fontFamily,
173
+ },
174
+ options: {
175
+ flexDirection: direction === 'horizontal' ? 'row' : 'column',
176
+ flexWrap: direction === 'horizontal' ? 'wrap' : 'nowrap',
177
+ gap: theme.spacing.gap * 1.5,
178
+ },
179
+ optionRow: {
180
+ flexDirection: 'row',
181
+ alignItems: 'flex-start',
182
+ gap: theme.spacing.gap,
183
+ minWidth: direction === 'horizontal' ? '40%' : undefined,
184
+ },
185
+ optionDisabled: {
186
+ opacity: 0.5,
187
+ },
188
+ optionIcon: {
189
+ marginTop: 1,
190
+ },
191
+ optionTextCol: {
192
+ flex: 1,
193
+ },
194
+ optionLabel: {
195
+ fontSize: theme.typography.fontSize,
196
+ color: theme.colors.text,
197
+ fontFamily: theme.typography.fontFamily,
198
+ },
199
+ optionDescription: {
200
+ fontSize: theme.typography.helperFontSize,
201
+ color: theme.colors.placeholder,
202
+ marginTop: 2,
203
+ },
204
+ helperText: {
205
+ marginTop: theme.spacing.gap / 1.5,
206
+ fontSize: theme.typography.helperFontSize,
207
+ color: theme.colors.label,
208
+ },
209
+ errorText: {
210
+ marginTop: theme.spacing.gap / 1.5,
211
+ fontSize: theme.typography.helperFontSize,
212
+ color: theme.colors.error,
213
+ },
214
+ });
215
+ }
@@ -0,0 +1,2 @@
1
+ export { FieldFlowRadio } from './FieldFlowRadio';
2
+ export type { FieldFlowRadioProps } from './FieldFlowRadio';