react-native-smart-date-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 (74) hide show
  1. package/README.md +163 -0
  2. package/lib/components/DateColumn.d.ts +3 -0
  3. package/lib/components/DateColumn.d.ts.map +1 -0
  4. package/lib/components/DateColumn.js +12 -0
  5. package/lib/components/HourColumn.d.ts +3 -0
  6. package/lib/components/HourColumn.d.ts.map +1 -0
  7. package/lib/components/HourColumn.js +12 -0
  8. package/lib/components/MinuteColumn.d.ts +3 -0
  9. package/lib/components/MinuteColumn.d.ts.map +1 -0
  10. package/lib/components/MinuteColumn.js +12 -0
  11. package/lib/components/MonthColumn.d.ts +3 -0
  12. package/lib/components/MonthColumn.d.ts.map +1 -0
  13. package/lib/components/MonthColumn.js +12 -0
  14. package/lib/components/PickerModal.d.ts +10 -0
  15. package/lib/components/PickerModal.d.ts.map +1 -0
  16. package/lib/components/PickerModal.js +27 -0
  17. package/lib/components/SelectionOverlay.d.ts +10 -0
  18. package/lib/components/SelectionOverlay.d.ts.map +1 -0
  19. package/lib/components/SelectionOverlay.js +26 -0
  20. package/lib/components/SmartDatePicker.d.ts +4 -0
  21. package/lib/components/SmartDatePicker.d.ts.map +1 -0
  22. package/lib/components/SmartDatePicker.js +154 -0
  23. package/lib/components/WheelColumn.d.ts +17 -0
  24. package/lib/components/WheelColumn.d.ts.map +1 -0
  25. package/lib/components/WheelColumn.js +126 -0
  26. package/lib/components/YearColumn.d.ts +3 -0
  27. package/lib/components/YearColumn.d.ts.map +1 -0
  28. package/lib/components/YearColumn.js +12 -0
  29. package/lib/hooks/useDatePicker.d.ts +13 -0
  30. package/lib/hooks/useDatePicker.d.ts.map +1 -0
  31. package/lib/hooks/useDatePicker.js +31 -0
  32. package/lib/hooks/useTheme.d.ts +13 -0
  33. package/lib/hooks/useTheme.d.ts.map +1 -0
  34. package/lib/hooks/useTheme.js +7 -0
  35. package/lib/hooks/useWheel.d.ts +5 -0
  36. package/lib/hooks/useWheel.d.ts.map +1 -0
  37. package/lib/hooks/useWheel.js +12 -0
  38. package/lib/index.d.ts +3 -0
  39. package/lib/index.d.ts.map +1 -0
  40. package/lib/index.js +23 -0
  41. package/lib/types/index.d.ts +59 -0
  42. package/lib/types/index.d.ts.map +1 -0
  43. package/lib/types/index.js +2 -0
  44. package/lib/utils/constants.d.ts +4 -0
  45. package/lib/utils/constants.d.ts.map +1 -0
  46. package/lib/utils/constants.js +19 -0
  47. package/lib/utils/dateUtils.d.ts +4 -0
  48. package/lib/utils/dateUtils.d.ts.map +1 -0
  49. package/lib/utils/dateUtils.js +21 -0
  50. package/lib/utils/generateData.d.ts +21 -0
  51. package/lib/utils/generateData.d.ts.map +1 -0
  52. package/lib/utils/generateData.js +95 -0
  53. package/lib/utils/validateDOB.d.ts +2 -0
  54. package/lib/utils/validateDOB.d.ts.map +1 -0
  55. package/lib/utils/validateDOB.js +17 -0
  56. package/package.json +54 -0
  57. package/src/components/DateColumn.tsx +14 -0
  58. package/src/components/HourColumn.tsx +14 -0
  59. package/src/components/MinuteColumn.tsx +14 -0
  60. package/src/components/MonthColumn.tsx +14 -0
  61. package/src/components/PickerModal.tsx +60 -0
  62. package/src/components/SelectionOverlay.tsx +45 -0
  63. package/src/components/SmartDatePicker.tsx +318 -0
  64. package/src/components/WheelColumn.tsx +231 -0
  65. package/src/components/YearColumn.tsx +14 -0
  66. package/src/hooks/useDatePicker.ts +47 -0
  67. package/src/hooks/useTheme.ts +18 -0
  68. package/src/hooks/useWheel.ts +13 -0
  69. package/src/index.ts +3 -0
  70. package/src/types/index.ts +77 -0
  71. package/src/utils/constants.ts +17 -0
  72. package/src/utils/dateUtils.ts +38 -0
  73. package/src/utils/generateData.ts +134 -0
  74. package/src/utils/validateDOB.ts +24 -0
@@ -0,0 +1,318 @@
1
+ import React, { useState, useMemo } from "react";
2
+ import {
3
+ View,
4
+ TouchableOpacity,
5
+ Text,
6
+ StyleSheet,
7
+ } from "react-native";
8
+
9
+ import PickerModal from "./PickerModal";
10
+
11
+ import DateColumn from "./DateColumn";
12
+ import MonthColumn from "./MonthColumn";
13
+ import YearColumn from "./YearColumn";
14
+ import HourColumn from "./HourColumn";
15
+ import MinuteColumn from "./MinuteColumn";
16
+ import SelectionOverlay from "./SelectionOverlay";
17
+
18
+ import {
19
+ SmartDatePickerProps,
20
+ } from "../types";
21
+
22
+ import { useDatePicker } from "../hooks/useDatePicker";
23
+ import { useTheme } from "../hooks/useTheme";
24
+
25
+ import { buildDate, clampDate } from "../utils/dateUtils";
26
+
27
+ export default function SmartDatePicker({
28
+ visible,
29
+ value = new Date(),
30
+ onConfirm,
31
+ onCancel,
32
+ mode = "datetime",
33
+ showSelectionOverlay = true,
34
+ minimumDate,
35
+ maximumDate,
36
+ primaryColor,
37
+ backgroundColor,
38
+ textColor,
39
+ buttonTextColor,
40
+ overlayBorderColor,
41
+ disabledTextColor,
42
+ selectionOverlayColor,
43
+ headerBackgroundColor,
44
+ wheelBackgroundColor,
45
+ theme,
46
+ customTheme,
47
+ minDateTheme,
48
+ maxDateTheme,
49
+ // style overrides
50
+ containerStyle,
51
+ wheelStyle,
52
+ overlayStyle,
53
+ textStyle,
54
+ selectedTextStyle,
55
+ itemHeight,
56
+ }: SmartDatePickerProps) {
57
+ const boundedValue = useMemo(
58
+ () => clampDate(value, minimumDate, maximumDate),
59
+ [value, minimumDate, maximumDate]
60
+ );
61
+
62
+ const picker = useDatePicker(boundedValue);
63
+
64
+ // view toggles between 'date' and 'time' when mode is 'datetime'
65
+ const [view, setView] = useState<"date" | "time">(
66
+ mode === "time" ? "time" : "date"
67
+ );
68
+
69
+ React.useEffect(() => {
70
+ if (!visible) return;
71
+ setView(mode === "time" ? "time" : "date");
72
+ }, [visible, mode]);
73
+
74
+ const selectedDate = buildDate(
75
+ picker.day,
76
+ picker.month,
77
+ picker.year,
78
+ picker.hour,
79
+ picker.minute
80
+ );
81
+
82
+ const effectiveDate = clampDate(
83
+ selectedDate,
84
+ minimumDate,
85
+ maximumDate
86
+ );
87
+
88
+ const selectedBoundaryTheme = useMemo(() => {
89
+ if (
90
+ minimumDate &&
91
+ effectiveDate.getTime() === minimumDate.getTime()
92
+ ) {
93
+ return minDateTheme;
94
+ }
95
+
96
+ if (
97
+ maximumDate &&
98
+ effectiveDate.getTime() === maximumDate.getTime()
99
+ ) {
100
+ return maxDateTheme;
101
+ }
102
+
103
+ return undefined;
104
+ }, [effectiveDate, minimumDate, maximumDate, minDateTheme, maxDateTheme]);
105
+
106
+ const themeObj =
107
+ customTheme ?? (typeof theme === "object" ? theme : undefined);
108
+
109
+ const activeTheme = useTheme({
110
+ ...(themeObj as any),
111
+ ...selectedBoundaryTheme,
112
+ primaryColor,
113
+ backgroundColor,
114
+ textColor,
115
+ buttonTextColor,
116
+ overlayBorderColor,
117
+ disabledTextColor,
118
+ selectionOverlayColor,
119
+ headerBackgroundColor,
120
+ wheelBackgroundColor,
121
+ });
122
+
123
+ const handleDone = () => {
124
+ onConfirm(effectiveDate);
125
+ };
126
+
127
+ const handleNext = () => setView("time");
128
+
129
+ const renderDateColumns = () => (
130
+ <>
131
+ <DateColumn
132
+ selected={picker.day}
133
+ onChange={picker.setDay}
134
+ textColor={activeTheme.textColor}
135
+ selectedTextColor={activeTheme.primaryColor}
136
+ wheelBackgroundColor={activeTheme.wheelBackgroundColor}
137
+ wheelStyle={wheelStyle}
138
+ textStyle={textStyle}
139
+ selectedTextStyle={selectedTextStyle}
140
+ />
141
+ <MonthColumn
142
+ selected={picker.month}
143
+ onChange={picker.setMonth}
144
+ textColor={activeTheme.textColor}
145
+ selectedTextColor={activeTheme.primaryColor}
146
+ wheelBackgroundColor={activeTheme.wheelBackgroundColor}
147
+ wheelStyle={wheelStyle}
148
+ textStyle={textStyle}
149
+ selectedTextStyle={selectedTextStyle}
150
+ />
151
+ <YearColumn
152
+ selected={picker.year}
153
+ onChange={picker.setYear}
154
+ textColor={activeTheme.textColor}
155
+ selectedTextColor={activeTheme.primaryColor}
156
+ wheelBackgroundColor={activeTheme.wheelBackgroundColor}
157
+ wheelStyle={wheelStyle}
158
+ textStyle={textStyle}
159
+ selectedTextStyle={selectedTextStyle}
160
+ />
161
+ </>
162
+ );
163
+
164
+ const renderTimeColumns = () => (
165
+ <>
166
+ <HourColumn
167
+ selected={picker.hour}
168
+ onChange={picker.setHour}
169
+ textColor={activeTheme.textColor}
170
+ selectedTextColor={activeTheme.primaryColor}
171
+ wheelBackgroundColor={activeTheme.wheelBackgroundColor}
172
+ wheelStyle={wheelStyle}
173
+ textStyle={textStyle}
174
+ selectedTextStyle={selectedTextStyle}
175
+ />
176
+ <MinuteColumn
177
+ selected={picker.minute}
178
+ onChange={picker.setMinute}
179
+ textColor={activeTheme.textColor}
180
+ selectedTextColor={activeTheme.primaryColor}
181
+ wheelBackgroundColor={activeTheme.wheelBackgroundColor}
182
+ wheelStyle={wheelStyle}
183
+ textStyle={textStyle}
184
+ selectedTextStyle={selectedTextStyle}
185
+ />
186
+ </>
187
+ );
188
+
189
+ const selectedText = view === "time"
190
+ ? selectedDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
191
+ : selectedDate.toLocaleDateString();
192
+
193
+ return (
194
+ <PickerModal
195
+ visible={visible}
196
+ backgroundColor={activeTheme.backgroundColor}
197
+ containerStyle={containerStyle}
198
+ overlayStyle={overlayStyle}
199
+ >
200
+ <View
201
+ style={[
202
+ styles.header,
203
+ {
204
+ backgroundColor:
205
+ activeTheme.headerBackgroundColor,
206
+ },
207
+ ]}
208
+ >
209
+ <View>
210
+ <TouchableOpacity onPress={onCancel}>
211
+ <Text
212
+ style={[
213
+ styles.buttonText,
214
+ {
215
+ color:
216
+ activeTheme.buttonTextColor ||
217
+ activeTheme.primaryColor,
218
+ },
219
+ ]}
220
+ >
221
+ Cancel
222
+ </Text>
223
+ </TouchableOpacity>
224
+ <Text
225
+ style={[
226
+ styles.previewText,
227
+ {
228
+ color: activeTheme.textColor,
229
+ },
230
+ ]}
231
+ >
232
+ {selectedText}
233
+ </Text>
234
+ </View>
235
+
236
+ {mode === "datetime" && view === "date" ? (
237
+ <TouchableOpacity onPress={handleNext}>
238
+ <Text
239
+ style={[
240
+ styles.buttonText,
241
+ {
242
+ color:
243
+ activeTheme.buttonTextColor ||
244
+ activeTheme.primaryColor,
245
+ },
246
+ ]}
247
+ >
248
+ Next
249
+ </Text>
250
+ </TouchableOpacity>
251
+ ) : (
252
+ <TouchableOpacity onPress={handleDone}>
253
+ <Text
254
+ style={[
255
+ styles.buttonText,
256
+ {
257
+ color:
258
+ activeTheme.buttonTextColor ||
259
+ activeTheme.primaryColor,
260
+ },
261
+ ]}
262
+ >
263
+ Done
264
+ </Text>
265
+ </TouchableOpacity>
266
+ )}
267
+ </View>
268
+
269
+ <View
270
+ style={[
271
+ styles.row,
272
+ {
273
+ backgroundColor:
274
+ activeTheme.wheelBackgroundColor,
275
+ },
276
+ ]}
277
+ >
278
+ {view === "date" ? renderDateColumns() : renderTimeColumns()}
279
+ {showSelectionOverlay && (
280
+ <SelectionOverlay
281
+ itemHeight={itemHeight}
282
+ borderColor={activeTheme.overlayBorderColor}
283
+ selectionOverlayColor={activeTheme.selectionOverlayColor}
284
+ overlayStyle={overlayStyle}
285
+ />
286
+ )}
287
+ </View>
288
+ </PickerModal>
289
+ );
290
+ }
291
+
292
+ const styles = StyleSheet.create({
293
+ header: {
294
+ height: 50,
295
+ paddingHorizontal: 20,
296
+ justifyContent:
297
+ "space-between",
298
+ alignItems: "center",
299
+ flexDirection: "row",
300
+ },
301
+
302
+ row: {
303
+ flex: 1,
304
+ flexDirection: "row",
305
+ position: "relative",
306
+ },
307
+
308
+ buttonText: {
309
+ fontSize: 16,
310
+ fontWeight: "600",
311
+ },
312
+
313
+ previewText: {
314
+ marginTop: 6,
315
+ fontSize: 12,
316
+ color: "#666",
317
+ },
318
+ });
@@ -0,0 +1,231 @@
1
+ import React, { useEffect, useMemo, useRef } from "react";
2
+ import {
3
+ FlatList,
4
+ Text,
5
+ TouchableOpacity,
6
+ StyleSheet,
7
+ NativeSyntheticEvent,
8
+ NativeScrollEvent,
9
+ TextStyle,
10
+ ViewStyle,
11
+ } from "react-native";
12
+
13
+ interface Props {
14
+ data: any[];
15
+ selected: any;
16
+ onChange: (v: any) => void;
17
+ textColor?: string;
18
+ selectedTextColor?: string;
19
+ wheelBackgroundColor?: string;
20
+ selectedFontSize?: number;
21
+ // additional style overrides
22
+ textStyle?: TextStyle;
23
+ selectedTextStyle?: TextStyle;
24
+ wheelStyle?: ViewStyle;
25
+ }
26
+
27
+ const ITEM_HEIGHT = 44;
28
+ const VISIBLE_ROWS = 5;
29
+ const WHEEL_HEIGHT = ITEM_HEIGHT * VISIBLE_ROWS;
30
+
31
+ export default function WheelColumn({
32
+ data,
33
+ selected,
34
+ onChange,
35
+ textColor,
36
+ selectedTextColor,
37
+ wheelBackgroundColor,
38
+ selectedFontSize,
39
+ // forwarded style props
40
+ textStyle,
41
+ selectedTextStyle,
42
+ wheelStyle,
43
+ }: Props) {
44
+ const listRef = useRef<FlatList>(null);
45
+
46
+ // Repeat data for infinite effect
47
+ const repeatedData = data;
48
+ // const repeatedData = useMemo(
49
+ // () => [...data, ...data, ...data, ...data, ...data],
50
+ // [data]
51
+ // );
52
+
53
+ const selectedIndex = data.findIndex(
54
+ (item) => item.value === selected
55
+ );
56
+
57
+ const middleIndex =
58
+ selectedIndex >= 0
59
+ ? selectedIndex
60
+ : 0;
61
+ // const middleIndex =
62
+ // selectedIndex >= 0
63
+ // ? selectedIndex + data.length * 2
64
+ // : data.length * 2;
65
+
66
+ useEffect(() => {
67
+ setTimeout(() => {
68
+ listRef.current?.scrollToOffset({
69
+ offset:
70
+ middleIndex * ITEM_HEIGHT,
71
+ animated: false,
72
+ });
73
+ }, 50);
74
+ }, []);
75
+
76
+
77
+ // const handleScrollEnd = (
78
+ // e: NativeSyntheticEvent<NativeScrollEvent>
79
+ // ) => {
80
+ // const offsetY =
81
+ // e.nativeEvent.contentOffset.y;
82
+
83
+ // const index = Math.round(
84
+ // offsetY / ITEM_HEIGHT
85
+ // );
86
+
87
+ // const clampedIndex = Math.max(
88
+ // 0,
89
+ // Math.min(index, data.length - 1)
90
+ // );
91
+
92
+ // listRef.current?.scrollToOffset({
93
+ // offset:
94
+ // clampedIndex * ITEM_HEIGHT,
95
+ // animated: true,
96
+ // });
97
+
98
+ // const item = data[clampedIndex];
99
+
100
+ // if (item) {
101
+ // onChange(item.value);
102
+ // }
103
+ // };
104
+
105
+
106
+ const handleScrollEnd = (
107
+ e: NativeSyntheticEvent<NativeScrollEvent>
108
+ ) => {
109
+ const offsetY = e.nativeEvent.contentOffset.y;
110
+
111
+ const index = Math.round(offsetY / ITEM_HEIGHT);
112
+
113
+ const clampedIndex = Math.max(
114
+ 0,
115
+ Math.min(index, data.length - 1)
116
+ );
117
+
118
+ listRef.current?.scrollToOffset({
119
+ offset: clampedIndex * ITEM_HEIGHT,
120
+ animated: false,
121
+ });
122
+ const item = data[clampedIndex];
123
+
124
+ if (item) {
125
+ onChange(item.value);
126
+ }
127
+ };
128
+
129
+
130
+ return (
131
+ <FlatList
132
+ ref={listRef}
133
+ data={repeatedData}
134
+ keyExtractor={(_, index) =>
135
+ index.toString()
136
+ }
137
+ style={[
138
+ {
139
+ height: WHEEL_HEIGHT,
140
+ backgroundColor:
141
+ wheelBackgroundColor || "transparent",
142
+ },
143
+ wheelStyle,
144
+ ]}
145
+ showsVerticalScrollIndicator={
146
+ false
147
+ }
148
+ bounces={false}
149
+ removeClippedSubviews={false}
150
+ disableIntervalMomentum={true}
151
+ snapToInterval={ITEM_HEIGHT}
152
+ snapToAlignment="center"
153
+ decelerationRate="fast"
154
+ onMomentumScrollEnd={handleScrollEnd}
155
+ getItemLayout={(_, index) => ({
156
+ length: ITEM_HEIGHT,
157
+ offset:
158
+ ITEM_HEIGHT * index,
159
+ index,
160
+ })}
161
+ renderItem={({ item }) => (
162
+ <TouchableOpacity
163
+ style={styles.item}
164
+ onPress={() => {
165
+ const index = data.findIndex(
166
+ (d) => d.value === item.value
167
+ );
168
+
169
+ listRef.current?.scrollToOffset({
170
+ offset: index * ITEM_HEIGHT,
171
+ animated: true,
172
+ });
173
+
174
+ onChange(item.value);
175
+ }}
176
+ // onPress={() => {
177
+ // onChange(item.value);
178
+ // }}
179
+ >
180
+ <Text
181
+ style={[
182
+ styles.text,
183
+ textStyle,
184
+ {
185
+ color:
186
+ selectedTextColor ||
187
+ textColor ||
188
+ styles.text.color,
189
+ },
190
+ selected === item.value && [
191
+ {
192
+ fontWeight: "700",
193
+ fontSize:
194
+ selectedFontSize || 20,
195
+ color:
196
+ selectedTextColor ||
197
+ textColor ||
198
+ styles.text.color,
199
+ },
200
+ selectedTextStyle,
201
+ ],
202
+ ]}
203
+ >
204
+ {item.label}
205
+ </Text>
206
+ </TouchableOpacity>
207
+ )}
208
+ contentContainerStyle={{
209
+ paddingTop:
210
+ ((VISIBLE_ROWS - 1) / 2) *
211
+ ITEM_HEIGHT,
212
+ paddingBottom:
213
+ ((VISIBLE_ROWS - 1) / 2) *
214
+ ITEM_HEIGHT,
215
+ }}
216
+ />
217
+ );
218
+ }
219
+
220
+ const styles = StyleSheet.create({
221
+ item: {
222
+ height: ITEM_HEIGHT,
223
+ justifyContent: "center",
224
+ alignItems: "center",
225
+ },
226
+
227
+ text: {
228
+ fontSize: 18,
229
+ color: "#111827",
230
+ },
231
+ });
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import WheelColumn from "./WheelColumn";
3
+ import { generateYears } from "../utils/generateData";
4
+
5
+ export default function YearColumn(
6
+ props: any
7
+ ) {
8
+ return (
9
+ <WheelColumn
10
+ data={generateYears()}
11
+ {...props}
12
+ />
13
+ );
14
+ }
@@ -0,0 +1,47 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ export const useDatePicker = (
4
+ date: Date
5
+ ) => {
6
+ const [day, setDay] = useState(
7
+ date.getDate()
8
+ );
9
+
10
+ const [month, setMonth] = useState(
11
+ date.getMonth() + 1
12
+ );
13
+
14
+ const [year, setYear] = useState(
15
+ date.getFullYear()
16
+ );
17
+
18
+ const [hour, setHour] = useState(
19
+ date.getHours()
20
+ );
21
+
22
+ const [minute, setMinute] = useState(
23
+ date.getMinutes()
24
+ );
25
+
26
+ useEffect(() => {
27
+ setDay(date.getDate());
28
+ setMonth(date.getMonth() + 1);
29
+ setYear(date.getFullYear());
30
+ setHour(date.getHours());
31
+ setMinute(date.getMinutes());
32
+ }, [date]);
33
+
34
+ return {
35
+ day,
36
+ month,
37
+ year,
38
+ hour,
39
+ minute,
40
+
41
+ setDay,
42
+ setMonth,
43
+ setYear,
44
+ setHour,
45
+ setMinute,
46
+ };
47
+ };
@@ -0,0 +1,18 @@
1
+ import { SmartDatePickerTheme } from "../types";
2
+
3
+ export const useTheme = (
4
+ theme?: Partial<SmartDatePickerTheme>
5
+ ) => {
6
+ return {
7
+ primaryColor: "#4F46E5",
8
+ backgroundColor: "#FFFFFF",
9
+ textColor: "#111827",
10
+ buttonTextColor: "#4F46E5",
11
+ overlayBorderColor: "#D1D5DB",
12
+ disabledTextColor: "#9CA3AF",
13
+ selectionOverlayColor: "transparent",
14
+ headerBackgroundColor: "#FFFFFF",
15
+ wheelBackgroundColor: "#FFFFFF",
16
+ ...theme,
17
+ };
18
+ };
@@ -0,0 +1,13 @@
1
+ import { useState } from "react";
2
+
3
+ export const useWheel = (
4
+ initialValue: number
5
+ ) => {
6
+ const [selected, setSelected] =
7
+ useState(initialValue);
8
+
9
+ return {
10
+ selected,
11
+ setSelected,
12
+ };
13
+ };
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { default as SmartDatePicker } from "./components/SmartDatePicker";
2
+
3
+ export * from "./types";