@utilitywarehouse/hearth-react-native 0.23.0 → 0.25.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 (107) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +13 -13
  3. package/CHANGELOG.md +77 -0
  4. package/build/components/DatePicker/DatePickerCalendar.js +4 -9
  5. package/build/components/Modal/Modal.js +5 -4
  6. package/build/components/Modal/Modal.props.d.ts +10 -4
  7. package/build/components/ProgressBar/ProgressBar.d.ts +6 -0
  8. package/build/components/ProgressBar/ProgressBar.js +35 -0
  9. package/build/components/ProgressBar/ProgressBar.props.d.ts +60 -0
  10. package/build/components/ProgressBar/ProgressBar.props.js +1 -0
  11. package/build/components/ProgressBar/ProgressBarCircular.d.ts +6 -0
  12. package/build/components/ProgressBar/ProgressBarCircular.js +115 -0
  13. package/build/components/ProgressBar/ProgressBarLinear.d.ts +6 -0
  14. package/build/components/ProgressBar/ProgressBarLinear.js +79 -0
  15. package/build/components/ProgressBar/index.d.ts +2 -0
  16. package/build/components/ProgressBar/index.js +1 -0
  17. package/build/components/TimePicker/TimePicker.d.ts +6 -0
  18. package/build/components/TimePicker/TimePicker.js +78 -0
  19. package/build/components/TimePicker/TimePicker.props.d.ts +45 -0
  20. package/build/components/TimePicker/TimePicker.props.js +1 -0
  21. package/build/components/TimePicker/TimePickerView.d.ts +12 -0
  22. package/build/components/TimePicker/TimePickerView.js +130 -0
  23. package/build/components/TimePicker/TimePickerWheel.d.ts +8 -0
  24. package/build/components/TimePicker/TimePickerWheel.js +78 -0
  25. package/build/components/{DatePicker/time-picker/wheel-web.d.ts → TimePicker/TimePickerWheel.web.d.ts} +4 -4
  26. package/build/components/TimePicker/TimePickerWheel.web.js +122 -0
  27. package/build/components/TimePicker/index.d.ts +6 -0
  28. package/build/components/TimePicker/index.js +3 -0
  29. package/build/components/TimePickerInput/TimePickerInput.d.ts +6 -0
  30. package/build/components/TimePickerInput/TimePickerInput.js +127 -0
  31. package/build/components/TimePickerInput/TimePickerInput.props.d.ts +52 -0
  32. package/build/components/TimePickerInput/TimePickerInput.props.js +1 -0
  33. package/build/components/TimePickerInput/TimePickerInputDoneButton.d.ts +8 -0
  34. package/build/components/TimePickerInput/TimePickerInputDoneButton.js +19 -0
  35. package/build/components/TimePickerInput/TimePickerInputDoneButton.web.d.ts +5 -0
  36. package/build/components/TimePickerInput/TimePickerInputDoneButton.web.js +5 -0
  37. package/build/components/TimePickerInput/index.d.ts +2 -0
  38. package/build/components/TimePickerInput/index.js +1 -0
  39. package/build/components/index.d.ts +3 -0
  40. package/build/components/index.js +3 -0
  41. package/docs/components/AllComponents.web.tsx +36 -0
  42. package/package.json +2 -1
  43. package/src/components/DatePicker/DatePickerCalendar.tsx +30 -13
  44. package/src/components/Modal/Modal.props.ts +13 -4
  45. package/src/components/Modal/Modal.stories.tsx +1 -1
  46. package/src/components/Modal/Modal.tsx +28 -11
  47. package/src/components/ProgressBar/ProgressBar.docs.mdx +90 -0
  48. package/src/components/ProgressBar/ProgressBar.figma.tsx +79 -0
  49. package/src/components/ProgressBar/ProgressBar.props.ts +60 -0
  50. package/src/components/ProgressBar/ProgressBar.stories.tsx +117 -0
  51. package/src/components/ProgressBar/ProgressBar.tsx +74 -0
  52. package/src/components/ProgressBar/ProgressBarCircular.tsx +181 -0
  53. package/src/components/ProgressBar/ProgressBarLinear.tsx +127 -0
  54. package/src/components/ProgressBar/index.ts +7 -0
  55. package/src/components/TimePicker/TimePicker.docs.mdx +84 -0
  56. package/src/components/TimePicker/TimePicker.figma.tsx +29 -0
  57. package/src/components/TimePicker/TimePicker.props.ts +45 -0
  58. package/src/components/TimePicker/TimePicker.stories.tsx +85 -0
  59. package/src/components/TimePicker/TimePicker.tsx +150 -0
  60. package/src/components/TimePicker/TimePickerView.tsx +216 -0
  61. package/src/components/TimePicker/TimePickerWheel.tsx +154 -0
  62. package/src/components/TimePicker/TimePickerWheel.web.tsx +217 -0
  63. package/src/components/TimePicker/index.ts +8 -0
  64. package/src/components/TimePickerInput/TimePickerInput.docs.mdx +135 -0
  65. package/src/components/TimePickerInput/TimePickerInput.figma.tsx +34 -0
  66. package/src/components/TimePickerInput/TimePickerInput.props.ts +55 -0
  67. package/src/components/TimePickerInput/TimePickerInput.stories.tsx +175 -0
  68. package/src/components/TimePickerInput/TimePickerInput.tsx +283 -0
  69. package/src/components/TimePickerInput/TimePickerInputDoneButton.tsx +42 -0
  70. package/src/components/TimePickerInput/TimePickerInputDoneButton.web.tsx +7 -0
  71. package/src/components/TimePickerInput/index.ts +2 -0
  72. package/src/components/index.ts +3 -0
  73. package/build/components/DatePicker/TimePicker.d.ts +0 -3
  74. package/build/components/DatePicker/TimePicker.js +0 -84
  75. package/build/components/DatePicker/time-picker/animated-math.d.ts +0 -4
  76. package/build/components/DatePicker/time-picker/animated-math.js +0 -19
  77. package/build/components/DatePicker/time-picker/period-native.d.ts +0 -6
  78. package/build/components/DatePicker/time-picker/period-native.js +0 -17
  79. package/build/components/DatePicker/time-picker/period-picker.d.ts +0 -6
  80. package/build/components/DatePicker/time-picker/period-picker.js +0 -10
  81. package/build/components/DatePicker/time-picker/period-web.d.ts +0 -6
  82. package/build/components/DatePicker/time-picker/period-web.js +0 -21
  83. package/build/components/DatePicker/time-picker/wheel-native.d.ts +0 -8
  84. package/build/components/DatePicker/time-picker/wheel-native.js +0 -19
  85. package/build/components/DatePicker/time-picker/wheel-picker/index.d.ts +0 -2
  86. package/build/components/DatePicker/time-picker/wheel-picker/index.js +0 -2
  87. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.d.ts +0 -16
  88. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.js +0 -97
  89. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.d.ts +0 -21
  90. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.js +0 -88
  91. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.d.ts +0 -23
  92. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.js +0 -21
  93. package/build/components/DatePicker/time-picker/wheel-web.js +0 -146
  94. package/build/components/DatePicker/time-picker/wheel.d.ts +0 -8
  95. package/build/components/DatePicker/time-picker/wheel.js +0 -10
  96. package/src/components/DatePicker/TimePicker.tsx +0 -141
  97. package/src/components/DatePicker/time-picker/animated-math.ts +0 -33
  98. package/src/components/DatePicker/time-picker/period-native.tsx +0 -34
  99. package/src/components/DatePicker/time-picker/period-picker.tsx +0 -16
  100. package/src/components/DatePicker/time-picker/period-web.tsx +0 -36
  101. package/src/components/DatePicker/time-picker/wheel-native.tsx +0 -37
  102. package/src/components/DatePicker/time-picker/wheel-picker/index.ts +0 -3
  103. package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.tsx +0 -132
  104. package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.ts +0 -22
  105. package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.tsx +0 -200
  106. package/src/components/DatePicker/time-picker/wheel-web.tsx +0 -180
  107. package/src/components/DatePicker/time-picker/wheel.tsx +0 -18
@@ -0,0 +1,85 @@
1
+ import { Meta, StoryObj } from '@storybook/react-native';
2
+ import { useRef, useState } from 'react';
3
+ import { Platform, View } from 'react-native';
4
+ import { DateType, TimePicker } from '.';
5
+ import { ViewWrap } from '../../../docs/components';
6
+ import { BottomSheetModal } from '../BottomSheet';
7
+ import { Button } from '../Button';
8
+
9
+ const meta = {
10
+ title: 'Stories / TimePicker',
11
+ component: TimePicker,
12
+ parameters: {
13
+ layout: 'centered',
14
+ },
15
+ argTypes: {
16
+ use12Hours: {
17
+ control: 'boolean',
18
+ description: 'Display a 12-hour clock with AM/PM selector',
19
+ defaultValue: false,
20
+ },
21
+ minuteInterval: {
22
+ control: 'number',
23
+ description: 'Step interval for minutes shown in the picker',
24
+ defaultValue: 1,
25
+ },
26
+ },
27
+ args: {
28
+ use12Hours: false,
29
+ minuteInterval: 1,
30
+ },
31
+ } satisfies Meta<typeof TimePicker>;
32
+
33
+ export default meta;
34
+
35
+ type Story = StoryObj<typeof meta>;
36
+ type StoryArgs = Story['args'];
37
+
38
+ export const Playground: Story = {
39
+ render: (args: StoryArgs) => {
40
+ const [selected, setSelected] = useState<DateType>();
41
+ const modalRef = useRef<BottomSheetModal>(null);
42
+
43
+ return (
44
+ <View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
45
+ <ViewWrap>
46
+ <Button onPress={() => modalRef.current?.present()}>Show Time Picker</Button>
47
+ <TimePicker
48
+ ref={modalRef}
49
+ date={selected}
50
+ use12Hours={args.use12Hours}
51
+ minuteInterval={args.minuteInterval}
52
+ onChange={({ date }) => setSelected(date)}
53
+ onCancel={() => setSelected(undefined)}
54
+ />
55
+ </ViewWrap>
56
+ </View>
57
+ );
58
+ },
59
+ };
60
+
61
+ export const TwelveHour: Story = {
62
+ args: {
63
+ use12Hours: true,
64
+ },
65
+ render: (args: StoryArgs) => {
66
+ const [selected, setSelected] = useState<DateType>();
67
+ const modalRef = useRef<BottomSheetModal>(null);
68
+
69
+ return (
70
+ <View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
71
+ <ViewWrap>
72
+ <Button onPress={() => modalRef.current?.present()}>Show 12-hour Time Picker</Button>
73
+ <TimePicker
74
+ ref={modalRef}
75
+ date={selected}
76
+ use12Hours={args.use12Hours}
77
+ minuteInterval={args.minuteInterval}
78
+ onChange={({ date }) => setSelected(date)}
79
+ onCancel={() => setSelected(undefined)}
80
+ />
81
+ </ViewWrap>
82
+ </View>
83
+ );
84
+ },
85
+ };
@@ -0,0 +1,150 @@
1
+ import dayjs from 'dayjs';
2
+ import timezone from 'dayjs/plugin/timezone';
3
+ import utc from 'dayjs/plugin/utc';
4
+ import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
5
+ import { AccessibilityInfo, findNodeHandle, Platform, View as RNView } from 'react-native';
6
+ import { StyleSheet } from 'react-native-unistyles';
7
+ import { BottomSheetModal, BottomSheetView } from '../BottomSheet';
8
+ import { Button } from '../Button';
9
+ import type { DateType, TimePickerProps } from './TimePicker.props';
10
+ import TimePickerView from './TimePickerView';
11
+
12
+ dayjs.extend(utc);
13
+ dayjs.extend(timezone);
14
+
15
+ type FooterProps = {
16
+ onCancel: () => void;
17
+ onConfirm: () => void;
18
+ };
19
+
20
+ const Footer = ({ onCancel, onConfirm }: FooterProps) => {
21
+ return (
22
+ <RNView style={styles.footer} testID="footer">
23
+ <Button variant="ghost" colorScheme="functional" onPress={onCancel}>
24
+ Cancel
25
+ </Button>
26
+ <Button variant="ghost" colorScheme="functional" onPress={onConfirm}>
27
+ Ok
28
+ </Button>
29
+ </RNView>
30
+ );
31
+ };
32
+
33
+ const TimePicker = ({
34
+ timeZone,
35
+ date,
36
+ onChange,
37
+ use12Hours,
38
+ minuteInterval,
39
+ hideFooter,
40
+ style,
41
+ ref,
42
+ onCancel = () => {},
43
+ }: TimePickerProps) => {
44
+ dayjs.tz.setDefault(timeZone);
45
+ dayjs.locale('en');
46
+
47
+ const modalRef = useRef<BottomSheetModal>(null);
48
+ const pickerViewRef = useRef<RNView>(null);
49
+
50
+ useImperativeHandle(ref, () => modalRef.current as BottomSheetModal);
51
+
52
+ const [currentDate, setCurrentDate] = useState<DateType>(() => {
53
+ return date ? dayjs.tz(date, timeZone) : dayjs().tz(timeZone);
54
+ });
55
+
56
+ useEffect(() => {
57
+ const nextDate = date ? dayjs.tz(date, timeZone) : dayjs().tz(timeZone);
58
+ const isSameMinute = dayjs(currentDate).isSame(nextDate, 'minute');
59
+
60
+ if (!isSameMinute) {
61
+ setCurrentDate(nextDate);
62
+ }
63
+ }, [currentDate, date, timeZone]);
64
+
65
+ const closeTimePicker = useCallback(() => {
66
+ modalRef.current?.close();
67
+ }, []);
68
+
69
+ const handleSelectDate = useCallback(
70
+ (selectedDate: DateType) => {
71
+ const newDate = dayjs.tz(selectedDate ?? currentDate, timeZone);
72
+ if (!dayjs(currentDate).isSame(newDate, 'minute')) {
73
+ setCurrentDate(newDate);
74
+ }
75
+ onChange?.({ date: newDate ? dayjs(newDate).toDate() : newDate });
76
+ },
77
+ [currentDate, onChange, timeZone]
78
+ );
79
+
80
+ const handleCancel = useCallback(() => {
81
+ onCancel?.();
82
+ closeTimePicker();
83
+ }, [closeTimePicker, onCancel]);
84
+
85
+ const handleConfirm = useCallback(() => {
86
+ closeTimePicker();
87
+ }, [closeTimePicker]);
88
+
89
+ const handleChange = useCallback((index: number) => {
90
+ if (index > -1) {
91
+ setTimeout(() => {
92
+ AccessibilityInfo.announceForAccessibility('Time picker opened.');
93
+
94
+ const targetRef = pickerViewRef.current;
95
+ if (targetRef) {
96
+ const nodeHandle = findNodeHandle(targetRef);
97
+ if (nodeHandle) {
98
+ AccessibilityInfo.setAccessibilityFocus(nodeHandle);
99
+ }
100
+ }
101
+ }, 50);
102
+ }
103
+ }, []);
104
+
105
+ const contentStyle = useMemo(() => [styles.container, style], [style]);
106
+
107
+ return (
108
+ <BottomSheetModal
109
+ ref={modalRef}
110
+ onChange={handleChange}
111
+ accessible={false}
112
+ enableContentPanningGesture={false}
113
+ >
114
+ <BottomSheetView>
115
+ <RNView
116
+ ref={pickerViewRef}
117
+ accessible={Platform.OS === 'android' ? true : undefined}
118
+ accessibilityLabel={Platform.OS === 'android' ? 'Time picker' : undefined}
119
+ importantForAccessibility={Platform.OS === 'android' ? 'yes' : 'auto'}
120
+ style={contentStyle}
121
+ >
122
+ <TimePickerView
123
+ currentDate={currentDate}
124
+ onSelectDate={handleSelectDate}
125
+ timeZone={timeZone}
126
+ use12Hours={use12Hours}
127
+ minuteInterval={minuteInterval}
128
+ />
129
+ {!hideFooter ? <Footer onCancel={handleCancel} onConfirm={handleConfirm} /> : null}
130
+ </RNView>
131
+ </BottomSheetView>
132
+ </BottomSheetModal>
133
+ );
134
+ };
135
+
136
+ TimePicker.displayName = 'TimePicker';
137
+
138
+ const styles = StyleSheet.create(theme => ({
139
+ container: {
140
+ backgroundColor: theme.color.background.secondary,
141
+ gap: theme.components.datePicker.calendar.gap,
142
+ },
143
+ footer: {
144
+ flexDirection: 'row',
145
+ gap: theme.components.datePicker.calendar.footer.gap,
146
+ justifyContent: 'flex-end',
147
+ },
148
+ }));
149
+
150
+ export default TimePicker;
@@ -0,0 +1,216 @@
1
+ import dayjs from 'dayjs';
2
+ import { memo, useCallback, useMemo } from 'react';
3
+ import { View } from 'react-native';
4
+ import { StyleSheet } from 'react-native-unistyles';
5
+ import { BodyText } from '../BodyText';
6
+ import { Numerals } from '../DatePicker/DatePicker.props';
7
+ import { formatNumber, getParsedDate } from '../DatePicker/utils';
8
+ import type { DateType, PickerOption } from './TimePicker.props';
9
+ import TimePickerWheel from './TimePickerWheel';
10
+
11
+ export type Period = 'AM' | 'PM';
12
+
13
+ type TimePickerViewProps = {
14
+ currentDate: DateType;
15
+ onSelectDate: (date: DateType) => void;
16
+ timeZone?: string;
17
+ use12Hours?: boolean;
18
+ minuteInterval?: number;
19
+ containerHeight?: number;
20
+ };
21
+
22
+ const createNumberList = (
23
+ num: number,
24
+ numerals: Numerals,
25
+ startFrom: number = 0
26
+ ): PickerOption[] => {
27
+ return Array.from({ length: num }, (_, i) => ({
28
+ value: i + startFrom,
29
+ text:
30
+ i + startFrom < 10
31
+ ? `${formatNumber(0, numerals)}${formatNumber(i + startFrom, numerals)}`
32
+ : `${formatNumber(i + startFrom, numerals)}`,
33
+ }));
34
+ };
35
+
36
+ const createMinuteList = (interval: number, numerals: Numerals): PickerOption[] => {
37
+ const safeInterval = Math.min(59, Math.max(1, Math.floor(interval)));
38
+ const values = Array.from({ length: Math.ceil(60 / safeInterval) }, (_, index) =>
39
+ Math.min(index * safeInterval, 59)
40
+ ).filter((value, index, array) => array.indexOf(value) === index && value < 60);
41
+
42
+ return values.map(value => ({
43
+ value,
44
+ text:
45
+ value < 10
46
+ ? `${formatNumber(0, numerals)}${formatNumber(value, numerals)}`
47
+ : `${formatNumber(value, numerals)}`,
48
+ }));
49
+ };
50
+
51
+ const getClosestMinute = (value: number, options: PickerOption[]) => {
52
+ if (!options.length) return value;
53
+ const values = options.map(option => option.value as number);
54
+ if (values.includes(value)) return value;
55
+
56
+ let closest = values[0] ?? value;
57
+ let closestDiff = Math.abs(value - closest);
58
+
59
+ values.forEach(optionValue => {
60
+ const diff = Math.abs(value - optionValue);
61
+ if (diff < closestDiff) {
62
+ closestDiff = diff;
63
+ closest = optionValue;
64
+ }
65
+ });
66
+
67
+ return closest;
68
+ };
69
+
70
+ const TimePickerView = ({
71
+ currentDate,
72
+ onSelectDate,
73
+ timeZone,
74
+ use12Hours,
75
+ minuteInterval = 1,
76
+ }: TimePickerViewProps) => {
77
+ const hours = useMemo(
78
+ () => createNumberList(use12Hours ? 12 : 24, 'latn', use12Hours ? 1 : 0),
79
+ [use12Hours]
80
+ );
81
+
82
+ const minutes = useMemo(() => createMinuteList(minuteInterval, 'latn'), [minuteInterval]);
83
+
84
+ const periodOptions = useMemo(
85
+ () => [
86
+ { value: 'AM', text: 'AM' },
87
+ { value: 'PM', text: 'PM' },
88
+ ],
89
+ []
90
+ );
91
+
92
+ const baseDate = currentDate;
93
+ const { hour, hour12, minute, period } = getParsedDate(baseDate);
94
+ const minuteValue = useMemo(() => getClosestMinute(minute, minutes), [minute, minutes]);
95
+
96
+ const handleChangeHour = useCallback(
97
+ (value: number) => {
98
+ let hour24 = value;
99
+
100
+ if (use12Hours) {
101
+ if (period === 'AM' && value === 12) {
102
+ hour24 = 0;
103
+ } else if (period === 'PM' && value < 12) {
104
+ hour24 = value + 12;
105
+ } else {
106
+ hour24 = value;
107
+ }
108
+ }
109
+
110
+ const newDate = dayjs.tz(baseDate, timeZone).hour(hour24).minute(minuteValue);
111
+ onSelectDate(newDate);
112
+ },
113
+ [baseDate, onSelectDate, timeZone, use12Hours, period, minuteValue]
114
+ );
115
+
116
+ const handleChangeMinute = useCallback(
117
+ (value: number) => {
118
+ const newDate = dayjs.tz(baseDate, timeZone).minute(value);
119
+ onSelectDate(newDate);
120
+ },
121
+ [baseDate, onSelectDate, timeZone]
122
+ );
123
+
124
+ const handlePeriodChange = useCallback(
125
+ (newPeriod: Period) => {
126
+ let newHour = hour12;
127
+ if (newPeriod === 'PM' && hour12 < 12) {
128
+ newHour = hour12 + 12;
129
+ } else if (newPeriod === 'AM' && hour12 === 12) {
130
+ newHour = 0;
131
+ } else if (newPeriod === 'AM' && hour >= 12) {
132
+ newHour = hour12;
133
+ }
134
+
135
+ const newDate = dayjs.tz(baseDate, timeZone).hour(newHour);
136
+ onSelectDate(newDate);
137
+ },
138
+ [baseDate, onSelectDate, timeZone, hour, hour12]
139
+ );
140
+
141
+ return (
142
+ <View style={styles.container} testID="time-selector">
143
+ <View style={styles.timePickerContainer}>
144
+ <View style={styles.wheelContainer}>
145
+ <TimePickerWheel
146
+ value={use12Hours ? hour12 : hour}
147
+ items={hours}
148
+ setValue={handleChangeHour}
149
+ />
150
+ </View>
151
+ <BodyText style={styles.timeSeparator} size="lg">
152
+ :
153
+ </BodyText>
154
+ <View style={styles.wheelContainer}>
155
+ <TimePickerWheel value={minuteValue} items={minutes} setValue={handleChangeMinute} />
156
+ </View>
157
+ </View>
158
+ {use12Hours && period ? (
159
+ <View style={styles.periodContainer}>
160
+ <TimePickerWheel value={period} items={periodOptions} setValue={handlePeriodChange} />
161
+ </View>
162
+ ) : null}
163
+ </View>
164
+ );
165
+ };
166
+
167
+ const styles = StyleSheet.create({
168
+ container: {
169
+ flex: 1,
170
+ alignItems: 'center',
171
+ justifyContent: 'center',
172
+ flexDirection: 'row',
173
+ },
174
+ wheelContainer: {
175
+ flex: 1,
176
+ },
177
+ timePickerContainer: {
178
+ alignItems: 'center',
179
+ justifyContent: 'center',
180
+ width: 146,
181
+ height: 208,
182
+ flexDirection: 'row',
183
+ marginBottom: -30,
184
+ },
185
+ timeSeparator: {
186
+ marginHorizontal: 5,
187
+ },
188
+ periodContainer: {
189
+ marginLeft: 10,
190
+ },
191
+ });
192
+
193
+ const customComparator = (
194
+ prev: Readonly<TimePickerViewProps>,
195
+ next: Readonly<TimePickerViewProps>
196
+ ) => {
197
+ if (prev.onSelectDate !== next.onSelectDate) {
198
+ return false;
199
+ }
200
+
201
+ if (prev.timeZone !== next.timeZone) {
202
+ return false;
203
+ }
204
+
205
+ if (prev.use12Hours !== next.use12Hours) {
206
+ return false;
207
+ }
208
+
209
+ if (prev.minuteInterval !== next.minuteInterval) {
210
+ return false;
211
+ }
212
+
213
+ return dayjs(prev.currentDate).isSame(next.currentDate, 'minute');
214
+ };
215
+
216
+ export default memo(TimePickerView, customComparator);
@@ -0,0 +1,154 @@
1
+ import WheelPicker from '@quidone/react-native-wheel-picker';
2
+ import { useCallback, useMemo } from 'react';
3
+ import { View } from 'react-native';
4
+ import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
5
+ import { StyleSheet } from 'react-native-unistyles';
6
+ import { useTheme } from '../../hooks';
7
+ import { BodyText } from '../BodyText';
8
+ import type { PickerOption } from './TimePicker.props';
9
+
10
+ type TimePickerWheelProps = {
11
+ value: number | string;
12
+ setValue?: (value: any) => void;
13
+ items: PickerOption[];
14
+ };
15
+
16
+ const ITEM_HEIGHT = 40;
17
+ const VISIBLE_REST = 3;
18
+
19
+ const TimePickerWheel = ({ value, setValue = () => {}, items }: TimePickerWheelProps) => {
20
+ const theme = useTheme();
21
+ const fadeHeight = ITEM_HEIGHT * 1.5;
22
+ const gradientId = useMemo(() => `wheel-fade-${Math.random().toString(36).slice(2)}`, []);
23
+ const displayCount = VISIBLE_REST * 2 + 1;
24
+ const pickerHeight = ITEM_HEIGHT * displayCount;
25
+
26
+ const data = useMemo(
27
+ () =>
28
+ items.map(item => ({
29
+ value: item.value,
30
+ label: item.text,
31
+ })),
32
+ [items]
33
+ );
34
+
35
+ const handleValueChanged = useCallback(
36
+ ({ item }: { item: { value: number | string } }) => {
37
+ if (item?.value === value) {
38
+ return;
39
+ }
40
+ if (item && item.value !== undefined) {
41
+ setValue(item.value);
42
+ }
43
+ },
44
+ [setValue, value]
45
+ );
46
+
47
+ const renderOverlay = useCallback(
48
+ () => (
49
+ <View style={[styles.overlayContainer]} pointerEvents="none">
50
+ <View pointerEvents="none" style={[styles.fadeOverlay, { height: fadeHeight }]}>
51
+ <Svg width="100%" height="100%" preserveAspectRatio="none">
52
+ <Defs>
53
+ <LinearGradient id={`${gradientId}-top`} x1="0" y1="0" x2="0" y2="1">
54
+ <Stop offset="0" stopColor={theme.color.background.secondary} stopOpacity={1} />
55
+ <Stop offset="1" stopColor={theme.color.background.secondary} stopOpacity={0} />
56
+ </LinearGradient>
57
+ </Defs>
58
+ <Rect width="100%" height="100%" fill={`url(#${gradientId}-top)`} />
59
+ </Svg>
60
+ </View>
61
+ <View
62
+ pointerEvents="none"
63
+ style={[styles.fadeOverlay, styles.fadeOverlayBottom, { height: fadeHeight }]}
64
+ >
65
+ <Svg width="100%" height="100%" preserveAspectRatio="none">
66
+ <Defs>
67
+ <LinearGradient id={`${gradientId}-bottom`} x1="0" y1="0" x2="0" y2="1">
68
+ <Stop offset="0" stopColor={theme.color.background.secondary} stopOpacity={0} />
69
+ <Stop offset="1" stopColor={theme.color.background.secondary} stopOpacity={1} />
70
+ </LinearGradient>
71
+ </Defs>
72
+ <Rect width="100%" height="100%" fill={`url(#${gradientId}-bottom)`} />
73
+ </Svg>
74
+ </View>
75
+ </View>
76
+ ),
77
+ [fadeHeight, gradientId, theme.color.background.secondary]
78
+ );
79
+
80
+ const renderItem = useCallback(
81
+ ({ item }: { item: { label: string } }) => (
82
+ <View style={styles.indicator}>
83
+ <BodyText size="lg">{item.label}</BodyText>
84
+ </View>
85
+ ),
86
+ []
87
+ );
88
+
89
+ return (
90
+ <View style={[styles.container, { height: pickerHeight }]}>
91
+ <View style={styles.overlayContainer}>
92
+ <View style={[styles.selection]} />
93
+ </View>
94
+ <WheelPicker
95
+ data={data}
96
+ value={value}
97
+ onValueChanged={handleValueChanged}
98
+ itemHeight={ITEM_HEIGHT}
99
+ visibleItemCount={displayCount}
100
+ width={theme.components.timePicker.time.item.width}
101
+ renderItem={renderItem}
102
+ renderOverlay={renderOverlay}
103
+ />
104
+ </View>
105
+ );
106
+ };
107
+
108
+ const styles = StyleSheet.create(theme => ({
109
+ container: {
110
+ minWidth: theme.components.timePicker.time.item.width,
111
+ overflow: 'hidden',
112
+ alignItems: 'center',
113
+ justifyContent: 'center',
114
+ position: 'relative',
115
+ },
116
+ overlay: {
117
+ position: 'absolute',
118
+ top: 0,
119
+ left: 0,
120
+ right: 0,
121
+ bottom: 0,
122
+ alignItems: 'center',
123
+ },
124
+ indicator: {
125
+ width: theme.components.timePicker.time.item.width,
126
+ height: theme.components.timePicker.time.item.height,
127
+ alignItems: 'center',
128
+ justifyContent: 'center',
129
+ },
130
+ overlayContainer: {
131
+ ...StyleSheet.absoluteFillObject,
132
+ justifyContent: 'center',
133
+ alignItems: 'center',
134
+ },
135
+ selection: {
136
+ alignSelf: 'stretch',
137
+ backgroundColor: theme.color.interactive.neutral.surface.subtle.active,
138
+ borderRadius: theme.borderRadius.md,
139
+ width: theme.components.timePicker.time.item.width,
140
+ height: theme.components.timePicker.time.item.height,
141
+ },
142
+ fadeOverlay: {
143
+ position: 'absolute',
144
+ top: 0,
145
+ left: 0,
146
+ right: 0,
147
+ },
148
+ fadeOverlayBottom: {
149
+ top: undefined,
150
+ bottom: 0,
151
+ },
152
+ }));
153
+
154
+ export default TimePickerWheel;