ferns-ui 1.3.0 → 1.5.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/Common.d.ts +7 -0
- package/dist/Common.js.map +1 -1
- package/dist/DateTimeActionSheet.js +4 -4
- package/dist/DateTimeActionSheet.js.map +1 -1
- package/dist/DateTimeField.d.ts +1 -1
- package/dist/DateTimeField.js +329 -135
- package/dist/DateTimeField.js.map +1 -1
- package/dist/DateUtilities.d.ts +4 -0
- package/dist/DateUtilities.js +32 -0
- package/dist/DateUtilities.js.map +1 -1
- package/dist/Field.js.map +1 -1
- package/dist/SegmentedControl.d.ts +1 -1
- package/dist/SegmentedControl.js +61 -26
- package/dist/SegmentedControl.js.map +1 -1
- package/dist/TimezonePicker.d.ts +10 -2
- package/dist/TimezonePicker.js +22 -19
- package/dist/TimezonePicker.js.map +1 -1
- package/package.json +1 -1
- package/src/Common.ts +8 -0
- package/src/DateTimeActionSheet.tsx +5 -9
- package/src/DateTimeField.tsx +487 -148
- package/src/DateUtilities.tsx +35 -0
- package/src/Field.tsx +1 -1
- package/src/SegmentedControl.tsx +95 -29
- package/src/TimezonePicker.tsx +36 -27
package/src/DateTimeField.tsx
CHANGED
|
@@ -1,33 +1,156 @@
|
|
|
1
|
-
import {getCalendars} from "expo-localization";
|
|
2
1
|
import {DateTime} from "luxon";
|
|
3
|
-
import React, {useCallback, useEffect, useState} from "react";
|
|
2
|
+
import React, {useCallback, useEffect, useRef, useState} from "react";
|
|
3
|
+
import {TextInput, View} from "react-native";
|
|
4
4
|
|
|
5
|
+
import {Box} from "./Box";
|
|
5
6
|
import {DateTimeFieldProps, IconName} from "./Common";
|
|
6
7
|
import {DateTimeActionSheet} from "./DateTimeActionSheet";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import {IconButton} from "./IconButton";
|
|
9
|
+
import {isMobileDevice} from "./MediaQuery";
|
|
10
|
+
import {SelectField} from "./SelectField";
|
|
11
|
+
import {Text} from "./Text";
|
|
12
|
+
import {useTheme} from "./Theme";
|
|
13
|
+
import {TimezonePicker} from "./TimezonePicker";
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
interface SeparatorProps {
|
|
16
|
+
type: "date" | "time";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const Separator: React.FC<SeparatorProps> = ({type}) => {
|
|
20
|
+
return (
|
|
21
|
+
<View>
|
|
22
|
+
<Text>{type === "time" ? ":" : "/"}</Text>
|
|
23
|
+
</View>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
interface DateTimeSegmentProps {
|
|
28
|
+
config: FieldConfig;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
getFieldValue: (index: number) => string;
|
|
31
|
+
handleFieldChange: (index: number, text: string, config: FieldConfig) => void;
|
|
32
|
+
onBlur: (override?: {amPm?: "am" | "pm"; timezone?: string}) => void;
|
|
33
|
+
onRef: (ref: TextInput | null, index: number) => void;
|
|
34
|
+
index: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DateTimeSegment: React.FC<DateTimeSegmentProps> = ({
|
|
38
|
+
disabled,
|
|
39
|
+
getFieldValue,
|
|
40
|
+
handleFieldChange,
|
|
41
|
+
onBlur,
|
|
42
|
+
onRef,
|
|
43
|
+
index,
|
|
44
|
+
config,
|
|
45
|
+
}): React.ReactElement => {
|
|
46
|
+
return (
|
|
47
|
+
<View
|
|
48
|
+
style={{
|
|
49
|
+
flexDirection: "row",
|
|
50
|
+
alignItems: "center",
|
|
51
|
+
width: config.width,
|
|
52
|
+
borderColor: "transparent",
|
|
53
|
+
backgroundColor: "transparent",
|
|
54
|
+
overflow: "hidden",
|
|
55
|
+
padding: 0,
|
|
56
|
+
flexShrink: 1,
|
|
57
|
+
height: 40,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<TextInput
|
|
61
|
+
ref={(el) => onRef(el, index)}
|
|
62
|
+
accessibilityHint={`Enter the ${config.placeholder}`}
|
|
63
|
+
aria-label="Text input field"
|
|
64
|
+
inputMode="numeric"
|
|
65
|
+
placeholder={config.placeholder}
|
|
66
|
+
readOnly={disabled}
|
|
67
|
+
selectTextOnFocus
|
|
68
|
+
style={{width: config.width - 2, textAlign: "center"}}
|
|
69
|
+
value={getFieldValue(index)}
|
|
70
|
+
onBlur={() => onBlur()}
|
|
71
|
+
onChangeText={(text) => {
|
|
72
|
+
handleFieldChange(index, text, config);
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
</View>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
interface DateTimeProps extends Omit<DateTimeSegmentProps, "index" | "config"> {
|
|
80
|
+
fieldConfigs: FieldConfig[];
|
|
81
|
+
type: "date" | "datetime" | "time";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const DateField: React.FC<DateTimeProps> = (segmentProps) => {
|
|
85
|
+
return (
|
|
86
|
+
<View style={{flexDirection: "row", alignItems: "center", justifyContent: "space-between"}}>
|
|
87
|
+
<DateTimeSegment {...segmentProps} config={segmentProps.fieldConfigs[0]} index={0} />
|
|
88
|
+
<Separator type="date" />
|
|
89
|
+
<DateTimeSegment {...segmentProps} config={segmentProps.fieldConfigs[1]} index={1} />
|
|
90
|
+
<Separator type="date" />
|
|
91
|
+
<DateTimeSegment {...segmentProps} config={segmentProps.fieldConfigs[2]} index={2} />
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const TimeField: React.FC<DateTimeProps> = ({
|
|
97
|
+
type,
|
|
98
|
+
onBlur,
|
|
99
|
+
|
|
100
|
+
...segmentProps
|
|
101
|
+
}) => {
|
|
102
|
+
return (
|
|
103
|
+
<View style={{flexDirection: "row", alignItems: "center", justifyContent: "space-between"}}>
|
|
104
|
+
<DateTimeSegment
|
|
105
|
+
{...segmentProps}
|
|
106
|
+
config={segmentProps.fieldConfigs[0]}
|
|
107
|
+
index={type === "time" ? 0 : 3}
|
|
108
|
+
onBlur={onBlur}
|
|
109
|
+
/>
|
|
110
|
+
<Separator type="time" />
|
|
111
|
+
<DateTimeSegment
|
|
112
|
+
{...segmentProps}
|
|
113
|
+
config={segmentProps.fieldConfigs[1]}
|
|
114
|
+
index={type === "time" ? 1 : 4}
|
|
115
|
+
onBlur={onBlur}
|
|
116
|
+
/>
|
|
117
|
+
</View>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type FieldConfig = {
|
|
122
|
+
maxLength: number;
|
|
123
|
+
placeholder: string;
|
|
124
|
+
width: number;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const DateTimeField: React.FC<DateTimeFieldProps> = ({
|
|
12
128
|
type,
|
|
13
129
|
value,
|
|
14
130
|
onChange,
|
|
15
|
-
timezone:
|
|
131
|
+
timezone: providedTimezone,
|
|
132
|
+
onTimezoneChange,
|
|
16
133
|
errorText,
|
|
17
134
|
disabled,
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
135
|
+
}): React.ReactElement => {
|
|
136
|
+
const {theme} = useTheme();
|
|
137
|
+
const dateActionSheetRef: React.RefObject<any> = React.createRef();
|
|
138
|
+
const [amPm, setAmPm] = useState<"am" | "pm">("am");
|
|
139
|
+
const [showDate, setShowDate] = useState(false);
|
|
140
|
+
const [month, setMonth] = useState("");
|
|
141
|
+
const [day, setDay] = useState("");
|
|
142
|
+
const [year, setYear] = useState("");
|
|
143
|
+
const [hour, setHour] = useState("");
|
|
144
|
+
const [minute, setMinute] = useState("");
|
|
145
|
+
const [localTimezone, setLocalTimezone] = useState(
|
|
146
|
+
providedTimezone ?? DateTime.local().zoneName ?? "UTC"
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Use provided timezone if available, otherwise use local
|
|
150
|
+
const timezone = providedTimezone ?? localTimezone;
|
|
151
|
+
const lastTimezoneRef = useRef(timezone);
|
|
152
|
+
|
|
153
|
+
const inputRefs = useRef<(TextInput | null)[]>([]);
|
|
31
154
|
|
|
32
155
|
let iconName: IconName | undefined;
|
|
33
156
|
if (disabled) {
|
|
@@ -38,166 +161,382 @@ export const DateTimeField = ({
|
|
|
38
161
|
iconName = "calendar";
|
|
39
162
|
}
|
|
40
163
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
164
|
+
let borderColor = theme.border.dark;
|
|
165
|
+
if (disabled) {
|
|
166
|
+
borderColor = theme.border.activeNeutral;
|
|
167
|
+
} else if (errorText) {
|
|
168
|
+
borderColor = theme.border.error;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const getFieldConfigs = useCallback((): FieldConfig[] => {
|
|
172
|
+
const configs: FieldConfig[] = [];
|
|
173
|
+
if (type === "date" || type === "datetime") {
|
|
174
|
+
configs.push(
|
|
175
|
+
{maxLength: 2, placeholder: "MM", width: 40},
|
|
176
|
+
{maxLength: 2, placeholder: "DD", width: 30},
|
|
177
|
+
{maxLength: 4, placeholder: "YYYY", width: 50}
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (type === "time" || type === "datetime") {
|
|
181
|
+
configs.push(
|
|
182
|
+
{maxLength: 2, placeholder: "HH", width: 30},
|
|
183
|
+
{maxLength: 2, placeholder: "MM", width: 30}
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return configs;
|
|
187
|
+
}, [type]);
|
|
188
|
+
|
|
189
|
+
// Set the inputRefs array to the correct length
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
const configs = getFieldConfigs();
|
|
192
|
+
inputRefs.current = configs.map(() => null);
|
|
193
|
+
}, [getFieldConfigs]);
|
|
194
|
+
|
|
195
|
+
const getISOFromFields = useCallback(
|
|
196
|
+
(override?: {amPm?: "am" | "pm"; timezone?: string; minute?: string}): string | undefined => {
|
|
197
|
+
const ampPmVal = override?.amPm ?? amPm;
|
|
198
|
+
const minuteVal = override?.minute ?? minute;
|
|
199
|
+
let date;
|
|
200
|
+
if (type === "datetime") {
|
|
201
|
+
if (!month || !day || !year || !hour || !minuteVal) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
let hourNum = parseInt(hour);
|
|
205
|
+
if (ampPmVal === "pm" && hourNum !== 12) {
|
|
206
|
+
hourNum += 12;
|
|
207
|
+
} else if (ampPmVal === "am" && hourNum === 12) {
|
|
208
|
+
hourNum = 0;
|
|
209
|
+
}
|
|
210
|
+
date = DateTime.fromObject(
|
|
211
|
+
{
|
|
212
|
+
year: parseInt(year),
|
|
213
|
+
month: parseInt(month),
|
|
214
|
+
day: parseInt(day),
|
|
215
|
+
hour: hourNum,
|
|
216
|
+
minute: parseInt(minuteVal),
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
zone: override?.timezone ?? timezone,
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
} else if (type === "date") {
|
|
223
|
+
if (!month || !day || !year) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
date = DateTime.fromObject(
|
|
227
|
+
{
|
|
228
|
+
year: parseInt(year),
|
|
229
|
+
month: parseInt(month),
|
|
230
|
+
day: parseInt(day),
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
zone: override?.timezone ?? timezone,
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
} else {
|
|
237
|
+
if (!hour || !minuteVal) {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
let hourNum = parseInt(hour);
|
|
241
|
+
if (ampPmVal === "pm" && hourNum !== 12) {
|
|
242
|
+
hourNum += 12;
|
|
243
|
+
} else if (ampPmVal === "am" && hourNum === 12) {
|
|
244
|
+
hourNum = 0;
|
|
245
|
+
}
|
|
246
|
+
date = DateTime.fromObject(
|
|
247
|
+
{
|
|
248
|
+
hour: hourNum,
|
|
249
|
+
minute: parseInt(minuteVal),
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
zone: override?.timezone ?? timezone,
|
|
253
|
+
}
|
|
254
|
+
);
|
|
45
255
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return printDateAndTime(val, {timezone, showTimezone: true});
|
|
51
|
-
case "date":
|
|
52
|
-
default:
|
|
53
|
-
return printDate(val, {timezone, ignoreTime: true});
|
|
256
|
+
|
|
257
|
+
if (date.isValid) {
|
|
258
|
+
// Always return UTC ISO string
|
|
259
|
+
return date.toUTC().toISO();
|
|
54
260
|
}
|
|
261
|
+
return undefined;
|
|
55
262
|
},
|
|
56
|
-
[timezone, type]
|
|
263
|
+
[amPm, month, day, year, hour, minute, timezone, type]
|
|
57
264
|
);
|
|
58
265
|
|
|
59
|
-
const
|
|
60
|
-
(
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
formatted += `/${cleanedInput.slice(4, 8)}`;
|
|
80
|
-
}
|
|
81
|
-
if (cleanedInput.length > 8) {
|
|
82
|
-
formatted += ` ${cleanedInput.slice(8, 10)}:${cleanedInput.slice(10, 12)}`;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
break;
|
|
86
|
-
case "date":
|
|
87
|
-
default:
|
|
88
|
-
if (cleanedInput.length >= 2) {
|
|
89
|
-
formatted = `${cleanedInput.slice(0, 2)}`;
|
|
90
|
-
if (cleanedInput.length > 2) {
|
|
91
|
-
formatted += `/${cleanedInput.slice(2, 4)}`;
|
|
92
|
-
}
|
|
93
|
-
if (cleanedInput.length > 4) {
|
|
94
|
-
formatted += `/${cleanedInput.slice(4, 8)}`;
|
|
266
|
+
const handleFieldChange = useCallback(
|
|
267
|
+
(index: number, text: string, config: FieldConfig) => {
|
|
268
|
+
const numericValue = text.replace(/[^0-9]/g, "");
|
|
269
|
+
|
|
270
|
+
// For minutes, just ensure it's at most 2 digits and valid (0-59)
|
|
271
|
+
if ((type === "time" && index === 1) || (type === "datetime" && index === 4)) {
|
|
272
|
+
// For minutes, take the last two digits and remove leading zeros unless it would be empty
|
|
273
|
+
const finalValue = numericValue.slice(-2).replace(/^0+(?=\d)/, "");
|
|
274
|
+
const minuteNum = parseInt(finalValue);
|
|
275
|
+
|
|
276
|
+
// Only update if it's a valid minute value
|
|
277
|
+
if (!isNaN(minuteNum) && minuteNum >= 0 && minuteNum <= 59) {
|
|
278
|
+
setMinute(finalValue);
|
|
279
|
+
|
|
280
|
+
// Pass the new minute value directly to getISOFromFields
|
|
281
|
+
const result = getISOFromFields({minute: finalValue});
|
|
282
|
+
if (result) {
|
|
283
|
+
const currentValueUTC = value ? DateTime.fromISO(value).toUTC().toISO() : undefined;
|
|
284
|
+
if (result !== currentValueUTC) {
|
|
285
|
+
onChange(result);
|
|
95
286
|
}
|
|
96
287
|
}
|
|
97
|
-
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Auto-advance to next field if current field is full
|
|
291
|
+
const configs = getFieldConfigs();
|
|
292
|
+
if (finalValue.length === config.maxLength && index < configs.length - 1) {
|
|
293
|
+
inputRefs.current[index + 1]?.focus();
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
98
296
|
}
|
|
99
297
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
298
|
+
// For other fields, handle leading zeros
|
|
299
|
+
const finalValue =
|
|
300
|
+
numericValue.length > config.maxLength
|
|
301
|
+
? numericValue.replace(/^0+/, "").slice(0, config.maxLength)
|
|
302
|
+
: numericValue;
|
|
104
303
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
304
|
+
if (type === "date" || type === "datetime") {
|
|
305
|
+
if (index === 0) setMonth(finalValue);
|
|
306
|
+
if (index === 1) setDay(finalValue);
|
|
307
|
+
if (index === 2) setYear(finalValue);
|
|
308
|
+
}
|
|
109
309
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const formattedInput = formatInputDate(inputDate);
|
|
113
|
-
const cleanedInput = formattedInput.replace(/[^0-9]/g, "");
|
|
114
|
-
|
|
115
|
-
let parsedDate;
|
|
116
|
-
if (type === "datetime" && cleanedInput.length === 12) {
|
|
117
|
-
const month = cleanedInput.slice(0, 2);
|
|
118
|
-
const day = cleanedInput.slice(2, 4);
|
|
119
|
-
const year = cleanedInput.slice(4, 8);
|
|
120
|
-
const hour = cleanedInput.slice(8, 10);
|
|
121
|
-
const minute = cleanedInput.slice(10, 12);
|
|
122
|
-
parsedDate = DateTime.fromFormat(`${month}${day}${year}${hour}${minute}`, "MMddyyyyHHmm", {
|
|
123
|
-
zone: timezone,
|
|
124
|
-
});
|
|
125
|
-
} else if (type === "time" && cleanedInput.length === 4) {
|
|
126
|
-
const hour = cleanedInput.slice(0, 2);
|
|
127
|
-
const minute = cleanedInput.slice(2, 4);
|
|
128
|
-
parsedDate = DateTime.fromFormat(`${hour}${minute}`, "HHmm", {
|
|
129
|
-
zone: timezone,
|
|
130
|
-
});
|
|
131
|
-
} else if (type === "date" && cleanedInput.length === 8) {
|
|
132
|
-
const month = cleanedInput.slice(0, 2);
|
|
133
|
-
const day = cleanedInput.slice(2, 4);
|
|
134
|
-
const year = cleanedInput.slice(4, 8);
|
|
135
|
-
parsedDate = DateTime.fromFormat(`${month}${day}${year}`, "MMddyyyy", {zone: timezone})
|
|
136
|
-
.startOf("day")
|
|
137
|
-
.toUTC();
|
|
310
|
+
if (type === "time") {
|
|
311
|
+
if (index === 0) setHour(finalValue);
|
|
138
312
|
}
|
|
139
313
|
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
setFormattedDate(formattedInput);
|
|
143
|
-
onChange("");
|
|
144
|
-
return;
|
|
314
|
+
if (type === "datetime") {
|
|
315
|
+
if (index === 3) setHour(finalValue);
|
|
145
316
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
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);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Auto-advance to next field if current field is full
|
|
328
|
+
const configs = getFieldConfigs();
|
|
329
|
+
if (finalValue.length === config.maxLength && index < configs.length - 1) {
|
|
330
|
+
inputRefs.current[index + 1]?.focus();
|
|
157
331
|
}
|
|
158
332
|
},
|
|
159
|
-
[
|
|
333
|
+
[type, getFieldConfigs, getISOFromFields, onChange, value]
|
|
160
334
|
);
|
|
161
335
|
|
|
162
336
|
const onActionSheetChange = useCallback(
|
|
163
337
|
(inputDate: string) => {
|
|
338
|
+
const parsedDate = DateTime.fromISO(inputDate);
|
|
339
|
+
if (!parsedDate.isValid) {
|
|
340
|
+
console.warn("Invalid date passed to DateTimeField", inputDate);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
setAmPm(parsedDate.hour >= 12 ? "pm" : "am");
|
|
344
|
+
|
|
345
|
+
if (type === "date" || type === "datetime") {
|
|
346
|
+
setMonth(parsedDate.month.toString().padStart(2, "0"));
|
|
347
|
+
setDay(parsedDate.day.toString().padStart(2, "0"));
|
|
348
|
+
setYear(parsedDate.year.toString());
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (type === "time" || type === "datetime") {
|
|
352
|
+
let hourNum = parsedDate.hour % 12;
|
|
353
|
+
hourNum = hourNum === 0 ? 12 : hourNum;
|
|
354
|
+
setHour(hourNum.toString().padStart(2, "0"));
|
|
355
|
+
setMinute(parsedDate.minute.toString().padStart(2, "0"));
|
|
356
|
+
}
|
|
164
357
|
onChange(inputDate);
|
|
165
|
-
setFormattedDate(formatValue(inputDate));
|
|
166
358
|
setShowDate(false);
|
|
167
|
-
setLocalError("");
|
|
168
359
|
},
|
|
169
|
-
[
|
|
360
|
+
[onChange, type]
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// When fields change, send the value to onChange
|
|
364
|
+
const onBlur = useCallback(
|
|
365
|
+
(override?: {amPm?: "am" | "pm"}) => {
|
|
366
|
+
const iso = getISOFromFields(override);
|
|
367
|
+
// Compare in UTC to avoid timezone issues
|
|
368
|
+
const currentValueUTC = value ? DateTime.fromISO(value).toUTC().toISO() : undefined;
|
|
369
|
+
if (iso && iso !== currentValueUTC) {
|
|
370
|
+
onChange(iso);
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
[getISOFromFields, onChange, value]
|
|
170
374
|
);
|
|
171
375
|
|
|
172
|
-
//
|
|
173
|
-
// update the formattedDate to keep the value of the TextField and DateTimeActionSheet in sync
|
|
376
|
+
// Handle external value changes
|
|
174
377
|
useEffect(() => {
|
|
175
|
-
if (value) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
378
|
+
if (!value) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// // If only timezone changed, don't recalculate fields
|
|
383
|
+
const isOnlyTimezoneChange =
|
|
384
|
+
lastTimezoneRef.current !== timezone &&
|
|
385
|
+
DateTime.fromISO(value).toUTC().toISO() ===
|
|
386
|
+
DateTime.fromISO(value).setZone(timezone).toUTC().toISO();
|
|
387
|
+
|
|
388
|
+
lastTimezoneRef.current = timezone;
|
|
389
|
+
|
|
390
|
+
if (isOnlyTimezoneChange) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const parsedDate = DateTime.fromISO(value).setZone(timezone);
|
|
395
|
+
if (!parsedDate.isValid) {
|
|
396
|
+
console.warn("Invalid date passed to DateTimeField", value);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
setAmPm(parsedDate.hour >= 12 ? "pm" : "am");
|
|
400
|
+
|
|
401
|
+
if (type === "date" || type === "datetime") {
|
|
402
|
+
setMonth(parsedDate.month.toString().padStart(2, "0"));
|
|
403
|
+
setDay(parsedDate.day.toString().padStart(2, "0"));
|
|
404
|
+
setYear(parsedDate.year.toString());
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (type === "time" || type === "datetime") {
|
|
408
|
+
let hourNum = parsedDate.hour % 12;
|
|
409
|
+
hourNum = hourNum === 0 ? 12 : hourNum;
|
|
410
|
+
setHour(hourNum.toString().padStart(2, "0"));
|
|
411
|
+
setMinute(parsedDate.minute.toString().padStart(2, "0"));
|
|
412
|
+
}
|
|
413
|
+
}, [value, type, timezone]);
|
|
414
|
+
|
|
415
|
+
// JOSH: This is where the infinite loop is happening
|
|
416
|
+
// We update the value of the date according to the zone and then this get triggered
|
|
417
|
+
// and we update the value of the date according to the zone again
|
|
418
|
+
const getFieldValue = useCallback(
|
|
419
|
+
(index: number): string => {
|
|
420
|
+
if (type === "date" || type === "datetime") {
|
|
421
|
+
if (index === 0) return month;
|
|
422
|
+
if (index === 1) return day;
|
|
423
|
+
if (index === 2) return year;
|
|
179
424
|
}
|
|
180
|
-
|
|
181
|
-
|
|
425
|
+
|
|
426
|
+
if (type === "time") {
|
|
427
|
+
if (index === 0) return hour;
|
|
428
|
+
if (index === 1) return minute;
|
|
182
429
|
}
|
|
183
|
-
|
|
184
|
-
|
|
430
|
+
|
|
431
|
+
if (type === "datetime") {
|
|
432
|
+
if (index === 3) return hour;
|
|
433
|
+
if (index === 4) return minute;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return "";
|
|
437
|
+
},
|
|
438
|
+
[type, month, day, year, hour, minute]
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const fieldConfigs = getFieldConfigs();
|
|
442
|
+
|
|
443
|
+
const segmentProps = {
|
|
444
|
+
disabled,
|
|
445
|
+
getFieldValue,
|
|
446
|
+
handleFieldChange,
|
|
447
|
+
onBlur,
|
|
448
|
+
fieldConfigs,
|
|
449
|
+
onRef: (el: TextInput | null, i: number) => (inputRefs.current[i] = el),
|
|
450
|
+
};
|
|
185
451
|
|
|
186
452
|
return (
|
|
187
453
|
<>
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
454
|
+
<View
|
|
455
|
+
style={{
|
|
456
|
+
flexDirection: isMobileDevice() ? "column" : "row",
|
|
457
|
+
borderColor,
|
|
458
|
+
backgroundColor: theme.surface.base,
|
|
459
|
+
borderWidth: 1,
|
|
460
|
+
paddingHorizontal: 12,
|
|
461
|
+
paddingVertical: 8,
|
|
462
|
+
borderRadius: 4,
|
|
463
|
+
alignItems: "center",
|
|
464
|
+
justifyContent: "space-between",
|
|
198
465
|
}}
|
|
199
|
-
|
|
200
|
-
|
|
466
|
+
>
|
|
467
|
+
{(type === "date" || type === "datetime") && (
|
|
468
|
+
<View style={{flexDirection: "row", alignItems: "center"}}>
|
|
469
|
+
<DateField {...segmentProps} type={type} />
|
|
470
|
+
{!disabled && type === "date" && (
|
|
471
|
+
<IconButton
|
|
472
|
+
accessibilityHint="Opens the calendar to select a date and time"
|
|
473
|
+
accessibilityLabel="Show calendar"
|
|
474
|
+
iconName={iconName!}
|
|
475
|
+
variant="muted"
|
|
476
|
+
onClick={() => setShowDate(true)}
|
|
477
|
+
/>
|
|
478
|
+
)}
|
|
479
|
+
</View>
|
|
480
|
+
)}
|
|
481
|
+
|
|
482
|
+
<View style={{flexDirection: "row", alignItems: "center"}}>
|
|
483
|
+
{(type === "time" || type === "datetime") && <TimeField {...segmentProps} type={type} />}
|
|
484
|
+
{Boolean(type === "datetime" || type === "time") && (
|
|
485
|
+
<>
|
|
486
|
+
<Box direction="column" marginLeft={2} marginRight={2} width={60}>
|
|
487
|
+
<SelectField
|
|
488
|
+
disabled={disabled}
|
|
489
|
+
options={[
|
|
490
|
+
{label: "am", value: "am"},
|
|
491
|
+
{label: "pm", value: "pm"},
|
|
492
|
+
]}
|
|
493
|
+
requireValue
|
|
494
|
+
value={amPm}
|
|
495
|
+
onChange={(result) => {
|
|
496
|
+
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"});
|
|
499
|
+
}}
|
|
500
|
+
/>
|
|
501
|
+
</Box>
|
|
502
|
+
<Box direction="column" marginRight={2} width={70}>
|
|
503
|
+
<TimezonePicker
|
|
504
|
+
disabled={disabled}
|
|
505
|
+
hideTitle
|
|
506
|
+
shortTimezone
|
|
507
|
+
timezone={timezone}
|
|
508
|
+
onChange={(t) => {
|
|
509
|
+
if (onTimezoneChange) {
|
|
510
|
+
onTimezoneChange(t);
|
|
511
|
+
} else {
|
|
512
|
+
setLocalTimezone(t);
|
|
513
|
+
}
|
|
514
|
+
const iso = getISOFromFields({timezone: t});
|
|
515
|
+
// Compare in UTC to avoid timezone issues
|
|
516
|
+
const currentValueUTC = value
|
|
517
|
+
? DateTime.fromISO(value).toUTC().toISO()
|
|
518
|
+
: undefined;
|
|
519
|
+
if (iso && iso !== currentValueUTC) {
|
|
520
|
+
onChange(iso);
|
|
521
|
+
}
|
|
522
|
+
}}
|
|
523
|
+
/>
|
|
524
|
+
</Box>
|
|
525
|
+
</>
|
|
526
|
+
)}
|
|
527
|
+
|
|
528
|
+
{!disabled && type === "datetime" && (
|
|
529
|
+
<IconButton
|
|
530
|
+
accessibilityHint="Opens the calendar to select a date and time"
|
|
531
|
+
accessibilityLabel="Show calendar"
|
|
532
|
+
iconName={iconName!}
|
|
533
|
+
variant="muted"
|
|
534
|
+
onClick={() => setShowDate(true)}
|
|
535
|
+
/>
|
|
536
|
+
)}
|
|
537
|
+
</View>
|
|
538
|
+
</View>
|
|
539
|
+
|
|
201
540
|
{!disabled && (
|
|
202
541
|
<DateTimeActionSheet
|
|
203
542
|
actionSheetRef={dateActionSheetRef}
|