@idealyst/datepicker 1.0.41 → 1.0.59
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 +7 -6
- package/src/DateInput/DateInput.native.tsx +86 -0
- package/src/DateInput/DateInput.styles.tsx +80 -0
- package/src/DateInput/DateInput.web.tsx +85 -0
- package/src/DateInput/DateInputBase.tsx +230 -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/DateTimePicker/DateTimePickerBase.tsx +1 -38
- package/src/DateTimeRangePicker/DateTimeRangePickerBase.tsx +1 -55
- package/src/index.native.ts +4 -0
- package/src/index.ts +4 -0
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/datepicker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.59",
|
|
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,8 +36,8 @@
|
|
|
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.59",
|
|
40
|
+
"@idealyst/theme": "^1.0.59",
|
|
40
41
|
"react": ">=16.8.0",
|
|
41
42
|
"react-native": ">=0.60.0",
|
|
42
43
|
"react-native-svg": ">=13.0.0",
|
|
@@ -78,4 +79,4 @@
|
|
|
78
79
|
"cross-platform",
|
|
79
80
|
"picker"
|
|
80
81
|
]
|
|
81
|
-
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
// Determine input style based on state
|
|
22
|
+
const getInputStyle = () => {
|
|
23
|
+
const styles = [dateInputStyles.input];
|
|
24
|
+
|
|
25
|
+
if (disabled) {
|
|
26
|
+
styles.push(dateInputStyles.inputDisabled);
|
|
27
|
+
} else if (error) {
|
|
28
|
+
styles.push(dateInputStyles.inputError);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return styles;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View style={style} testID={testID}>
|
|
36
|
+
{label && (
|
|
37
|
+
<Text style={dateInputStyles.label} testID={testID ? `${testID}-label` : undefined}>
|
|
38
|
+
{label}
|
|
39
|
+
</Text>
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
<DateInputBase
|
|
43
|
+
{...baseProps}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
testID={testID}
|
|
46
|
+
renderInput={({
|
|
47
|
+
value,
|
|
48
|
+
onChangeText,
|
|
49
|
+
onFocus,
|
|
50
|
+
onBlur,
|
|
51
|
+
placeholder,
|
|
52
|
+
editable,
|
|
53
|
+
style: inputStyleProp,
|
|
54
|
+
testID: inputTestID,
|
|
55
|
+
}) => (
|
|
56
|
+
<TextInput
|
|
57
|
+
value={value}
|
|
58
|
+
onChangeText={onChangeText}
|
|
59
|
+
onFocus={onFocus}
|
|
60
|
+
onBlur={onBlur}
|
|
61
|
+
placeholder={placeholder}
|
|
62
|
+
editable={editable}
|
|
63
|
+
style={[...getInputStyle(), inputStyle]}
|
|
64
|
+
testID={inputTestID}
|
|
65
|
+
autoComplete="off"
|
|
66
|
+
autoCorrect={false}
|
|
67
|
+
spellCheck={false}
|
|
68
|
+
keyboardType="default"
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
{error && (
|
|
74
|
+
<Text style={dateInputStyles.errorText} testID={testID ? `${testID}-error` : undefined}>
|
|
75
|
+
{error}
|
|
76
|
+
</Text>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{!error && helperText && (
|
|
80
|
+
<Text style={dateInputStyles.helperText} testID={testID ? `${testID}-helper` : undefined}>
|
|
81
|
+
{helperText}
|
|
82
|
+
</Text>
|
|
83
|
+
)}
|
|
84
|
+
</View>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
|
|
3
|
+
export const dateInputStyles = StyleSheet.create((theme) => ({
|
|
4
|
+
container: {
|
|
5
|
+
width: '100%',
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
input: {
|
|
9
|
+
borderWidth: 1,
|
|
10
|
+
borderColor: theme.colors?.border?.primary || '#e5e7eb',
|
|
11
|
+
borderRadius: theme.borderRadius?.md || 8,
|
|
12
|
+
paddingHorizontal: theme.spacing?.md || 12,
|
|
13
|
+
paddingVertical: theme.spacing?.sm || 8,
|
|
14
|
+
fontSize: theme.typography?.sizes?.medium || 16,
|
|
15
|
+
color: theme.colors?.text?.primary || '#1f2937',
|
|
16
|
+
backgroundColor: theme.colors?.surface?.primary || '#ffffff',
|
|
17
|
+
|
|
18
|
+
_web: {
|
|
19
|
+
outlineStyle: 'none',
|
|
20
|
+
cursor: 'text',
|
|
21
|
+
border: `1px solid ${theme.colors?.border?.primary || '#e5e7eb'}`,
|
|
22
|
+
borderWidth: undefined,
|
|
23
|
+
borderColor: undefined,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
inputError: {
|
|
28
|
+
borderColor: theme.colors?.text?.error || '#dc2626',
|
|
29
|
+
borderWidth: 2,
|
|
30
|
+
|
|
31
|
+
_web: {
|
|
32
|
+
border: `2px solid ${theme.colors?.text?.error || '#dc2626'}`,
|
|
33
|
+
borderWidth: undefined,
|
|
34
|
+
borderColor: undefined,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
inputFocused: {
|
|
39
|
+
borderColor: theme.colors?.primary || '#3b82f6',
|
|
40
|
+
borderWidth: 2,
|
|
41
|
+
|
|
42
|
+
_web: {
|
|
43
|
+
border: `2px solid ${theme.colors?.primary || '#3b82f6'}`,
|
|
44
|
+
borderWidth: undefined,
|
|
45
|
+
borderColor: undefined,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
inputDisabled: {
|
|
50
|
+
backgroundColor: theme.colors?.surface?.disabled || '#f9fafb',
|
|
51
|
+
color: theme.colors?.text?.disabled || '#9ca3af',
|
|
52
|
+
borderColor: theme.colors?.border?.disabled || '#d1d5db',
|
|
53
|
+
|
|
54
|
+
_web: {
|
|
55
|
+
cursor: 'not-allowed',
|
|
56
|
+
border: `1px solid ${theme.colors?.border?.disabled || '#d1d5db'}`,
|
|
57
|
+
borderWidth: undefined,
|
|
58
|
+
borderColor: undefined,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
label: {
|
|
63
|
+
fontSize: theme.typography?.sizes?.small || 14,
|
|
64
|
+
color: theme.colors?.text?.primary || '#1f2937',
|
|
65
|
+
marginBottom: theme.spacing?.xs || 4,
|
|
66
|
+
fontWeight: '500',
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
helperText: {
|
|
70
|
+
fontSize: theme.typography?.sizes?.small || 12,
|
|
71
|
+
color: theme.colors?.text?.secondary || '#6b7280',
|
|
72
|
+
marginTop: theme.spacing?.xs || 4,
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
errorText: {
|
|
76
|
+
fontSize: theme.typography?.sizes?.small || 12,
|
|
77
|
+
color: theme.colors?.text?.error || '#dc2626',
|
|
78
|
+
marginTop: theme.spacing?.xs || 4,
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
// Determine input style based on state
|
|
22
|
+
const getInputStyle = () => {
|
|
23
|
+
const styles = [dateInputStyles.input];
|
|
24
|
+
|
|
25
|
+
if (disabled) {
|
|
26
|
+
styles.push(dateInputStyles.inputDisabled);
|
|
27
|
+
} else if (error) {
|
|
28
|
+
styles.push(dateInputStyles.inputError);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return styles;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View style={style} testID={testID}>
|
|
36
|
+
{label && (
|
|
37
|
+
<Text style={dateInputStyles.label} testID={testID ? `${testID}-label` : undefined}>
|
|
38
|
+
{label}
|
|
39
|
+
</Text>
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
<DateInputBase
|
|
43
|
+
{...baseProps}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
testID={testID}
|
|
46
|
+
renderInput={({
|
|
47
|
+
value,
|
|
48
|
+
onChangeText,
|
|
49
|
+
onFocus,
|
|
50
|
+
onBlur,
|
|
51
|
+
placeholder,
|
|
52
|
+
editable,
|
|
53
|
+
style: inputStyleProp,
|
|
54
|
+
testID: inputTestID,
|
|
55
|
+
}) => (
|
|
56
|
+
<TextInput
|
|
57
|
+
value={value}
|
|
58
|
+
onChangeText={onChangeText}
|
|
59
|
+
onFocus={onFocus}
|
|
60
|
+
onBlur={onBlur}
|
|
61
|
+
placeholder={placeholder}
|
|
62
|
+
editable={editable}
|
|
63
|
+
style={[...getInputStyle(), inputStyle]}
|
|
64
|
+
testID={inputTestID}
|
|
65
|
+
autoComplete="off"
|
|
66
|
+
autoCorrect={false}
|
|
67
|
+
spellCheck={false}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
{error && (
|
|
73
|
+
<Text style={dateInputStyles.errorText} testID={testID ? `${testID}-error` : undefined}>
|
|
74
|
+
{error}
|
|
75
|
+
</Text>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{!error && helperText && (
|
|
79
|
+
<Text style={dateInputStyles.helperText} testID={testID ? `${testID}-helper` : undefined}>
|
|
80
|
+
{helperText}
|
|
81
|
+
</Text>
|
|
82
|
+
)}
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
@@ -0,0 +1,230 @@
|
|
|
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: inputStyleProp,
|
|
226
|
+
testID: testID ? `${testID}-input` : undefined,
|
|
227
|
+
})}
|
|
228
|
+
</View>
|
|
229
|
+
);
|
|
230
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import { View,
|
|
2
|
+
import { View, Button } from '@idealyst/components';
|
|
3
3
|
import { DateTimePickerProps } from './types';
|
|
4
4
|
import { dateTimePickerStyles } from './DateTimePicker.styles';
|
|
5
5
|
import { getDimensions, addEventListener } from './utils/dimensions';
|
|
@@ -90,37 +90,10 @@ export const DateTimePickerBase: React.FC<DateTimePickerBaseProps> = ({
|
|
|
90
90
|
}
|
|
91
91
|
}, [value, onChange]);
|
|
92
92
|
|
|
93
|
-
const formatSelectedDateTime = () => {
|
|
94
|
-
if (!value) return 'No date/time selected';
|
|
95
|
-
|
|
96
|
-
const dateStr = value.toLocaleDateString('en-US', {
|
|
97
|
-
weekday: 'short',
|
|
98
|
-
month: 'short',
|
|
99
|
-
day: 'numeric',
|
|
100
|
-
year: 'numeric'
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const timeStr = value.toLocaleTimeString('en-US', {
|
|
104
|
-
hour: 'numeric',
|
|
105
|
-
minute: '2-digit',
|
|
106
|
-
hour12: timeMode === '12h'
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
return `${dateStr} at ${timeStr}`;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
93
|
// Side-by-side layout for larger screens
|
|
113
94
|
if (viewMode === 'responsive' && shouldUseSideBySide) {
|
|
114
95
|
return (
|
|
115
96
|
<View style={[dateTimePickerStyles.container, style]} testID={testID} data-testid={testID}>
|
|
116
|
-
{/* Selected DateTime Header */}
|
|
117
|
-
<View style={dateTimePickerStyles.selectedDateTimeHeader}>
|
|
118
|
-
<Text style={dateTimePickerStyles.selectedDateTimeLabel}>Selected Date & Time</Text>
|
|
119
|
-
<Text style={dateTimePickerStyles.selectedDateTimeValue}>
|
|
120
|
-
{formatSelectedDateTime()}
|
|
121
|
-
</Text>
|
|
122
|
-
</View>
|
|
123
|
-
|
|
124
97
|
{/* Side by side layout */}
|
|
125
98
|
<View style={{
|
|
126
99
|
flexDirection: 'row',
|
|
@@ -159,16 +132,6 @@ export const DateTimePickerBase: React.FC<DateTimePickerBaseProps> = ({
|
|
|
159
132
|
|
|
160
133
|
return (
|
|
161
134
|
<View style={[dateTimePickerStyles.container, style]} testID={testID} data-testid={testID}>
|
|
162
|
-
{/* Selected DateTime Header */}
|
|
163
|
-
<View style={dateTimePickerStyles.selectedDateTimeHeader}>
|
|
164
|
-
<Text style={dateTimePickerStyles.selectedDateTimeLabel}>
|
|
165
|
-
{isDateStep ? 'Select Date' : isTimeStep ? 'Select Time' : 'Selected Date & Time'}
|
|
166
|
-
</Text>
|
|
167
|
-
<Text style={dateTimePickerStyles.selectedDateTimeValue}>
|
|
168
|
-
{formatSelectedDateTime()}
|
|
169
|
-
</Text>
|
|
170
|
-
</View>
|
|
171
|
-
|
|
172
135
|
{/* Step Navigation */}
|
|
173
136
|
<View style={{
|
|
174
137
|
flexDirection: 'row',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import { View,
|
|
2
|
+
import { View, Button } from '@idealyst/components';
|
|
3
3
|
import { DateTimeRangePickerProps, DateTimeRange } from './types';
|
|
4
4
|
import { dateTimeRangePickerStyles } from './DateTimeRangePicker.styles';
|
|
5
5
|
import { timePickerStyles } from '../DateTimePicker/TimePicker.styles';
|
|
@@ -122,40 +122,6 @@ export const DateTimeRangePickerBase: React.FC<DateTimeRangePickerBaseProps> = (
|
|
|
122
122
|
}
|
|
123
123
|
}, [value, onChange]);
|
|
124
124
|
|
|
125
|
-
const formatSelectedRange = () => {
|
|
126
|
-
if (!value?.startDate) return 'No date range selected';
|
|
127
|
-
|
|
128
|
-
const startStr = value.startDate.toLocaleDateString('en-US', {
|
|
129
|
-
month: 'short',
|
|
130
|
-
day: 'numeric',
|
|
131
|
-
year: 'numeric'
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const startTimeStr = value.startDate.toLocaleTimeString('en-US', {
|
|
135
|
-
hour: 'numeric',
|
|
136
|
-
minute: '2-digit',
|
|
137
|
-
hour12: timeMode === '12h'
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
if (!value.endDate) {
|
|
141
|
-
return `${startStr} at ${startTimeStr} - (end date not selected)`;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const endStr = value.endDate.toLocaleDateString('en-US', {
|
|
145
|
-
month: 'short',
|
|
146
|
-
day: 'numeric',
|
|
147
|
-
year: 'numeric'
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
const endTimeStr = value.endDate.toLocaleTimeString('en-US', {
|
|
151
|
-
hour: 'numeric',
|
|
152
|
-
minute: '2-digit',
|
|
153
|
-
hour12: timeMode === '12h'
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
return `${startStr} ${startTimeStr} - ${endStr} ${endTimeStr}`;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
125
|
// Initialize styles
|
|
160
126
|
timePickerStyles.useVariants({});
|
|
161
127
|
|
|
@@ -163,14 +129,6 @@ export const DateTimeRangePickerBase: React.FC<DateTimeRangePickerBaseProps> = (
|
|
|
163
129
|
if (viewMode === 'responsive' && shouldUseSideBySide) {
|
|
164
130
|
return (
|
|
165
131
|
<View style={[dateTimeRangePickerStyles.container, style]} testID={testID} data-testid={testID}>
|
|
166
|
-
{/* Selected Range Header */}
|
|
167
|
-
<View style={dateTimeRangePickerStyles.selectedRangeHeader}>
|
|
168
|
-
<Text style={dateTimeRangePickerStyles.selectedRangeLabel}>Selected Date & Time Range</Text>
|
|
169
|
-
<Text style={dateTimeRangePickerStyles.selectedRangeValue}>
|
|
170
|
-
{formatSelectedRange()}
|
|
171
|
-
</Text>
|
|
172
|
-
</View>
|
|
173
|
-
|
|
174
132
|
{/* Side by side layout */}
|
|
175
133
|
<View style={{
|
|
176
134
|
flexDirection: 'row',
|
|
@@ -257,18 +215,6 @@ export const DateTimeRangePickerBase: React.FC<DateTimeRangePickerBaseProps> = (
|
|
|
257
215
|
|
|
258
216
|
return (
|
|
259
217
|
<View style={[dateTimeRangePickerStyles.container, style]} testID={testID} data-testid={testID}>
|
|
260
|
-
{/* Selected Range Header */}
|
|
261
|
-
<View style={dateTimeRangePickerStyles.selectedRangeHeader}>
|
|
262
|
-
<Text style={dateTimeRangePickerStyles.selectedRangeLabel}>
|
|
263
|
-
{isDateStep ? 'Select Date Range' :
|
|
264
|
-
isStartTimeStep ? 'Set Start Time' :
|
|
265
|
-
isEndTimeStep ? 'Set End Time' : 'Selected Date & Time Range'}
|
|
266
|
-
</Text>
|
|
267
|
-
<Text style={dateTimeRangePickerStyles.selectedRangeValue}>
|
|
268
|
-
{formatSelectedRange()}
|
|
269
|
-
</Text>
|
|
270
|
-
</View>
|
|
271
|
-
|
|
272
218
|
{/* Step Navigation */}
|
|
273
219
|
<View style={timePickerStyles.tabBar}>
|
|
274
220
|
<Button
|
package/src/index.native.ts
CHANGED
|
@@ -14,3 +14,7 @@ export type { DateRangePickerProps, RangeCalendarProps, DateRange } from './Date
|
|
|
14
14
|
export { DateTimeRangePicker } from './DateTimeRangePicker/index.native';
|
|
15
15
|
export type { DateTimeRangePickerProps, DateTimeRange } from './DateTimeRangePicker';
|
|
16
16
|
|
|
17
|
+
// Date Input Component
|
|
18
|
+
export { DateInput } from './DateInput/index.native';
|
|
19
|
+
export type { DateInputProps } from './DateInput';
|
|
20
|
+
|
package/src/index.ts
CHANGED
|
@@ -14,3 +14,7 @@ export type { DateRangePickerProps, RangeCalendarProps, DateRange } from './Date
|
|
|
14
14
|
export { DateTimeRangePicker } from './DateTimeRangePicker';
|
|
15
15
|
export type { DateTimeRangePickerProps, DateTimeRange } from './DateTimeRangePicker';
|
|
16
16
|
|
|
17
|
+
// Date Input Component
|
|
18
|
+
export { DateInput } from './DateInput';
|
|
19
|
+
export type { DateInputProps } from './DateInput';
|
|
20
|
+
|