@idealyst/datepicker 1.1.4 → 1.1.5
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 +6 -5
- package/src/DateInput.native.tsx +155 -0
- package/src/DateInput.tsx +2 -0
- package/src/DateInput.web.tsx +146 -0
- package/src/DatePicker.tsx +276 -0
- package/src/DateTimePicker.tsx +89 -0
- package/src/TimeInput.native.tsx +175 -0
- package/src/TimeInput.tsx +2 -0
- package/src/TimeInput.web.tsx +171 -0
- package/src/TimePicker.tsx +106 -0
- package/src/examples/DatePickerExamples.tsx +113 -149
- package/src/examples/index.ts +1 -1
- package/src/index.native.ts +15 -20
- package/src/index.ts +14 -19
- package/src/styles.ts +127 -0
- package/src/types.ts +56 -0
- package/src/DateInput/DateInput.native.tsx +0 -61
- package/src/DateInput/DateInput.styles.tsx +0 -26
- package/src/DateInput/DateInput.web.tsx +0 -61
- package/src/DateInput/DateInputBase.tsx +0 -228
- package/src/DateInput/index.native.ts +0 -2
- package/src/DateInput/index.ts +0 -2
- package/src/DateInput/types.ts +0 -60
- package/src/DatePicker/Calendar.native.tsx +0 -261
- package/src/DatePicker/Calendar.styles.tsx +0 -230
- package/src/DatePicker/Calendar.web.tsx +0 -159
- package/src/DatePicker/DatePicker.native.tsx +0 -51
- package/src/DatePicker/DatePicker.styles.tsx +0 -76
- package/src/DatePicker/DatePicker.web.tsx +0 -31
- package/src/DatePicker/index.native.ts +0 -3
- package/src/DatePicker/index.ts +0 -3
- package/src/DatePicker/types.ts +0 -78
- package/src/DateRangePicker/DateRangePicker.native.tsx +0 -39
- package/src/DateRangePicker/DateRangePicker.styles.tsx +0 -83
- package/src/DateRangePicker/DateRangePicker.web.tsx +0 -40
- package/src/DateRangePicker/RangeCalendar.native.tsx +0 -355
- package/src/DateRangePicker/RangeCalendar.styles.tsx +0 -200
- package/src/DateRangePicker/RangeCalendar.web.tsx +0 -384
- package/src/DateRangePicker/index.native.ts +0 -3
- package/src/DateRangePicker/index.ts +0 -3
- package/src/DateRangePicker/types.ts +0 -107
- package/src/DateTimePicker/DateTimePicker.native.tsx +0 -24
- package/src/DateTimePicker/DateTimePicker.styles.tsx +0 -104
- package/src/DateTimePicker/DateTimePicker.tsx +0 -21
- package/src/DateTimePicker/DateTimePickerBase.tsx +0 -185
- package/src/DateTimePicker/TimePicker.native.tsx +0 -17
- package/src/DateTimePicker/TimePicker.styles.tsx +0 -115
- package/src/DateTimePicker/TimePicker.tsx +0 -14
- package/src/DateTimePicker/TimePickerBase.tsx +0 -232
- package/src/DateTimePicker/index.native.ts +0 -3
- package/src/DateTimePicker/index.ts +0 -3
- package/src/DateTimePicker/primitives/ClockFace.native.tsx +0 -195
- package/src/DateTimePicker/primitives/ClockFace.web.tsx +0 -168
- package/src/DateTimePicker/primitives/TimeInput.native.tsx +0 -53
- package/src/DateTimePicker/primitives/TimeInput.web.tsx +0 -66
- package/src/DateTimePicker/primitives/index.native.ts +0 -2
- package/src/DateTimePicker/primitives/index.ts +0 -2
- package/src/DateTimePicker/primitives/index.web.ts +0 -2
- package/src/DateTimePicker/types.ts +0 -80
- package/src/DateTimePicker/utils/dimensions.native.ts +0 -9
- package/src/DateTimePicker/utils/dimensions.ts +0 -9
- package/src/DateTimePicker/utils/dimensions.web.ts +0 -33
- package/src/DateTimeRangePicker/DateTimeRangePicker.native.tsx +0 -24
- package/src/DateTimeRangePicker/DateTimeRangePicker.styles.tsx +0 -118
- package/src/DateTimeRangePicker/DateTimeRangePicker.web.tsx +0 -21
- package/src/DateTimeRangePicker/DateTimeRangePickerBase.tsx +0 -327
- package/src/DateTimeRangePicker/index.native.ts +0 -2
- package/src/DateTimeRangePicker/index.ts +0 -2
- package/src/DateTimeRangePicker/types.ts +0 -70
- package/src/primitives/CalendarGrid/CalendarGrid.styles.tsx +0 -51
- package/src/primitives/CalendarGrid/CalendarGrid.tsx +0 -146
- package/src/primitives/CalendarGrid/index.ts +0 -1
- package/src/primitives/CalendarHeader/CalendarHeader.styles.tsx +0 -25
- package/src/primitives/CalendarHeader/CalendarHeader.tsx +0 -69
- package/src/primitives/CalendarHeader/index.ts +0 -1
- package/src/primitives/CalendarOverlay/CalendarOverlay.styles.tsx +0 -86
- package/src/primitives/CalendarOverlay/CalendarOverlay.tsx +0 -136
- package/src/primitives/CalendarOverlay/index.ts +0 -1
- package/src/primitives/Wrapper/Wrapper.web.tsx +0 -33
- package/src/primitives/Wrapper/index.ts +0 -1
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { View, Input, Text } from '@idealyst/components';
|
|
3
|
-
import { DateInputBase } from './DateInputBase';
|
|
4
|
-
import { DateInputProps } from './types';
|
|
5
|
-
import { dateInputStyles } from './DateInput.styles';
|
|
6
|
-
|
|
7
|
-
export const DateInput: React.FC<DateInputProps> = (props) => {
|
|
8
|
-
const {
|
|
9
|
-
label,
|
|
10
|
-
error,
|
|
11
|
-
helperText,
|
|
12
|
-
size = 'md',
|
|
13
|
-
variant = 'outlined',
|
|
14
|
-
disabled = false,
|
|
15
|
-
style,
|
|
16
|
-
inputStyle,
|
|
17
|
-
testID,
|
|
18
|
-
...baseProps
|
|
19
|
-
} = props;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<View style={style} testID={testID}>
|
|
24
|
-
{label && (
|
|
25
|
-
<Text style={dateInputStyles.label} testID={testID ? `${testID}-label` : undefined}>
|
|
26
|
-
{label}
|
|
27
|
-
</Text>
|
|
28
|
-
)}
|
|
29
|
-
|
|
30
|
-
<DateInputBase {...baseProps} disabled={disabled} testID={testID}>
|
|
31
|
-
{({ value, onChangeText, onFocus, onBlur, placeholder, disabled: inputDisabled, testID: inputTestID }) => (
|
|
32
|
-
<Input
|
|
33
|
-
value={value}
|
|
34
|
-
onChangeText={onChangeText}
|
|
35
|
-
onFocus={onFocus}
|
|
36
|
-
onBlur={onBlur}
|
|
37
|
-
placeholder={placeholder}
|
|
38
|
-
disabled={inputDisabled}
|
|
39
|
-
size={size}
|
|
40
|
-
variant={variant}
|
|
41
|
-
hasError={error ? true : false}
|
|
42
|
-
style={inputStyle}
|
|
43
|
-
testID={inputTestID}
|
|
44
|
-
/>
|
|
45
|
-
)}
|
|
46
|
-
</DateInputBase>
|
|
47
|
-
|
|
48
|
-
{error && (
|
|
49
|
-
<Text style={dateInputStyles.errorText} testID={testID ? `${testID}-error` : undefined}>
|
|
50
|
-
{error}
|
|
51
|
-
</Text>
|
|
52
|
-
)}
|
|
53
|
-
|
|
54
|
-
{!error && helperText && (
|
|
55
|
-
<Text style={dateInputStyles.helperText} testID={testID ? `${testID}-helper` : undefined}>
|
|
56
|
-
{helperText}
|
|
57
|
-
</Text>
|
|
58
|
-
)}
|
|
59
|
-
</View>
|
|
60
|
-
);
|
|
61
|
-
};
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
-
import { View } from '@idealyst/components';
|
|
3
|
-
import { DateInputProps } from './types';
|
|
4
|
-
import { dateInputStyles } from './DateInput.styles';
|
|
5
|
-
|
|
6
|
-
interface DateInputBaseProps extends DateInputProps {
|
|
7
|
-
children: (props: {
|
|
8
|
-
value: string;
|
|
9
|
-
onChangeText: (text: string) => void;
|
|
10
|
-
onFocus: () => void;
|
|
11
|
-
onBlur: () => void;
|
|
12
|
-
placeholder?: string;
|
|
13
|
-
disabled: boolean;
|
|
14
|
-
testID?: string;
|
|
15
|
-
}) => React.ReactNode;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Common date formats for parsing
|
|
19
|
-
const DEFAULT_INPUT_FORMATS = [
|
|
20
|
-
'MM/dd/yyyy',
|
|
21
|
-
'M/d/yyyy',
|
|
22
|
-
'MM/dd/yy',
|
|
23
|
-
'M/d/yy',
|
|
24
|
-
'yyyy-MM-dd',
|
|
25
|
-
'MM-dd-yyyy',
|
|
26
|
-
'M-d-yyyy',
|
|
27
|
-
'dd/MM/yyyy',
|
|
28
|
-
'd/M/yyyy',
|
|
29
|
-
'dd-MM-yyyy',
|
|
30
|
-
'd-M-yyyy',
|
|
31
|
-
'yyyy/MM/dd',
|
|
32
|
-
'MMM dd, yyyy',
|
|
33
|
-
'MMM d, yyyy',
|
|
34
|
-
'MMMM dd, yyyy',
|
|
35
|
-
'MMMM d, yyyy',
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
const DEFAULT_DISPLAY_FORMAT = 'MMMM d, yyyy';
|
|
39
|
-
|
|
40
|
-
export const DateInputBase: React.FC<DateInputBaseProps> = ({
|
|
41
|
-
value,
|
|
42
|
-
onChange,
|
|
43
|
-
minDate,
|
|
44
|
-
maxDate,
|
|
45
|
-
disabled = false,
|
|
46
|
-
placeholder = 'Enter date...',
|
|
47
|
-
displayFormat = DEFAULT_DISPLAY_FORMAT,
|
|
48
|
-
inputFormats = DEFAULT_INPUT_FORMATS,
|
|
49
|
-
locale = 'en-US',
|
|
50
|
-
style,
|
|
51
|
-
testID,
|
|
52
|
-
onFocus,
|
|
53
|
-
onBlur,
|
|
54
|
-
children,
|
|
55
|
-
}) => {
|
|
56
|
-
const [inputValue, setInputValue] = useState('');
|
|
57
|
-
const [isFocused, setIsFocused] = useState(false);
|
|
58
|
-
const [hasError, setHasError] = useState(false);
|
|
59
|
-
const inputRef = useRef<any>(null);
|
|
60
|
-
|
|
61
|
-
// Format date for display when not focused
|
|
62
|
-
const formatDateForDisplay = useCallback((date: Date) => {
|
|
63
|
-
try {
|
|
64
|
-
return date.toLocaleDateString(locale, {
|
|
65
|
-
year: 'numeric',
|
|
66
|
-
month: 'long',
|
|
67
|
-
day: 'numeric',
|
|
68
|
-
});
|
|
69
|
-
} catch {
|
|
70
|
-
return date.toLocaleDateString('en-US', {
|
|
71
|
-
year: 'numeric',
|
|
72
|
-
month: 'long',
|
|
73
|
-
day: 'numeric',
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}, [locale]);
|
|
77
|
-
|
|
78
|
-
// Parse date from various input formats
|
|
79
|
-
const parseDate = useCallback((dateString: string): Date | null => {
|
|
80
|
-
if (!dateString.trim()) return null;
|
|
81
|
-
|
|
82
|
-
// Try direct Date parsing first
|
|
83
|
-
const directParse = new Date(dateString);
|
|
84
|
-
if (!isNaN(directParse.getTime())) {
|
|
85
|
-
return directParse;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Try common formats
|
|
89
|
-
const trimmed = dateString.trim();
|
|
90
|
-
|
|
91
|
-
// Handle MM/dd/yyyy and variations
|
|
92
|
-
const slashFormats = [
|
|
93
|
-
/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/,
|
|
94
|
-
/^(\d{1,2})\/(\d{1,2})\/(\d{2})$/,
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
for (const format of slashFormats) {
|
|
98
|
-
const match = trimmed.match(format);
|
|
99
|
-
if (match) {
|
|
100
|
-
const [, month, day, year] = match;
|
|
101
|
-
const fullYear = year.length === 2 ? 2000 + parseInt(year) : parseInt(year);
|
|
102
|
-
const date = new Date(fullYear, parseInt(month) - 1, parseInt(day));
|
|
103
|
-
if (!isNaN(date.getTime())) return date;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Handle dd/MM/yyyy variations (European format)
|
|
108
|
-
const europeanMatch = trimmed.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
|
109
|
-
if (europeanMatch) {
|
|
110
|
-
const [, day, month, year] = europeanMatch;
|
|
111
|
-
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
112
|
-
if (!isNaN(date.getTime())) return date;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Handle yyyy-MM-dd (ISO format)
|
|
116
|
-
const isoMatch = trimmed.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
|
117
|
-
if (isoMatch) {
|
|
118
|
-
const [, year, month, day] = isoMatch;
|
|
119
|
-
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
120
|
-
if (!isNaN(date.getTime())) return date;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Handle dash formats
|
|
124
|
-
const dashFormats = [
|
|
125
|
-
/^(\d{1,2})-(\d{1,2})-(\d{4})$/,
|
|
126
|
-
/^(\d{4})-(\d{1,2})-(\d{1,2})$/,
|
|
127
|
-
];
|
|
128
|
-
|
|
129
|
-
for (const format of dashFormats) {
|
|
130
|
-
const match = trimmed.match(format);
|
|
131
|
-
if (match) {
|
|
132
|
-
const [, first, second, third] = match;
|
|
133
|
-
let date: Date;
|
|
134
|
-
|
|
135
|
-
if (third.length === 4) {
|
|
136
|
-
// MM-dd-yyyy or dd-MM-yyyy
|
|
137
|
-
date = new Date(parseInt(third), parseInt(first) - 1, parseInt(second));
|
|
138
|
-
} else {
|
|
139
|
-
// yyyy-MM-dd
|
|
140
|
-
date = new Date(parseInt(first), parseInt(second) - 1, parseInt(third));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (!isNaN(date.getTime())) return date;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return null;
|
|
148
|
-
}, []);
|
|
149
|
-
|
|
150
|
-
// Validate date against constraints
|
|
151
|
-
const validateDate = useCallback((date: Date): boolean => {
|
|
152
|
-
if (minDate && date < minDate) return false;
|
|
153
|
-
if (maxDate && date > maxDate) return false;
|
|
154
|
-
return true;
|
|
155
|
-
}, [minDate, maxDate]);
|
|
156
|
-
|
|
157
|
-
// Update input value when value prop changes
|
|
158
|
-
useEffect(() => {
|
|
159
|
-
if (isFocused) {
|
|
160
|
-
// When focused, keep the raw input value
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (value) {
|
|
165
|
-
setInputValue(formatDateForDisplay(value));
|
|
166
|
-
setHasError(false);
|
|
167
|
-
} else {
|
|
168
|
-
setInputValue('');
|
|
169
|
-
setHasError(false);
|
|
170
|
-
}
|
|
171
|
-
}, [value, isFocused, formatDateForDisplay]);
|
|
172
|
-
|
|
173
|
-
const handleFocus = useCallback(() => {
|
|
174
|
-
setIsFocused(true);
|
|
175
|
-
// Switch to raw input format when focused
|
|
176
|
-
if (value) {
|
|
177
|
-
// Show in a common input format for editing
|
|
178
|
-
const editFormat = value.toLocaleDateString('en-US');
|
|
179
|
-
setInputValue(editFormat);
|
|
180
|
-
}
|
|
181
|
-
onFocus?.();
|
|
182
|
-
}, [value, onFocus]);
|
|
183
|
-
|
|
184
|
-
const handleBlur = useCallback(() => {
|
|
185
|
-
setIsFocused(false);
|
|
186
|
-
|
|
187
|
-
if (inputValue.trim()) {
|
|
188
|
-
const parsedDate = parseDate(inputValue);
|
|
189
|
-
|
|
190
|
-
if (parsedDate && validateDate(parsedDate)) {
|
|
191
|
-
onChange(parsedDate);
|
|
192
|
-
setHasError(false);
|
|
193
|
-
} else {
|
|
194
|
-
setHasError(true);
|
|
195
|
-
// Revert to previous valid value
|
|
196
|
-
if (value) {
|
|
197
|
-
setInputValue(formatDateForDisplay(value));
|
|
198
|
-
} else {
|
|
199
|
-
setInputValue('');
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
} else {
|
|
203
|
-
onChange(null);
|
|
204
|
-
setHasError(false);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
onBlur?.();
|
|
208
|
-
}, [inputValue, parseDate, validateDate, onChange, value, formatDateForDisplay, onBlur]);
|
|
209
|
-
|
|
210
|
-
const handleChangeText = useCallback((text: string) => {
|
|
211
|
-
setInputValue(text);
|
|
212
|
-
setHasError(false);
|
|
213
|
-
}, []);
|
|
214
|
-
|
|
215
|
-
return (
|
|
216
|
-
<View style={[dateInputStyles.container, style]} testID={testID}>
|
|
217
|
-
{children({
|
|
218
|
-
value: inputValue,
|
|
219
|
-
onChangeText: handleChangeText,
|
|
220
|
-
onFocus: handleFocus,
|
|
221
|
-
onBlur: handleBlur,
|
|
222
|
-
placeholder,
|
|
223
|
-
disabled,
|
|
224
|
-
testID: testID ? `${testID}-input` : undefined,
|
|
225
|
-
})}
|
|
226
|
-
</View>
|
|
227
|
-
);
|
|
228
|
-
};
|
package/src/DateInput/index.ts
DELETED
package/src/DateInput/types.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { ViewStyle, TextStyle } from 'react-native';
|
|
2
|
-
|
|
3
|
-
export interface DateInputProps {
|
|
4
|
-
/** Current selected date */
|
|
5
|
-
value?: Date;
|
|
6
|
-
|
|
7
|
-
/** Called when date changes */
|
|
8
|
-
onChange: (date: Date | null) => void;
|
|
9
|
-
|
|
10
|
-
/** Minimum selectable date */
|
|
11
|
-
minDate?: Date;
|
|
12
|
-
|
|
13
|
-
/** Maximum selectable date */
|
|
14
|
-
maxDate?: Date;
|
|
15
|
-
|
|
16
|
-
/** Disabled state */
|
|
17
|
-
disabled?: boolean;
|
|
18
|
-
|
|
19
|
-
/** Placeholder text when no date is selected */
|
|
20
|
-
placeholder?: string;
|
|
21
|
-
|
|
22
|
-
/** Label for the input */
|
|
23
|
-
label?: string;
|
|
24
|
-
|
|
25
|
-
/** Error message to display */
|
|
26
|
-
error?: string;
|
|
27
|
-
|
|
28
|
-
/** Helper text */
|
|
29
|
-
helperText?: string;
|
|
30
|
-
|
|
31
|
-
/** Date format for display when not focused (default: 'MMMM d, yyyy') */
|
|
32
|
-
displayFormat?: string;
|
|
33
|
-
|
|
34
|
-
/** Accepted input formats for parsing (default includes common formats) */
|
|
35
|
-
inputFormats?: string[];
|
|
36
|
-
|
|
37
|
-
/** Locale for date formatting */
|
|
38
|
-
locale?: string;
|
|
39
|
-
|
|
40
|
-
/** Size variant */
|
|
41
|
-
size?: 'sm' | 'md' | 'lg';
|
|
42
|
-
|
|
43
|
-
/** Visual variant */
|
|
44
|
-
variant?: 'outlined' | 'filled';
|
|
45
|
-
|
|
46
|
-
/** Custom styles */
|
|
47
|
-
style?: ViewStyle;
|
|
48
|
-
|
|
49
|
-
/** Custom text input styles */
|
|
50
|
-
inputStyle?: TextStyle;
|
|
51
|
-
|
|
52
|
-
/** Test ID for testing */
|
|
53
|
-
testID?: string;
|
|
54
|
-
|
|
55
|
-
/** Called when input is focused */
|
|
56
|
-
onFocus?: () => void;
|
|
57
|
-
|
|
58
|
-
/** Called when input is blurred */
|
|
59
|
-
onBlur?: () => void;
|
|
60
|
-
}
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import React, { useState, useMemo, useCallback, memo, useRef } from 'react';
|
|
2
|
-
import { View, Text, Button } from '@idealyst/components';
|
|
3
|
-
import { TouchableOpacity, StyleSheet } from 'react-native';
|
|
4
|
-
import { CalendarProps } from './types';
|
|
5
|
-
import { calendarStyles } from './Calendar.styles';
|
|
6
|
-
|
|
7
|
-
// Memoized day cell component for better performance
|
|
8
|
-
const DayCell = memo(({
|
|
9
|
-
date,
|
|
10
|
-
isSelected,
|
|
11
|
-
isDisabled,
|
|
12
|
-
onPress,
|
|
13
|
-
dayButtonStyle,
|
|
14
|
-
textColorSelected,
|
|
15
|
-
textColorDefault
|
|
16
|
-
}: {
|
|
17
|
-
date: Date | null;
|
|
18
|
-
isSelected: boolean;
|
|
19
|
-
isDisabled: boolean;
|
|
20
|
-
onPress: (date: Date) => void;
|
|
21
|
-
dayButtonStyle: any;
|
|
22
|
-
textColorSelected: string;
|
|
23
|
-
textColorDefault: string;
|
|
24
|
-
}) => {
|
|
25
|
-
const handlePress = useCallback(() => {
|
|
26
|
-
if (date && !isDisabled) {
|
|
27
|
-
onPress(date);
|
|
28
|
-
}
|
|
29
|
-
}, [date, isDisabled, onPress]);
|
|
30
|
-
|
|
31
|
-
if (!date) {
|
|
32
|
-
return <View style={calendarStyles.dayCell} />;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<View style={calendarStyles.dayCell}>
|
|
37
|
-
<TouchableOpacity
|
|
38
|
-
onPressIn={handlePress}
|
|
39
|
-
disabled={isDisabled}
|
|
40
|
-
style={[
|
|
41
|
-
dayButtonStyle,
|
|
42
|
-
isSelected && styles.selectedButton,
|
|
43
|
-
isDisabled && styles.disabledButton
|
|
44
|
-
]}
|
|
45
|
-
>
|
|
46
|
-
<Text
|
|
47
|
-
style={[
|
|
48
|
-
styles.dayText,
|
|
49
|
-
{ color: isSelected ? textColorSelected : textColorDefault }
|
|
50
|
-
]}
|
|
51
|
-
>
|
|
52
|
-
{date.getDate()}
|
|
53
|
-
</Text>
|
|
54
|
-
</TouchableOpacity>
|
|
55
|
-
</View>
|
|
56
|
-
);
|
|
57
|
-
}, (prevProps, nextProps) => {
|
|
58
|
-
// Custom comparison function to prevent unnecessary re-renders
|
|
59
|
-
// Only re-render if the relevant props actually changed
|
|
60
|
-
return (
|
|
61
|
-
prevProps.date === nextProps.date &&
|
|
62
|
-
prevProps.isSelected === nextProps.isSelected &&
|
|
63
|
-
prevProps.isDisabled === nextProps.isDisabled &&
|
|
64
|
-
prevProps.onPress === nextProps.onPress
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
DayCell.displayName = 'DayCell';
|
|
69
|
-
|
|
70
|
-
export const Calendar: React.FC<CalendarProps> = memo(({
|
|
71
|
-
value,
|
|
72
|
-
onChange,
|
|
73
|
-
minDate,
|
|
74
|
-
maxDate,
|
|
75
|
-
disabled = false,
|
|
76
|
-
currentMonth: controlledCurrentMonth,
|
|
77
|
-
onMonthChange,
|
|
78
|
-
style,
|
|
79
|
-
testID,
|
|
80
|
-
}) => {
|
|
81
|
-
const [internalCurrentMonth, setInternalCurrentMonth] = useState(
|
|
82
|
-
controlledCurrentMonth || value || new Date()
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const currentMonth = controlledCurrentMonth || internalCurrentMonth;
|
|
86
|
-
|
|
87
|
-
// Store latest callbacks in refs to avoid recreating functions
|
|
88
|
-
const onChangeRef = useRef(onChange);
|
|
89
|
-
const onMonthChangeRef = useRef(onMonthChange);
|
|
90
|
-
onChangeRef.current = onChange;
|
|
91
|
-
onMonthChangeRef.current = onMonthChange;
|
|
92
|
-
|
|
93
|
-
const handleMonthChange = useCallback((newMonth: Date) => {
|
|
94
|
-
if (onMonthChangeRef.current) {
|
|
95
|
-
onMonthChangeRef.current(newMonth);
|
|
96
|
-
} else {
|
|
97
|
-
setInternalCurrentMonth(newMonth);
|
|
98
|
-
}
|
|
99
|
-
}, []);
|
|
100
|
-
|
|
101
|
-
// Memoize calendar data calculation with selection state
|
|
102
|
-
const { calendarDays, monthName } = useMemo(() => {
|
|
103
|
-
const monthStart = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1);
|
|
104
|
-
const monthEnd = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0);
|
|
105
|
-
const daysInMonth = monthEnd.getDate();
|
|
106
|
-
const startingDayOfWeek = monthStart.getDay();
|
|
107
|
-
|
|
108
|
-
// Pre-calculate all calendar days with their states
|
|
109
|
-
const days: Array<{
|
|
110
|
-
date: Date | null;
|
|
111
|
-
isSelected: boolean;
|
|
112
|
-
isDisabled: boolean;
|
|
113
|
-
key: string;
|
|
114
|
-
}> = [];
|
|
115
|
-
|
|
116
|
-
// Add empty cells for days before month starts
|
|
117
|
-
for (let i = 0; i < startingDayOfWeek; i++) {
|
|
118
|
-
days.push({
|
|
119
|
-
date: null,
|
|
120
|
-
isSelected: false,
|
|
121
|
-
isDisabled: false,
|
|
122
|
-
key: `empty-${i}`
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Add days of the month with pre-calculated states
|
|
127
|
-
for (let day = 1; day <= daysInMonth; day++) {
|
|
128
|
-
const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
|
|
129
|
-
const dateTime = date.getTime();
|
|
130
|
-
|
|
131
|
-
// Pre-calculate selection state
|
|
132
|
-
const isSelected = value ? (
|
|
133
|
-
date.getDate() === value.getDate() &&
|
|
134
|
-
date.getMonth() === value.getMonth() &&
|
|
135
|
-
date.getFullYear() === value.getFullYear()
|
|
136
|
-
) : false;
|
|
137
|
-
|
|
138
|
-
// Pre-calculate disabled state
|
|
139
|
-
const isDisabled = disabled ||
|
|
140
|
-
(minDate && dateTime < minDate.getTime()) ||
|
|
141
|
-
(maxDate && dateTime > maxDate.getTime());
|
|
142
|
-
|
|
143
|
-
days.push({
|
|
144
|
-
date,
|
|
145
|
-
isSelected,
|
|
146
|
-
isDisabled: !!isDisabled,
|
|
147
|
-
key: `${date.getFullYear()}-${date.getMonth()}-${day}`
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const name = currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
|
152
|
-
|
|
153
|
-
return { calendarDays: days, monthName: name };
|
|
154
|
-
}, [currentMonth, value, disabled, minDate, maxDate]);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const handleDateClick = useCallback((date: Date) => {
|
|
158
|
-
onChangeRef.current(date);
|
|
159
|
-
}, []);
|
|
160
|
-
|
|
161
|
-
const goToPreviousMonth = useCallback(() => {
|
|
162
|
-
const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1);
|
|
163
|
-
handleMonthChange(newMonth);
|
|
164
|
-
}, [currentMonth, handleMonthChange]);
|
|
165
|
-
|
|
166
|
-
const goToNextMonth = useCallback(() => {
|
|
167
|
-
const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1);
|
|
168
|
-
handleMonthChange(newMonth);
|
|
169
|
-
}, [currentMonth, handleMonthChange]);
|
|
170
|
-
|
|
171
|
-
// Use Unistyles
|
|
172
|
-
calendarStyles.useVariants({});
|
|
173
|
-
|
|
174
|
-
// Pre-calculate styles to avoid inline objects
|
|
175
|
-
const dayButtonStyle = calendarStyles.dayButton;
|
|
176
|
-
|
|
177
|
-
return (
|
|
178
|
-
<View style={[calendarStyles.container, style]} testID={testID}>
|
|
179
|
-
{/* Header */}
|
|
180
|
-
<View style={calendarStyles.header}>
|
|
181
|
-
<Button
|
|
182
|
-
variant="text"
|
|
183
|
-
size="sm"
|
|
184
|
-
onPress={goToPreviousMonth}
|
|
185
|
-
disabled={disabled}
|
|
186
|
-
style={calendarStyles.headerButton}
|
|
187
|
-
>
|
|
188
|
-
←
|
|
189
|
-
</Button>
|
|
190
|
-
<Text weight="semibold">{monthName}</Text>
|
|
191
|
-
<Button
|
|
192
|
-
variant="text"
|
|
193
|
-
size="sm"
|
|
194
|
-
onPress={goToNextMonth}
|
|
195
|
-
disabled={disabled}
|
|
196
|
-
style={calendarStyles.headerButton}
|
|
197
|
-
>
|
|
198
|
-
→
|
|
199
|
-
</Button>
|
|
200
|
-
</View>
|
|
201
|
-
|
|
202
|
-
{/* Weekday headers */}
|
|
203
|
-
<View style={calendarStyles.weekdayHeader}>
|
|
204
|
-
{weekdays.map((day) => (
|
|
205
|
-
<View key={day} style={calendarStyles.weekdayCell}>
|
|
206
|
-
<Text style={calendarStyles.weekdayText}>
|
|
207
|
-
{day}
|
|
208
|
-
</Text>
|
|
209
|
-
</View>
|
|
210
|
-
))}
|
|
211
|
-
</View>
|
|
212
|
-
|
|
213
|
-
{/* Calendar grid */}
|
|
214
|
-
<View style={calendarStyles.calendarGrid}>
|
|
215
|
-
{calendarDays.map((dayInfo) => (
|
|
216
|
-
<DayCell
|
|
217
|
-
key={dayInfo.key}
|
|
218
|
-
date={dayInfo.date}
|
|
219
|
-
isSelected={dayInfo.isSelected}
|
|
220
|
-
isDisabled={dayInfo.isDisabled}
|
|
221
|
-
onPress={handleDateClick}
|
|
222
|
-
dayButtonStyle={dayButtonStyle}
|
|
223
|
-
textColorSelected="#ffffff"
|
|
224
|
-
textColorDefault="#000000"
|
|
225
|
-
/>
|
|
226
|
-
))}
|
|
227
|
-
</View>
|
|
228
|
-
</View>
|
|
229
|
-
);
|
|
230
|
-
}, (prevProps, nextProps) => {
|
|
231
|
-
// Custom comparison to prevent unnecessary re-renders
|
|
232
|
-
// Skip checking callbacks since we handle them with refs
|
|
233
|
-
return (
|
|
234
|
-
prevProps.value?.getTime() === nextProps.value?.getTime() &&
|
|
235
|
-
prevProps.minDate?.getTime() === nextProps.minDate?.getTime() &&
|
|
236
|
-
prevProps.maxDate?.getTime() === nextProps.maxDate?.getTime() &&
|
|
237
|
-
prevProps.disabled === nextProps.disabled &&
|
|
238
|
-
prevProps.currentMonth?.getTime() === nextProps.currentMonth?.getTime() &&
|
|
239
|
-
prevProps.style === nextProps.style &&
|
|
240
|
-
prevProps.testID === nextProps.testID
|
|
241
|
-
);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
Calendar.displayName = 'Calendar';
|
|
245
|
-
|
|
246
|
-
// Static weekday labels
|
|
247
|
-
const weekdays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
248
|
-
|
|
249
|
-
// Pre-defined styles to avoid inline style objects
|
|
250
|
-
const styles = StyleSheet.create({
|
|
251
|
-
selectedButton: {
|
|
252
|
-
backgroundColor: '#3b82f6'
|
|
253
|
-
},
|
|
254
|
-
disabledButton: {
|
|
255
|
-
opacity: 0.5
|
|
256
|
-
},
|
|
257
|
-
dayText: {
|
|
258
|
-
fontSize: 13,
|
|
259
|
-
textAlign: 'center'
|
|
260
|
-
}
|
|
261
|
-
});
|