@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
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/datepicker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.58",
|
|
4
4
|
"description": "Cross-platform date and time picker components for React and React Native",
|
|
5
|
-
"documentation": "https://github.com/
|
|
5
|
+
"documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/datepicker#readme",
|
|
6
|
+
"readme": "README.md",
|
|
6
7
|
"main": "src/index.ts",
|
|
7
8
|
"module": "src/index.ts",
|
|
8
9
|
"types": "src/index.ts",
|
|
9
10
|
"react-native": "src/index.native.ts",
|
|
10
11
|
"repository": {
|
|
11
12
|
"type": "git",
|
|
12
|
-
"url": "https://github.com/
|
|
13
|
+
"url": "https://github.com/IdealystIO/idealyst-framework.git",
|
|
13
14
|
"directory": "packages/datepicker"
|
|
14
15
|
},
|
|
15
16
|
"author": "Your Name <your.email@example.com>",
|
|
@@ -35,10 +36,11 @@
|
|
|
35
36
|
"publish:npm": "npm publish"
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|
|
38
|
-
"@idealyst/components": "^1.0.
|
|
39
|
-
"@idealyst/theme": "^1.0.
|
|
39
|
+
"@idealyst/components": "^1.0.58",
|
|
40
|
+
"@idealyst/theme": "^1.0.58",
|
|
40
41
|
"react": ">=16.8.0",
|
|
41
42
|
"react-native": ">=0.60.0",
|
|
43
|
+
"react-native-svg": ">=13.0.0",
|
|
42
44
|
"react-native-unistyles": "^3.0.4"
|
|
43
45
|
},
|
|
44
46
|
"peerDependenciesMeta": {
|
|
@@ -51,6 +53,9 @@
|
|
|
51
53
|
"react-native": {
|
|
52
54
|
"optional": true
|
|
53
55
|
},
|
|
56
|
+
"react-native-svg": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
54
59
|
"react-native-unistyles": {
|
|
55
60
|
"optional": true
|
|
56
61
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, TextInput, 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 = 'medium',
|
|
13
|
+
variant = 'outlined',
|
|
14
|
+
disabled = false,
|
|
15
|
+
style,
|
|
16
|
+
inputStyle,
|
|
17
|
+
testID,
|
|
18
|
+
...baseProps
|
|
19
|
+
} = props;
|
|
20
|
+
|
|
21
|
+
// Initialize styles
|
|
22
|
+
dateInputStyles.useVariants({
|
|
23
|
+
size,
|
|
24
|
+
variant,
|
|
25
|
+
state: disabled ? 'disabled' : error ? 'error' : undefined,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<View style={style} testID={testID}>
|
|
30
|
+
{label && (
|
|
31
|
+
<Text style={dateInputStyles.label} testID={testID ? `${testID}-label` : undefined}>
|
|
32
|
+
{label}
|
|
33
|
+
</Text>
|
|
34
|
+
)}
|
|
35
|
+
|
|
36
|
+
<DateInputBase
|
|
37
|
+
{...baseProps}
|
|
38
|
+
disabled={disabled}
|
|
39
|
+
testID={testID}
|
|
40
|
+
renderInput={({
|
|
41
|
+
value,
|
|
42
|
+
onChangeText,
|
|
43
|
+
onFocus,
|
|
44
|
+
onBlur,
|
|
45
|
+
placeholder,
|
|
46
|
+
editable,
|
|
47
|
+
style: inputStyleProp,
|
|
48
|
+
testID: inputTestID,
|
|
49
|
+
}) => (
|
|
50
|
+
<TextInput
|
|
51
|
+
value={value}
|
|
52
|
+
onChangeText={onChangeText}
|
|
53
|
+
onFocus={onFocus}
|
|
54
|
+
onBlur={onBlur}
|
|
55
|
+
placeholder={placeholder}
|
|
56
|
+
editable={editable}
|
|
57
|
+
style={[inputStyleProp, inputStyle]}
|
|
58
|
+
testID={inputTestID}
|
|
59
|
+
autoComplete="off"
|
|
60
|
+
autoCorrect={false}
|
|
61
|
+
spellCheck={false}
|
|
62
|
+
keyboardType="default"
|
|
63
|
+
/>
|
|
64
|
+
)}
|
|
65
|
+
/>
|
|
66
|
+
|
|
67
|
+
{error && (
|
|
68
|
+
<Text style={dateInputStyles.errorText} testID={testID ? `${testID}-error` : undefined}>
|
|
69
|
+
{error}
|
|
70
|
+
</Text>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{!error && helperText && (
|
|
74
|
+
<Text style={dateInputStyles.helperText} testID={testID ? `${testID}-helper` : undefined}>
|
|
75
|
+
{helperText}
|
|
76
|
+
</Text>
|
|
77
|
+
)}
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createStyleSheet } from 'react-native-unistyles';
|
|
2
|
+
|
|
3
|
+
export const dateInputStyles = createStyleSheet((theme) => ({
|
|
4
|
+
container: {
|
|
5
|
+
width: '100%',
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
input: {
|
|
9
|
+
borderWidth: 1,
|
|
10
|
+
borderColor: theme.colors.border,
|
|
11
|
+
borderRadius: theme.radius.medium,
|
|
12
|
+
paddingHorizontal: theme.spacing.medium,
|
|
13
|
+
paddingVertical: theme.spacing.small,
|
|
14
|
+
fontSize: theme.typography.body.fontSize,
|
|
15
|
+
fontFamily: theme.typography.body.fontFamily,
|
|
16
|
+
color: theme.colors.text,
|
|
17
|
+
backgroundColor: theme.colors.background,
|
|
18
|
+
|
|
19
|
+
variants: {
|
|
20
|
+
size: {
|
|
21
|
+
small: {
|
|
22
|
+
paddingHorizontal: theme.spacing.small,
|
|
23
|
+
paddingVertical: theme.spacing.xs,
|
|
24
|
+
fontSize: theme.typography.caption.fontSize,
|
|
25
|
+
},
|
|
26
|
+
medium: {
|
|
27
|
+
paddingHorizontal: theme.spacing.medium,
|
|
28
|
+
paddingVertical: theme.spacing.small,
|
|
29
|
+
fontSize: theme.typography.body.fontSize,
|
|
30
|
+
},
|
|
31
|
+
large: {
|
|
32
|
+
paddingHorizontal: theme.spacing.large,
|
|
33
|
+
paddingVertical: theme.spacing.medium,
|
|
34
|
+
fontSize: theme.typography.subtitle.fontSize,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
variant: {
|
|
39
|
+
outlined: {
|
|
40
|
+
borderWidth: 1,
|
|
41
|
+
backgroundColor: 'transparent',
|
|
42
|
+
},
|
|
43
|
+
filled: {
|
|
44
|
+
borderWidth: 0,
|
|
45
|
+
backgroundColor: theme.colors.surfaceVariant,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
state: {
|
|
50
|
+
focused: {
|
|
51
|
+
borderColor: theme.colors.primary,
|
|
52
|
+
borderWidth: 2,
|
|
53
|
+
},
|
|
54
|
+
disabled: {
|
|
55
|
+
backgroundColor: theme.colors.disabled,
|
|
56
|
+
color: theme.colors.onDisabled,
|
|
57
|
+
borderColor: theme.colors.outline,
|
|
58
|
+
},
|
|
59
|
+
error: {
|
|
60
|
+
borderColor: theme.colors.error,
|
|
61
|
+
borderWidth: 2,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
_web: {
|
|
67
|
+
outlineStyle: 'none',
|
|
68
|
+
cursor: 'text',
|
|
69
|
+
|
|
70
|
+
':focus': {
|
|
71
|
+
borderColor: theme.colors.primary,
|
|
72
|
+
borderWidth: 2,
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
':disabled': {
|
|
76
|
+
cursor: 'not-allowed',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
inputError: {
|
|
82
|
+
borderColor: theme.colors.error,
|
|
83
|
+
borderWidth: 2,
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
inputFocused: {
|
|
87
|
+
borderColor: theme.colors.primary,
|
|
88
|
+
borderWidth: 2,
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
inputDisabled: {
|
|
92
|
+
backgroundColor: theme.colors.disabled,
|
|
93
|
+
color: theme.colors.onDisabled,
|
|
94
|
+
borderColor: theme.colors.outline,
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
label: {
|
|
98
|
+
fontSize: theme.typography.caption.fontSize,
|
|
99
|
+
fontFamily: theme.typography.caption.fontFamily,
|
|
100
|
+
color: theme.colors.onSurface,
|
|
101
|
+
marginBottom: theme.spacing.xs,
|
|
102
|
+
fontWeight: '500',
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
helperText: {
|
|
106
|
+
fontSize: theme.typography.caption.fontSize,
|
|
107
|
+
fontFamily: theme.typography.caption.fontFamily,
|
|
108
|
+
color: theme.colors.onSurfaceVariant,
|
|
109
|
+
marginTop: theme.spacing.xs,
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
errorText: {
|
|
113
|
+
fontSize: theme.typography.caption.fontSize,
|
|
114
|
+
fontFamily: theme.typography.caption.fontFamily,
|
|
115
|
+
color: theme.colors.error,
|
|
116
|
+
marginTop: theme.spacing.xs,
|
|
117
|
+
},
|
|
118
|
+
}));
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, TextInput, 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 = 'medium',
|
|
13
|
+
variant = 'outlined',
|
|
14
|
+
disabled = false,
|
|
15
|
+
style,
|
|
16
|
+
inputStyle,
|
|
17
|
+
testID,
|
|
18
|
+
...baseProps
|
|
19
|
+
} = props;
|
|
20
|
+
|
|
21
|
+
// Initialize styles
|
|
22
|
+
dateInputStyles.useVariants({
|
|
23
|
+
size,
|
|
24
|
+
variant,
|
|
25
|
+
state: disabled ? 'disabled' : error ? 'error' : undefined,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<View style={style} testID={testID}>
|
|
30
|
+
{label && (
|
|
31
|
+
<Text style={dateInputStyles.label} testID={testID ? `${testID}-label` : undefined}>
|
|
32
|
+
{label}
|
|
33
|
+
</Text>
|
|
34
|
+
)}
|
|
35
|
+
|
|
36
|
+
<DateInputBase
|
|
37
|
+
{...baseProps}
|
|
38
|
+
disabled={disabled}
|
|
39
|
+
testID={testID}
|
|
40
|
+
renderInput={({
|
|
41
|
+
value,
|
|
42
|
+
onChangeText,
|
|
43
|
+
onFocus,
|
|
44
|
+
onBlur,
|
|
45
|
+
placeholder,
|
|
46
|
+
editable,
|
|
47
|
+
style: inputStyleProp,
|
|
48
|
+
testID: inputTestID,
|
|
49
|
+
}) => (
|
|
50
|
+
<TextInput
|
|
51
|
+
value={value}
|
|
52
|
+
onChangeText={onChangeText}
|
|
53
|
+
onFocus={onFocus}
|
|
54
|
+
onBlur={onBlur}
|
|
55
|
+
placeholder={placeholder}
|
|
56
|
+
editable={editable}
|
|
57
|
+
style={[inputStyleProp, inputStyle]}
|
|
58
|
+
testID={inputTestID}
|
|
59
|
+
autoComplete="off"
|
|
60
|
+
autoCorrect={false}
|
|
61
|
+
spellCheck={false}
|
|
62
|
+
/>
|
|
63
|
+
)}
|
|
64
|
+
/>
|
|
65
|
+
|
|
66
|
+
{error && (
|
|
67
|
+
<Text style={dateInputStyles.errorText} testID={testID ? `${testID}-error` : undefined}>
|
|
68
|
+
{error}
|
|
69
|
+
</Text>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{!error && helperText && (
|
|
73
|
+
<Text style={dateInputStyles.helperText} testID={testID ? `${testID}-helper` : undefined}>
|
|
74
|
+
{helperText}
|
|
75
|
+
</Text>
|
|
76
|
+
)}
|
|
77
|
+
</View>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import { View, TextInput } from '@idealyst/components';
|
|
3
|
+
import { DateInputProps } from './types';
|
|
4
|
+
import { dateInputStyles } from './DateInput.styles';
|
|
5
|
+
|
|
6
|
+
interface DateInputBaseProps extends DateInputProps {
|
|
7
|
+
renderInput: (props: {
|
|
8
|
+
value: string;
|
|
9
|
+
onChangeText: (text: string) => void;
|
|
10
|
+
onFocus: () => void;
|
|
11
|
+
onBlur: () => void;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
editable: boolean;
|
|
14
|
+
style?: any;
|
|
15
|
+
testID?: string;
|
|
16
|
+
}) => React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Common date formats for parsing
|
|
20
|
+
const DEFAULT_INPUT_FORMATS = [
|
|
21
|
+
'MM/dd/yyyy',
|
|
22
|
+
'M/d/yyyy',
|
|
23
|
+
'MM/dd/yy',
|
|
24
|
+
'M/d/yy',
|
|
25
|
+
'yyyy-MM-dd',
|
|
26
|
+
'MM-dd-yyyy',
|
|
27
|
+
'M-d-yyyy',
|
|
28
|
+
'dd/MM/yyyy',
|
|
29
|
+
'd/M/yyyy',
|
|
30
|
+
'dd-MM-yyyy',
|
|
31
|
+
'd-M-yyyy',
|
|
32
|
+
'yyyy/MM/dd',
|
|
33
|
+
'MMM dd, yyyy',
|
|
34
|
+
'MMM d, yyyy',
|
|
35
|
+
'MMMM dd, yyyy',
|
|
36
|
+
'MMMM d, yyyy',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const DEFAULT_DISPLAY_FORMAT = 'MMMM d, yyyy';
|
|
40
|
+
|
|
41
|
+
export const DateInputBase: React.FC<DateInputBaseProps> = ({
|
|
42
|
+
value,
|
|
43
|
+
onChange,
|
|
44
|
+
minDate,
|
|
45
|
+
maxDate,
|
|
46
|
+
disabled = false,
|
|
47
|
+
placeholder = 'Enter date...',
|
|
48
|
+
displayFormat = DEFAULT_DISPLAY_FORMAT,
|
|
49
|
+
inputFormats = DEFAULT_INPUT_FORMATS,
|
|
50
|
+
locale = 'en-US',
|
|
51
|
+
style,
|
|
52
|
+
testID,
|
|
53
|
+
onFocus,
|
|
54
|
+
onBlur,
|
|
55
|
+
renderInput,
|
|
56
|
+
}) => {
|
|
57
|
+
const [inputValue, setInputValue] = useState('');
|
|
58
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
59
|
+
const [hasError, setHasError] = useState(false);
|
|
60
|
+
const inputRef = useRef<any>(null);
|
|
61
|
+
|
|
62
|
+
// Format date for display when not focused
|
|
63
|
+
const formatDateForDisplay = useCallback((date: Date) => {
|
|
64
|
+
try {
|
|
65
|
+
return date.toLocaleDateString(locale, {
|
|
66
|
+
year: 'numeric',
|
|
67
|
+
month: 'long',
|
|
68
|
+
day: 'numeric',
|
|
69
|
+
});
|
|
70
|
+
} catch {
|
|
71
|
+
return date.toLocaleDateString('en-US', {
|
|
72
|
+
year: 'numeric',
|
|
73
|
+
month: 'long',
|
|
74
|
+
day: 'numeric',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}, [locale]);
|
|
78
|
+
|
|
79
|
+
// Parse date from various input formats
|
|
80
|
+
const parseDate = useCallback((dateString: string): Date | null => {
|
|
81
|
+
if (!dateString.trim()) return null;
|
|
82
|
+
|
|
83
|
+
// Try direct Date parsing first
|
|
84
|
+
const directParse = new Date(dateString);
|
|
85
|
+
if (!isNaN(directParse.getTime())) {
|
|
86
|
+
return directParse;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Try common formats
|
|
90
|
+
const trimmed = dateString.trim();
|
|
91
|
+
|
|
92
|
+
// Handle MM/dd/yyyy and variations
|
|
93
|
+
const slashFormats = [
|
|
94
|
+
/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/,
|
|
95
|
+
/^(\d{1,2})\/(\d{1,2})\/(\d{2})$/,
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
for (const format of slashFormats) {
|
|
99
|
+
const match = trimmed.match(format);
|
|
100
|
+
if (match) {
|
|
101
|
+
const [, month, day, year] = match;
|
|
102
|
+
const fullYear = year.length === 2 ? 2000 + parseInt(year) : parseInt(year);
|
|
103
|
+
const date = new Date(fullYear, parseInt(month) - 1, parseInt(day));
|
|
104
|
+
if (!isNaN(date.getTime())) return date;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle dd/MM/yyyy variations (European format)
|
|
109
|
+
const europeanMatch = trimmed.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
|
110
|
+
if (europeanMatch) {
|
|
111
|
+
const [, day, month, year] = europeanMatch;
|
|
112
|
+
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
113
|
+
if (!isNaN(date.getTime())) return date;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Handle yyyy-MM-dd (ISO format)
|
|
117
|
+
const isoMatch = trimmed.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
|
118
|
+
if (isoMatch) {
|
|
119
|
+
const [, year, month, day] = isoMatch;
|
|
120
|
+
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
121
|
+
if (!isNaN(date.getTime())) return date;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Handle dash formats
|
|
125
|
+
const dashFormats = [
|
|
126
|
+
/^(\d{1,2})-(\d{1,2})-(\d{4})$/,
|
|
127
|
+
/^(\d{4})-(\d{1,2})-(\d{1,2})$/,
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
for (const format of dashFormats) {
|
|
131
|
+
const match = trimmed.match(format);
|
|
132
|
+
if (match) {
|
|
133
|
+
const [, first, second, third] = match;
|
|
134
|
+
let date: Date;
|
|
135
|
+
|
|
136
|
+
if (third.length === 4) {
|
|
137
|
+
// MM-dd-yyyy or dd-MM-yyyy
|
|
138
|
+
date = new Date(parseInt(third), parseInt(first) - 1, parseInt(second));
|
|
139
|
+
} else {
|
|
140
|
+
// yyyy-MM-dd
|
|
141
|
+
date = new Date(parseInt(first), parseInt(second) - 1, parseInt(third));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!isNaN(date.getTime())) return date;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return null;
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
// Validate date against constraints
|
|
152
|
+
const validateDate = useCallback((date: Date): boolean => {
|
|
153
|
+
if (minDate && date < minDate) return false;
|
|
154
|
+
if (maxDate && date > maxDate) return false;
|
|
155
|
+
return true;
|
|
156
|
+
}, [minDate, maxDate]);
|
|
157
|
+
|
|
158
|
+
// Update input value when value prop changes
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (isFocused) {
|
|
161
|
+
// When focused, keep the raw input value
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (value) {
|
|
166
|
+
setInputValue(formatDateForDisplay(value));
|
|
167
|
+
setHasError(false);
|
|
168
|
+
} else {
|
|
169
|
+
setInputValue('');
|
|
170
|
+
setHasError(false);
|
|
171
|
+
}
|
|
172
|
+
}, [value, isFocused, formatDateForDisplay]);
|
|
173
|
+
|
|
174
|
+
const handleFocus = useCallback(() => {
|
|
175
|
+
setIsFocused(true);
|
|
176
|
+
// Switch to raw input format when focused
|
|
177
|
+
if (value) {
|
|
178
|
+
// Show in a common input format for editing
|
|
179
|
+
const editFormat = value.toLocaleDateString('en-US');
|
|
180
|
+
setInputValue(editFormat);
|
|
181
|
+
}
|
|
182
|
+
onFocus?.();
|
|
183
|
+
}, [value, onFocus]);
|
|
184
|
+
|
|
185
|
+
const handleBlur = useCallback(() => {
|
|
186
|
+
setIsFocused(false);
|
|
187
|
+
|
|
188
|
+
if (inputValue.trim()) {
|
|
189
|
+
const parsedDate = parseDate(inputValue);
|
|
190
|
+
|
|
191
|
+
if (parsedDate && validateDate(parsedDate)) {
|
|
192
|
+
onChange(parsedDate);
|
|
193
|
+
setHasError(false);
|
|
194
|
+
} else {
|
|
195
|
+
setHasError(true);
|
|
196
|
+
// Revert to previous valid value
|
|
197
|
+
if (value) {
|
|
198
|
+
setInputValue(formatDateForDisplay(value));
|
|
199
|
+
} else {
|
|
200
|
+
setInputValue('');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
onChange(null);
|
|
205
|
+
setHasError(false);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
onBlur?.();
|
|
209
|
+
}, [inputValue, parseDate, validateDate, onChange, value, formatDateForDisplay, onBlur]);
|
|
210
|
+
|
|
211
|
+
const handleChangeText = useCallback((text: string) => {
|
|
212
|
+
setInputValue(text);
|
|
213
|
+
setHasError(false);
|
|
214
|
+
}, []);
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<View style={[dateInputStyles.container, style]} testID={testID}>
|
|
218
|
+
{renderInput({
|
|
219
|
+
value: inputValue,
|
|
220
|
+
onChangeText: handleChangeText,
|
|
221
|
+
onFocus: handleFocus,
|
|
222
|
+
onBlur: handleBlur,
|
|
223
|
+
placeholder,
|
|
224
|
+
editable: !disabled,
|
|
225
|
+
style: [
|
|
226
|
+
dateInputStyles.input,
|
|
227
|
+
hasError && dateInputStyles.inputError,
|
|
228
|
+
],
|
|
229
|
+
testID: testID ? `${testID}-input` : undefined,
|
|
230
|
+
})}
|
|
231
|
+
</View>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
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?: 'small' | 'medium' | 'large';
|
|
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
|
+
}
|