ferns-ui 1.5.0 → 1.6.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.
@@ -5,6 +5,7 @@ import {TextInput, View} from "react-native";
5
5
  import {Box} from "./Box";
6
6
  import {DateTimeFieldProps, IconName} from "./Common";
7
7
  import {DateTimeActionSheet} from "./DateTimeActionSheet";
8
+ import {FieldError, FieldTitle} from "./fieldElements";
8
9
  import {IconButton} from "./IconButton";
9
10
  import {isMobileDevice} from "./MediaQuery";
10
11
  import {SelectField} from "./SelectField";
@@ -126,6 +127,7 @@ type FieldConfig = {
126
127
 
127
128
  export const DateTimeField: React.FC<DateTimeFieldProps> = ({
128
129
  type,
130
+ title,
129
131
  value,
130
132
  onChange,
131
133
  timezone: providedTimezone,
@@ -145,6 +147,20 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
145
147
  const [localTimezone, setLocalTimezone] = useState(
146
148
  providedTimezone ?? DateTime.local().zoneName ?? "UTC"
147
149
  );
150
+ // We need to store the pending value in a ref because the state changes don't trigger
151
+ // immediately, so onBlur may use stale values.
152
+ const pendingValueRef = useRef<
153
+ | {
154
+ amPm?: "am" | "pm";
155
+ timezone?: string;
156
+ minute?: string;
157
+ month?: string;
158
+ day?: string;
159
+ year?: string;
160
+ hour?: string;
161
+ }
162
+ | undefined
163
+ >(undefined);
148
164
 
149
165
  // Use provided timezone if available, otherwise use local
150
166
  const timezone = providedTimezone ?? localTimezone;
@@ -179,8 +195,8 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
179
195
  }
180
196
  if (type === "time" || type === "datetime") {
181
197
  configs.push(
182
- {maxLength: 2, placeholder: "HH", width: 30},
183
- {maxLength: 2, placeholder: "MM", width: 30}
198
+ {maxLength: 2, placeholder: "hh", width: 30},
199
+ {maxLength: 2, placeholder: "mm", width: 30}
184
200
  );
185
201
  }
186
202
  return configs;
@@ -193,12 +209,22 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
193
209
  }, [getFieldConfigs]);
194
210
 
195
211
  const getISOFromFields = useCallback(
196
- (override?: {amPm?: "am" | "pm"; timezone?: string; minute?: string}): string | undefined => {
212
+ (override?: {
213
+ amPm?: "am" | "pm";
214
+ timezone?: string;
215
+ minute?: string;
216
+ month?: string;
217
+ day?: string;
218
+ year?: string;
219
+ }): string | undefined => {
197
220
  const ampPmVal = override?.amPm ?? amPm;
198
221
  const minuteVal = override?.minute ?? minute;
222
+ const monthVal = override?.month ?? month;
223
+ const dayVal = override?.day ?? day;
224
+ const yearVal = override?.year ?? year;
199
225
  let date;
200
226
  if (type === "datetime") {
201
- if (!month || !day || !year || !hour || !minuteVal) {
227
+ if (!monthVal || !dayVal || !yearVal || !hour || !minuteVal) {
202
228
  return undefined;
203
229
  }
204
230
  let hourNum = parseInt(hour);
@@ -209,9 +235,9 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
209
235
  }
210
236
  date = DateTime.fromObject(
211
237
  {
212
- year: parseInt(year),
213
- month: parseInt(month),
214
- day: parseInt(day),
238
+ year: parseInt(yearVal),
239
+ month: parseInt(monthVal),
240
+ day: parseInt(dayVal),
215
241
  hour: hourNum,
216
242
  minute: parseInt(minuteVal),
217
243
  },
@@ -220,14 +246,14 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
220
246
  }
221
247
  );
222
248
  } else if (type === "date") {
223
- if (!month || !day || !year) {
249
+ if (!monthVal || !dayVal || !yearVal) {
224
250
  return undefined;
225
251
  }
226
252
  date = DateTime.fromObject(
227
253
  {
228
- year: parseInt(year),
229
- month: parseInt(month),
230
- day: parseInt(day),
254
+ year: parseInt(yearVal),
255
+ month: parseInt(monthVal),
256
+ day: parseInt(dayVal),
231
257
  },
232
258
  {
233
259
  zone: override?.timezone ?? timezone,
@@ -276,6 +302,7 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
276
302
  // Only update if it's a valid minute value
277
303
  if (!isNaN(minuteNum) && minuteNum >= 0 && minuteNum <= 59) {
278
304
  setMinute(finalValue);
305
+ pendingValueRef.current = {minute: finalValue};
279
306
 
280
307
  // Pass the new minute value directly to getISOFromFields
281
308
  const result = getISOFromFields({minute: finalValue});
@@ -302,25 +329,31 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
302
329
  : numericValue;
303
330
 
304
331
  if (type === "date" || type === "datetime") {
305
- if (index === 0) setMonth(finalValue);
306
- if (index === 1) setDay(finalValue);
307
- if (index === 2) setYear(finalValue);
332
+ if (index === 0) {
333
+ setMonth(finalValue);
334
+ pendingValueRef.current = {month: finalValue};
335
+ }
336
+ if (index === 1) {
337
+ setDay(finalValue);
338
+ pendingValueRef.current = {day: finalValue};
339
+ }
340
+ if (index === 2) {
341
+ setYear(finalValue);
342
+ pendingValueRef.current = {year: finalValue};
343
+ }
308
344
  }
309
345
 
310
346
  if (type === "time") {
311
- if (index === 0) setHour(finalValue);
347
+ if (index === 0) {
348
+ setHour(finalValue);
349
+ pendingValueRef.current = {hour: finalValue};
350
+ }
312
351
  }
313
352
 
314
353
  if (type === "datetime") {
315
- if (index === 3) setHour(finalValue);
316
- }
317
-
318
- // We use getISOFromFields to ensure the value is valid and current
319
- const result = getISOFromFields();
320
- if (result) {
321
- const currentValueUTC = value ? DateTime.fromISO(value).toUTC().toISO() : undefined;
322
- if (result !== currentValueUTC) {
323
- onChange(result);
354
+ if (index === 3) {
355
+ setHour(finalValue);
356
+ pendingValueRef.current = {hour: finalValue};
324
357
  }
325
358
  }
326
359
 
@@ -363,12 +396,15 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
363
396
  // When fields change, send the value to onChange
364
397
  const onBlur = useCallback(
365
398
  (override?: {amPm?: "am" | "pm"}) => {
366
- const iso = getISOFromFields(override);
399
+ const iso = getISOFromFields({...override, ...pendingValueRef.current});
367
400
  // Compare in UTC to avoid timezone issues
368
401
  const currentValueUTC = value ? DateTime.fromISO(value).toUTC().toISO() : undefined;
369
402
  if (iso && iso !== currentValueUTC) {
370
403
  onChange(iso);
371
404
  }
405
+
406
+ // Clear the pending value after processing
407
+ pendingValueRef.current = undefined;
372
408
  },
373
409
  [getISOFromFields, onChange, value]
374
410
  );
@@ -391,7 +427,13 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
391
427
  return;
392
428
  }
393
429
 
394
- const parsedDate = DateTime.fromISO(value).setZone(timezone);
430
+ // Handle dates which should have 00:00:00.000Z as the time component, ignore timezones.
431
+ let parsedDate = DateTime.fromISO(value);
432
+ if (type === "date") {
433
+ parsedDate = parsedDate.setZone("UTC");
434
+ } else {
435
+ parsedDate = parsedDate.setZone(timezone);
436
+ }
395
437
  if (!parsedDate.isValid) {
396
438
  console.warn("Invalid date passed to DateTimeField", value);
397
439
  return;
@@ -451,6 +493,8 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
451
493
 
452
494
  return (
453
495
  <>
496
+ {Boolean(title) && <FieldTitle text={title!} />}
497
+ {Boolean(errorText) && <FieldError text={errorText!} />}
454
498
  <View
455
499
  style={{
456
500
  flexDirection: isMobileDevice() ? "column" : "row",
@@ -494,8 +538,15 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
494
538
  value={amPm}
495
539
  onChange={(result) => {
496
540
  setAmPm(result as "am" | "pm");
497
- // We need to call onBlur manually because the SelectField doesn't support it
498
- onBlur({amPm: result as "am" | "pm"});
541
+ // No onblur, so we need to manually update the value
542
+ const iso = getISOFromFields({amPm: result as "am" | "pm"});
543
+ // Compare in UTC to avoid timezone issues
544
+ const currentValueUTC = value
545
+ ? DateTime.fromISO(value).toUTC().toISO()
546
+ : undefined;
547
+ if (iso && iso !== currentValueUTC) {
548
+ onChange(iso);
549
+ }
499
550
  }}
500
551
  />
501
552
  </Box>
@@ -25,7 +25,7 @@ export const PhoneNumberField: FC<PhoneNumberFieldProps> = ({
25
25
 
26
26
  const validatePhoneNumber = useCallback(
27
27
  (phoneNumber: string): string | undefined => {
28
- if (phoneNumber.trim() === "") {
28
+ if (!phoneNumber || phoneNumber.trim() === "") {
29
29
  return undefined;
30
30
  }
31
31
  const parsedNumber = parsePhoneNumberFromString(phoneNumber, defaultCountryCode);
@@ -85,10 +85,10 @@ export const PhoneNumberField: FC<PhoneNumberFieldProps> = ({
85
85
  if (error && !validationError) {
86
86
  setError(undefined);
87
87
  }
88
+
89
+ // Ensure invalid values don't propagate up
88
90
  if (!validationError) {
89
91
  onChange(formattedValue);
90
- } else {
91
- onChange(""); // Ensure invalid values don't propagate up
92
92
  }
93
93
  },
94
94
  [onChange, error, validatePhoneNumber, formatPhoneNumber]
package/src/setupTests.ts CHANGED
@@ -1,2 +1,42 @@
1
1
  process.env.TZ = "America/New_York";
2
+
3
+ // Create mocks for libraries that cause issues with testing
4
+ jest.mock("@react-native-async-storage/async-storage", () => ({
5
+ setItem: jest.fn(() => Promise.resolve()),
6
+ getItem: jest.fn(() => Promise.resolve(null)),
7
+ removeItem: jest.fn(() => Promise.resolve()),
8
+ clear: jest.fn(() => Promise.resolve()),
9
+ getAllKeys: jest.fn(() => Promise.resolve([])),
10
+ multiGet: jest.fn(() => Promise.resolve([])),
11
+ multiSet: jest.fn(() => Promise.resolve()),
12
+ multiRemove: jest.fn(() => Promise.resolve()),
13
+ mergeItem: jest.fn(() => Promise.resolve()),
14
+ }));
15
+
16
+ jest.mock("react-native-signature-canvas", () => ({
17
+ Signature: jest.fn().mockImplementation(() => null),
18
+ }));
19
+
20
+ // Mock components that cause testing issues.
21
+ jest.mock("./IconButton", () => ({
22
+ IconButton: jest.fn().mockImplementation(() => null),
23
+ }));
24
+
25
+ jest.mock("./Icon", () => ({
26
+ Icon: jest.fn().mockImplementation(() => null),
27
+ }));
28
+
29
+ // Mock DateTimeActionSheet
30
+ jest.mock("./DateTimeActionSheet", () => ({
31
+ DateTimeActionSheet: jest.fn().mockImplementation(() => null),
32
+ }));
33
+
34
+ // Mock MediaQuery
35
+ jest.mock("./MediaQuery", () => ({
36
+ isMobileDevice: jest.fn().mockReturnValue(false),
37
+ }));
38
+
39
+ // Make sure we can test date/time functionality
40
+ global.Date.now = jest.fn(() => new Date("2023-05-15T10:30:00.000Z").getTime());
41
+
2
42
  export {};
@@ -60,7 +60,7 @@ export const TableBoolean: FC<TableBooleanProps> = ({value, isEditing = false})
60
60
  justifyContent: "center",
61
61
  }}
62
62
  >
63
- <Icon color={value ? "success" : "secondaryLight"} iconName={value ? "check" : "x"} />
63
+ <Icon color={value ? "success" : "error"} iconName={value ? "check" : "x"} />
64
64
  </View>
65
65
  </View>
66
66
  );