@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,12 +1,16 @@
1
- import React, { useState, useMemo } from 'react';
1
+ import React, { useState, useMemo, useEffect, useRef } from 'react';
2
2
  import { View, Text, Button } from '@idealyst/components';
3
3
  import { getWebProps } from 'react-native-unistyles/web';
4
4
  import { RangeCalendarProps, DateRange } from './types';
5
5
  import { rangeCalendarStyles } from './RangeCalendar.styles';
6
+ import { CalendarOverlay } from '../primitives/CalendarOverlay';
6
7
 
7
8
  export const RangeCalendar: React.FC<RangeCalendarProps> = ({
8
9
  value = {},
9
10
  onChange,
11
+ onDateSelected,
12
+ showTimes = false,
13
+ timeMode = '12h',
10
14
  minDate,
11
15
  maxDate,
12
16
  disabled = false,
@@ -21,9 +25,28 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
21
25
  controlledCurrentMonth || value?.startDate || new Date()
22
26
  );
23
27
  const [selectingEnd, setSelectingEnd] = useState(false);
28
+ const [overlayMode, setOverlayMode] = useState<'month' | 'year' | null>(null);
29
+ const containerRef = useRef<View>(null);
24
30
 
25
31
  const currentMonth = controlledCurrentMonth || internalCurrentMonth;
26
32
 
33
+ // Close overlay when clicking outside
34
+ useEffect(() => {
35
+ const handleClickOutside = (event: MouseEvent) => {
36
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
37
+ setOverlayMode(null);
38
+ }
39
+ };
40
+
41
+ if (overlayMode) {
42
+ document.addEventListener('mousedown', handleClickOutside);
43
+ }
44
+
45
+ return () => {
46
+ document.removeEventListener('mousedown', handleClickOutside);
47
+ };
48
+ }, [overlayMode]);
49
+
27
50
  const handleMonthChange = (newMonth: Date) => {
28
51
  if (onMonthChange) {
29
52
  onMonthChange(newMonth);
@@ -78,6 +101,34 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
78
101
  return isDateRangeStart(date) || isDateRangeEnd(date);
79
102
  };
80
103
 
104
+ const formatTime = (date: Date): string => {
105
+ let hours = date.getHours();
106
+ const minutes = String(date.getMinutes()).padStart(2, '0');
107
+
108
+ if (timeMode === '12h') {
109
+ const ampm = hours >= 12 ? 'PM' : 'AM';
110
+ hours = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
111
+ return `${hours}:${minutes}${ampm}`;
112
+ } else {
113
+ return `${String(hours).padStart(2, '0')}:${minutes}`;
114
+ }
115
+ };
116
+
117
+ const getDateTimeInfo = (date: Date) => {
118
+ if (!showTimes) return null;
119
+
120
+ const isStart = isDateRangeStart(date);
121
+ const isEnd = isDateRangeEnd(date);
122
+
123
+ if (isStart && value?.startDate) {
124
+ return { type: 'start', time: formatTime(value.startDate) };
125
+ }
126
+ if (isEnd && value?.endDate) {
127
+ return { type: 'end', time: formatTime(value.endDate) };
128
+ }
129
+ return null;
130
+ };
131
+
81
132
  const handleDateClick = (date: Date) => {
82
133
  if (isDateDisabled(date)) return;
83
134
 
@@ -87,6 +138,7 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
87
138
  if (!startDate || (startDate && endDate)) {
88
139
  onChange({ startDate: date, endDate: undefined });
89
140
  setSelectingEnd(true);
141
+ onDateSelected?.('start');
90
142
  return;
91
143
  }
92
144
 
@@ -116,6 +168,7 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
116
168
 
117
169
  onChange({ startDate: newStartDate, endDate: newEndDate });
118
170
  setSelectingEnd(false);
171
+ onDateSelected?.('end');
119
172
  }
120
173
  };
121
174
 
@@ -129,34 +182,56 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
129
182
  handleMonthChange(newMonth);
130
183
  };
131
184
 
132
- const handlePresetRange = (days: number) => {
133
- const startDate = new Date();
134
- const endDate = new Date();
135
- endDate.setDate(endDate.getDate() + days - 1);
136
-
137
- onChange({ startDate, endDate });
138
- setSelectingEnd(false);
185
+ const createDateWithDayAdjustment = (year: number, month: number, day: number): Date => {
186
+ // Get the last day of the target month
187
+ const lastDayOfMonth = new Date(year, month + 1, 0).getDate();
188
+ // Use the smaller of the requested day or the last day of the month
189
+ const adjustedDay = Math.min(day, lastDayOfMonth);
190
+ return new Date(year, month, adjustedDay);
139
191
  };
140
192
 
141
- const clearRange = () => {
142
- onChange({});
143
- setSelectingEnd(false);
193
+ const handleMonthSelect = (monthIndex: number) => {
194
+ const newMonth = new Date(currentMonth.getFullYear(), monthIndex, 1);
195
+ handleMonthChange(newMonth);
196
+ setOverlayMode(null);
144
197
  };
145
198
 
199
+ const handleYearSelect = (year: number) => {
200
+ const newMonth = new Date(year, currentMonth.getMonth(), 1);
201
+ handleMonthChange(newMonth);
202
+ setOverlayMode(null);
203
+ };
204
+
205
+
146
206
  const monthName = currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
147
207
 
148
208
  // Create calendar grid
149
209
  const calendarDays = [];
150
210
 
151
- // Add empty cells for days before month starts
152
- for (let i = 0; i < startingDayOfWeek; i++) {
153
- calendarDays.push(null);
211
+ // Add previous month days to fill start of week
212
+ const prevMonthEnd = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 0);
213
+ const prevMonthDaysToShow = startingDayOfWeek;
214
+ for (let i = prevMonthDaysToShow - 1; i >= 0; i--) {
215
+ const day = prevMonthEnd.getDate() - i;
216
+ const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, day);
217
+ calendarDays.push({ date, isCurrentMonth: false });
154
218
  }
155
219
 
156
- // Add days of the month
220
+ // Add days of the current month
157
221
  for (let day = 1; day <= daysInMonth; day++) {
158
222
  const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
159
- calendarDays.push(date);
223
+ calendarDays.push({ date, isCurrentMonth: true });
224
+ }
225
+
226
+ // Add next month days to complete partial weeks
227
+ const currentLength = calendarDays.length;
228
+ const weeksNeeded = Math.ceil(currentLength / 7);
229
+ const totalCalendarDays = weeksNeeded * 7;
230
+
231
+ const remainingDays = totalCalendarDays - currentLength;
232
+ for (let day = 1; day <= remainingDays; day++) {
233
+ const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, day);
234
+ calendarDays.push({ date, isCurrentMonth: false });
160
235
  }
161
236
 
162
237
  rangeCalendarStyles.useVariants({});
@@ -166,7 +241,7 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
166
241
  const headerTitleProps = getWebProps([rangeCalendarStyles.headerTitle]);
167
242
 
168
243
  return (
169
- <div {...containerProps} data-testid={testID}>
244
+ <div {...containerProps} data-testid={testID} ref={containerRef}>
170
245
  {/* Header */}
171
246
  <div {...headerProps}>
172
247
  <Button
@@ -179,8 +254,25 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
179
254
 
180
255
  </Button>
181
256
 
182
- <div {...headerTitleProps}>
183
- <Text weight="semibold">{monthName}</Text>
257
+ <div {...headerTitleProps} style={{ display: 'flex', gap: '8px' }}>
258
+ <Button
259
+ variant="text"
260
+ size="small"
261
+ onPress={() => setOverlayMode('month')}
262
+ disabled={disabled}
263
+ style={{ padding: '4px 8px' }}
264
+ >
265
+ <Text weight="semibold">{currentMonth.toLocaleDateString('en-US', { month: 'long' })}</Text>
266
+ </Button>
267
+ <Button
268
+ variant="text"
269
+ size="small"
270
+ onPress={() => setOverlayMode('year')}
271
+ disabled={disabled}
272
+ style={{ padding: '4px 8px' }}
273
+ >
274
+ <Text weight="semibold">{currentMonth.getFullYear()}</Text>
275
+ </Button>
184
276
  </div>
185
277
 
186
278
  <Button
@@ -207,69 +299,86 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
207
299
 
208
300
  {/* Calendar grid */}
209
301
  <div {...getWebProps([rangeCalendarStyles.calendarGrid])}>
210
- {calendarDays.map((date, index) => (
302
+ {calendarDays.map((dayInfo, index) => (
211
303
  <div key={index} {...getWebProps([rangeCalendarStyles.dayCell])}>
212
- {date && (
304
+ {dayInfo && (
213
305
  <Button
214
306
  variant="text"
215
- disabled={isDateDisabled(date)}
216
- onPress={() => handleDateClick(date)}
307
+ disabled={isDateDisabled(dayInfo.date)}
308
+ onPress={() => handleDateClick(dayInfo.date)}
217
309
  size="small"
218
310
  style={{
219
311
  ...getWebProps([rangeCalendarStyles.dayButton]).style,
220
- backgroundColor: isDateSelected(date)
312
+ backgroundColor: isDateSelected(dayInfo.date)
221
313
  ? '#3b82f6'
222
- : isDateInRange(date)
314
+ : isDateInRange(dayInfo.date)
223
315
  ? '#3b82f620'
224
316
  : 'transparent',
225
- color: isDateSelected(date) ? 'white' : 'black',
226
- fontWeight: isDateSelected(date) ? '600' : '400',
227
- borderRadius: isDateRangeStart(date)
317
+ color: isDateSelected(dayInfo.date) ? 'white' : dayInfo.isCurrentMonth ? 'black' : '#9ca3af',
318
+ fontWeight: isDateSelected(dayInfo.date) ? '600' : '400',
319
+ borderRadius: isDateRangeStart(dayInfo.date)
228
320
  ? '6px 0 0 6px'
229
- : isDateRangeEnd(date)
321
+ : isDateRangeEnd(dayInfo.date)
230
322
  ? '0 6px 6px 0'
231
- : isDateInRange(date)
323
+ : isDateInRange(dayInfo.date)
232
324
  ? '0'
233
325
  : '6px',
326
+ flexDirection: 'column',
327
+ justifyContent: 'center',
328
+ alignItems: 'center',
329
+ position: 'relative',
330
+ width: '100%',
331
+ height: '100%',
332
+ minWidth: '36px',
333
+ minHeight: '36px',
334
+ opacity: dayInfo.isCurrentMonth ? 1 : 0.5,
234
335
  }}
235
336
  >
236
- {date.getDate()}
337
+ <div style={{ textAlign: 'center', fontSize: '13px' }}>
338
+ {dayInfo.date.getDate()}
339
+ </div>
340
+ {(() => {
341
+ const timeInfo = getDateTimeInfo(dayInfo.date);
342
+ if (timeInfo) {
343
+ return (
344
+ <div
345
+ style={{
346
+ color: isDateSelected(dayInfo.date) ? 'white' : '#666',
347
+ fontSize: '7px',
348
+ fontWeight: '500',
349
+ textAlign: 'center',
350
+ lineHeight: '1',
351
+ marginTop: '1px',
352
+ whiteSpace: 'nowrap',
353
+ overflow: 'hidden',
354
+ textOverflow: 'ellipsis',
355
+ }}
356
+ >
357
+ {timeInfo.time}
358
+ </div>
359
+ );
360
+ }
361
+ return null;
362
+ })()}
237
363
  </Button>
238
364
  )}
239
365
  </div>
240
366
  ))}
241
367
  </div>
242
368
 
243
- {/* Range presets */}
244
- <div {...getWebProps([rangeCalendarStyles.rangePresets])}>
245
- <Button
246
- variant="text"
247
- size="small"
248
- onPress={() => handlePresetRange(7)}
369
+ {/* Overlay for month/year selection */}
370
+ {overlayMode && (
371
+ <CalendarOverlay
372
+ mode={overlayMode}
373
+ currentMonth={currentMonth.getMonth()}
374
+ currentYear={currentMonth.getFullYear()}
375
+ onMonthSelect={handleMonthSelect}
376
+ onYearSelect={handleYearSelect}
377
+ onClose={() => setOverlayMode(null)}
249
378
  disabled={disabled}
250
- style={getWebProps([rangeCalendarStyles.presetButton]).style}
251
- >
252
- Next 7 days
253
- </Button>
254
- <Button
255
- variant="text"
256
- size="small"
257
- onPress={() => handlePresetRange(30)}
258
- disabled={disabled}
259
- style={getWebProps([rangeCalendarStyles.presetButton]).style}
260
- >
261
- Next 30 days
262
- </Button>
263
- <Button
264
- variant="outlined"
265
- size="small"
266
- onPress={clearRange}
267
- disabled={disabled}
268
- style={getWebProps([rangeCalendarStyles.clearButton]).style}
269
- >
270
- Clear Range
271
- </Button>
272
- </div>
379
+ />
380
+ )}
381
+
273
382
  </div>
274
383
  );
275
384
  };
@@ -69,6 +69,15 @@ export interface RangeCalendarProps {
69
69
  /** Called when range is selected */
70
70
  onChange: (range: DateRange) => void;
71
71
 
72
+ /** Called when a date is selected to indicate which end (start/end) */
73
+ onDateSelected?: (type: 'start' | 'end') => void;
74
+
75
+ /** Show times in calendar cells (for DateTimeRangePicker) */
76
+ showTimes?: boolean;
77
+
78
+ /** Time mode for display */
79
+ timeMode?: '12h' | '24h';
80
+
72
81
  /** Minimum selectable date */
73
82
  minDate?: Date;
74
83
 
@@ -1,81 +1,23 @@
1
1
  import React from 'react';
2
- import { View, Text } from '@idealyst/components';
3
2
  import { DateTimePickerProps } from './types';
3
+ import { DateTimePickerBase } from './DateTimePickerBase';
4
4
  import { Calendar } from '../DatePicker/Calendar.native';
5
5
  import { TimePicker } from './TimePicker.native';
6
6
  import { dateTimePickerStyles } from './DateTimePicker.styles';
7
7
 
8
- const DateTimePicker: React.FC<DateTimePickerProps> = ({
9
- value,
10
- onChange,
11
- minDate,
12
- maxDate,
13
- disabled = false,
14
- timeMode = '12h',
15
- showSeconds = false,
16
- timeStep = 1,
17
- style,
18
- testID,
19
- }) => {
20
-
21
- const handleDateChange = (newDate: Date) => {
22
- if (value) {
23
- // Preserve the time from current value
24
- const updatedDate = new Date(newDate);
25
- updatedDate.setHours(value.getHours(), value.getMinutes(), value.getSeconds());
26
- onChange(updatedDate);
27
- } else {
28
- onChange(newDate);
29
- }
30
- };
31
-
32
- const handleTimeChange = (newTime: Date) => {
33
- if (value) {
34
- // Update time while preserving the date
35
- const updatedDate = new Date(value);
36
- updatedDate.setHours(newTime.getHours(), newTime.getMinutes(), newTime.getSeconds());
37
- onChange(updatedDate);
38
- } else {
39
- // If no date is selected, use today's date with the new time
40
- const today = new Date();
41
- today.setHours(newTime.getHours(), newTime.getMinutes(), newTime.getSeconds());
42
- onChange(today);
43
- }
44
- };
45
-
8
+ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
46
9
  dateTimePickerStyles.useVariants({});
47
10
 
48
11
  return (
49
- <View style={[dateTimePickerStyles.container, style]} testID={testID}>
50
- {/* Date Section */}
51
- <View style={dateTimePickerStyles.section}>
52
- <Text style={dateTimePickerStyles.sectionLabel}>Date</Text>
53
- <View style={dateTimePickerStyles.sectionContent}>
54
- <Calendar
55
- value={value}
56
- onChange={handleDateChange}
57
- minDate={minDate}
58
- maxDate={maxDate}
59
- disabled={disabled}
60
- />
61
- </View>
62
- </View>
63
-
64
- {/* Time Section */}
65
- <View style={dateTimePickerStyles.section}>
66
- <Text style={dateTimePickerStyles.sectionLabel}>Time</Text>
67
- <View style={dateTimePickerStyles.sectionContent}>
68
- <TimePicker
69
- value={value || new Date()}
70
- onChange={handleTimeChange}
71
- disabled={disabled}
72
- mode={timeMode}
73
- showSeconds={showSeconds}
74
- step={timeStep}
75
- />
76
- </View>
77
- </View>
78
- </View>
12
+ <DateTimePickerBase
13
+ {...props}
14
+ renderCalendar={(calendarProps) => (
15
+ <Calendar {...calendarProps} />
16
+ )}
17
+ renderTimePicker={(timePickerProps) => (
18
+ <TimePicker {...timePickerProps} />
19
+ )}
20
+ />
79
21
  );
80
22
  };
81
23
 
@@ -1,78 +1,20 @@
1
1
  import React from 'react';
2
- import { View } from '@idealyst/components';
3
2
  import { DateTimePickerProps } from './types';
4
- import { Calendar } from '../DatePicker/Calendar';
3
+ import { DateTimePickerBase } from './DateTimePickerBase';
4
+ import { Calendar } from '../DatePicker/Calendar.web';
5
5
  import { TimePicker } from './TimePicker';
6
6
 
7
- const DateTimePicker: React.FC<DateTimePickerProps> = ({
8
- value,
9
- onChange,
10
- minDate,
11
- maxDate,
12
- disabled = false,
13
- timeMode = '12h',
14
- showSeconds = false,
15
- timeStep = 1,
16
- style,
17
- testID,
18
- }) => {
19
-
20
- const handleDateChange = (newDate: Date) => {
21
- if (value) {
22
- // Preserve the time from current value
23
- const updatedDate = new Date(newDate);
24
- updatedDate.setHours(value.getHours(), value.getMinutes(), value.getSeconds());
25
- onChange(updatedDate);
26
- } else {
27
- onChange(newDate);
28
- }
29
- };
30
-
31
- const handleTimeChange = (newTime: Date) => {
32
- if (value) {
33
- // Update time while preserving the date
34
- const updatedDate = new Date(value);
35
- updatedDate.setHours(newTime.getHours(), newTime.getMinutes(), newTime.getSeconds());
36
- onChange(updatedDate);
37
- } else {
38
- // If no date is selected, use today's date with the new time
39
- const today = new Date();
40
- today.setHours(newTime.getHours(), newTime.getMinutes(), newTime.getSeconds());
41
- onChange(today);
42
- }
43
- };
44
-
45
- const containerStyle = {
46
- flexDirection: 'row' as const,
47
- gap: 16,
48
- alignItems: 'flex-start',
49
-
50
- _web: {
51
- display: 'flex',
52
- flexDirection: 'row',
53
- alignItems: 'flex-start',
54
- }
55
- };
56
-
7
+ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
57
8
  return (
58
- <View style={[containerStyle, style]} data-testid={testID}>
59
- <Calendar
60
- value={value}
61
- onChange={handleDateChange}
62
- minDate={minDate}
63
- maxDate={maxDate}
64
- disabled={disabled}
65
- />
66
-
67
- <TimePicker
68
- value={value || new Date()}
69
- onChange={handleTimeChange}
70
- disabled={disabled}
71
- mode={timeMode}
72
- showSeconds={showSeconds}
73
- step={timeStep}
74
- />
75
- </View>
9
+ <DateTimePickerBase
10
+ {...props}
11
+ renderCalendar={(calendarProps) => (
12
+ <Calendar {...calendarProps} />
13
+ )}
14
+ renderTimePicker={(timePickerProps) => (
15
+ <TimePicker {...timePickerProps} />
16
+ )}
17
+ />
76
18
  );
77
19
  };
78
20