@idealyst/datepicker 1.0.0 → 1.0.58
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/package.json +10 -5
- package/src/DateInput/DateInput.native.tsx +80 -0
- package/src/DateInput/DateInput.styles.tsx +118 -0
- package/src/DateInput/DateInput.web.tsx +79 -0
- package/src/DateInput/DateInputBase.tsx +233 -0
- package/src/DateInput/index.native.ts +2 -0
- package/src/DateInput/index.ts +2 -0
- package/src/DateInput/types.ts +60 -0
- package/src/DatePicker/Calendar.native.tsx +180 -78
- package/src/DatePicker/Calendar.styles.tsx +73 -70
- package/src/DatePicker/DatePicker.native.tsx +24 -6
- package/src/DatePicker/DatePicker.styles.tsx +18 -11
- package/src/DatePicker/DatePicker.web.tsx +1 -1
- package/src/DatePicker/index.ts +1 -1
- package/src/DateRangePicker/RangeCalendar.native.tsx +143 -55
- package/src/DateRangePicker/RangeCalendar.styles.tsx +65 -39
- package/src/DateRangePicker/RangeCalendar.web.tsx +169 -60
- package/src/DateRangePicker/types.ts +9 -0
- package/src/DateTimePicker/DateTimePicker.native.tsx +11 -69
- package/src/DateTimePicker/DateTimePicker.tsx +12 -70
- package/src/DateTimePicker/DateTimePickerBase.tsx +204 -0
- package/src/DateTimePicker/TimePicker.native.tsx +9 -196
- package/src/DateTimePicker/TimePicker.styles.tsx +4 -2
- package/src/DateTimePicker/TimePicker.tsx +9 -401
- package/src/DateTimePicker/TimePickerBase.tsx +232 -0
- package/src/DateTimePicker/primitives/ClockFace.native.tsx +195 -0
- package/src/DateTimePicker/primitives/ClockFace.web.tsx +168 -0
- package/src/DateTimePicker/primitives/TimeInput.native.tsx +53 -0
- package/src/DateTimePicker/primitives/TimeInput.web.tsx +66 -0
- package/src/DateTimePicker/primitives/index.native.ts +2 -0
- package/src/DateTimePicker/primitives/index.ts +2 -0
- package/src/DateTimePicker/primitives/index.web.ts +2 -0
- package/src/DateTimePicker/types.ts +0 -4
- package/src/DateTimePicker/utils/dimensions.native.ts +9 -0
- package/src/DateTimePicker/utils/dimensions.ts +9 -0
- package/src/DateTimePicker/utils/dimensions.web.ts +33 -0
- package/src/DateTimeRangePicker/DateTimeRangePicker.native.tsx +10 -199
- package/src/DateTimeRangePicker/DateTimeRangePicker.styles.tsx +3 -0
- package/src/DateTimeRangePicker/DateTimeRangePicker.web.tsx +11 -131
- package/src/DateTimeRangePicker/DateTimeRangePickerBase.tsx +337 -0
- package/src/DateTimeRangePicker/types.ts +0 -2
- package/src/examples/DatePickerExamples.tsx +42 -118
- package/src/index.native.ts +4 -0
- package/src/index.ts +4 -0
- /package/src/DatePicker/{Calendar.tsx → Calendar.web.tsx} +0 -0
|
@@ -1,406 +1,14 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import { Button, View, Input } from '@idealyst/components';
|
|
1
|
+
import React from 'react';
|
|
3
2
|
import { TimePickerProps } from './types';
|
|
4
|
-
import {
|
|
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
|
-
<
|
|
273
|
-
{
|
|
274
|
-
<
|
|
275
|
-
|
|
276
|
-
|
|
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
|
+
};
|