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.
@@ -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 {printDate, printDateAndTime, printTime} from "./DateUtilities";
8
- import {TextField} from "./TextField";
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
- // TODO: allow use of keyboard to type in date/time
11
- export const DateTimeField = ({
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: tz,
131
+ timezone: providedTimezone,
132
+ onTimezoneChange,
16
133
  errorText,
17
134
  disabled,
18
- ...rest
19
- }: DateTimeFieldProps): React.ReactElement => {
20
- const calendar = getCalendars()[0];
21
- const timezone = (tz || calendar?.timeZone) ?? "UTC"; // Fallback to UTC if timezone is undefined
22
-
23
- let placeholder: string = "";
24
- if (type === "time") {
25
- placeholder = "hh:mm a";
26
- } else if (type === "datetime") {
27
- placeholder = "mm/dd/yyyy hh:mm a";
28
- } else if (type === "date") {
29
- placeholder = "mm/dd/yyyy";
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
- const formatValue = useCallback(
42
- (val: string) => {
43
- if (!val) {
44
- return "";
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
- switch (type) {
47
- case "time":
48
- return printTime(val, {timezone, showTimezone: true});
49
- case "datetime":
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 formatInputDate = useCallback(
60
- (input: string) => {
61
- const cleanedInput = input.replace(/[^0-9]/g, ""); // Remove non-numeric characters
62
- let formatted = input;
63
-
64
- switch (type) {
65
- case "time":
66
- if (cleanedInput.length >= 2) {
67
- formatted = `${cleanedInput.slice(0, 2)}:${cleanedInput.slice(2, 4)}`;
68
- } else {
69
- formatted = cleanedInput;
70
- }
71
- break;
72
- case "datetime":
73
- if (cleanedInput.length >= 2) {
74
- formatted = `${cleanedInput.slice(0, 2)}`;
75
- if (cleanedInput.length > 2) {
76
- formatted += `/${cleanedInput.slice(2, 4)}`;
77
- }
78
- if (cleanedInput.length > 4) {
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
- break;
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
- return formatted;
101
- },
102
- [type]
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
- const dateActionSheetRef: React.RefObject<any> = React.createRef();
106
- const [formattedDate, setFormattedDate] = useState<string>(value ? formatValue(value) : "");
107
- const [showDate, setShowDate] = useState(false);
108
- const [localError, setLocalError] = useState<string>("");
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
- const onTextFieldChange = useCallback(
111
- (inputDate: string) => {
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 (!parsedDate) {
141
- setLocalError("Invalid date/time. Please format as " + `${placeholder}`);
142
- setFormattedDate(formattedInput);
143
- onChange("");
144
- return;
314
+ if (type === "datetime") {
315
+ if (index === 3) setHour(finalValue);
145
316
  }
146
- if (parsedDate?.isValid) {
147
- setFormattedDate(formatValue(parsedDate.toISO()));
148
- setLocalError("");
149
- onChange(parsedDate.toISO());
150
- } else if (cleanedInput.length > (type === "datetime" ? 12 : type === "time" ? 4 : 8)) {
151
- setLocalError("Invalid date/time");
152
- setFormattedDate(formattedInput);
153
- onChange("");
154
- } else {
155
- setFormattedDate(formattedInput);
156
- setLocalError("Invalid date/time. Please format as " + `${placeholder}`);
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
- [formatInputDate, type, timezone, formatValue, onChange, placeholder]
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
- [formatValue, onChange]
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
- // if the value of the overall field changes via prop from the parent,
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
- const formatted = formatValue(value);
177
- if (formattedDate !== formatted) {
178
- setFormattedDate(formatted);
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
- if (errorText) {
181
- setLocalError(errorText);
425
+
426
+ if (type === "time") {
427
+ if (index === 0) return hour;
428
+ if (index === 1) return minute;
182
429
  }
183
- }
184
- }, [value, formatValue, formattedDate, errorText]);
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
- <TextField
189
- disabled={disabled}
190
- errorText={localError}
191
- iconName={iconName}
192
- placeholder={placeholder}
193
- type="text"
194
- value={formattedDate}
195
- onChange={onTextFieldChange}
196
- onIconClick={() => {
197
- setShowDate(true);
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
- {...rest}
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}