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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +269 -0
  3. package/lib/commonjs/BirthdayPicker.js +177 -0
  4. package/lib/commonjs/BirthdayPicker.js.map +1 -0
  5. package/lib/commonjs/BirthdayPickerModal.js +176 -0
  6. package/lib/commonjs/BirthdayPickerModal.js.map +1 -0
  7. package/lib/commonjs/constants.js +80 -0
  8. package/lib/commonjs/constants.js.map +1 -0
  9. package/lib/commonjs/hooks/useBirthdayPicker.js +90 -0
  10. package/lib/commonjs/hooks/useBirthdayPicker.js.map +1 -0
  11. package/lib/commonjs/index.js +89 -0
  12. package/lib/commonjs/index.js.map +1 -0
  13. package/lib/commonjs/types.js +6 -0
  14. package/lib/commonjs/types.js.map +1 -0
  15. package/lib/commonjs/utils/dateUtils.js +103 -0
  16. package/lib/commonjs/utils/dateUtils.js.map +1 -0
  17. package/lib/commonjs/utils/localeUtils.js +90 -0
  18. package/lib/commonjs/utils/localeUtils.js.map +1 -0
  19. package/lib/module/BirthdayPicker.js +169 -0
  20. package/lib/module/BirthdayPicker.js.map +1 -0
  21. package/lib/module/BirthdayPickerModal.js +169 -0
  22. package/lib/module/BirthdayPickerModal.js.map +1 -0
  23. package/lib/module/constants.js +74 -0
  24. package/lib/module/constants.js.map +1 -0
  25. package/lib/module/hooks/useBirthdayPicker.js +84 -0
  26. package/lib/module/hooks/useBirthdayPicker.js.map +1 -0
  27. package/lib/module/index.js +16 -0
  28. package/lib/module/index.js.map +1 -0
  29. package/lib/module/types.js +2 -0
  30. package/lib/module/types.js.map +1 -0
  31. package/lib/module/utils/dateUtils.js +92 -0
  32. package/lib/module/utils/dateUtils.js.map +1 -0
  33. package/lib/module/utils/localeUtils.js +81 -0
  34. package/lib/module/utils/localeUtils.js.map +1 -0
  35. package/lib/typescript/BirthdayPicker.d.ts +25 -0
  36. package/lib/typescript/BirthdayPicker.d.ts.map +1 -0
  37. package/lib/typescript/BirthdayPickerModal.d.ts +24 -0
  38. package/lib/typescript/BirthdayPickerModal.d.ts.map +1 -0
  39. package/lib/typescript/constants.d.ts +39 -0
  40. package/lib/typescript/constants.d.ts.map +1 -0
  41. package/lib/typescript/hooks/useBirthdayPicker.d.ts +17 -0
  42. package/lib/typescript/hooks/useBirthdayPicker.d.ts.map +1 -0
  43. package/lib/typescript/index.d.ts +8 -0
  44. package/lib/typescript/index.d.ts.map +1 -0
  45. package/lib/typescript/types.d.ts +160 -0
  46. package/lib/typescript/types.d.ts.map +1 -0
  47. package/lib/typescript/utils/dateUtils.d.ts +43 -0
  48. package/lib/typescript/utils/dateUtils.d.ts.map +1 -0
  49. package/lib/typescript/utils/localeUtils.d.ts +28 -0
  50. package/lib/typescript/utils/localeUtils.d.ts.map +1 -0
  51. package/package.json +137 -0
  52. package/src/BirthdayPicker.tsx +210 -0
  53. package/src/BirthdayPickerModal.tsx +192 -0
  54. package/src/constants.ts +64 -0
  55. package/src/hooks/useBirthdayPicker.ts +106 -0
  56. package/src/index.ts +31 -0
  57. package/src/types.ts +189 -0
  58. package/src/utils/dateUtils.ts +101 -0
  59. package/src/utils/localeUtils.ts +99 -0
@@ -0,0 +1,17 @@
1
+ import type { UseBirthdayPickerOptions, UseBirthdayPickerReturn } from '../types';
2
+ /**
3
+ * Hook for managing birthday picker state
4
+ *
5
+ * @param options - Configuration options
6
+ * @returns State and handlers for birthday picker
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * const { value, setMonth, setDay, daysInMonth } = useBirthdayPicker({
11
+ * initialValue: { month: 6, day: 15 },
12
+ * });
13
+ * ```
14
+ */
15
+ export declare function useBirthdayPicker(options?: UseBirthdayPickerOptions): UseBirthdayPickerReturn;
16
+ export default useBirthdayPicker;
17
+ //# sourceMappingURL=useBirthdayPicker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBirthdayPicker.d.ts","sourceRoot":"","sources":["../../../src/hooks/useBirthdayPicker.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,UAAU,CAAC;AASlB;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,GAAE,wBAA6B,GACrC,uBAAuB,CA0EzB;AAED,eAAe,iBAAiB,CAAC"}
@@ -0,0 +1,8 @@
1
+ export { BirthdayPicker } from './BirthdayPicker';
2
+ export { BirthdayPickerModal } from './BirthdayPickerModal';
3
+ export { useBirthdayPicker } from './hooks/useBirthdayPicker';
4
+ export type { BirthdayValue, MonthFormat, BirthdayPickerProps, BirthdayPickerModalProps, UseBirthdayPickerOptions, UseBirthdayPickerReturn, } from './types';
5
+ export { getDaysInMonth, clampDay, isValidBirthday, getDaysArray, getMonthsArray, normalizeBirthday, } from './utils/dateUtils';
6
+ export { getMonthNames, formatMonth, formatDay } from './utils/localeUtils';
7
+ export { BirthdayPicker as default } from './BirthdayPicker';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAG5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAG9D,YAAY,EACV,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,SAAS,CAAC;AAGjB,OAAO,EACL,cAAc,EACd,QAAQ,EACR,eAAe,EACf,YAAY,EACZ,cAAc,EACd,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAG5E,OAAO,EAAE,cAAc,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,160 @@
1
+ import type { ViewStyle } from 'react-native';
2
+ /**
3
+ * Represents a month-day birthday value without year.
4
+ * Month is 1-indexed (1 = January, 12 = December)
5
+ * Day is 1-indexed (1-31 depending on month)
6
+ */
7
+ export type BirthdayValue = {
8
+ month: number;
9
+ day: number;
10
+ };
11
+ /**
12
+ * Format for displaying month names
13
+ */
14
+ export type MonthFormat = 'long' | 'short' | 'numeric';
15
+ /**
16
+ * Props for the BirthdayPicker component
17
+ */
18
+ export interface BirthdayPickerProps {
19
+ /**
20
+ * Current value (controlled mode)
21
+ */
22
+ value?: BirthdayValue;
23
+ /**
24
+ * Default value (uncontrolled mode)
25
+ */
26
+ defaultValue?: BirthdayValue;
27
+ /**
28
+ * Called when value changes
29
+ */
30
+ onChange?: (value: BirthdayValue) => void;
31
+ /**
32
+ * BCP 47 locale string for month name formatting
33
+ * @default 'en-US'
34
+ */
35
+ locale?: string;
36
+ /**
37
+ * How to format month names
38
+ * @default 'long'
39
+ */
40
+ monthFormat?: MonthFormat;
41
+ /**
42
+ * Whether to allow Feb 29 as a valid birthday
43
+ * @default true
44
+ */
45
+ allowLeapDay?: boolean;
46
+ /**
47
+ * Disable interaction
48
+ * @default false
49
+ */
50
+ disabled?: boolean;
51
+ /**
52
+ * Test ID for testing
53
+ */
54
+ testID?: string;
55
+ /**
56
+ * Container style
57
+ */
58
+ style?: ViewStyle;
59
+ /**
60
+ * Height of each item in the wheel
61
+ * @default 40
62
+ */
63
+ itemHeight?: number;
64
+ /**
65
+ * Number of visible items in each wheel (must be odd)
66
+ * @default 5
67
+ */
68
+ visibleItems?: number;
69
+ /**
70
+ * Accessibility label for month picker
71
+ * @default 'Month picker'
72
+ */
73
+ monthAccessibilityLabel?: string;
74
+ /**
75
+ * Accessibility label for day picker
76
+ * @default 'Day picker'
77
+ */
78
+ dayAccessibilityLabel?: string;
79
+ }
80
+ /**
81
+ * Props for the BirthdayPickerModal component
82
+ */
83
+ export interface BirthdayPickerModalProps extends BirthdayPickerProps {
84
+ /**
85
+ * Whether the modal is visible
86
+ */
87
+ visible: boolean;
88
+ /**
89
+ * Called when user confirms selection
90
+ */
91
+ onConfirm: (value: BirthdayValue) => void;
92
+ /**
93
+ * Called when user cancels
94
+ */
95
+ onCancel: () => void;
96
+ /**
97
+ * Modal title
98
+ * @default 'Select Birthday'
99
+ */
100
+ title?: string;
101
+ /**
102
+ * Confirm button text
103
+ * @default 'Confirm'
104
+ */
105
+ confirmText?: string;
106
+ /**
107
+ * Cancel button text
108
+ * @default 'Cancel'
109
+ */
110
+ cancelText?: string;
111
+ /**
112
+ * Animation type for modal presentation
113
+ * @default 'slide'
114
+ */
115
+ animationType?: 'slide' | 'fade' | 'none';
116
+ }
117
+ /**
118
+ * Options for useBirthdayPicker hook
119
+ */
120
+ export interface UseBirthdayPickerOptions {
121
+ /**
122
+ * Initial value
123
+ */
124
+ initialValue?: BirthdayValue;
125
+ /**
126
+ * Whether to allow Feb 29
127
+ * @default true
128
+ */
129
+ allowLeapDay?: boolean;
130
+ }
131
+ /**
132
+ * Return type of useBirthdayPicker hook
133
+ */
134
+ export interface UseBirthdayPickerReturn {
135
+ /**
136
+ * Current birthday value
137
+ */
138
+ value: BirthdayValue;
139
+ /**
140
+ * Set the month (will clamp day if needed)
141
+ */
142
+ setMonth: (month: number) => void;
143
+ /**
144
+ * Set the day
145
+ */
146
+ setDay: (day: number) => void;
147
+ /**
148
+ * Set the entire value
149
+ */
150
+ setValue: (value: BirthdayValue) => void;
151
+ /**
152
+ * Number of days in the currently selected month
153
+ */
154
+ daysInMonth: number;
155
+ /**
156
+ * Whether the current value is valid
157
+ */
158
+ isValid: boolean;
159
+ }
160
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;IAEtB;;OAEG;IACH,YAAY,CAAC,EAAE,aAAa,CAAC;IAE7B;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAE1C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAE1B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAEjC;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACnE;;OAEG;IACH,OAAO,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,SAAS,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAE1C;;OAEG;IACH,QAAQ,EAAE,MAAM,IAAI,CAAC;IAErB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;CAC3C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC;;OAEG;IACH,YAAY,CAAC,EAAE,aAAa,CAAC;IAE7B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;OAEG;IACH,KAAK,EAAE,aAAa,CAAC;IAErB;;OAEG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAElC;;OAEG;IACH,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAE9B;;OAEG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAEzC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB"}
@@ -0,0 +1,43 @@
1
+ import type { BirthdayValue } from '../types';
2
+ /**
3
+ * Get the number of days in a given month
4
+ * @param month - Month (1-12)
5
+ * @param allowLeapDay - Whether to allow Feb 29
6
+ * @returns Number of days in the month
7
+ */
8
+ export declare function getDaysInMonth(month: number, allowLeapDay?: boolean): number;
9
+ /**
10
+ * Clamp a day value to be valid for a given month
11
+ * @param day - Day to clamp
12
+ * @param month - Month (1-12)
13
+ * @param allowLeapDay - Whether to allow Feb 29
14
+ * @returns Clamped day value
15
+ */
16
+ export declare function clampDay(day: number, month: number, allowLeapDay?: boolean): number;
17
+ /**
18
+ * Check if a birthday value is valid
19
+ * @param value - Birthday value to validate
20
+ * @param allowLeapDay - Whether to allow Feb 29
21
+ * @returns Whether the value is valid
22
+ */
23
+ export declare function isValidBirthday(value: BirthdayValue, allowLeapDay?: boolean): boolean;
24
+ /**
25
+ * Generate an array of day numbers for a given month
26
+ * @param month - Month (1-12)
27
+ * @param allowLeapDay - Whether to allow Feb 29
28
+ * @returns Array of day numbers [1, 2, 3, ..., n]
29
+ */
30
+ export declare function getDaysArray(month: number, allowLeapDay?: boolean): number[];
31
+ /**
32
+ * Generate an array of month numbers
33
+ * @returns Array of month numbers [1, 2, 3, ..., 12]
34
+ */
35
+ export declare function getMonthsArray(): number[];
36
+ /**
37
+ * Normalize a birthday value to ensure it's valid
38
+ * @param value - Birthday value to normalize
39
+ * @param allowLeapDay - Whether to allow Feb 29
40
+ * @returns Normalized birthday value
41
+ */
42
+ export declare function normalizeBirthday(value: BirthdayValue, allowLeapDay?: boolean): BirthdayValue;
43
+ //# sourceMappingURL=dateUtils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dateUtils.d.ts","sourceRoot":"","sources":["../../../src/utils/dateUtils.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,UAAO,GAAG,MAAM,CAUzE;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,YAAY,UAAO,GAClB,MAAM,CAGR;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,EACpB,YAAY,UAAO,GAClB,OAAO,CAeT;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,UAAO,GAAG,MAAM,EAAE,CAGzE;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAEzC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,aAAa,EACpB,YAAY,UAAO,GAClB,aAAa,CAIf"}
@@ -0,0 +1,28 @@
1
+ import type { MonthFormat } from '../types';
2
+ /**
3
+ * Get month names for a given locale and format
4
+ * @param locale - BCP 47 locale string (e.g., 'en-US', 'ja-JP')
5
+ * @param format - Format for month names ('long', 'short', 'numeric')
6
+ * @returns Array of month names/numbers (1-indexed, index 0 is empty string)
7
+ */
8
+ export declare function getMonthNames(locale?: string, format?: MonthFormat): string[];
9
+ /**
10
+ * Format a single month number to a localized string
11
+ * @param month - Month number (1-12)
12
+ * @param locale - BCP 47 locale string
13
+ * @param format - Format for month name
14
+ * @returns Formatted month name
15
+ */
16
+ export declare function formatMonth(month: number, locale?: string, format?: MonthFormat): string;
17
+ /**
18
+ * Format a day number with optional locale-specific formatting
19
+ * @param day - Day number (1-31)
20
+ * @param locale - BCP 47 locale string (currently unused, for future i18n)
21
+ * @returns Formatted day string
22
+ */
23
+ export declare function formatDay(day: number, locale?: string): string;
24
+ /**
25
+ * Clear the month names cache (useful for testing)
26
+ */
27
+ export declare function clearMonthNamesCache(): void;
28
+ //# sourceMappingURL=localeUtils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localeUtils.d.ts","sourceRoot":"","sources":["../../../src/utils/localeUtils.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAc5C;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,MAAM,GAAE,MAAuB,EAC/B,MAAM,GAAE,WAAkC,GACzC,MAAM,EAAE,CA8BV;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,MAAuB,EAC/B,MAAM,GAAE,WAAkC,GACzC,MAAM,CAOR;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,MAAuB,GAC9B,MAAM,CAER;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C"}
package/package.json ADDED
@@ -0,0 +1,137 @@
1
+ {
2
+ "name": "react-native-month-day-picker",
3
+ "version": "0.1.0",
4
+ "description": "A privacy-friendly birthday picker for React Native that uses only month and day (no year)",
5
+ "main": "lib/commonjs/index.js",
6
+ "module": "lib/module/index.js",
7
+ "types": "lib/typescript/index.d.ts",
8
+ "react-native": "src/index.ts",
9
+ "source": "src/index.ts",
10
+ "files": [
11
+ "src",
12
+ "lib",
13
+ "!**/__tests__",
14
+ "!**/__fixtures__",
15
+ "!**/__mocks__"
16
+ ],
17
+ "scripts": {
18
+ "build": "bob build",
19
+ "clean": "del-cli lib",
20
+ "prepare": "node scripts/prepare.js",
21
+ "test": "jest --watchman=false",
22
+ "test:watch": "jest --watch --watchman=false",
23
+ "test:coverage": "jest --coverage --watchman=false",
24
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
25
+ "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
26
+ "format": "prettier --write .",
27
+ "format:check": "prettier --check .",
28
+ "typecheck": "tsc --noEmit",
29
+ "check": "npm run lint && npm run typecheck && npm run test",
30
+ "example": "cd example && npm start"
31
+ },
32
+ "keywords": [
33
+ "react-native",
34
+ "birthday",
35
+ "picker",
36
+ "month",
37
+ "day",
38
+ "date",
39
+ "wheel-picker",
40
+ "expo"
41
+ ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/mpadel78/react-native-month-day-picker.git"
45
+ },
46
+ "author": "mpadel78",
47
+ "license": "MIT",
48
+ "bugs": {
49
+ "url": "https://github.com/mpadel78/react-native-month-day-picker/issues"
50
+ },
51
+ "homepage": "https://github.com/mpadel78/react-native-month-day-picker#readme",
52
+ "publishConfig": {
53
+ "registry": "https://registry.npmjs.org/"
54
+ },
55
+ "dependencies": {
56
+ "@quidone/react-native-wheel-picker": "^1.3.5"
57
+ },
58
+ "devDependencies": {
59
+ "@babel/core": "^7.24.0",
60
+ "@babel/preset-env": "^7.24.0",
61
+ "@babel/preset-react": "^7.24.0",
62
+ "@babel/preset-typescript": "^7.24.0",
63
+ "@testing-library/react-native": "^12.4.0",
64
+ "@types/jest": "^29.5.12",
65
+ "@types/react": "^18.2.0",
66
+ "@types/react-native": "^0.72.8",
67
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
68
+ "@typescript-eslint/parser": "^7.0.0",
69
+ "del-cli": "^5.1.0",
70
+ "eslint": "^8.57.0",
71
+ "eslint-config-prettier": "^10.1.8",
72
+ "eslint-plugin-react": "^7.34.0",
73
+ "eslint-plugin-react-hooks": "^4.6.0",
74
+ "husky": "^9.1.7",
75
+ "jest": "^29.7.0",
76
+ "jest-environment-jsdom": "^29.7.0",
77
+ "lint-staged": "^17.0.5",
78
+ "prettier": "^3.8.3",
79
+ "react": "18.2.0",
80
+ "react-native": "0.74.0",
81
+ "react-native-builder-bob": "^0.23.0",
82
+ "react-test-renderer": "18.2.0",
83
+ "typescript": "^5.4.0"
84
+ },
85
+ "peerDependencies": {
86
+ "react": ">=17.0.0",
87
+ "react-native": ">=0.64.0",
88
+ "react-native-gesture-handler": ">=2.0.0",
89
+ "react-native-reanimated": ">=3.0.0"
90
+ },
91
+ "peerDependenciesMeta": {
92
+ "react-native-gesture-handler": {
93
+ "optional": false
94
+ },
95
+ "react-native-reanimated": {
96
+ "optional": false
97
+ }
98
+ },
99
+ "react-native-builder-bob": {
100
+ "source": "src",
101
+ "output": "lib",
102
+ "targets": [
103
+ "commonjs",
104
+ "module",
105
+ [
106
+ "typescript",
107
+ {
108
+ "project": "tsconfig.build.json"
109
+ }
110
+ ]
111
+ ]
112
+ },
113
+ "eslintConfig": {
114
+ "root": true,
115
+ "parser": "@typescript-eslint/parser",
116
+ "plugins": [
117
+ "@typescript-eslint",
118
+ "react",
119
+ "react-hooks"
120
+ ],
121
+ "extends": [
122
+ "eslint:recommended",
123
+ "plugin:@typescript-eslint/recommended",
124
+ "plugin:react/recommended",
125
+ "plugin:react-hooks/recommended",
126
+ "prettier"
127
+ ],
128
+ "settings": {
129
+ "react": {
130
+ "version": "detect"
131
+ }
132
+ },
133
+ "rules": {
134
+ "react/react-in-jsx-scope": "off"
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,210 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import WheelPicker from '@quidone/react-native-wheel-picker';
4
+ import type { BirthdayPickerProps, BirthdayValue } from './types';
5
+ import {
6
+ DEFAULT_BIRTHDAY_VALUE,
7
+ DEFAULT_LOCALE,
8
+ DEFAULT_MONTH_FORMAT,
9
+ DEFAULT_ITEM_HEIGHT,
10
+ DEFAULT_VISIBLE_ITEMS,
11
+ } from './constants';
12
+ import {
13
+ getDaysArray,
14
+ getMonthsArray,
15
+ clampDay,
16
+ getDaysInMonth,
17
+ } from './utils/dateUtils';
18
+ import { getMonthNames, formatDay } from './utils/localeUtils';
19
+
20
+ /**
21
+ * Item type for wheel picker
22
+ */
23
+ interface WheelItem {
24
+ value: number;
25
+ label: string;
26
+ }
27
+
28
+ /**
29
+ * A birthday picker component that allows selecting month and day.
30
+ * Does not include year for privacy-friendly birthday selection.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * // Uncontrolled mode
35
+ * <BirthdayPicker
36
+ * defaultValue={{ month: 6, day: 15 }}
37
+ * onChange={(value) => console.log(value)}
38
+ * />
39
+ *
40
+ * // Controlled mode
41
+ * const [birthday, setBirthday] = useState({ month: 1, day: 1 });
42
+ * <BirthdayPicker
43
+ * value={birthday}
44
+ * onChange={setBirthday}
45
+ * />
46
+ * ```
47
+ */
48
+ export function BirthdayPicker({
49
+ value: controlledValue,
50
+ defaultValue = DEFAULT_BIRTHDAY_VALUE,
51
+ onChange,
52
+ locale = DEFAULT_LOCALE,
53
+ monthFormat = DEFAULT_MONTH_FORMAT,
54
+ allowLeapDay = true,
55
+ disabled = false,
56
+ testID,
57
+ style,
58
+ itemHeight = DEFAULT_ITEM_HEIGHT,
59
+ visibleItems = DEFAULT_VISIBLE_ITEMS,
60
+ monthAccessibilityLabel = 'Month picker',
61
+ dayAccessibilityLabel = 'Day picker',
62
+ }: BirthdayPickerProps): React.ReactElement {
63
+ // Determine if we're in controlled or uncontrolled mode
64
+ const isControlled = controlledValue !== undefined;
65
+
66
+ // Internal state for uncontrolled mode
67
+ const [internalValue, setInternalValue] = React.useState<BirthdayValue>(
68
+ () => controlledValue ?? defaultValue
69
+ );
70
+
71
+ // The actual value to display
72
+ const value = isControlled ? controlledValue : internalValue;
73
+
74
+ // Track previous month to detect changes for day clamping
75
+ const prevMonthRef = useRef(value.month);
76
+
77
+ // Get month names based on locale and format
78
+ const monthNames = useMemo(
79
+ () => getMonthNames(locale, monthFormat),
80
+ [locale, monthFormat]
81
+ );
82
+
83
+ // Generate month items for wheel picker
84
+ const monthItems: WheelItem[] = useMemo(() => {
85
+ return getMonthsArray().map((month) => ({
86
+ value: month,
87
+ label: monthNames[month],
88
+ }));
89
+ }, [monthNames]);
90
+
91
+ // Generate day items based on selected month
92
+ const dayItems: WheelItem[] = useMemo(() => {
93
+ return getDaysArray(value.month, allowLeapDay).map((day) => ({
94
+ value: day,
95
+ label: formatDay(day, locale),
96
+ }));
97
+ }, [value.month, allowLeapDay, locale]);
98
+
99
+ // Handle internal value changes
100
+ const handleValueChange = useCallback(
101
+ (newValue: BirthdayValue) => {
102
+ if (!isControlled) {
103
+ setInternalValue(newValue);
104
+ }
105
+ onChange?.(newValue);
106
+ },
107
+ [isControlled, onChange]
108
+ );
109
+
110
+ // Handle month change
111
+ const handleMonthChange = useCallback(
112
+ ({ item }: { item: WheelItem }) => {
113
+ const newMonth = item.value;
114
+ const clampedDay = clampDay(value.day, newMonth, allowLeapDay);
115
+
116
+ const newValue: BirthdayValue = {
117
+ month: newMonth,
118
+ day: clampedDay,
119
+ };
120
+
121
+ handleValueChange(newValue);
122
+ },
123
+ [value.day, allowLeapDay, handleValueChange]
124
+ );
125
+
126
+ // Handle day change
127
+ const handleDayChange = useCallback(
128
+ ({ item }: { item: WheelItem }) => {
129
+ const newValue: BirthdayValue = {
130
+ month: value.month,
131
+ day: item.value,
132
+ };
133
+
134
+ handleValueChange(newValue);
135
+ },
136
+ [value.month, handleValueChange]
137
+ );
138
+
139
+ // Sync internal state when controlled value changes
140
+ useEffect(() => {
141
+ if (isControlled && controlledValue) {
142
+ setInternalValue(controlledValue);
143
+ }
144
+ }, [isControlled, controlledValue]);
145
+
146
+ // Clamp day when month changes and day exceeds max days
147
+ useEffect(() => {
148
+ if (value.month !== prevMonthRef.current) {
149
+ const maxDays = getDaysInMonth(value.month, allowLeapDay);
150
+ if (value.day > maxDays) {
151
+ const clampedValue: BirthdayValue = {
152
+ month: value.month,
153
+ day: maxDays,
154
+ };
155
+ handleValueChange(clampedValue);
156
+ }
157
+ prevMonthRef.current = value.month;
158
+ }
159
+ }, [value.month, value.day, allowLeapDay, handleValueChange]);
160
+
161
+ return (
162
+ <View
163
+ style={[styles.container, style]}
164
+ testID={testID}
165
+ pointerEvents={disabled ? 'none' : 'auto'}
166
+ accessibilityRole="adjustable"
167
+ >
168
+ <View
169
+ style={styles.pickerContainer}
170
+ accessibilityLabel={monthAccessibilityLabel}
171
+ >
172
+ <WheelPicker
173
+ data={monthItems}
174
+ value={value.month}
175
+ onValueChanged={handleMonthChange}
176
+ itemHeight={itemHeight}
177
+ visibleItemCount={visibleItems}
178
+ testID={testID ? `${testID}-month` : undefined}
179
+ />
180
+ </View>
181
+
182
+ <View
183
+ style={styles.pickerContainer}
184
+ accessibilityLabel={dayAccessibilityLabel}
185
+ >
186
+ <WheelPicker
187
+ data={dayItems}
188
+ value={value.day}
189
+ onValueChanged={handleDayChange}
190
+ itemHeight={itemHeight}
191
+ visibleItemCount={visibleItems}
192
+ testID={testID ? `${testID}-day` : undefined}
193
+ />
194
+ </View>
195
+ </View>
196
+ );
197
+ }
198
+
199
+ const styles = StyleSheet.create({
200
+ container: {
201
+ flexDirection: 'row',
202
+ alignItems: 'center',
203
+ justifyContent: 'center',
204
+ },
205
+ pickerContainer: {
206
+ flex: 1,
207
+ },
208
+ });
209
+
210
+ export default BirthdayPicker;