react-native-month-day-picker 0.1.0
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/LICENSE +21 -0
- package/README.md +269 -0
- package/lib/commonjs/BirthdayPicker.js +177 -0
- package/lib/commonjs/BirthdayPicker.js.map +1 -0
- package/lib/commonjs/BirthdayPickerModal.js +176 -0
- package/lib/commonjs/BirthdayPickerModal.js.map +1 -0
- package/lib/commonjs/constants.js +80 -0
- package/lib/commonjs/constants.js.map +1 -0
- package/lib/commonjs/hooks/useBirthdayPicker.js +90 -0
- package/lib/commonjs/hooks/useBirthdayPicker.js.map +1 -0
- package/lib/commonjs/index.js +89 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/utils/dateUtils.js +103 -0
- package/lib/commonjs/utils/dateUtils.js.map +1 -0
- package/lib/commonjs/utils/localeUtils.js +90 -0
- package/lib/commonjs/utils/localeUtils.js.map +1 -0
- package/lib/module/BirthdayPicker.js +169 -0
- package/lib/module/BirthdayPicker.js.map +1 -0
- package/lib/module/BirthdayPickerModal.js +169 -0
- package/lib/module/BirthdayPickerModal.js.map +1 -0
- package/lib/module/constants.js +74 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/hooks/useBirthdayPicker.js +84 -0
- package/lib/module/hooks/useBirthdayPicker.js.map +1 -0
- package/lib/module/index.js +16 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/dateUtils.js +92 -0
- package/lib/module/utils/dateUtils.js.map +1 -0
- package/lib/module/utils/localeUtils.js +81 -0
- package/lib/module/utils/localeUtils.js.map +1 -0
- package/lib/typescript/BirthdayPicker.d.ts +25 -0
- package/lib/typescript/BirthdayPicker.d.ts.map +1 -0
- package/lib/typescript/BirthdayPickerModal.d.ts +24 -0
- package/lib/typescript/BirthdayPickerModal.d.ts.map +1 -0
- package/lib/typescript/constants.d.ts +39 -0
- package/lib/typescript/constants.d.ts.map +1 -0
- package/lib/typescript/hooks/useBirthdayPicker.d.ts +17 -0
- package/lib/typescript/hooks/useBirthdayPicker.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +8 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +160 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/utils/dateUtils.d.ts +43 -0
- package/lib/typescript/utils/dateUtils.d.ts.map +1 -0
- package/lib/typescript/utils/localeUtils.d.ts +28 -0
- package/lib/typescript/utils/localeUtils.d.ts.map +1 -0
- package/package.json +137 -0
- package/src/BirthdayPicker.tsx +210 -0
- package/src/BirthdayPickerModal.tsx +192 -0
- package/src/constants.ts +64 -0
- package/src/hooks/useBirthdayPicker.ts +106 -0
- package/src/index.ts +31 -0
- package/src/types.ts +189 -0
- package/src/utils/dateUtils.ts +101 -0
- package/src/utils/localeUtils.ts +99 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
View,
|
|
5
|
+
Text,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
SafeAreaView,
|
|
9
|
+
Platform,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import { BirthdayPicker } from './BirthdayPicker';
|
|
12
|
+
import type { BirthdayPickerModalProps, BirthdayValue } from './types';
|
|
13
|
+
import { DEFAULT_BIRTHDAY_VALUE } from './constants';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A modal wrapper for BirthdayPicker that provides confirm/cancel functionality.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const [visible, setVisible] = useState(false);
|
|
21
|
+
* const [birthday, setBirthday] = useState({ month: 1, day: 1 });
|
|
22
|
+
*
|
|
23
|
+
* <BirthdayPickerModal
|
|
24
|
+
* visible={visible}
|
|
25
|
+
* value={birthday}
|
|
26
|
+
* onConfirm={(value) => {
|
|
27
|
+
* setBirthday(value);
|
|
28
|
+
* setVisible(false);
|
|
29
|
+
* }}
|
|
30
|
+
* onCancel={() => setVisible(false)}
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function BirthdayPickerModal({
|
|
35
|
+
visible,
|
|
36
|
+
onConfirm,
|
|
37
|
+
onCancel,
|
|
38
|
+
value: externalValue,
|
|
39
|
+
defaultValue = DEFAULT_BIRTHDAY_VALUE,
|
|
40
|
+
title = 'Select Birthday',
|
|
41
|
+
confirmText = 'Confirm',
|
|
42
|
+
cancelText = 'Cancel',
|
|
43
|
+
animationType = 'slide',
|
|
44
|
+
locale,
|
|
45
|
+
monthFormat,
|
|
46
|
+
allowLeapDay,
|
|
47
|
+
disabled,
|
|
48
|
+
testID,
|
|
49
|
+
itemHeight,
|
|
50
|
+
visibleItems,
|
|
51
|
+
monthAccessibilityLabel,
|
|
52
|
+
dayAccessibilityLabel,
|
|
53
|
+
}: BirthdayPickerModalProps): React.ReactElement {
|
|
54
|
+
// Internal state to track the selection while modal is open
|
|
55
|
+
const [internalValue, setInternalValue] = useState<BirthdayValue>(
|
|
56
|
+
() => externalValue ?? defaultValue
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Reset internal value when modal opens or external value changes
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (visible) {
|
|
62
|
+
setInternalValue(externalValue ?? defaultValue);
|
|
63
|
+
}
|
|
64
|
+
}, [visible, externalValue, defaultValue]);
|
|
65
|
+
|
|
66
|
+
// Handle value changes from the picker
|
|
67
|
+
const handleChange = useCallback((newValue: BirthdayValue) => {
|
|
68
|
+
setInternalValue(newValue);
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
// Handle confirm button press
|
|
72
|
+
const handleConfirm = useCallback(() => {
|
|
73
|
+
onConfirm(internalValue);
|
|
74
|
+
}, [onConfirm, internalValue]);
|
|
75
|
+
|
|
76
|
+
// Handle cancel button press
|
|
77
|
+
const handleCancel = useCallback(() => {
|
|
78
|
+
onCancel();
|
|
79
|
+
}, [onCancel]);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Modal
|
|
83
|
+
visible={visible}
|
|
84
|
+
transparent
|
|
85
|
+
animationType={animationType}
|
|
86
|
+
onRequestClose={handleCancel}
|
|
87
|
+
testID={testID}
|
|
88
|
+
>
|
|
89
|
+
<View style={styles.overlay}>
|
|
90
|
+
<SafeAreaView style={styles.safeArea}>
|
|
91
|
+
<View style={styles.container}>
|
|
92
|
+
{/* Header */}
|
|
93
|
+
<View style={styles.header}>
|
|
94
|
+
<TouchableOpacity
|
|
95
|
+
onPress={handleCancel}
|
|
96
|
+
style={styles.headerButton}
|
|
97
|
+
accessibilityRole="button"
|
|
98
|
+
accessibilityLabel={cancelText}
|
|
99
|
+
testID={testID ? `${testID}-cancel` : undefined}
|
|
100
|
+
>
|
|
101
|
+
<Text style={styles.cancelText}>{cancelText}</Text>
|
|
102
|
+
</TouchableOpacity>
|
|
103
|
+
|
|
104
|
+
<Text style={styles.title}>{title}</Text>
|
|
105
|
+
|
|
106
|
+
<TouchableOpacity
|
|
107
|
+
onPress={handleConfirm}
|
|
108
|
+
style={styles.headerButton}
|
|
109
|
+
accessibilityRole="button"
|
|
110
|
+
accessibilityLabel={confirmText}
|
|
111
|
+
testID={testID ? `${testID}-confirm` : undefined}
|
|
112
|
+
>
|
|
113
|
+
<Text style={styles.confirmText}>{confirmText}</Text>
|
|
114
|
+
</TouchableOpacity>
|
|
115
|
+
</View>
|
|
116
|
+
|
|
117
|
+
{/* Picker */}
|
|
118
|
+
<View style={styles.pickerWrapper}>
|
|
119
|
+
<BirthdayPicker
|
|
120
|
+
value={internalValue}
|
|
121
|
+
onChange={handleChange}
|
|
122
|
+
locale={locale}
|
|
123
|
+
monthFormat={monthFormat}
|
|
124
|
+
allowLeapDay={allowLeapDay}
|
|
125
|
+
disabled={disabled}
|
|
126
|
+
itemHeight={itemHeight}
|
|
127
|
+
visibleItems={visibleItems}
|
|
128
|
+
monthAccessibilityLabel={monthAccessibilityLabel}
|
|
129
|
+
dayAccessibilityLabel={dayAccessibilityLabel}
|
|
130
|
+
testID={testID ? `${testID}-picker` : undefined}
|
|
131
|
+
/>
|
|
132
|
+
</View>
|
|
133
|
+
</View>
|
|
134
|
+
</SafeAreaView>
|
|
135
|
+
</View>
|
|
136
|
+
</Modal>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const styles = StyleSheet.create({
|
|
141
|
+
overlay: {
|
|
142
|
+
flex: 1,
|
|
143
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
144
|
+
justifyContent: 'flex-end',
|
|
145
|
+
},
|
|
146
|
+
safeArea: {
|
|
147
|
+
backgroundColor: 'white',
|
|
148
|
+
borderTopLeftRadius: 16,
|
|
149
|
+
borderTopRightRadius: 16,
|
|
150
|
+
overflow: 'hidden',
|
|
151
|
+
},
|
|
152
|
+
container: {
|
|
153
|
+
backgroundColor: 'white',
|
|
154
|
+
paddingBottom: Platform.OS === 'android' ? 16 : 0,
|
|
155
|
+
},
|
|
156
|
+
header: {
|
|
157
|
+
flexDirection: 'row',
|
|
158
|
+
alignItems: 'center',
|
|
159
|
+
justifyContent: 'space-between',
|
|
160
|
+
paddingHorizontal: 16,
|
|
161
|
+
paddingVertical: 12,
|
|
162
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
163
|
+
borderBottomColor: '#ccc',
|
|
164
|
+
},
|
|
165
|
+
headerButton: {
|
|
166
|
+
paddingVertical: 8,
|
|
167
|
+
paddingHorizontal: 4,
|
|
168
|
+
minWidth: 60,
|
|
169
|
+
},
|
|
170
|
+
title: {
|
|
171
|
+
fontSize: 17,
|
|
172
|
+
fontWeight: '600',
|
|
173
|
+
color: '#000',
|
|
174
|
+
textAlign: 'center',
|
|
175
|
+
flex: 1,
|
|
176
|
+
},
|
|
177
|
+
cancelText: {
|
|
178
|
+
fontSize: 17,
|
|
179
|
+
color: '#007AFF',
|
|
180
|
+
},
|
|
181
|
+
confirmText: {
|
|
182
|
+
fontSize: 17,
|
|
183
|
+
fontWeight: '600',
|
|
184
|
+
color: '#007AFF',
|
|
185
|
+
textAlign: 'right',
|
|
186
|
+
},
|
|
187
|
+
pickerWrapper: {
|
|
188
|
+
paddingVertical: 16,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
export default BirthdayPickerModal;
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { BirthdayValue, MonthFormat } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default birthday value (January 1st)
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_BIRTHDAY_VALUE: BirthdayValue = {
|
|
7
|
+
month: 1,
|
|
8
|
+
day: 1,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default locale for month formatting
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_LOCALE = 'en-US';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default month format
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_MONTH_FORMAT: MonthFormat = 'long';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default item height in pixels
|
|
23
|
+
*/
|
|
24
|
+
export const DEFAULT_ITEM_HEIGHT = 40;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Default number of visible items
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_VISIBLE_ITEMS = 5;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Days in each month (1-indexed, index 0 is unused)
|
|
33
|
+
* February is set to 28, leap day handling is separate
|
|
34
|
+
*/
|
|
35
|
+
export const DAYS_IN_MONTH: readonly number[] = [
|
|
36
|
+
0, // Index 0 unused
|
|
37
|
+
31, // January
|
|
38
|
+
28, // February (leap day handled separately)
|
|
39
|
+
31, // March
|
|
40
|
+
30, // April
|
|
41
|
+
31, // May
|
|
42
|
+
30, // June
|
|
43
|
+
31, // July
|
|
44
|
+
31, // August
|
|
45
|
+
30, // September
|
|
46
|
+
31, // October
|
|
47
|
+
30, // November
|
|
48
|
+
31, // December
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Number of months in a year
|
|
53
|
+
*/
|
|
54
|
+
export const MONTHS_IN_YEAR = 12;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* February month number
|
|
58
|
+
*/
|
|
59
|
+
export const FEBRUARY = 2;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Leap day (Feb 29)
|
|
63
|
+
*/
|
|
64
|
+
export const LEAP_DAY = 29;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
BirthdayValue,
|
|
4
|
+
UseBirthdayPickerOptions,
|
|
5
|
+
UseBirthdayPickerReturn,
|
|
6
|
+
} from '../types';
|
|
7
|
+
import { DEFAULT_BIRTHDAY_VALUE } from '../constants';
|
|
8
|
+
import {
|
|
9
|
+
getDaysInMonth,
|
|
10
|
+
clampDay,
|
|
11
|
+
isValidBirthday,
|
|
12
|
+
normalizeBirthday,
|
|
13
|
+
} from '../utils/dateUtils';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hook for managing birthday picker state
|
|
17
|
+
*
|
|
18
|
+
* @param options - Configuration options
|
|
19
|
+
* @returns State and handlers for birthday picker
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* const { value, setMonth, setDay, daysInMonth } = useBirthdayPicker({
|
|
24
|
+
* initialValue: { month: 6, day: 15 },
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function useBirthdayPicker(
|
|
29
|
+
options: UseBirthdayPickerOptions = {}
|
|
30
|
+
): UseBirthdayPickerReturn {
|
|
31
|
+
const { initialValue = DEFAULT_BIRTHDAY_VALUE, allowLeapDay = true } =
|
|
32
|
+
options;
|
|
33
|
+
|
|
34
|
+
// Normalize the initial value
|
|
35
|
+
const normalizedInitial = useMemo(
|
|
36
|
+
() => normalizeBirthday(initialValue, allowLeapDay),
|
|
37
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
38
|
+
[] // Only compute once on mount
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const [value, setValueInternal] = useState<BirthdayValue>(normalizedInitial);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Set the month, clamping the day if necessary
|
|
45
|
+
*/
|
|
46
|
+
const setMonth = useCallback(
|
|
47
|
+
(month: number) => {
|
|
48
|
+
setValueInternal((prev) => {
|
|
49
|
+
const clampedMonth = Math.max(1, Math.min(month, 12));
|
|
50
|
+
const clampedDay = clampDay(prev.day, clampedMonth, allowLeapDay);
|
|
51
|
+
return { month: clampedMonth, day: clampedDay };
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
[allowLeapDay]
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set the day, clamping to valid range for current month
|
|
59
|
+
*/
|
|
60
|
+
const setDay = useCallback(
|
|
61
|
+
(day: number) => {
|
|
62
|
+
setValueInternal((prev) => {
|
|
63
|
+
const clampedDay = clampDay(day, prev.month, allowLeapDay);
|
|
64
|
+
return { ...prev, day: clampedDay };
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
[allowLeapDay]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Set the entire value, normalizing both month and day
|
|
72
|
+
*/
|
|
73
|
+
const setValue = useCallback(
|
|
74
|
+
(newValue: BirthdayValue) => {
|
|
75
|
+
setValueInternal(normalizeBirthday(newValue, allowLeapDay));
|
|
76
|
+
},
|
|
77
|
+
[allowLeapDay]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Number of days in the currently selected month
|
|
82
|
+
*/
|
|
83
|
+
const daysInMonth = useMemo(
|
|
84
|
+
() => getDaysInMonth(value.month, allowLeapDay),
|
|
85
|
+
[value.month, allowLeapDay]
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether the current value is valid
|
|
90
|
+
*/
|
|
91
|
+
const isValid = useMemo(
|
|
92
|
+
() => isValidBirthday(value, allowLeapDay),
|
|
93
|
+
[value, allowLeapDay]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
value,
|
|
98
|
+
setMonth,
|
|
99
|
+
setDay,
|
|
100
|
+
setValue,
|
|
101
|
+
daysInMonth,
|
|
102
|
+
isValid,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default useBirthdayPicker;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Main components
|
|
2
|
+
export { BirthdayPicker } from './BirthdayPicker';
|
|
3
|
+
export { BirthdayPickerModal } from './BirthdayPickerModal';
|
|
4
|
+
|
|
5
|
+
// Hook
|
|
6
|
+
export { useBirthdayPicker } from './hooks/useBirthdayPicker';
|
|
7
|
+
|
|
8
|
+
// Types
|
|
9
|
+
export type {
|
|
10
|
+
BirthdayValue,
|
|
11
|
+
MonthFormat,
|
|
12
|
+
BirthdayPickerProps,
|
|
13
|
+
BirthdayPickerModalProps,
|
|
14
|
+
UseBirthdayPickerOptions,
|
|
15
|
+
UseBirthdayPickerReturn,
|
|
16
|
+
} from './types';
|
|
17
|
+
|
|
18
|
+
// Utilities (for advanced usage)
|
|
19
|
+
export {
|
|
20
|
+
getDaysInMonth,
|
|
21
|
+
clampDay,
|
|
22
|
+
isValidBirthday,
|
|
23
|
+
getDaysArray,
|
|
24
|
+
getMonthsArray,
|
|
25
|
+
normalizeBirthday,
|
|
26
|
+
} from './utils/dateUtils';
|
|
27
|
+
|
|
28
|
+
export { getMonthNames, formatMonth, formatDay } from './utils/localeUtils';
|
|
29
|
+
|
|
30
|
+
// Default export
|
|
31
|
+
export { BirthdayPicker as default } from './BirthdayPicker';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents a month-day birthday value without year.
|
|
5
|
+
* Month is 1-indexed (1 = January, 12 = December)
|
|
6
|
+
* Day is 1-indexed (1-31 depending on month)
|
|
7
|
+
*/
|
|
8
|
+
export type BirthdayValue = {
|
|
9
|
+
month: number; // 1-12
|
|
10
|
+
day: number; // 1-31
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format for displaying month names
|
|
15
|
+
*/
|
|
16
|
+
export type MonthFormat = 'long' | 'short' | 'numeric';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Props for the BirthdayPicker component
|
|
20
|
+
*/
|
|
21
|
+
export interface BirthdayPickerProps {
|
|
22
|
+
/**
|
|
23
|
+
* Current value (controlled mode)
|
|
24
|
+
*/
|
|
25
|
+
value?: BirthdayValue;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default value (uncontrolled mode)
|
|
29
|
+
*/
|
|
30
|
+
defaultValue?: BirthdayValue;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Called when value changes
|
|
34
|
+
*/
|
|
35
|
+
onChange?: (value: BirthdayValue) => void;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* BCP 47 locale string for month name formatting
|
|
39
|
+
* @default 'en-US'
|
|
40
|
+
*/
|
|
41
|
+
locale?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* How to format month names
|
|
45
|
+
* @default 'long'
|
|
46
|
+
*/
|
|
47
|
+
monthFormat?: MonthFormat;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether to allow Feb 29 as a valid birthday
|
|
51
|
+
* @default true
|
|
52
|
+
*/
|
|
53
|
+
allowLeapDay?: boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Disable interaction
|
|
57
|
+
* @default false
|
|
58
|
+
*/
|
|
59
|
+
disabled?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Test ID for testing
|
|
63
|
+
*/
|
|
64
|
+
testID?: string;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Container style
|
|
68
|
+
*/
|
|
69
|
+
style?: ViewStyle;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Height of each item in the wheel
|
|
73
|
+
* @default 40
|
|
74
|
+
*/
|
|
75
|
+
itemHeight?: number;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Number of visible items in each wheel (must be odd)
|
|
79
|
+
* @default 5
|
|
80
|
+
*/
|
|
81
|
+
visibleItems?: number;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Accessibility label for month picker
|
|
85
|
+
* @default 'Month picker'
|
|
86
|
+
*/
|
|
87
|
+
monthAccessibilityLabel?: string;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Accessibility label for day picker
|
|
91
|
+
* @default 'Day picker'
|
|
92
|
+
*/
|
|
93
|
+
dayAccessibilityLabel?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Props for the BirthdayPickerModal component
|
|
98
|
+
*/
|
|
99
|
+
export interface BirthdayPickerModalProps extends BirthdayPickerProps {
|
|
100
|
+
/**
|
|
101
|
+
* Whether the modal is visible
|
|
102
|
+
*/
|
|
103
|
+
visible: boolean;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Called when user confirms selection
|
|
107
|
+
*/
|
|
108
|
+
onConfirm: (value: BirthdayValue) => void;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Called when user cancels
|
|
112
|
+
*/
|
|
113
|
+
onCancel: () => void;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Modal title
|
|
117
|
+
* @default 'Select Birthday'
|
|
118
|
+
*/
|
|
119
|
+
title?: string;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Confirm button text
|
|
123
|
+
* @default 'Confirm'
|
|
124
|
+
*/
|
|
125
|
+
confirmText?: string;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Cancel button text
|
|
129
|
+
* @default 'Cancel'
|
|
130
|
+
*/
|
|
131
|
+
cancelText?: string;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Animation type for modal presentation
|
|
135
|
+
* @default 'slide'
|
|
136
|
+
*/
|
|
137
|
+
animationType?: 'slide' | 'fade' | 'none';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Options for useBirthdayPicker hook
|
|
142
|
+
*/
|
|
143
|
+
export interface UseBirthdayPickerOptions {
|
|
144
|
+
/**
|
|
145
|
+
* Initial value
|
|
146
|
+
*/
|
|
147
|
+
initialValue?: BirthdayValue;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Whether to allow Feb 29
|
|
151
|
+
* @default true
|
|
152
|
+
*/
|
|
153
|
+
allowLeapDay?: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Return type of useBirthdayPicker hook
|
|
158
|
+
*/
|
|
159
|
+
export interface UseBirthdayPickerReturn {
|
|
160
|
+
/**
|
|
161
|
+
* Current birthday value
|
|
162
|
+
*/
|
|
163
|
+
value: BirthdayValue;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Set the month (will clamp day if needed)
|
|
167
|
+
*/
|
|
168
|
+
setMonth: (month: number) => void;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Set the day
|
|
172
|
+
*/
|
|
173
|
+
setDay: (day: number) => void;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Set the entire value
|
|
177
|
+
*/
|
|
178
|
+
setValue: (value: BirthdayValue) => void;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Number of days in the currently selected month
|
|
182
|
+
*/
|
|
183
|
+
daysInMonth: number;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Whether the current value is valid
|
|
187
|
+
*/
|
|
188
|
+
isValid: boolean;
|
|
189
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DAYS_IN_MONTH,
|
|
3
|
+
FEBRUARY,
|
|
4
|
+
LEAP_DAY,
|
|
5
|
+
MONTHS_IN_YEAR,
|
|
6
|
+
} from '../constants';
|
|
7
|
+
import type { BirthdayValue } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the number of days in a given month
|
|
11
|
+
* @param month - Month (1-12)
|
|
12
|
+
* @param allowLeapDay - Whether to allow Feb 29
|
|
13
|
+
* @returns Number of days in the month
|
|
14
|
+
*/
|
|
15
|
+
export function getDaysInMonth(month: number, allowLeapDay = true): number {
|
|
16
|
+
if (month < 1 || month > MONTHS_IN_YEAR) {
|
|
17
|
+
throw new Error(`Invalid month: ${month}. Must be between 1 and 12.`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (month === FEBRUARY && allowLeapDay) {
|
|
21
|
+
return LEAP_DAY;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return DAYS_IN_MONTH[month];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Clamp a day value to be valid for a given month
|
|
29
|
+
* @param day - Day to clamp
|
|
30
|
+
* @param month - Month (1-12)
|
|
31
|
+
* @param allowLeapDay - Whether to allow Feb 29
|
|
32
|
+
* @returns Clamped day value
|
|
33
|
+
*/
|
|
34
|
+
export function clampDay(
|
|
35
|
+
day: number,
|
|
36
|
+
month: number,
|
|
37
|
+
allowLeapDay = true
|
|
38
|
+
): number {
|
|
39
|
+
const maxDays = getDaysInMonth(month, allowLeapDay);
|
|
40
|
+
return Math.max(1, Math.min(day, maxDays));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a birthday value is valid
|
|
45
|
+
* @param value - Birthday value to validate
|
|
46
|
+
* @param allowLeapDay - Whether to allow Feb 29
|
|
47
|
+
* @returns Whether the value is valid
|
|
48
|
+
*/
|
|
49
|
+
export function isValidBirthday(
|
|
50
|
+
value: BirthdayValue,
|
|
51
|
+
allowLeapDay = true
|
|
52
|
+
): boolean {
|
|
53
|
+
const { month, day } = value;
|
|
54
|
+
|
|
55
|
+
// Check month range
|
|
56
|
+
if (month < 1 || month > MONTHS_IN_YEAR) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check day range
|
|
61
|
+
const maxDays = getDaysInMonth(month, allowLeapDay);
|
|
62
|
+
if (day < 1 || day > maxDays) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generate an array of day numbers for a given month
|
|
71
|
+
* @param month - Month (1-12)
|
|
72
|
+
* @param allowLeapDay - Whether to allow Feb 29
|
|
73
|
+
* @returns Array of day numbers [1, 2, 3, ..., n]
|
|
74
|
+
*/
|
|
75
|
+
export function getDaysArray(month: number, allowLeapDay = true): number[] {
|
|
76
|
+
const maxDays = getDaysInMonth(month, allowLeapDay);
|
|
77
|
+
return Array.from({ length: maxDays }, (_, i) => i + 1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate an array of month numbers
|
|
82
|
+
* @returns Array of month numbers [1, 2, 3, ..., 12]
|
|
83
|
+
*/
|
|
84
|
+
export function getMonthsArray(): number[] {
|
|
85
|
+
return Array.from({ length: MONTHS_IN_YEAR }, (_, i) => i + 1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Normalize a birthday value to ensure it's valid
|
|
90
|
+
* @param value - Birthday value to normalize
|
|
91
|
+
* @param allowLeapDay - Whether to allow Feb 29
|
|
92
|
+
* @returns Normalized birthday value
|
|
93
|
+
*/
|
|
94
|
+
export function normalizeBirthday(
|
|
95
|
+
value: BirthdayValue,
|
|
96
|
+
allowLeapDay = true
|
|
97
|
+
): BirthdayValue {
|
|
98
|
+
const month = Math.max(1, Math.min(value.month, MONTHS_IN_YEAR));
|
|
99
|
+
const day = clampDay(value.day, month, allowLeapDay);
|
|
100
|
+
return { month, day };
|
|
101
|
+
}
|