@umituz/react-native-design-system 2.6.94 → 2.6.95
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/package.json +1 -1
- package/src/atoms/AtomicAvatar.README.md +284 -397
- package/src/atoms/AtomicBadge.README.md +123 -358
- package/src/atoms/AtomicCard.README.md +358 -247
- package/src/atoms/AtomicDatePicker.README.md +127 -332
- package/src/atoms/AtomicFab.README.md +194 -352
- package/src/atoms/AtomicIcon.README.md +241 -274
- package/src/atoms/AtomicProgress.README.md +100 -338
- package/src/atoms/AtomicSpinner.README.md +304 -337
- package/src/atoms/AtomicText.README.md +153 -389
- package/src/atoms/AtomicTextArea.README.md +267 -268
- package/src/atoms/EmptyState.README.md +247 -292
- package/src/atoms/GlassView/README.md +313 -444
- package/src/atoms/button/README.md +186 -297
- package/src/atoms/button/STRATEGY.md +252 -0
- package/src/atoms/chip/README.md +242 -290
- package/src/atoms/input/README.md +296 -290
- package/src/atoms/picker/README.md +278 -309
- package/src/atoms/skeleton/AtomicSkeleton.README.md +394 -252
- package/src/molecules/BaseModal/README.md +356 -0
- package/src/molecules/BaseModal.README.md +324 -200
- package/src/molecules/ConfirmationModal.README.md +349 -302
- package/src/molecules/Divider/README.md +293 -376
- package/src/molecules/FormField.README.md +321 -534
- package/src/molecules/GlowingCard/GlowingCard.tsx +1 -1
- package/src/molecules/GlowingCard/README.md +230 -372
- package/src/molecules/List/README.md +281 -488
- package/src/molecules/ListItem.README.md +320 -315
- package/src/molecules/SearchBar/README.md +332 -430
- package/src/molecules/StepHeader/README.md +311 -411
- package/src/molecules/StepProgress/README.md +281 -448
- package/src/molecules/alerts/README.md +272 -355
- package/src/molecules/avatar/README.md +295 -356
- package/src/molecules/bottom-sheet/README.md +303 -340
- package/src/molecules/calendar/README.md +301 -265
- package/src/molecules/countdown/README.md +347 -456
- package/src/molecules/emoji/README.md +281 -514
- package/src/molecules/listitem/README.md +307 -399
- package/src/molecules/media-card/MediaCard.tsx +31 -34
- package/src/molecules/media-card/README.md +217 -319
- package/src/molecules/navigation/README.md +263 -284
- package/src/molecules/splash/README.md +76 -80
- package/src/molecules/swipe-actions/README.md +376 -588
|
@@ -1,636 +1,423 @@
|
|
|
1
1
|
# FormField
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A molecule component that combines label, input field, and validation messages into a complete form input unit.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Import & Usage
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- 🔴 **Required Indicator**: Visual required field marker
|
|
10
|
-
- 🎨 **Theme-Aware**: Design token integration
|
|
11
|
-
- ♿ **Accessible**: Full accessibility support
|
|
12
|
-
- 🎯 **Simple API**: Easy to use with minimal props
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
15
|
-
|
|
16
|
-
```tsx
|
|
17
|
-
import { FormField } from 'react-native-design-system';
|
|
7
|
+
```typescript
|
|
8
|
+
import { FormField } from 'react-native-design-system/src/molecules/FormField';
|
|
18
9
|
```
|
|
19
10
|
|
|
20
|
-
|
|
11
|
+
**Location:** `src/molecules/FormField.tsx`
|
|
21
12
|
|
|
22
|
-
|
|
23
|
-
import React, { useState } from 'react';
|
|
24
|
-
import { View } from 'react-native';
|
|
25
|
-
import { FormField } from 'react-native-design-system';
|
|
26
|
-
|
|
27
|
-
export const BasicExample = () => {
|
|
28
|
-
const [email, setEmail] = useState('');
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<View style={{ padding: 16 }}>
|
|
32
|
-
<FormField
|
|
33
|
-
label="Email"
|
|
34
|
-
value={email}
|
|
35
|
-
onChangeText={setEmail}
|
|
36
|
-
placeholder="Enter your email"
|
|
37
|
-
/>
|
|
38
|
-
</View>
|
|
39
|
-
);
|
|
40
|
-
};
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Basic Input
|
|
13
|
+
## Basic Usage
|
|
44
14
|
|
|
45
15
|
```tsx
|
|
46
|
-
|
|
47
|
-
label="Username"
|
|
48
|
-
value={username}
|
|
49
|
-
onChangeText={setUsername}
|
|
50
|
-
placeholder="Enter username"
|
|
51
|
-
/>
|
|
52
|
-
```
|
|
16
|
+
const [email, setEmail] = useState('');
|
|
53
17
|
|
|
54
|
-
## With Error
|
|
55
|
-
|
|
56
|
-
```tsx
|
|
57
18
|
<FormField
|
|
58
19
|
label="Email"
|
|
59
20
|
value={email}
|
|
60
21
|
onChangeText={setEmail}
|
|
61
22
|
placeholder="Enter your email"
|
|
62
|
-
error="Please enter a valid email address"
|
|
63
23
|
/>
|
|
64
24
|
```
|
|
65
25
|
|
|
66
|
-
##
|
|
26
|
+
## Strategy
|
|
67
27
|
|
|
68
|
-
|
|
69
|
-
<FormField
|
|
70
|
-
label="Password"
|
|
71
|
-
value={password}
|
|
72
|
-
onChangeText={setPassword}
|
|
73
|
-
placeholder="Enter password"
|
|
74
|
-
secureTextEntry
|
|
75
|
-
required
|
|
76
|
-
/>
|
|
77
|
-
```
|
|
28
|
+
**Purpose**: Provide a consistent, accessible form input unit with integrated labeling, validation, and helper text.
|
|
78
29
|
|
|
79
|
-
|
|
30
|
+
**When to Use**:
|
|
31
|
+
- All form inputs requiring labels
|
|
32
|
+
- Data entry forms (login, registration, settings)
|
|
33
|
+
- Multi-field forms
|
|
34
|
+
- Input fields with validation requirements
|
|
35
|
+
- Fields needing helper text or error messages
|
|
80
36
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
37
|
+
**When NOT to Use**:
|
|
38
|
+
- For standalone inputs without labels (use AtomicInput instead)
|
|
39
|
+
- For search-only inputs (use SearchBar instead)
|
|
40
|
+
- For simple text display (use AtomicText instead)
|
|
41
|
+
|
|
42
|
+
## Rules
|
|
43
|
+
|
|
44
|
+
### Required
|
|
45
|
+
|
|
46
|
+
1. **MUST** have a `label` prop for accessibility
|
|
47
|
+
2. **ALWAYS** provide `value` and `onChangeText` for controlled inputs
|
|
48
|
+
3. **MUST** show clear error messages when validation fails
|
|
49
|
+
4. **SHOULD** provide helper text for format requirements
|
|
50
|
+
5. **ALWAYS** mark required fields visually with `required` prop
|
|
51
|
+
6. **MUST** clear errors when user starts typing
|
|
52
|
+
7. **SHOULD** use appropriate `keyboardType` for the input type
|
|
53
|
+
|
|
54
|
+
### Error Handling
|
|
55
|
+
|
|
56
|
+
1. **MUST** provide specific, actionable error messages
|
|
57
|
+
2. **SHOULD** show errors only after validation (not on focus)
|
|
58
|
+
3. **MUST** clear error when user corrects the input
|
|
59
|
+
4. **NEVER** show generic errors like "Invalid input"
|
|
60
|
+
|
|
61
|
+
### Validation
|
|
62
|
+
|
|
63
|
+
1. **Validate on blur**: Validate when user leaves the field
|
|
64
|
+
2. **Clear on type**: Clear error when user starts typing
|
|
65
|
+
3. **Show inline errors**: Display errors below the input
|
|
66
|
+
4. **Required fields**: Always validate required fields
|
|
90
67
|
|
|
91
|
-
|
|
68
|
+
### Helper Text
|
|
69
|
+
|
|
70
|
+
1. **Use for guidance**: Explain format or requirements
|
|
71
|
+
2. **Keep concise**: One short sentence
|
|
72
|
+
3. **Don't state obvious**: Avoid "Enter your name"
|
|
73
|
+
4. **Provide examples**: Show format when helpful
|
|
74
|
+
|
|
75
|
+
## Forbidden
|
|
76
|
+
|
|
77
|
+
❌ **NEVER** do these:
|
|
92
78
|
|
|
93
79
|
```tsx
|
|
80
|
+
// ❌ No label
|
|
94
81
|
<FormField
|
|
95
|
-
label="Email"
|
|
96
82
|
value={email}
|
|
97
83
|
onChangeText={setEmail}
|
|
98
|
-
placeholder="
|
|
99
|
-
required
|
|
100
|
-
requiredIndicator=" (required)"
|
|
84
|
+
placeholder="Email" // ❌ Placeholder is not a label
|
|
101
85
|
/>
|
|
102
|
-
```
|
|
103
86
|
|
|
104
|
-
|
|
87
|
+
// ❌ Generic error message
|
|
88
|
+
<FormField
|
|
89
|
+
label="Email"
|
|
90
|
+
error="Invalid" // ❌ Not actionable
|
|
91
|
+
/>
|
|
105
92
|
|
|
106
|
-
|
|
93
|
+
// ❌ Error persists after correction
|
|
107
94
|
<FormField
|
|
108
95
|
label="Email"
|
|
109
96
|
value={email}
|
|
97
|
+
error={emailError} // ❌ Error still shows when typing
|
|
110
98
|
onChangeText={setEmail}
|
|
111
|
-
placeholder="your@email.com"
|
|
112
|
-
leftIcon="mail-outline"
|
|
113
|
-
rightIcon="checkmark-circle-outline"
|
|
114
99
|
/>
|
|
115
|
-
```
|
|
116
100
|
|
|
117
|
-
|
|
101
|
+
// ❌ Required but not visually marked
|
|
102
|
+
<FormField
|
|
103
|
+
label="Email" // ❌ Missing required prop
|
|
104
|
+
required={false} // Actually required
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
// ❌ Unhelpful helper text
|
|
108
|
+
<FormField
|
|
109
|
+
label="Email"
|
|
110
|
+
helperText="Enter your email here" // ❌ Obvious
|
|
111
|
+
/>
|
|
118
112
|
|
|
119
|
-
|
|
113
|
+
// ❌ Wrong keyboard type
|
|
114
|
+
<FormField
|
|
115
|
+
label="Email"
|
|
116
|
+
keyboardType="default" // ❌ Should be email-address
|
|
117
|
+
/>
|
|
120
118
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const [errors, setErrors] = useState({});
|
|
126
|
-
|
|
127
|
-
const validate = () => {
|
|
128
|
-
const newErrors = {};
|
|
129
|
-
|
|
130
|
-
if (!email) {
|
|
131
|
-
newErrors.email = 'Email is required';
|
|
132
|
-
} else if (!isValidEmail(email)) {
|
|
133
|
-
newErrors.email = 'Please enter a valid email';
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (!password) {
|
|
137
|
-
newErrors.password = 'Password is required';
|
|
138
|
-
} else if (password.length < 6) {
|
|
139
|
-
newErrors.password = 'Password must be at least 6 characters';
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
setErrors(newErrors);
|
|
143
|
-
return Object.keys(newErrors).length === 0;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const handleSubmit = () => {
|
|
147
|
-
if (validate()) {
|
|
148
|
-
login({ email, password });
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<View style={{ padding: 16 }}>
|
|
154
|
-
<FormField
|
|
155
|
-
label="Email"
|
|
156
|
-
value={email}
|
|
157
|
-
onChangeText={setEmail}
|
|
158
|
-
placeholder="your@email.com"
|
|
159
|
-
keyboardType="email-address"
|
|
160
|
-
autoCapitalize="none"
|
|
161
|
-
error={errors.email}
|
|
162
|
-
required
|
|
163
|
-
/>
|
|
164
|
-
|
|
165
|
-
<FormField
|
|
166
|
-
label="Password"
|
|
167
|
-
value={password}
|
|
168
|
-
onChangeText={setPassword}
|
|
169
|
-
placeholder="Enter password"
|
|
170
|
-
secureTextEntry
|
|
171
|
-
error={errors.password}
|
|
172
|
-
required
|
|
173
|
-
/>
|
|
174
|
-
|
|
175
|
-
<Button title="Login" onPress={handleSubmit} />
|
|
176
|
-
</View>
|
|
177
|
-
);
|
|
119
|
+
// ❌ Not clearing errors
|
|
120
|
+
const handleChange = (text) => {
|
|
121
|
+
setEmail(text);
|
|
122
|
+
// ❌ Error not cleared
|
|
178
123
|
};
|
|
179
124
|
```
|
|
180
125
|
|
|
181
|
-
|
|
126
|
+
## Best Practices
|
|
127
|
+
|
|
128
|
+
### Error Handling
|
|
182
129
|
|
|
130
|
+
✅ **DO**:
|
|
183
131
|
```tsx
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
const [errors, setErrors] = useState({});
|
|
194
|
-
|
|
195
|
-
const handleChange = (field, value) => {
|
|
196
|
-
setFormData({ ...formData, [field]: value });
|
|
197
|
-
// Clear error when user starts typing
|
|
198
|
-
if (errors[field]) {
|
|
199
|
-
setErrors({ ...errors, [field]: null });
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const validate = () => {
|
|
204
|
-
const newErrors = {};
|
|
205
|
-
|
|
206
|
-
if (!formData.firstName) newErrors.firstName = 'First name is required';
|
|
207
|
-
if (!formData.lastName) newErrors.lastName = 'Last name is required';
|
|
208
|
-
if (!formData.email) newErrors.email = 'Email is required';
|
|
209
|
-
if (!formData.password) newErrors.password = 'Password is required';
|
|
210
|
-
if (formData.password !== formData.confirmPassword) {
|
|
211
|
-
newErrors.confirmPassword = 'Passwords do not match';
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
setErrors(newErrors);
|
|
215
|
-
return Object.keys(newErrors).length === 0;
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
return (
|
|
219
|
-
<ScrollView style={{ padding: 16 }}>
|
|
220
|
-
<FormField
|
|
221
|
-
label="First Name"
|
|
222
|
-
value={formData.firstName}
|
|
223
|
-
onChangeText={(value) => handleChange('firstName', value)}
|
|
224
|
-
placeholder="John"
|
|
225
|
-
error={errors.firstName}
|
|
226
|
-
required
|
|
227
|
-
/>
|
|
228
|
-
|
|
229
|
-
<FormField
|
|
230
|
-
label="Last Name"
|
|
231
|
-
value={formData.lastName}
|
|
232
|
-
onChangeText={(value) => handleChange('lastName', value)}
|
|
233
|
-
placeholder="Doe"
|
|
234
|
-
error={errors.lastName}
|
|
235
|
-
required
|
|
236
|
-
/>
|
|
237
|
-
|
|
238
|
-
<FormField
|
|
239
|
-
label="Email"
|
|
240
|
-
value={formData.email}
|
|
241
|
-
onChangeText={(value) => handleChange('email', value)}
|
|
242
|
-
placeholder="john.doe@example.com"
|
|
243
|
-
keyboardType="email-address"
|
|
244
|
-
autoCapitalize="none"
|
|
245
|
-
error={errors.email}
|
|
246
|
-
required
|
|
247
|
-
/>
|
|
248
|
-
|
|
249
|
-
<FormField
|
|
250
|
-
label="Password"
|
|
251
|
-
value={formData.password}
|
|
252
|
-
onChangeText={(value) => handleChange('password', value)}
|
|
253
|
-
placeholder="Create a password"
|
|
254
|
-
secureTextEntry
|
|
255
|
-
error={errors.password}
|
|
256
|
-
helperText="Must be at least 8 characters"
|
|
257
|
-
required
|
|
258
|
-
/>
|
|
259
|
-
|
|
260
|
-
<FormField
|
|
261
|
-
label="Confirm Password"
|
|
262
|
-
value={formData.confirmPassword}
|
|
263
|
-
onChangeText={(value) => handleChange('confirmPassword', value)}
|
|
264
|
-
placeholder="Confirm your password"
|
|
265
|
-
secureTextEntry
|
|
266
|
-
error={errors.confirmPassword}
|
|
267
|
-
required
|
|
268
|
-
/>
|
|
269
|
-
|
|
270
|
-
<Button title="Create Account" onPress={validate} />
|
|
271
|
-
</ScrollView>
|
|
272
|
-
);
|
|
132
|
+
const [email, setEmail] = useState('');
|
|
133
|
+
const [emailError, setEmailError] = useState('');
|
|
134
|
+
|
|
135
|
+
const validateEmail = (email) => {
|
|
136
|
+
if (!email) return 'Email is required';
|
|
137
|
+
if (!isValidEmail(email)) return 'Please enter a valid email address';
|
|
138
|
+
return '';
|
|
273
139
|
};
|
|
274
|
-
```
|
|
275
140
|
|
|
276
|
-
|
|
141
|
+
const handleChange = (text) => {
|
|
142
|
+
setEmail(text);
|
|
143
|
+
if (emailError) setEmailError(''); // Clear error on type
|
|
144
|
+
};
|
|
277
145
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
displayName: '',
|
|
282
|
-
username: '',
|
|
283
|
-
bio: '',
|
|
284
|
-
location: '',
|
|
285
|
-
website: '',
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
return (
|
|
289
|
-
<ScrollView style={{ padding: 16 }}>
|
|
290
|
-
<FormField
|
|
291
|
-
label="Display Name"
|
|
292
|
-
value={profile.displayName}
|
|
293
|
-
onChangeText={(value) => setProfile({ ...profile, displayName: value })}
|
|
294
|
-
placeholder="John Doe"
|
|
295
|
-
helperText="This is how you'll appear on your profile"
|
|
296
|
-
/>
|
|
297
|
-
|
|
298
|
-
<FormField
|
|
299
|
-
label="Username"
|
|
300
|
-
value={profile.username}
|
|
301
|
-
onChangeText={(value) => setProfile({ ...profile, username: value })}
|
|
302
|
-
placeholder="johndoe"
|
|
303
|
-
helperText="https://example.com/username"
|
|
304
|
-
leftIcon="at-outline"
|
|
305
|
-
/>
|
|
306
|
-
|
|
307
|
-
<FormField
|
|
308
|
-
label="Bio"
|
|
309
|
-
value={profile.bio}
|
|
310
|
-
onChangeText={(value) => setProfile({ ...profile, bio: value })}
|
|
311
|
-
placeholder="Tell us about yourself"
|
|
312
|
-
multiline
|
|
313
|
-
numberOfLines={4}
|
|
314
|
-
helperText="Maximum 150 characters"
|
|
315
|
-
/>
|
|
316
|
-
|
|
317
|
-
<FormField
|
|
318
|
-
label="Location"
|
|
319
|
-
value={profile.location}
|
|
320
|
-
onChangeText={(value) => setProfile({ ...profile, location: value })}
|
|
321
|
-
placeholder="New York, NY"
|
|
322
|
-
leftIcon="location-outline"
|
|
323
|
-
/>
|
|
324
|
-
|
|
325
|
-
<FormField
|
|
326
|
-
label="Website"
|
|
327
|
-
value={profile.website}
|
|
328
|
-
onChangeText={(value) => setProfile({ ...profile, website: value })}
|
|
329
|
-
placeholder="https://yourwebsite.com"
|
|
330
|
-
keyboardType="url"
|
|
331
|
-
autoCapitalize="none"
|
|
332
|
-
leftIcon="link-outline"
|
|
333
|
-
/>
|
|
334
|
-
|
|
335
|
-
<Button title="Save Changes" onPress={handleSave} />
|
|
336
|
-
</ScrollView>
|
|
337
|
-
);
|
|
146
|
+
const handleBlur = () => {
|
|
147
|
+
const error = validateEmail(email);
|
|
148
|
+
setEmailError(error);
|
|
338
149
|
};
|
|
339
|
-
```
|
|
340
150
|
|
|
341
|
-
|
|
151
|
+
<FormField
|
|
152
|
+
label="Email"
|
|
153
|
+
value={email}
|
|
154
|
+
onChangeText={handleChange}
|
|
155
|
+
onBlur={handleBlur}
|
|
156
|
+
error={emailError}
|
|
157
|
+
keyboardType="email-address"
|
|
158
|
+
autoCapitalize="none"
|
|
159
|
+
required
|
|
160
|
+
/>
|
|
161
|
+
```
|
|
342
162
|
|
|
163
|
+
❌ **DON'T**:
|
|
343
164
|
```tsx
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return (
|
|
355
|
-
<View style={{ padding: 16 }}>
|
|
356
|
-
<FormField
|
|
357
|
-
label="Street Address"
|
|
358
|
-
value={address.street}
|
|
359
|
-
onChangeText={(value) => setAddress({ ...address, street: value })}
|
|
360
|
-
placeholder="123 Main St"
|
|
361
|
-
required
|
|
362
|
-
/>
|
|
363
|
-
|
|
364
|
-
<FormField
|
|
365
|
-
label="Apartment/Suite (optional)"
|
|
366
|
-
value={address.apartment}
|
|
367
|
-
onChangeText={(value) => setAddress({ ...address, apartment: value })}
|
|
368
|
-
placeholder="Apt 4B"
|
|
369
|
-
/>
|
|
370
|
-
|
|
371
|
-
<FormField
|
|
372
|
-
label="City"
|
|
373
|
-
value={address.city}
|
|
374
|
-
onChangeText={(value) => setAddress({ ...address, city: value })}
|
|
375
|
-
placeholder="New York"
|
|
376
|
-
required
|
|
377
|
-
/>
|
|
378
|
-
|
|
379
|
-
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
380
|
-
<View style={{ flex: 1 }}>
|
|
381
|
-
<FormField
|
|
382
|
-
label="State"
|
|
383
|
-
value={address.state}
|
|
384
|
-
onChangeText={(value) => setAddress({ ...address, state: value })}
|
|
385
|
-
placeholder="NY"
|
|
386
|
-
required
|
|
387
|
-
/>
|
|
388
|
-
</View>
|
|
389
|
-
|
|
390
|
-
<View style={{ flex: 1 }}>
|
|
391
|
-
<FormField
|
|
392
|
-
label="ZIP Code"
|
|
393
|
-
value={address.zipCode}
|
|
394
|
-
onChangeText={(value) => setAddress({ ...address, zipCode: value })}
|
|
395
|
-
placeholder="10001"
|
|
396
|
-
keyboardType="number-pad"
|
|
397
|
-
required
|
|
398
|
-
/>
|
|
399
|
-
</View>
|
|
400
|
-
</View>
|
|
401
|
-
|
|
402
|
-
<FormField
|
|
403
|
-
label="Country"
|
|
404
|
-
value={address.country}
|
|
405
|
-
onChangeText={(value) => setAddress({ ...address, country: value })}
|
|
406
|
-
placeholder="United States"
|
|
407
|
-
required
|
|
408
|
-
/>
|
|
409
|
-
</View>
|
|
410
|
-
);
|
|
165
|
+
// ❌ Generic error
|
|
166
|
+
<FormField
|
|
167
|
+
label="Email"
|
|
168
|
+
error="Invalid input" // Not specific
|
|
169
|
+
/>
|
|
170
|
+
|
|
171
|
+
// ❌ Error persists
|
|
172
|
+
const handleChange = (text) => {
|
|
173
|
+
setEmail(text);
|
|
174
|
+
// Error still shows
|
|
411
175
|
};
|
|
412
176
|
```
|
|
413
177
|
|
|
414
|
-
###
|
|
178
|
+
### Helper Text
|
|
415
179
|
|
|
180
|
+
✅ **DO**:
|
|
416
181
|
```tsx
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
cvv: '',
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
const formatCardNumber = (text) => {
|
|
426
|
-
return text.replace(/\s/g, '').replace(/(.{4})/g, '$1 ').trim();
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
const formatExpiry = (text) => {
|
|
430
|
-
if (text.length === 2 && !text.includes('/')) {
|
|
431
|
-
return text + '/';
|
|
432
|
-
}
|
|
433
|
-
return text;
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
return (
|
|
437
|
-
<View style={{ padding: 16 }}>
|
|
438
|
-
<FormField
|
|
439
|
-
label="Card Number"
|
|
440
|
-
value={card.cardNumber}
|
|
441
|
-
onChangeText={(value) => setCard({ ...card, cardNumber: formatCardNumber(value) })}
|
|
442
|
-
placeholder="1234 5678 9012 3456"
|
|
443
|
-
keyboardType="number-pad"
|
|
444
|
-
maxLength={19}
|
|
445
|
-
leftIcon="card-outline"
|
|
446
|
-
required
|
|
447
|
-
/>
|
|
448
|
-
|
|
449
|
-
<FormField
|
|
450
|
-
label="Cardholder Name"
|
|
451
|
-
value={card.cardHolder}
|
|
452
|
-
onChangeText={(value) => setCard({ ...card, cardHolder: value })}
|
|
453
|
-
placeholder="JOHN DOE"
|
|
454
|
-
autoCapitalize="characters"
|
|
455
|
-
required
|
|
456
|
-
/>
|
|
457
|
-
|
|
458
|
-
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
459
|
-
<View style={{ flex: 1 }}>
|
|
460
|
-
<FormField
|
|
461
|
-
label="Expiry Date"
|
|
462
|
-
value={card.expiryDate}
|
|
463
|
-
onChangeText={(value) => setCard({ ...card, expiryDate: formatExpiry(value) })}
|
|
464
|
-
placeholder="MM/YY"
|
|
465
|
-
keyboardType="number-pad"
|
|
466
|
-
maxLength={5}
|
|
467
|
-
required
|
|
468
|
-
/>
|
|
469
|
-
</View>
|
|
470
|
-
|
|
471
|
-
<View style={{ flex: 1 }}>
|
|
472
|
-
<FormField
|
|
473
|
-
label="CVV"
|
|
474
|
-
value={card.cvv}
|
|
475
|
-
onChangeText={(value) => setCard({ ...card, cvv: value })}
|
|
476
|
-
placeholder="123"
|
|
477
|
-
keyboardType="number-pad"
|
|
478
|
-
maxLength={4}
|
|
479
|
-
secureTextEntry
|
|
480
|
-
helperText="3 or 4 digits on back of card"
|
|
481
|
-
required
|
|
482
|
-
/>
|
|
483
|
-
</View>
|
|
484
|
-
</View>
|
|
485
|
-
</View>
|
|
486
|
-
);
|
|
487
|
-
};
|
|
182
|
+
<FormField
|
|
183
|
+
label="Password"
|
|
184
|
+
helperText="Must be at least 8 characters with 1 number"
|
|
185
|
+
secureTextEntry
|
|
186
|
+
/>
|
|
488
187
|
```
|
|
489
188
|
|
|
490
|
-
|
|
491
|
-
|
|
189
|
+
❌ **DON'T**:
|
|
492
190
|
```tsx
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
minPrice: '',
|
|
498
|
-
maxPrice: '',
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
return (
|
|
502
|
-
<View style={{ padding: 16 }}>
|
|
503
|
-
<FormField
|
|
504
|
-
label="Search"
|
|
505
|
-
value={searchTerm}
|
|
506
|
-
onChangeText={setSearchTerm}
|
|
507
|
-
placeholder="What are you looking for?"
|
|
508
|
-
leftIcon="search-outline"
|
|
509
|
-
/>
|
|
510
|
-
|
|
511
|
-
<FormField
|
|
512
|
-
label="Category"
|
|
513
|
-
value={filters.category}
|
|
514
|
-
onChangeText={(value) => setFilters({ ...filters, category: value })}
|
|
515
|
-
placeholder="Select a category"
|
|
516
|
-
rightIcon="chevron-down-outline"
|
|
517
|
-
/>
|
|
518
|
-
|
|
519
|
-
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
520
|
-
<View style={{ flex: 1 }}>
|
|
521
|
-
<FormField
|
|
522
|
-
label="Min Price"
|
|
523
|
-
value={filters.minPrice}
|
|
524
|
-
onChangeText={(value) => setFilters({ ...filters, minPrice: value })}
|
|
525
|
-
placeholder="$0"
|
|
526
|
-
keyboardType="number-pad"
|
|
527
|
-
/>
|
|
528
|
-
</View>
|
|
529
|
-
|
|
530
|
-
<View style={{ flex: 1 }}>
|
|
531
|
-
<FormField
|
|
532
|
-
label="Max Price"
|
|
533
|
-
value={filters.maxPrice}
|
|
534
|
-
onChangeText={(value) => setFilters({ ...filters, maxPrice: value })}
|
|
535
|
-
placeholder="$1000"
|
|
536
|
-
keyboardType="number-pad"
|
|
537
|
-
/>
|
|
538
|
-
</View>
|
|
539
|
-
</View>
|
|
540
|
-
|
|
541
|
-
<Button title="Search" onPress={handleSearch} />
|
|
542
|
-
</View>
|
|
543
|
-
);
|
|
544
|
-
};
|
|
191
|
+
<FormField
|
|
192
|
+
label="Password"
|
|
193
|
+
helperText="Enter your password here" // Obvious
|
|
194
|
+
/>
|
|
545
195
|
```
|
|
546
196
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
### FormFieldProps
|
|
550
|
-
|
|
551
|
-
Extends `Omit<AtomicInputProps, 'state' | 'label'>`
|
|
552
|
-
|
|
553
|
-
| Prop | Type | Default | Description |
|
|
554
|
-
|------|------|---------|-------------|
|
|
555
|
-
| `label` | `string` | - | Field label |
|
|
556
|
-
| `error` | `string` | - | Error message |
|
|
557
|
-
| `helperText` | `string` | - | Helper text |
|
|
558
|
-
| `required` | `boolean` | `false` | Show required indicator |
|
|
559
|
-
| `containerStyle` | `ViewStyle` | - | Container style |
|
|
560
|
-
| `style` | `ViewStyle` | - | Alias for containerStyle |
|
|
561
|
-
| `requiredIndicator` | `string` | `' *'` | Required indicator text |
|
|
562
|
-
|
|
563
|
-
Plus all AtomicInput props:
|
|
564
|
-
- `value`, `onChangeText`, `placeholder`, `secureTextEntry`, `keyboardType`, etc.
|
|
565
|
-
|
|
566
|
-
## Best Practices
|
|
567
|
-
|
|
568
|
-
### 1. Error Handling
|
|
197
|
+
### Required Fields
|
|
569
198
|
|
|
199
|
+
✅ **DO**:
|
|
570
200
|
```tsx
|
|
571
|
-
// ✅ Good: Clear specific errors
|
|
572
201
|
<FormField
|
|
573
|
-
|
|
202
|
+
label="Email"
|
|
203
|
+
required
|
|
204
|
+
error={emailError}
|
|
574
205
|
/>
|
|
206
|
+
```
|
|
575
207
|
|
|
576
|
-
|
|
208
|
+
❌ **DON'T**:
|
|
209
|
+
```tsx
|
|
210
|
+
// ❌ No visual required indicator
|
|
577
211
|
<FormField
|
|
578
|
-
|
|
212
|
+
label="Email"
|
|
213
|
+
error={emailError || '* Required'}
|
|
579
214
|
/>
|
|
580
215
|
```
|
|
581
216
|
|
|
582
|
-
|
|
583
|
-
|
|
217
|
+
## AI Coding Guidelines
|
|
218
|
+
|
|
219
|
+
### For AI Agents
|
|
220
|
+
|
|
221
|
+
When generating FormField components, follow these rules:
|
|
222
|
+
|
|
223
|
+
1. **Always import from correct path**:
|
|
224
|
+
```typescript
|
|
225
|
+
import { FormField } from 'react-native-design-system/src/molecules/FormField';
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
2. **Always provide a label**:
|
|
229
|
+
```tsx
|
|
230
|
+
// ✅ Good
|
|
231
|
+
<FormField
|
|
232
|
+
label="Email"
|
|
233
|
+
value={email}
|
|
234
|
+
onChangeText={setEmail}
|
|
235
|
+
/>
|
|
236
|
+
|
|
237
|
+
// ❌ Bad - no label
|
|
238
|
+
<FormField
|
|
239
|
+
value={email}
|
|
240
|
+
onChangeText={setEmail}
|
|
241
|
+
placeholder="Email" // Placeholder is not a label
|
|
242
|
+
/>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
3. **Always handle errors properly**:
|
|
246
|
+
```tsx
|
|
247
|
+
// ✅ Good - clear errors on type
|
|
248
|
+
const handleChange = (text) => {
|
|
249
|
+
setValue(text);
|
|
250
|
+
if (error) setError('');
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// ❌ Bad - error persists
|
|
254
|
+
const handleChange = (text) => {
|
|
255
|
+
setValue(text);
|
|
256
|
+
// Error still shows
|
|
257
|
+
};
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
4. **Always use specific error messages**:
|
|
261
|
+
```tsx
|
|
262
|
+
// ✅ Good - specific errors
|
|
263
|
+
const validateEmail = (email) => {
|
|
264
|
+
if (!email) return 'Email is required';
|
|
265
|
+
if (!isValidEmail(email)) return 'Please enter a valid email address';
|
|
266
|
+
if (!isCompanyEmail(email)) return 'Please use your company email';
|
|
267
|
+
return '';
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// ❌ Bad - generic error
|
|
271
|
+
const validate = (value) => {
|
|
272
|
+
if (!value) return 'Invalid';
|
|
273
|
+
};
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
5. **Always use appropriate keyboard type**:
|
|
277
|
+
```tsx
|
|
278
|
+
// ✅ Good - correct keyboard types
|
|
279
|
+
<FormField
|
|
280
|
+
label="Email"
|
|
281
|
+
keyboardType="email-address"
|
|
282
|
+
autoCapitalize="none"
|
|
283
|
+
/>
|
|
284
|
+
<FormField
|
|
285
|
+
label="ZIP Code"
|
|
286
|
+
keyboardType="number-pad"
|
|
287
|
+
/>
|
|
288
|
+
<FormField
|
|
289
|
+
label="Website"
|
|
290
|
+
keyboardType="url"
|
|
291
|
+
/>
|
|
292
|
+
|
|
293
|
+
// ❌ Bad - always default
|
|
294
|
+
<FormField
|
|
295
|
+
label="Email"
|
|
296
|
+
keyboardType="default"
|
|
297
|
+
/>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Common Patterns
|
|
301
|
+
|
|
302
|
+
#### Basic Form Field
|
|
584
303
|
```tsx
|
|
585
|
-
// ✅ Good: Helpful guidance
|
|
586
304
|
<FormField
|
|
587
|
-
|
|
305
|
+
label="Email"
|
|
306
|
+
value={email}
|
|
307
|
+
onChangeText={setEmail}
|
|
308
|
+
placeholder="your@email.com"
|
|
309
|
+
keyboardType="email-address"
|
|
310
|
+
autoCapitalize="none"
|
|
311
|
+
required
|
|
588
312
|
/>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
#### Field with Error Handling
|
|
316
|
+
```tsx
|
|
317
|
+
const [email, setEmail] = useState('');
|
|
318
|
+
const [error, setError] = useState('');
|
|
319
|
+
|
|
320
|
+
const handleChange = (text) => {
|
|
321
|
+
setEmail(text);
|
|
322
|
+
if (error) setError('');
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const handleBlur = () => {
|
|
326
|
+
if (!email) setError('Email is required');
|
|
327
|
+
else if (!isValidEmail(email)) setError('Please enter a valid email');
|
|
328
|
+
};
|
|
589
329
|
|
|
590
|
-
// ❌ Bad: Obvious info
|
|
591
330
|
<FormField
|
|
592
|
-
|
|
331
|
+
label="Email"
|
|
332
|
+
value={email}
|
|
333
|
+
onChangeText={handleChange}
|
|
334
|
+
onBlur={handleBlur}
|
|
335
|
+
error={error}
|
|
336
|
+
required
|
|
593
337
|
/>
|
|
594
338
|
```
|
|
595
339
|
|
|
596
|
-
|
|
340
|
+
#### Field with Helper Text
|
|
341
|
+
```tsx
|
|
342
|
+
<FormField
|
|
343
|
+
label="Password"
|
|
344
|
+
value={password}
|
|
345
|
+
onChangeText={setPassword}
|
|
346
|
+
placeholder="Enter password"
|
|
347
|
+
secureTextEntry
|
|
348
|
+
helperText="Must be at least 8 characters with 1 number"
|
|
349
|
+
required
|
|
350
|
+
/>
|
|
351
|
+
```
|
|
597
352
|
|
|
353
|
+
#### Field with Icons
|
|
598
354
|
```tsx
|
|
599
|
-
// ✅ Good: Use sparingly
|
|
600
355
|
<FormField
|
|
601
356
|
label="Email"
|
|
602
|
-
|
|
357
|
+
value={email}
|
|
358
|
+
onChangeText={setEmail}
|
|
359
|
+
placeholder="your@email.com"
|
|
360
|
+
leftIcon="mail-outline"
|
|
361
|
+
keyboardType="email-address"
|
|
603
362
|
/>
|
|
363
|
+
```
|
|
604
364
|
|
|
605
|
-
|
|
365
|
+
#### Multiline Field
|
|
366
|
+
```tsx
|
|
606
367
|
<FormField
|
|
607
|
-
label="
|
|
608
|
-
|
|
368
|
+
label="Bio"
|
|
369
|
+
value={bio}
|
|
370
|
+
onChangeText={setBio}
|
|
371
|
+
placeholder="Tell us about yourself"
|
|
372
|
+
multiline
|
|
373
|
+
numberOfLines={4}
|
|
374
|
+
helperText="Maximum 150 characters"
|
|
609
375
|
/>
|
|
610
376
|
```
|
|
611
377
|
|
|
612
|
-
##
|
|
378
|
+
## Props Reference
|
|
379
|
+
|
|
380
|
+
| Prop | Type | Required | Default | Description |
|
|
381
|
+
|------|------|----------|---------|-------------|
|
|
382
|
+
| `label` | `string` | Yes | - | Field label |
|
|
383
|
+
| `value` | `string` | Yes | - | Input value |
|
|
384
|
+
| `onChangeText` | `(text: string) => void` | Yes | - | Change callback |
|
|
385
|
+
| `error` | `string` | No | - | Error message |
|
|
386
|
+
| `helperText` | `string` | No | - | Helper text |
|
|
387
|
+
| `required` | `boolean` | No | `false` | Show required indicator |
|
|
388
|
+
| `placeholder` | `string` | No | - | Placeholder text |
|
|
389
|
+
| `secureTextEntry` | `boolean` | No | `false` | Password field |
|
|
390
|
+
| `keyboardType` | `KeyboardType` | No | `'default'` | Keyboard type |
|
|
391
|
+
| `autoCapitalize` | `'none' \| 'sentences' \| 'words' \| 'characters'` | No | - | Auto capitalize |
|
|
392
|
+
| `multiline` | `boolean` | No | `false` | Multiline input |
|
|
393
|
+
| `numberOfLines` | `number` | No | - | Number of lines |
|
|
394
|
+
| `leftIcon` | `string` | No | - | Left icon name |
|
|
395
|
+
| `rightIcon` | `string` | No | - | Right icon name |
|
|
396
|
+
| `onBlur` | `() => void` | No | - | Blur callback |
|
|
397
|
+
| `onFocus` | `() => void` | No | - | Focus callback |
|
|
613
398
|
|
|
614
|
-
|
|
399
|
+
## Accessibility
|
|
615
400
|
|
|
616
|
-
- ✅ Screen reader
|
|
617
|
-
- ✅ Error
|
|
618
|
-
- ✅ Required
|
|
619
|
-
- ✅ Helper text
|
|
620
|
-
- ✅
|
|
401
|
+
- ✅ Screen reader announces label and current value
|
|
402
|
+
- ✅ Error messages are announced to screen readers
|
|
403
|
+
- ✅ Required fields are indicated visually
|
|
404
|
+
- ✅ Helper text provides additional context
|
|
405
|
+
- ✅ Proper label-input association
|
|
406
|
+
- ✅ Touch target size maintained (min 44x44pt)
|
|
621
407
|
|
|
622
408
|
## Performance Tips
|
|
623
409
|
|
|
624
|
-
1. **Controlled
|
|
625
|
-
2. **Validation
|
|
410
|
+
1. **Controlled inputs**: Always use controlled inputs with state
|
|
411
|
+
2. **Validation debounce**: Debounce validation for better UX
|
|
626
412
|
3. **Memoization**: Memo validation functions
|
|
413
|
+
4. **Clear errors**: Clear errors immediately on input change
|
|
627
414
|
|
|
628
415
|
## Related Components
|
|
629
416
|
|
|
630
417
|
- [`AtomicInput`](../atoms/input/README.md) - Base input component
|
|
631
418
|
- [`AtomicText`](../atoms/AtomicText/README.md) - Text component
|
|
632
419
|
- [`Button`](../atoms/button/README.md) - Button component
|
|
633
|
-
- [`
|
|
420
|
+
- [`BaseModal`](./BaseModal/README.md) - Modal for forms
|
|
634
421
|
|
|
635
422
|
## License
|
|
636
423
|
|