@hoddy-ui/core 2.5.49 → 2.5.52

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.
@@ -14,371 +14,80 @@ import {
14
14
  ms,
15
15
  verticalScale,
16
16
  } from "react-native-size-matters";
17
- import { useColors } from "../hooks";
17
+ import { useColors, useTheme } from "../hooks";
18
18
  import { TextFieldProps } from "../types";
19
19
  import { getFontFamily } from "../utility";
20
20
  import SelectMenu from "./SelectMenu";
21
21
  import Typography from "./Typography";
22
22
 
23
- const TextField: React.FC<TextFieldProps> = ({
24
- label,
25
- keyboardType,
26
- variant,
27
- color = "primary",
28
- value,
29
- type,
30
- placeholder = "",
31
- helperText,
32
- onChangeText,
33
- onSubmitEditing = () => {},
34
- onFocus = () => {},
35
- onBlur = () => {},
36
- error,
37
- start,
38
- size = "normal",
39
- rounded,
40
- disabled = false,
41
- style = {},
42
- inputStyles = {},
43
- gutterBottom = 0,
44
- end,
45
- options,
46
- selectMenuProps,
47
- ...props
48
- }) => {
49
- const colors = useColors();
50
- const [focused, setFocused] = useState(false);
51
- const [datePickerVisible, setDatePickerVisible] = useState(false);
52
- const isDate = type === "date";
53
- const height =
54
- moderateScale(variant === "text" ? 50 : 45) *
55
- (size === "large" ? 1.2 : size === "small" ? 0.8 : 1);
23
+ export type TextFieldLabelVariant = "floating" | "external";
56
24
 
57
- const labelAnim = useRef(
58
- new Animated.Value(height / moderateScale(variant === "text" ? 2.5 : 3.2)),
59
- ).current;
25
+ export interface TextFieldBaseProps extends TextFieldProps {
26
+ labelVariant: TextFieldLabelVariant;
27
+ }
60
28
 
61
- React.useEffect(() => {
62
- if (focused || value) {
63
- Animated.timing(labelAnim, {
64
- toValue: verticalScale(variant === "text" ? 2 : 4),
65
- duration: 300,
66
- useNativeDriver: false,
67
- }).start();
68
- } else {
69
- Animated.timing(labelAnim, {
70
- toValue: height / moderateScale(variant === "text" ? 2.5 : 3.2),
71
- duration: 300,
72
- useNativeDriver: false,
73
- }).start();
74
- }
75
- }, [focused, value]);
76
- const styles: any = ScaledSheet.create({
77
- root: {
78
- marginBottom: ms(gutterBottom),
79
- width: "100%",
80
- ...style,
81
- },
82
- container: {
83
- height: height,
84
- overflow: "hidden",
85
-
86
- backgroundColor:
87
- variant === "outlined" || variant === "text"
88
- ? "#fff0"
89
- : focused
90
- ? colors.white[3]
91
- : colors.white[4],
92
- flexDirection: "row",
93
- borderColor: error
94
- ? colors.error.main
95
- : focused
96
- ? colors[color].main
97
- : colors.textSecondary.main,
98
- borderWidth: error ? 1 : variant === "outlined" ? (focused ? 2 : 0.5) : 0,
99
- borderBottomWidth: variant === "text" ? 0.5 : undefined,
100
- width: "100%",
101
- borderRadius: variant === "text" ? 0 : rounded ? 30 : 7,
102
- alignItems: "center",
103
- ...inputStyles,
104
- },
105
- input: {
106
- fontSize: "14@s",
107
- flex: 1,
108
- alignSelf: "stretch",
109
- paddingLeft: variant === "text" ? 0 : moderateScale(15),
110
- paddingRight: moderateScale(10),
111
- paddingTop: "11@vs",
112
- fontFamily: getFontFamily(400),
113
- color: colors.black[1],
114
- zIndex: 10,
115
- // backgroundColor: "#284",
116
- },
117
- inputText: {
118
- fontSize: "14@ms",
119
- flex: 1,
120
- paddingLeft: variant === "text" ? 0 : moderateScale(15),
121
- paddingTop: "13@ms",
122
- },
123
- dateContent: {
124
- flexDirection: "row",
125
- alignItems: "center",
126
- flex: 1,
127
- paddingLeft: variant === "text" ? 0 : moderateScale(15),
128
- paddingRight: moderateScale(10),
129
- paddingTop: variant === "text" ? ms(13) : ms(12),
130
- },
131
- dateText: {
132
- fontSize: "14@ms",
133
- flex: 1,
134
- },
135
- datePlaceholder: {
136
- color: colors.textSecondary.main,
137
- },
138
- label: {
139
- fontFamily: getFontFamily(400),
140
- position: "absolute",
141
- left: variant === "text" ? 0 : moderateScale(15),
142
- fontSize: focused || value ? "10@s" : "13@s",
143
- color: focused ? colors[color].main : colors.textSecondary.main,
144
- },
145
- helperText: {
146
- paddingHorizontal: "15@s",
147
- flex: 1,
148
- color: focused ? colors[color].dark : colors.textSecondary.main,
149
- paddingTop: "4@ms",
150
- },
151
- error: {
152
- paddingLeft: 10,
153
- paddingRight: 10,
154
- paddingTop: 5,
155
- flexDirection: "row",
156
- alignItems: "center",
157
- },
158
- errorText: {
159
- fontSize: 12,
160
- marginLeft: 10,
161
- },
162
- });
163
- const formProps: any =
164
- type === "email"
165
- ? {
166
- textContentType: "emailAddress",
167
- keyboardType: "email-address",
168
- autoCapitalize: "none",
169
- autoCompleteType: "email",
170
- }
171
- : type === "number"
29
+ const formPropsFromType = (
30
+ type: TextFieldProps["type"],
31
+ showPassword: boolean,
32
+ ): Record<string, unknown> =>
33
+ type === "email"
34
+ ? {
35
+ textContentType: "emailAddress",
36
+ keyboardType: "email-address",
37
+ autoCapitalize: "none",
38
+ autoCompleteType: "email",
39
+ }
40
+ : type === "number"
41
+ ? { keyboardType: "numeric" }
42
+ : type === "tel"
172
43
  ? {
173
- keyboardType: "numeric",
44
+ textContentType: "telephoneNumber",
45
+ keyboardType: "phone-pad",
174
46
  }
175
- : type === "tel"
47
+ : type === "search"
176
48
  ? {
177
- textContentType: "telephoneNumber",
178
- keyboardType: "phone-pad",
49
+ keyboardType: "web-search",
50
+ returnKeyType: "search",
51
+ autoCapitalize: "none",
179
52
  }
180
- : type === "search"
53
+ : type === "password"
181
54
  ? {
182
- keyboardType: "web-search",
183
- returnKeyType: "search",
55
+ secureTextEntry: !showPassword,
56
+ autoCompleteType: "password",
184
57
  autoCapitalize: "none",
58
+ textContentType: "password",
185
59
  }
186
- : type === "password"
187
- ? {
188
- secureTextEntry: true,
189
- autoCompleteType: "password",
190
- autoCapitalize: "none",
191
- textContentType: "password",
192
- }
193
- : {};
194
- const parseDateValue = () => {
195
- if (!value) return new Date();
196
- if (value instanceof Date) return value;
197
- const parts = `${value}`.split("/");
198
- if (parts.length === 3) {
199
- const [day, month, year] = parts;
200
- const parsed = new Date(
201
- parseInt(year, 10),
202
- parseInt(month, 10) - 1,
203
- parseInt(day, 10),
204
- );
205
- if (!isNaN(parsed.getTime())) return parsed;
206
- }
207
- const fallback = new Date(value);
208
- return isNaN(fallback.getTime()) ? new Date() : fallback;
209
- };
210
-
211
- const handleDateConfirm = (date: Date) => {
212
- const day = date.getDate();
213
- const month = date.getMonth() + 1;
214
- const year = date.getFullYear();
215
- const dateString = `${day}/${month}/${year}`;
216
- onChangeText?.(dateString);
217
- setDatePickerVisible(false);
218
- };
219
-
220
- const handleContainerPress = () => {
221
- if (disabled) return;
222
- setFocused(true);
223
- if (isDate) {
224
- onFocus();
225
- setDatePickerVisible(true);
226
- }
227
- };
228
- return (
229
- <>
230
- <View style={styles.root}>
231
- <TouchableOpacity
232
- onPress={handleContainerPress}
233
- style={styles.container}
234
- >
235
- <Animated.Text style={{ ...styles.label, top: labelAnim }}>
236
- {label}
237
- </Animated.Text>
238
- {start}
239
- {options ? (
240
- <View
241
- style={{ flex: 1, alignItems: "center", flexDirection: "row" }}
242
- >
243
- {options.find((cur) => cur.value === value)?.start && (
244
- <View
245
- style={{
246
- paddingTop: variant !== "outlined" ? ms(13) : 0,
247
- paddingRight: 10,
248
- }}
249
- >
250
- {options.find((cur) => cur.value === value)?.start}
251
- </View>
252
- )}
60
+ : {};
253
61
 
254
- <Typography style={styles.inputText}>
255
- {options.find((cur) => cur.value === value)?.label}
256
- </Typography>
257
- </View>
258
- ) : isDate ? (
259
- <View style={styles.dateContent}>
260
- <Typography
261
- style={[
262
- styles.dateText,
263
- !value ? styles.datePlaceholder : undefined,
264
- ]}
265
- color={value ? "dark" : "textSecondary"}
266
- >
267
- {value || placeholder}
268
- </Typography>
269
- <View style={{ marginLeft: 8 }}>
270
- {end ?? (
271
- <Ionicons
272
- name="calendar-outline"
273
- size={22}
274
- color={colors.textSecondary.main}
275
- />
276
- )}
277
- </View>
278
- </View>
279
- ) : (
280
- <TextInput
281
- onFocus={() => {
282
- onFocus();
283
- setFocused(true);
284
- }}
285
- onBlur={() => {
286
- onBlur();
287
- setFocused(false);
288
- }}
289
- value={value}
290
- onChangeText={onChangeText}
291
- keyboardType={keyboardType}
292
- editable={!disabled}
293
- selectTextOnFocus={!disabled}
294
- onSubmitEditing={onSubmitEditing}
295
- placeholderTextColor={colors.textSecondary.main}
296
- {...formProps}
297
- {...props}
298
- style={styles.input}
299
- />
300
- )}
301
- {end && (
302
- <View
303
- style={{
304
- marginRight: 20,
305
- paddingTop: variant === "text" ? ms(13) : 0,
306
- }}
307
- >
308
- {end}
309
- </View>
310
- )}
311
- {options && (
312
- <View
313
- style={{
314
- marginRight: variant === "text" ? 0 : 20,
315
- paddingTop: variant === "text" ? ms(13) : 0,
316
- }}
317
- >
318
- <Ionicons
319
- name="chevron-down"
320
- color={colors.textSecondary.main}
321
- size={24}
322
- />
323
- </View>
324
- )}
325
- </TouchableOpacity>
326
- {helperText && (
327
- <Typography
328
- color="textSecondary"
329
- style={styles.helperText}
330
- variant="caption"
331
- >
332
- {helperText}
333
- </Typography>
334
- )}
335
- {error && (
336
- <View style={styles.error}>
337
- <MaterialIcons name="error" color={colors.error.main} size={16} />
338
- <Typography style={styles.errorText} color="error">
339
- {error}
340
- </Typography>
341
- </View>
342
- )}
343
- </View>
344
- {options && (
345
- <SelectMenu
346
- options={options}
347
- value={value}
348
- open={focused}
349
- onClose={() => setFocused(false)}
350
- label={label}
351
- helperText={helperText}
352
- onChange={onChangeText!}
353
- {...selectMenuProps}
354
- />
355
- )}
356
- {isDate && (
357
- <DateTimePickerModal
358
- isVisible={datePickerVisible}
359
- mode="date"
360
- date={parseDateValue()}
361
- onConfirm={handleDateConfirm}
362
- onCancel={() => {
363
- setDatePickerVisible(false);
364
- setFocused(false);
365
- }}
366
- />
367
- )}
368
- </>
369
- );
370
- };
62
+ function parseDateValue(value: unknown): Date {
63
+ if (!value) return new Date();
64
+ if (value instanceof Date) return value;
65
+ const isoParts = `${value}`.split("-");
66
+ if (isoParts.length === 3) {
67
+ const [year, month, day] = isoParts;
68
+ const parsed = new Date(
69
+ parseInt(year, 10),
70
+ parseInt(month, 10) - 1,
71
+ parseInt(day, 10),
72
+ );
73
+ if (!isNaN(parsed.getTime())) return parsed;
74
+ }
75
+ const fallback = new Date(value as string);
76
+ return isNaN(fallback.getTime()) ? new Date() : fallback;
77
+ }
371
78
 
372
- export const TextField2 = React.forwardRef<TextInput, TextFieldProps>(
79
+ export const TextFieldBase = React.forwardRef<TextInput, TextFieldBaseProps>(
373
80
  (
374
81
  {
375
82
  label,
376
83
  labelProps,
84
+ labelVariant,
377
85
  keyboardType,
86
+ variant = "outlined",
378
87
  color = "primary",
379
88
  value,
380
89
  type,
381
- placeholder,
90
+ placeholder = "",
382
91
  helperText,
383
92
  onChangeText,
384
93
  onSubmitEditing = () => {},
@@ -386,99 +95,204 @@ export const TextField2 = React.forwardRef<TextInput, TextFieldProps>(
386
95
  onBlur = () => {},
387
96
  error,
388
97
  start,
98
+ size = "normal",
389
99
  rounded,
390
100
  disabled = false,
391
101
  style = {},
392
102
  inputStyles = {},
393
- gutterBottom = 8,
103
+ gutterBottom,
394
104
  end,
395
105
  options,
396
106
  multiline,
397
107
  selectMenuProps,
108
+ labelAlwaysOpen,
398
109
  ...props
399
110
  },
400
111
  ref,
401
112
  ) => {
402
113
  const colors = useColors();
114
+ const theme = useTheme();
403
115
  const [focused, _setFocused] = useState(false);
404
116
  const [showPassword, setShowPassword] = useState(false);
405
117
  const [datePickerVisible, setDatePickerVisible] = useState(false);
406
118
  const isDate = type === "date";
407
119
 
408
- const height = moderateScale(
120
+ const setFocused = (next: boolean) => {
121
+ if (options && next) {
122
+ Keyboard.dismiss();
123
+ setTimeout(() => _setFocused(next), 100);
124
+ } else {
125
+ _setFocused(next);
126
+ }
127
+ };
128
+
129
+ const isFloating = labelVariant === "floating";
130
+ const labelOpen = labelAlwaysOpen || focused || value;
131
+
132
+ const baseHeight = moderateScale(
409
133
  multiline ? 50 + (props.numberOfLines || 1) * 18 : 50,
410
134
  );
135
+ const sizeMultiplier = isFloating
136
+ ? size === "large"
137
+ ? 1.2
138
+ : size === "small"
139
+ ? 0.8
140
+ : 1
141
+ : 1;
142
+ const height = baseHeight * sizeMultiplier;
411
143
 
412
- const setFocused = (value: boolean) => {
413
- if (options && value) {
414
- Keyboard.dismiss();
415
- setTimeout(() => {
416
- _setFocused(value);
417
- }, 100);
144
+ const labelAnim = useRef(
145
+ new Animated.Value(
146
+ height / moderateScale(variant === "text" ? 2.5 : 3.2),
147
+ ),
148
+ ).current;
149
+
150
+ React.useEffect(() => {
151
+ if (!isFloating) return;
152
+ if (labelOpen) {
153
+ Animated.timing(labelAnim, {
154
+ toValue: verticalScale(variant === "text" ? 2 : 4),
155
+ duration: 300,
156
+ useNativeDriver: false,
157
+ }).start();
418
158
  } else {
419
- _setFocused(value);
159
+ Animated.timing(labelAnim, {
160
+ toValue: height / moderateScale(variant === "text" ? 2.5 : 3.2),
161
+ duration: 300,
162
+ useNativeDriver: false,
163
+ }).start();
164
+ }
165
+ }, [isFloating, labelOpen, variant, height]);
166
+
167
+ const formProps = formPropsFromType(type, showPassword);
168
+
169
+ const handleDateConfirm = (date: Date) => {
170
+ const year = date.getFullYear();
171
+ const month = `${date.getMonth() + 1}`.padStart(2, "0");
172
+ const day = `${date.getDate()}`.padStart(2, "0");
173
+ onChangeText?.(`${year}-${month}-${day}`);
174
+ setDatePickerVisible(false);
175
+ setFocused(false);
176
+ };
177
+
178
+ const handleContainerPress = () => {
179
+ if (disabled) return;
180
+ setFocused(true);
181
+ if (isDate) {
182
+ onFocus();
183
+ setDatePickerVisible(true);
420
184
  }
421
185
  };
422
186
 
187
+ const inputPadding =
188
+ variant === "text" ? 0 : moderateScale(isFloating ? 15 : 10);
189
+ const containerBorderRadius =
190
+ variant === "text" ? 0 : rounded ? 30 : isFloating ? 7 : 10;
191
+ const datePlaceholderColor = colors.textSecondary.light;
192
+
423
193
  const styles: any = ScaledSheet.create({
424
194
  root: {
425
- marginBottom: ms(gutterBottom),
195
+ marginBottom: ms(gutterBottom ?? (isFloating ? 0 : 8)),
196
+ width: "100%",
426
197
  ...style,
427
198
  },
428
199
  container: {
429
200
  height: height,
430
201
  overflow: "hidden",
431
202
  flexDirection: "row",
203
+ backgroundColor:
204
+ variant === "outlined" || variant === "text"
205
+ ? "#fff0"
206
+ : focused
207
+ ? colors.white[2] + "dd"
208
+ : colors.white[2],
432
209
  borderColor: error
433
210
  ? colors.error.main
434
- : focused
435
- ? colors[color].main
436
- : colors.white[4],
437
- borderWidth: error ? 1 : focused ? 2 : 1,
211
+ : isFloating && focused && variant === "contained"
212
+ ? colors[color].light
213
+ : focused
214
+ ? colors[color].main
215
+ : colors.textSecondary.main,
216
+ borderWidth: error
217
+ ? 1
218
+ : isFloating && focused && variant === "contained"
219
+ ? 1
220
+ : variant === "outlined"
221
+ ? focused
222
+ ? 2
223
+ : 0.5
224
+ : 0,
225
+ borderBottomWidth: variant === "text" ? 0.5 : undefined,
438
226
  width: "100%",
439
- borderRadius: rounded ? 30 : 10,
227
+ borderRadius: containerBorderRadius,
440
228
  alignItems: multiline ? "flex-start" : "center",
441
229
  paddingVertical: multiline ? 10 : 0,
442
230
  ...inputStyles,
443
231
  },
444
232
  input: {
445
- fontSize: "14@s",
233
+ fontSize: "14@ms",
446
234
  flex: 1,
447
235
  alignSelf: "stretch",
448
- paddingLeft: moderateScale(10),
236
+ padding: 0,
237
+ paddingLeft: inputPadding,
238
+ lineHeight: "18@ms",
449
239
  paddingRight: moderateScale(10),
450
- color: colors.dark.main,
240
+ ...(isFloating
241
+ ? { marginTop: "13@ms", fontFamily: getFontFamily(400) }
242
+ : {}),
243
+ color: disabled ? colors.textSecondary.main : colors.dark.main,
451
244
  zIndex: 10,
452
- // backgroundColor: "#284",
453
245
  },
454
246
  inputText: {
455
247
  fontSize: "14@ms",
456
- color: colors.dark.main,
457
- paddingLeft: moderateScale(10),
458
- },
459
- placeholder: {
460
- fontSize: "14@ms",
461
- color: colors.textSecondary.light,
462
- paddingLeft: moderateScale(10),
248
+ flex: 1,
249
+ paddingLeft: inputPadding,
250
+ ...(isFloating ? { paddingTop: "13@ms" } : {}),
463
251
  },
464
252
  dateContent: {
465
253
  flexDirection: "row",
466
254
  alignItems: "center",
467
255
  flex: 1,
468
- paddingHorizontal: moderateScale(10),
469
- paddingTop: multiline ? 4 : 0,
470
- },
471
- dateText: {
472
- fontSize: "14@ms",
473
- flex: 1,
474
- },
475
- datePlaceholder: {
476
- color: colors.textSecondary.light,
256
+ paddingLeft: inputPadding,
257
+ paddingRight: moderateScale(10),
258
+ paddingTop: isFloating
259
+ ? variant === "text"
260
+ ? ms(13)
261
+ : ms(12)
262
+ : multiline
263
+ ? 4
264
+ : 0,
477
265
  },
478
- label: {},
266
+ dateText: { fontSize: "14@ms", flex: 1 },
267
+ datePlaceholder: { color: datePlaceholderColor },
268
+ ...(isFloating
269
+ ? {
270
+ contentWrapper: { flex: 1, position: "relative" as const },
271
+ label: {
272
+ fontFamily: getFontFamily(400),
273
+ position: "absolute",
274
+ left: inputPadding,
275
+ fontSize: labelOpen ? "10@ms" : "13@ms",
276
+ color: error
277
+ ? colors.error.main
278
+ : focused
279
+ ? theme === "dark"
280
+ ? colors[color].light
281
+ : colors[color].main
282
+ : colors.textSecondary.main,
283
+ },
284
+ }
285
+ : {}),
479
286
  helperText: {
480
- paddingHorizontal: "15@s",
481
- color: focused ? colors[color].dark : "#fffa",
287
+ paddingHorizontal: "15@ms",
288
+ flex: 1,
289
+ color: error
290
+ ? colors.error.main
291
+ : focused
292
+ ? theme === "dark"
293
+ ? colors[color].light
294
+ : colors[color].main
295
+ : colors.textSecondary.main,
482
296
  paddingTop: "4@ms",
483
297
  },
484
298
  error: {
@@ -488,100 +302,160 @@ export const TextField2 = React.forwardRef<TextInput, TextFieldProps>(
488
302
  flexDirection: "row",
489
303
  alignItems: "center",
490
304
  },
491
- errorText: {
492
- fontSize: 12,
493
- marginLeft: 10,
305
+ errorText: { fontSize: 12, marginLeft: 10, fontWeight: "400" },
306
+ placeholder: {
307
+ fontSize: "14@ms",
308
+ color: colors.textSecondary.light,
309
+ paddingLeft: inputPadding,
494
310
  },
495
311
  });
496
- const formProps: any =
497
- type === "email"
498
- ? {
499
- textContentType: "emailAddress",
500
- keyboardType: "email-address",
501
- autoCapitalize: "none",
502
- autoCompleteType: "email",
503
- }
504
- : type === "number"
505
- ? {
506
- keyboardType: "numeric",
507
- }
508
- : type === "tel"
509
- ? {
510
- textContentType: "telephoneNumber",
511
- keyboardType: "phone-pad",
512
- }
513
- : type === "search"
514
- ? {
515
- keyboardType: "web-search",
516
- returnKeyType: "search",
517
- autoCapitalize: "none",
518
- }
519
- : type === "password"
520
- ? {
521
- secureTextEntry: !showPassword,
522
- autoCompleteType: "password",
523
- autoCapitalize: "none",
524
- textContentType: "password",
525
- }
526
- : {};
527
- const parseDateValue = () => {
528
- if (!value) return new Date();
529
- if (value instanceof Date) return value;
530
- const parts = `${value}`.split("/");
531
- if (parts.length === 3) {
532
- const [day, month, year] = parts;
533
- const parsed = new Date(
534
- parseInt(year, 10),
535
- parseInt(month, 10) - 1,
536
- parseInt(day, 10),
537
- );
538
- if (!isNaN(parsed.getTime())) return parsed;
539
- }
540
- const fallback = new Date(value);
541
- return isNaN(fallback.getTime()) ? new Date() : fallback;
542
- };
543
312
 
544
- const handleDateConfirm = (date: Date) => {
545
- const day = date.getDate();
546
- const month = date.getMonth() + 1;
547
- const year = date.getFullYear();
548
- const dateString = `${day}/${month}/${year}`;
549
- onChangeText?.(dateString);
550
- setDatePickerVisible(false);
551
- };
313
+ const selectedOption = options?.find((cur) => cur.value === value);
314
+ const showPasswordToggle =
315
+ type === "password" && !end && !options && !isDate;
552
316
 
553
- const handleContainerPress = () => {
554
- if (disabled) return;
555
- setFocused(true);
556
- if (isDate) {
557
- onFocus();
558
- setDatePickerVisible(true);
559
- }
560
- };
561
317
  return (
562
318
  <>
563
319
  <View style={styles.root}>
564
- {label && (
320
+ {isFloating ? null : label != null && label !== "" ? (
565
321
  <Typography
566
322
  variant="body2"
567
- color="textSecondary"
323
+ color={error ? "error" : "textSecondary"}
568
324
  gutterBottom={7}
569
325
  {...labelProps}
570
326
  >
571
327
  {label}
572
328
  </Typography>
573
- )}
329
+ ) : null}
574
330
  <TouchableOpacity
575
331
  onPress={handleContainerPress}
576
332
  style={styles.container}
333
+ activeOpacity={1}
577
334
  >
578
- <View style={{ marginTop: multiline ? 5 : 0 }}>{start}</View>
579
-
580
- {options ? (
335
+ <View style={{ marginTop: multiline && !isFloating ? 5 : 0 }}>
336
+ {start}
337
+ </View>
338
+ {isFloating ? (
339
+ <View style={styles.contentWrapper}>
340
+ {label != null && label !== "" ? (
341
+ <Animated.Text
342
+ style={{ ...styles.label, top: labelAnim }}
343
+ pointerEvents="none"
344
+ >
345
+ {label}
346
+ </Animated.Text>
347
+ ) : null}
348
+ {options ? (
349
+ <View
350
+ style={{
351
+ flex: 1,
352
+ alignItems: "center",
353
+ flexDirection: "row",
354
+ }}
355
+ >
356
+ {isFloating && selectedOption?.start ? (
357
+ <View
358
+ style={{
359
+ paddingTop: variant !== "outlined" ? ms(13) : 0,
360
+ paddingRight: 10,
361
+ }}
362
+ >
363
+ {selectedOption.start}
364
+ </View>
365
+ ) : null}
366
+ {selectedOption ? (
367
+ <Typography style={styles.inputText}>
368
+ {selectedOption.label}
369
+ </Typography>
370
+ ) : (
371
+ !isFloating && (
372
+ <Typography style={styles.placeholder}>
373
+ {placeholder}
374
+ </Typography>
375
+ )
376
+ )}
377
+ {isFloating ? (
378
+ <View
379
+ style={{
380
+ marginRight: variant === "text" ? 0 : 20,
381
+ paddingTop: variant === "text" ? ms(13) : 0,
382
+ }}
383
+ >
384
+ <Ionicons
385
+ name="chevron-down"
386
+ color={colors.textSecondary.main}
387
+ size={24}
388
+ />
389
+ </View>
390
+ ) : (
391
+ <Ionicons
392
+ name="chevron-down"
393
+ size={24}
394
+ style={{ marginLeft: "auto", marginRight: 15 }}
395
+ color={colors.dark.light}
396
+ />
397
+ )}
398
+ </View>
399
+ ) : isDate ? (
400
+ <View style={styles.dateContent}>
401
+ <Typography
402
+ style={[
403
+ styles.dateText,
404
+ !value ? styles.datePlaceholder : undefined,
405
+ ]}
406
+ color={value ? "dark" : "textSecondary"}
407
+ >
408
+ {value || placeholder}
409
+ </Typography>
410
+ <View style={{ marginLeft: 8 }}>
411
+ {end ?? (
412
+ <Ionicons
413
+ name="calendar-outline"
414
+ size={22}
415
+ color={colors.textSecondary.main}
416
+ />
417
+ )}
418
+ </View>
419
+ </View>
420
+ ) : (
421
+ <TextInput
422
+ ref={ref}
423
+ onFocus={() => {
424
+ onFocus();
425
+ setFocused(true);
426
+ }}
427
+ onBlur={() => {
428
+ onBlur();
429
+ setFocused(false);
430
+ }}
431
+ value={value}
432
+ onChangeText={onChangeText}
433
+ key={
434
+ type === "password"
435
+ ? showPassword
436
+ ? "show"
437
+ : "hide"
438
+ : undefined
439
+ }
440
+ keyboardType={keyboardType}
441
+ placeholderTextColor={colors.textSecondary.light}
442
+ editable={!disabled}
443
+ placeholder={labelOpen ? placeholder : ""}
444
+ selectTextOnFocus={!disabled}
445
+ onSubmitEditing={onSubmitEditing}
446
+ multiline={multiline}
447
+ textAlignVertical={multiline ? "top" : "center"}
448
+ {...formProps}
449
+ {...props}
450
+ style={styles.input}
451
+ />
452
+ )}
453
+ </View>
454
+ ) : options ? (
581
455
  <>
582
- {value ? (
456
+ {selectedOption ? (
583
457
  <Typography style={styles.inputText}>
584
- {options.find((cur) => cur.value === value)?.label}
458
+ {selectedOption.label}
585
459
  </Typography>
586
460
  ) : (
587
461
  <Typography style={styles.placeholder}>
@@ -629,7 +503,13 @@ export const TextField2 = React.forwardRef<TextInput, TextFieldProps>(
629
503
  }}
630
504
  value={value}
631
505
  onChangeText={onChangeText}
632
- key={showPassword ? "show" : "hide"}
506
+ key={
507
+ type === "password"
508
+ ? showPassword
509
+ ? "show"
510
+ : "hide"
511
+ : undefined
512
+ }
633
513
  keyboardType={keyboardType}
634
514
  placeholderTextColor={colors.textSecondary.light}
635
515
  editable={!disabled}
@@ -637,61 +517,53 @@ export const TextField2 = React.forwardRef<TextInput, TextFieldProps>(
637
517
  selectTextOnFocus={!disabled}
638
518
  onSubmitEditing={onSubmitEditing}
639
519
  multiline={multiline}
640
- extAlignVertical={multiline ? "top" : "center"}
520
+ textAlignVertical={multiline ? "top" : "center"}
641
521
  {...formProps}
642
522
  {...props}
643
523
  style={styles.input}
644
524
  />
645
525
  )}
646
-
647
- {end ? (
648
- <View style={{ marginRight: 20 }}>{end}</View>
649
- ) : (
650
- type === "password" && (
651
- <TouchableOpacity
652
- style={{ marginRight: 20 }}
653
- onPress={() => setShowPassword(!showPassword)}
654
- >
655
- <Ionicons
656
- name={showPassword ? "eye-outline" : "eye-off-outline"}
657
- size={24}
658
- color={colors.textSecondary.main}
659
- />
660
- </TouchableOpacity>
661
- )
662
- )}
526
+ {end && !options ? (
527
+ <View
528
+ style={{
529
+ marginRight: 20,
530
+ paddingTop: isFloating && variant === "text" ? ms(13) : 0,
531
+ }}
532
+ >
533
+ {end}
534
+ </View>
535
+ ) : showPasswordToggle ? (
536
+ <TouchableOpacity
537
+ style={{ marginRight: 20 }}
538
+ onPress={() => setShowPassword(!showPassword)}
539
+ >
540
+ <Ionicons
541
+ name={showPassword ? "eye-outline" : "eye-off-outline"}
542
+ size={24}
543
+ color={colors.textSecondary.main}
544
+ />
545
+ </TouchableOpacity>
546
+ ) : null}
663
547
  </TouchableOpacity>
664
- {helperText && (
665
- <Typography
666
- color="textSecondary"
667
- style={styles.helperText}
668
- variant="caption"
669
- >
548
+ {helperText ? (
549
+ <Typography style={styles.helperText} variant="caption">
670
550
  {helperText}
671
551
  </Typography>
672
- )}
673
- {error && (
552
+ ) : null}
553
+ {error ? (
674
554
  <View style={styles.error}>
675
555
  <MaterialIcons name="error" color={colors.error.main} size={16} />
676
- <Typography style={styles.errorText} color="error">
556
+ <Typography
557
+ style={styles.errorText}
558
+ color="error"
559
+ fontWeight={400}
560
+ >
677
561
  {error}
678
562
  </Typography>
679
563
  </View>
680
- )}
564
+ ) : null}
681
565
  </View>
682
- {isDate && (
683
- <DateTimePickerModal
684
- isVisible={datePickerVisible}
685
- mode="date"
686
- date={parseDateValue()}
687
- onConfirm={handleDateConfirm}
688
- onCancel={() => {
689
- setDatePickerVisible(false);
690
- setFocused(false);
691
- }}
692
- />
693
- )}
694
- {options && (
566
+ {options ? (
695
567
  <SelectMenu
696
568
  options={options}
697
569
  value={value}
@@ -702,10 +574,49 @@ export const TextField2 = React.forwardRef<TextInput, TextFieldProps>(
702
574
  onChange={onChangeText!}
703
575
  {...selectMenuProps}
704
576
  />
705
- )}
577
+ ) : null}
578
+ {isDate ? (
579
+ <DateTimePickerModal
580
+ isVisible={datePickerVisible}
581
+ mode="date"
582
+ date={parseDateValue(value)}
583
+ onConfirm={handleDateConfirm}
584
+ onCancel={() => {
585
+ setDatePickerVisible(false);
586
+ setFocused(false);
587
+ }}
588
+ />
589
+ ) : null}
706
590
  </>
707
591
  );
708
592
  },
709
593
  );
710
594
 
595
+ TextFieldBase.displayName = "TextFieldBase";
596
+
597
+ const TextField = React.forwardRef<TextInput, TextFieldProps>((props, ref) => (
598
+ <TextFieldBase
599
+ {...props}
600
+ ref={ref}
601
+ labelVariant="floating"
602
+ gutterBottom={props.gutterBottom ?? 0}
603
+ placeholder={props.placeholder ?? ""}
604
+ />
605
+ ));
606
+
607
+ TextField.displayName = "TextField";
608
+
609
+ export const TextField2 = React.forwardRef<TextInput, TextFieldProps>(
610
+ (props, ref) => (
611
+ <TextFieldBase
612
+ {...props}
613
+ ref={ref}
614
+ labelVariant="external"
615
+ gutterBottom={props.gutterBottom ?? 8}
616
+ />
617
+ ),
618
+ );
619
+
620
+ TextField2.displayName = "TextField2";
621
+
711
622
  export default TextField;