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.
- package/dist/DateTimeActionSheet.js +2 -2
- package/dist/DateTimeActionSheet.js.map +1 -1
- package/dist/DateTimeField.js +58 -30
- package/dist/DateTimeField.js.map +1 -1
- package/dist/DateTimeField.test.d.ts +1 -0
- package/dist/DateTimeField.test.js +256 -0
- package/dist/DateTimeField.test.js.map +1 -0
- package/dist/PhoneNumberField.js +2 -4
- package/dist/PhoneNumberField.js.map +1 -1
- package/dist/setupTests.js +32 -0
- package/dist/setupTests.js.map +1 -1
- package/dist/table/TableBoolean.js +1 -1
- package/dist/table/TableBoolean.js.map +1 -1
- package/package.json +4 -6
- package/src/DateTimeActionSheet.tsx +7 -2
- package/src/DateTimeField.test.tsx +391 -0
- package/src/DateTimeField.tsx +79 -28
- package/src/PhoneNumberField.tsx +3 -3
- package/src/setupTests.ts +40 -0
- package/src/table/TableBoolean.tsx +1 -1
package/src/DateTimeField.tsx
CHANGED
|
@@ -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: "
|
|
183
|
-
{maxLength: 2, placeholder: "
|
|
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?: {
|
|
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 (!
|
|
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(
|
|
213
|
-
month: parseInt(
|
|
214
|
-
day: parseInt(
|
|
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 (!
|
|
249
|
+
if (!monthVal || !dayVal || !yearVal) {
|
|
224
250
|
return undefined;
|
|
225
251
|
}
|
|
226
252
|
date = DateTime.fromObject(
|
|
227
253
|
{
|
|
228
|
-
year: parseInt(
|
|
229
|
-
month: parseInt(
|
|
230
|
-
day: parseInt(
|
|
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)
|
|
306
|
-
|
|
307
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
//
|
|
498
|
-
|
|
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>
|
package/src/PhoneNumberField.tsx
CHANGED
|
@@ -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" : "
|
|
63
|
+
<Icon color={value ? "success" : "error"} iconName={value ? "check" : "x"} />
|
|
64
64
|
</View>
|
|
65
65
|
</View>
|
|
66
66
|
);
|