@idealyst/datepicker 1.0.0 → 1.0.41

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.
Files changed (36) hide show
  1. package/package.json +6 -2
  2. package/src/DatePicker/Calendar.native.tsx +180 -78
  3. package/src/DatePicker/Calendar.styles.tsx +73 -70
  4. package/src/DatePicker/DatePicker.native.tsx +24 -6
  5. package/src/DatePicker/DatePicker.styles.tsx +18 -11
  6. package/src/DatePicker/DatePicker.web.tsx +1 -1
  7. package/src/DatePicker/index.ts +1 -1
  8. package/src/DateRangePicker/RangeCalendar.native.tsx +143 -55
  9. package/src/DateRangePicker/RangeCalendar.styles.tsx +65 -39
  10. package/src/DateRangePicker/RangeCalendar.web.tsx +169 -60
  11. package/src/DateRangePicker/types.ts +9 -0
  12. package/src/DateTimePicker/DateTimePicker.native.tsx +11 -69
  13. package/src/DateTimePicker/DateTimePicker.tsx +12 -70
  14. package/src/DateTimePicker/DateTimePickerBase.tsx +241 -0
  15. package/src/DateTimePicker/TimePicker.native.tsx +9 -196
  16. package/src/DateTimePicker/TimePicker.styles.tsx +4 -2
  17. package/src/DateTimePicker/TimePicker.tsx +9 -401
  18. package/src/DateTimePicker/TimePickerBase.tsx +232 -0
  19. package/src/DateTimePicker/primitives/ClockFace.native.tsx +195 -0
  20. package/src/DateTimePicker/primitives/ClockFace.web.tsx +168 -0
  21. package/src/DateTimePicker/primitives/TimeInput.native.tsx +53 -0
  22. package/src/DateTimePicker/primitives/TimeInput.web.tsx +66 -0
  23. package/src/DateTimePicker/primitives/index.native.ts +2 -0
  24. package/src/DateTimePicker/primitives/index.ts +2 -0
  25. package/src/DateTimePicker/primitives/index.web.ts +2 -0
  26. package/src/DateTimePicker/types.ts +0 -4
  27. package/src/DateTimePicker/utils/dimensions.native.ts +9 -0
  28. package/src/DateTimePicker/utils/dimensions.ts +9 -0
  29. package/src/DateTimePicker/utils/dimensions.web.ts +33 -0
  30. package/src/DateTimeRangePicker/DateTimeRangePicker.native.tsx +10 -199
  31. package/src/DateTimeRangePicker/DateTimeRangePicker.styles.tsx +3 -0
  32. package/src/DateTimeRangePicker/DateTimeRangePicker.web.tsx +11 -131
  33. package/src/DateTimeRangePicker/DateTimeRangePickerBase.tsx +391 -0
  34. package/src/DateTimeRangePicker/types.ts +0 -2
  35. package/src/examples/DatePickerExamples.tsx +42 -118
  36. /package/src/DatePicker/{Calendar.tsx → Calendar.web.tsx} +0 -0
@@ -1,406 +1,14 @@
1
- import React, { useState, useEffect, useRef } from 'react';
2
- import { Button, View, Input } from '@idealyst/components';
1
+ import React from 'react';
3
2
  import { TimePickerProps } from './types';
4
- import { timePickerStyles } from './TimePicker.styles';
3
+ import { TimePickerBase } from './TimePickerBase';
4
+ import { ClockFace, TimeInput } from './primitives';
5
5
 
6
- export const TimePicker: React.FC<TimePickerProps> = ({
7
- value = new Date(),
8
- onChange,
9
- disabled = false,
10
- mode = '12h',
11
- showSeconds = false,
12
- step = 1,
13
- style,
14
- testID,
15
- }) => {
16
- const [activeSelection, setActiveSelection] = useState<'hour' | 'minute'>('hour');
17
- const [hourInputValue, setHourInputValue] = useState(String(value.getHours() > 12 && mode === '12h' ? value.getHours() - 12 : value.getHours()));
18
- const [minuteInputValue, setMinuteInputValue] = useState(String(value.getMinutes()).padStart(2, '0'));
19
- const [hourInputFocused, setHourInputFocused] = useState(false);
20
- const [minuteInputFocused, setMinuteInputFocused] = useState(false);
21
- const hourInputRef = useRef<HTMLInputElement | null>(null);
22
- const minuteInputRef = useRef<HTMLInputElement | null>(null);
23
- const hours = value.getHours();
24
- const minutes = value.getMinutes();
25
- const seconds = value.getSeconds();
26
-
27
- const displayHours = mode === '12h' ? (hours === 0 ? 12 : hours > 12 ? hours - 12 : hours) : hours;
28
- const ampm = mode === '12h' ? (hours >= 12 ? 'PM' : 'AM') : null;
29
-
30
- // Sync input values when time changes from external sources (like clock clicks)
31
- // Only update if the input is not currently focused
32
- useEffect(() => {
33
- if (!hourInputFocused) {
34
- setHourInputValue(String(displayHours));
35
- }
36
- if (!minuteInputFocused) {
37
- setMinuteInputValue(String(minutes).padStart(2, '0'));
38
- }
39
- }, [displayHours, minutes, hourInputFocused, minuteInputFocused]);
40
-
41
- const updateTime = (newHours: number, newMinutes: number, newSeconds?: number) => {
42
- const newDate = new Date(value);
43
- newDate.setHours(newHours, newMinutes, newSeconds || 0);
44
- onChange(newDate);
45
- };
46
-
47
- const handleHourChange = (delta: number) => {
48
- let newHours = hours + delta;
49
- if (mode === '12h') {
50
- if (newHours < 0) newHours = 23;
51
- if (newHours > 23) newHours = 0;
52
- } else {
53
- if (newHours < 0) newHours = 23;
54
- if (newHours > 23) newHours = 0;
55
- }
56
- updateTime(newHours, minutes, seconds);
57
- };
58
-
59
- const handleMinuteChange = (delta: number) => {
60
- let newMinutes = minutes + (delta * step);
61
- let newHours = hours;
62
-
63
- if (newMinutes < 0) {
64
- newMinutes = 60 + newMinutes;
65
- newHours = hours - 1;
66
- if (newHours < 0) newHours = 23;
67
- } else if (newMinutes >= 60) {
68
- newMinutes = newMinutes - 60;
69
- newHours = hours + 1;
70
- if (newHours > 23) newHours = 0;
71
- }
72
-
73
- updateTime(newHours, newMinutes, seconds);
74
- };
75
-
76
- const handleSecondChange = (delta: number) => {
77
- let newSeconds = seconds + delta;
78
- let newMinutes = minutes;
79
- let newHours = hours;
80
-
81
- if (newSeconds < 0) {
82
- newSeconds = 59;
83
- newMinutes = minutes - 1;
84
- if (newMinutes < 0) {
85
- newMinutes = 59;
86
- newHours = hours - 1;
87
- if (newHours < 0) newHours = 23;
88
- }
89
- } else if (newSeconds >= 60) {
90
- newSeconds = 0;
91
- newMinutes = minutes + 1;
92
- if (newMinutes >= 60) {
93
- newMinutes = 0;
94
- newHours = hours + 1;
95
- if (newHours > 23) newHours = 0;
96
- }
97
- }
98
-
99
- updateTime(newHours, newMinutes, newSeconds);
100
- };
101
-
102
- const toggleAmPm = () => {
103
- if (mode === '12h') {
104
- const newHours = hours >= 12 ? hours - 12 : hours + 12;
105
- updateTime(newHours, minutes, seconds);
106
- }
107
- };
108
-
109
- const handleHourClick = (hour: number) => {
110
- let hour24 = hour;
111
- if (mode === '12h') {
112
- const isPM = hours >= 12;
113
- if (hour === 12) hour24 = isPM ? 12 : 0;
114
- else hour24 = isPM ? hour + 12 : hour;
115
- }
116
- updateTime(hour24, minutes, seconds);
117
- setActiveSelection('minute');
118
- };
119
-
120
- const handleMinuteClick = (minute: number) => {
121
- updateTime(hours, minute, seconds);
122
- };
123
-
124
- const renderClockFace = () => {
125
- // Clock configuration
126
- const CLOCK_SIZE = 180;
127
- const CENTER = CLOCK_SIZE / 2;
128
- const CLOCK_RADIUS = CENTER - 5;
129
- const NUMBER_RADIUS = CENTER - 24;
130
- const HOUR_HAND_LENGTH = CENTER - 44;
131
- const MINUTE_HAND_LENGTH = CENTER - 36;
132
- const CIRCLE_RADIUS = 15;
133
-
134
- if (activeSelection === 'hour') {
135
- return (
136
- <View style={timePickerStyles.clockContainer}>
137
- <svg width={CLOCK_SIZE} height={CLOCK_SIZE} style={timePickerStyles.clockSvg}>
138
- {/* Clock face */}
139
- <circle cx={CENTER} cy={CENTER} r={CLOCK_RADIUS} fill="#f9fafb" stroke="#e5e7eb" strokeWidth="2"/>
140
-
141
- {/* Hour numbers - clickable */}
142
- {[...Array(12)].map((_, i) => {
143
- const hour = i === 0 ? 12 : i;
144
- const angle = (i * 30) - 90;
145
- const x = CENTER + NUMBER_RADIUS * Math.cos(angle * Math.PI / 180);
146
- const y = CENTER + NUMBER_RADIUS * Math.sin(angle * Math.PI / 180);
147
- const isSelected = (mode === '12h' ? displayHours : hours) === (hour === 12 ? 0 : hour);
148
-
149
- return (
150
- <g key={i} onClick={() => handleHourClick(hour)}>
151
- <circle
152
- cx={x}
153
- cy={y}
154
- r={CIRCLE_RADIUS}
155
- fill={isSelected ? '#3b82f6' : 'transparent'}
156
- stroke={isSelected ? '#3b82f6' : '#e5e7eb'}
157
- strokeWidth="1"
158
- style={{ cursor: 'pointer' }}
159
- />
160
- <text
161
- x={x}
162
- y={y + 4}
163
- textAnchor="middle"
164
- fontSize="14"
165
- fill={isSelected ? '#ffffff' : '#374151'}
166
- fontWeight="500"
167
- style={{ cursor: 'pointer', userSelect: 'none', pointerEvents: 'none' }}
168
- >
169
- {hour}
170
- </text>
171
- </g>
172
- );
173
- })}
174
-
175
- {/* Hour hand pointing to selected hour */}
176
- {(() => {
177
- const selectedHour = mode === '12h' ? displayHours : hours;
178
- // Convert 12 to 0 for angle calculation, but keep others as-is
179
- const hourFor12Clock = selectedHour === 12 ? 0 : selectedHour;
180
- const hourAngle = (hourFor12Clock * 30) - 90;
181
- const handX = CENTER + HOUR_HAND_LENGTH * Math.cos(hourAngle * Math.PI / 180);
182
- const handY = CENTER + HOUR_HAND_LENGTH * Math.sin(hourAngle * Math.PI / 180);
183
-
184
- return (
185
- <line
186
- x1={CENTER}
187
- y1={CENTER}
188
- x2={handX}
189
- y2={handY}
190
- stroke="#3b82f6"
191
- strokeWidth="3"
192
- strokeLinecap="round"
193
- />
194
- );
195
- })()}
196
-
197
- {/* Center dot */}
198
- <circle cx={CENTER} cy={CENTER} r="4" fill="#3b82f6"/>
199
- </svg>
200
- </View>
201
- );
202
- } else {
203
- return (
204
- <View style={timePickerStyles.clockContainer}>
205
- <svg width={CLOCK_SIZE} height={CLOCK_SIZE} style={timePickerStyles.clockSvg}>
206
- {/* Clock face */}
207
- <circle cx={CENTER} cy={CENTER} r={CLOCK_RADIUS} fill="#f9fafb" stroke="#e5e7eb" strokeWidth="2"/>
208
-
209
- {/* Minute markers - every 5 minutes */}
210
- {[...Array(12)].map((_, i) => {
211
- const minute = i * 5;
212
- const angle = (i * 30) - 90;
213
- const x = CENTER + NUMBER_RADIUS * Math.cos(angle * Math.PI / 180);
214
- const y = CENTER + NUMBER_RADIUS * Math.sin(angle * Math.PI / 180);
215
- const isSelected = Math.floor(minutes / 5) * 5 === minute;
216
-
217
- return (
218
- <g key={i} onClick={() => handleMinuteClick(minute)}>
219
- <circle
220
- cx={x}
221
- cy={y}
222
- r={CIRCLE_RADIUS}
223
- fill={isSelected ? '#3b82f6' : 'transparent'}
224
- stroke={isSelected ? '#3b82f6' : '#e5e7eb'}
225
- strokeWidth="1"
226
- style={{ cursor: 'pointer' }}
227
- />
228
- <text
229
- x={x}
230
- y={y + 4}
231
- textAnchor="middle"
232
- fontSize="12"
233
- fill={isSelected ? '#ffffff' : '#374151'}
234
- fontWeight="500"
235
- style={{ cursor: 'pointer', userSelect: 'none', pointerEvents: 'none' }}
236
- >
237
- {minute.toString().padStart(2, '0')}
238
- </text>
239
- </g>
240
- );
241
- })}
242
-
243
- {/* Minute hand pointing to selected minute */}
244
- {(() => {
245
- const minuteAngle = (minutes * 6) - 90;
246
- const handX = CENTER + MINUTE_HAND_LENGTH * Math.cos(minuteAngle * Math.PI / 180);
247
- const handY = CENTER + MINUTE_HAND_LENGTH * Math.sin(minuteAngle * Math.PI / 180);
248
-
249
- return (
250
- <line
251
- x1={CENTER}
252
- y1={CENTER}
253
- x2={handX}
254
- y2={handY}
255
- stroke="#3b82f6"
256
- strokeWidth="2"
257
- strokeLinecap="round"
258
- />
259
- );
260
- })()}
261
-
262
- {/* Center dot */}
263
- <circle cx={CENTER} cy={CENTER} r="4" fill="#3b82f6"/>
264
- </svg>
265
- </View>
266
- );
267
- }
268
- };
269
-
270
-
6
+ export const TimePicker: React.FC<TimePickerProps> = (props) => {
271
7
  return (
272
- <View style={[timePickerStyles.container, style]} data-testid={testID}>
273
- {/* Tab Bar */}
274
- <View style={timePickerStyles.tabBar}>
275
- <Button
276
- onPress={() => setActiveSelection('hour')}
277
- style={[
278
- timePickerStyles.tabButton,
279
- activeSelection === 'hour' ? timePickerStyles.activeTab : timePickerStyles.inactiveTab
280
- ]}
281
- disabled={disabled}
282
- >
283
- Hour
284
- </Button>
285
- <Button
286
- onPress={() => setActiveSelection('minute')}
287
- style={[
288
- timePickerStyles.tabButton,
289
- activeSelection === 'minute' ? timePickerStyles.activeTab : timePickerStyles.inactiveTab
290
- ]}
291
- disabled={disabled}
292
- >
293
- Minute
294
- </Button>
295
- </View>
296
-
297
- {/* Interactive Clock Face */}
298
- {renderClockFace()}
299
-
300
- {/* Time Input Row */}
301
- <View style={timePickerStyles.timeInputRow}>
302
- <Input
303
- ref={hourInputRef}
304
- variant="bare"
305
- value={hourInputValue}
306
- onChangeText={(value) => {
307
- setHourInputValue(value);
308
-
309
- // Smart focus switching: if user types 2 or higher, focus on minutes
310
- const num = parseInt(value);
311
- if (!isNaN(num) && num >= 2 && mode === '12h') {
312
- // Wait a moment then focus minutes
313
- setTimeout(() => {
314
- setActiveSelection('minute');
315
- setHourInputFocused(false);
316
- setMinuteInputFocused(true);
317
- minuteInputRef.current?.focus();
318
- }, 100);
319
- }
320
-
321
- // Try to update time if value is valid
322
- const hour = parseInt(value);
323
- if (!isNaN(hour)) {
324
- const maxHour = mode === '12h' ? 12 : 23;
325
- const minHour = mode === '12h' ? 1 : 0;
326
-
327
- if (hour >= minHour && hour <= maxHour) {
328
- let hour24 = hour;
329
- if (mode === '12h') {
330
- const isPM = hours >= 12;
331
- if (hour === 12) hour24 = isPM ? 12 : 0;
332
- else hour24 = isPM ? hour + 12 : hour;
333
- }
334
- updateTime(hour24, minutes, seconds);
335
- }
336
- }
337
- }}
338
- onFocus={() => {
339
- setActiveSelection('hour');
340
- setHourInputFocused(true);
341
- setHourInputValue(String(displayHours));
342
- }}
343
- onBlur={() => {
344
- setHourInputFocused(false);
345
- // Handle 0 -> 12 conversion for 12h mode
346
- const hour = parseInt(hourInputValue);
347
- if (hour === 0 && mode === '12h') {
348
- const isPM = hours >= 12;
349
- const hour24 = isPM ? 12 : 0;
350
- updateTime(hour24, minutes, seconds);
351
- }
352
- setHourInputValue(String(displayHours));
353
- }}
354
- style={[
355
- timePickerStyles.timeInput,
356
- activeSelection === 'hour' ? timePickerStyles.activeInput : {}
357
- ]}
358
- disabled={disabled}
359
- />
360
- <View style={timePickerStyles.timeSeparator}>:</View>
361
- <Input
362
- ref={minuteInputRef}
363
- variant="bare"
364
- value={minuteInputValue}
365
- onChangeText={(value) => {
366
- setMinuteInputValue(value);
367
-
368
- // Try to update time if value is valid
369
- const minute = parseInt(value);
370
- if (!isNaN(minute)) {
371
- if (minute >= 0 && minute <= 59) {
372
- updateTime(hours, minute, seconds);
373
- }
374
- }
375
- }}
376
- onFocus={() => {
377
- setActiveSelection('minute');
378
- setMinuteInputFocused(true);
379
- setMinuteInputValue(String(minutes));
380
- }}
381
- onBlur={() => {
382
- setMinuteInputFocused(false);
383
- setMinuteInputValue(String(minutes).padStart(2, '0'));
384
- }}
385
- style={[
386
- timePickerStyles.timeInput,
387
- activeSelection === 'minute' ? timePickerStyles.activeInput : {}
388
- ]}
389
- disabled={disabled}
390
- />
391
-
392
- {mode === '12h' && ampm && (
393
- <Button
394
- variant="outlined"
395
- size="small"
396
- onPress={toggleAmPm}
397
- disabled={disabled}
398
- style={timePickerStyles.ampmButton}
399
- >
400
- {ampm}
401
- </Button>
402
- )}
403
- </View>
404
- </View>
8
+ <TimePickerBase
9
+ {...props}
10
+ renderClock={(clockProps) => <ClockFace {...clockProps} />}
11
+ renderTimeInput={(inputProps) => <TimeInput {...inputProps} />}
12
+ />
405
13
  );
406
14
  };
@@ -0,0 +1,232 @@
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
+ import { View, Text, Button } from '@idealyst/components';
3
+ import { TimePickerProps } from './types';
4
+ import { timePickerStyles } from './TimePicker.styles';
5
+
6
+ interface TimePickerBaseProps extends TimePickerProps {
7
+ renderClock: (props: {
8
+ activeSelection: 'hour' | 'minute';
9
+ hours: number;
10
+ minutes: number;
11
+ displayHours: number;
12
+ mode: '12h' | '24h';
13
+ disabled: boolean;
14
+ onHourClick: (hour: number) => void;
15
+ onMinuteClick: (minute: number) => void;
16
+ }) => React.ReactNode;
17
+ renderTimeInput: (props: {
18
+ type: 'hour' | 'minute';
19
+ value: string;
20
+ onChangeText: (value: string) => void;
21
+ onFocus: () => void;
22
+ onBlur: () => void;
23
+ isActive: boolean;
24
+ disabled: boolean;
25
+ inputRef: React.RefObject<any>;
26
+ }) => React.ReactNode;
27
+ }
28
+
29
+ export const TimePickerBase: React.FC<TimePickerBaseProps> = ({
30
+ value = new Date(),
31
+ onChange,
32
+ disabled = false,
33
+ mode = '12h',
34
+ step = 1,
35
+ style,
36
+ testID,
37
+ renderClock,
38
+ renderTimeInput,
39
+ }) => {
40
+ const [activeSelection, setActiveSelection] = useState<'hour' | 'minute'>('hour');
41
+ const [hourInputValue, setHourInputValue] = useState(String(value.getHours() > 12 && mode === '12h' ? value.getHours() - 12 : value.getHours()));
42
+ const [minuteInputValue, setMinuteInputValue] = useState(String(value.getMinutes()).padStart(2, '0'));
43
+ const [hourInputFocused, setHourInputFocused] = useState(false);
44
+ const [minuteInputFocused, setMinuteInputFocused] = useState(false);
45
+ const hourInputRef = useRef<any>(null);
46
+ const minuteInputRef = useRef<any>(null);
47
+
48
+ const hours = value.getHours();
49
+ const minutes = value.getMinutes();
50
+
51
+ const displayHours = mode === '12h' ? (hours === 0 ? 12 : hours > 12 ? hours - 12 : hours) : hours;
52
+ const ampm = mode === '12h' ? (hours >= 12 ? 'PM' : 'AM') : null;
53
+
54
+ // Sync input values when time changes from external sources (like clock clicks)
55
+ // Only update if the input is not currently focused
56
+ useEffect(() => {
57
+ if (!hourInputFocused) {
58
+ setHourInputValue(String(displayHours));
59
+ }
60
+ if (!minuteInputFocused) {
61
+ setMinuteInputValue(String(minutes).padStart(2, '0'));
62
+ }
63
+ }, [displayHours, minutes, hourInputFocused, minuteInputFocused]);
64
+
65
+ const updateTime = useCallback((newHours: number, newMinutes: number) => {
66
+ const newDate = new Date(value);
67
+ newDate.setHours(newHours, newMinutes, 0);
68
+ onChange(newDate);
69
+ }, [value, onChange]);
70
+
71
+ const handleHourClick = useCallback((hour: number) => {
72
+ // Dismiss keyboard when interacting with clock
73
+ hourInputRef.current?.blur?.();
74
+ minuteInputRef.current?.blur?.();
75
+
76
+ let hour24 = hour;
77
+ if (mode === '12h') {
78
+ const isPM = hours >= 12;
79
+ if (hour === 12) hour24 = isPM ? 12 : 0;
80
+ else hour24 = isPM ? hour + 12 : hour;
81
+ }
82
+ updateTime(hour24, minutes);
83
+ setActiveSelection('minute');
84
+ }, [mode, hours, minutes, updateTime]);
85
+
86
+ const handleMinuteClick = useCallback((minute: number) => {
87
+ // Dismiss keyboard when interacting with clock
88
+ hourInputRef.current?.blur?.();
89
+ minuteInputRef.current?.blur?.();
90
+
91
+ updateTime(hours, minute);
92
+ }, [hours, updateTime]);
93
+
94
+ const toggleAmPm = useCallback(() => {
95
+ if (mode === '12h') {
96
+ const newHours = hours >= 12 ? hours - 12 : hours + 12;
97
+ updateTime(newHours, minutes);
98
+ }
99
+ }, [mode, hours, minutes, updateTime]);
100
+
101
+ const handleHourInputChange = useCallback((inputValue: string) => {
102
+ setHourInputValue(inputValue);
103
+
104
+ // Smart focus switching: if user types 2 or higher, focus on minutes
105
+ if (mode === '12h' && parseInt(inputValue) >= 2 && inputValue.length >= 1) {
106
+ minuteInputRef.current?.focus?.();
107
+ setActiveSelection('minute');
108
+ } else if (mode === '24h' && parseInt(inputValue) >= 3 && inputValue.length >= 1) {
109
+ minuteInputRef.current?.focus?.();
110
+ setActiveSelection('minute');
111
+ }
112
+
113
+ // Try to update time if value is valid
114
+ const hour = parseInt(inputValue);
115
+ if (!isNaN(hour) && hour >= 0) {
116
+ let hour24 = hour;
117
+ if (mode === '12h' && hour <= 12) {
118
+ const isPM = hours >= 12;
119
+ if (hour === 12) hour24 = isPM ? 12 : 0;
120
+ else hour24 = isPM ? hour + 12 : hour;
121
+ }
122
+ if (hour24 <= 23) {
123
+ updateTime(hour24, minutes);
124
+ }
125
+ }
126
+ }, [mode, hours, minutes, updateTime]);
127
+
128
+ const handleHourInputFocus = useCallback(() => {
129
+ setHourInputFocused(true);
130
+ setActiveSelection('hour');
131
+ }, []);
132
+
133
+ const handleHourInputBlur = useCallback(() => {
134
+ setHourInputFocused(false);
135
+ // Handle 0 -> 12 conversion for 12h mode
136
+ const hour = parseInt(hourInputValue);
137
+ if (!isNaN(hour)) {
138
+ let hour24 = hour;
139
+ if (mode === '12h') {
140
+ const isPM = hours >= 12;
141
+ if (hour === 0) hour24 = isPM ? 12 : 0;
142
+ else if (hour <= 12) hour24 = isPM ? (hour === 12 ? 12 : hour + 12) : (hour === 12 ? 0 : hour);
143
+ }
144
+ if (hour24 <= 23) {
145
+ updateTime(hour24, minutes);
146
+ }
147
+ }
148
+ setHourInputValue(String(displayHours));
149
+ }, [hourInputValue, mode, hours, minutes, displayHours, updateTime]);
150
+
151
+ const handleMinuteInputChange = useCallback((inputValue: string) => {
152
+ setMinuteInputValue(inputValue);
153
+
154
+ // Try to update time if value is valid
155
+ const minute = parseInt(inputValue);
156
+ if (!isNaN(minute) && minute >= 0 && minute <= 59) {
157
+ updateTime(hours, minute);
158
+ }
159
+
160
+ // Auto-focus hour input if user deletes and field becomes empty
161
+ if (inputValue === '') {
162
+ hourInputRef.current?.focus?.();
163
+ setActiveSelection('hour');
164
+ }
165
+ }, [hours, updateTime]);
166
+
167
+ const handleMinuteInputFocus = useCallback(() => {
168
+ setMinuteInputFocused(true);
169
+ setActiveSelection('minute');
170
+ }, []);
171
+
172
+ const handleMinuteInputBlur = useCallback(() => {
173
+ setMinuteInputFocused(false);
174
+ setMinuteInputValue(String(minutes).padStart(2, '0'));
175
+ }, [minutes]);
176
+
177
+ return (
178
+ <View style={[timePickerStyles.container, style]} testID={testID} data-testid={testID}>
179
+ {/* Clock face */}
180
+ {renderClock({
181
+ activeSelection,
182
+ hours,
183
+ minutes,
184
+ displayHours,
185
+ mode,
186
+ disabled,
187
+ onHourClick: handleHourClick,
188
+ onMinuteClick: handleMinuteClick,
189
+ })}
190
+
191
+ {/* Digital time display */}
192
+ <View style={timePickerStyles.timeInputRow}>
193
+ {renderTimeInput({
194
+ type: 'hour',
195
+ value: hourInputValue,
196
+ onChangeText: handleHourInputChange,
197
+ onFocus: handleHourInputFocus,
198
+ onBlur: handleHourInputBlur,
199
+ isActive: activeSelection === 'hour',
200
+ disabled,
201
+ inputRef: hourInputRef,
202
+ })}
203
+
204
+ <Text style={timePickerStyles.timeSeparator}>:</Text>
205
+
206
+ {renderTimeInput({
207
+ type: 'minute',
208
+ value: minuteInputValue,
209
+ onChangeText: handleMinuteInputChange,
210
+ onFocus: handleMinuteInputFocus,
211
+ onBlur: handleMinuteInputBlur,
212
+ isActive: activeSelection === 'minute',
213
+ disabled,
214
+ inputRef: minuteInputRef,
215
+ })}
216
+
217
+
218
+ {mode === '12h' && ampm && (
219
+ <Button
220
+ variant="outlined"
221
+ size="small"
222
+ onPress={toggleAmPm}
223
+ disabled={disabled}
224
+ style={timePickerStyles.ampmButton}
225
+ >
226
+ {ampm}
227
+ </Button>
228
+ )}
229
+ </View>
230
+ </View>
231
+ );
232
+ };