@utilitywarehouse/hearth-react-native 0.24.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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +11 -11
- package/CHANGELOG.md +41 -0
- package/build/components/DatePicker/DatePickerCalendar.js +4 -9
- package/build/components/TimePicker/TimePicker.d.ts +6 -0
- package/build/components/TimePicker/TimePicker.js +78 -0
- package/build/components/TimePicker/TimePicker.props.d.ts +45 -0
- package/build/components/TimePicker/TimePicker.props.js +1 -0
- package/build/components/TimePicker/TimePickerView.d.ts +12 -0
- package/build/components/TimePicker/TimePickerView.js +130 -0
- package/build/components/TimePicker/TimePickerWheel.d.ts +8 -0
- package/build/components/TimePicker/TimePickerWheel.js +78 -0
- package/build/components/{DatePicker/time-picker/wheel-web.d.ts → TimePicker/TimePickerWheel.web.d.ts} +4 -4
- package/build/components/TimePicker/TimePickerWheel.web.js +122 -0
- package/build/components/TimePicker/index.d.ts +6 -0
- package/build/components/TimePicker/index.js +3 -0
- package/build/components/TimePickerInput/TimePickerInput.d.ts +6 -0
- package/build/components/TimePickerInput/TimePickerInput.js +127 -0
- package/build/components/TimePickerInput/TimePickerInput.props.d.ts +52 -0
- package/build/components/TimePickerInput/TimePickerInput.props.js +1 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.d.ts +8 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.js +19 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.web.d.ts +5 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.web.js +5 -0
- package/build/components/TimePickerInput/index.d.ts +2 -0
- package/build/components/TimePickerInput/index.js +1 -0
- package/build/components/index.d.ts +2 -0
- package/build/components/index.js +2 -0
- package/docs/components/AllComponents.web.tsx +30 -0
- package/package.json +2 -1
- package/src/components/DatePicker/DatePickerCalendar.tsx +30 -13
- package/src/components/TimePicker/TimePicker.docs.mdx +84 -0
- package/src/components/TimePicker/TimePicker.figma.tsx +29 -0
- package/src/components/TimePicker/TimePicker.props.ts +45 -0
- package/src/components/TimePicker/TimePicker.stories.tsx +85 -0
- package/src/components/TimePicker/TimePicker.tsx +150 -0
- package/src/components/TimePicker/TimePickerView.tsx +216 -0
- package/src/components/TimePicker/TimePickerWheel.tsx +154 -0
- package/src/components/TimePicker/TimePickerWheel.web.tsx +217 -0
- package/src/components/TimePicker/index.ts +8 -0
- package/src/components/TimePickerInput/TimePickerInput.docs.mdx +135 -0
- package/src/components/TimePickerInput/TimePickerInput.figma.tsx +34 -0
- package/src/components/TimePickerInput/TimePickerInput.props.ts +55 -0
- package/src/components/TimePickerInput/TimePickerInput.stories.tsx +175 -0
- package/src/components/TimePickerInput/TimePickerInput.tsx +283 -0
- package/src/components/TimePickerInput/TimePickerInputDoneButton.tsx +42 -0
- package/src/components/TimePickerInput/TimePickerInputDoneButton.web.tsx +7 -0
- package/src/components/TimePickerInput/index.ts +2 -0
- package/src/components/index.ts +2 -0
- package/build/components/DatePicker/TimePicker.d.ts +0 -3
- package/build/components/DatePicker/TimePicker.js +0 -84
- package/build/components/DatePicker/time-picker/animated-math.d.ts +0 -4
- package/build/components/DatePicker/time-picker/animated-math.js +0 -19
- package/build/components/DatePicker/time-picker/period-native.d.ts +0 -6
- package/build/components/DatePicker/time-picker/period-native.js +0 -17
- package/build/components/DatePicker/time-picker/period-picker.d.ts +0 -6
- package/build/components/DatePicker/time-picker/period-picker.js +0 -10
- package/build/components/DatePicker/time-picker/period-web.d.ts +0 -6
- package/build/components/DatePicker/time-picker/period-web.js +0 -21
- package/build/components/DatePicker/time-picker/wheel-native.d.ts +0 -8
- package/build/components/DatePicker/time-picker/wheel-native.js +0 -19
- package/build/components/DatePicker/time-picker/wheel-picker/index.d.ts +0 -2
- package/build/components/DatePicker/time-picker/wheel-picker/index.js +0 -2
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.d.ts +0 -16
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.js +0 -97
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.d.ts +0 -21
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.js +0 -88
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.d.ts +0 -23
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.js +0 -21
- package/build/components/DatePicker/time-picker/wheel-web.js +0 -146
- package/build/components/DatePicker/time-picker/wheel.d.ts +0 -8
- package/build/components/DatePicker/time-picker/wheel.js +0 -10
- package/src/components/DatePicker/TimePicker.tsx +0 -141
- package/src/components/DatePicker/time-picker/animated-math.ts +0 -33
- package/src/components/DatePicker/time-picker/period-native.tsx +0 -34
- package/src/components/DatePicker/time-picker/period-picker.tsx +0 -16
- package/src/components/DatePicker/time-picker/period-web.tsx +0 -36
- package/src/components/DatePicker/time-picker/wheel-native.tsx +0 -37
- package/src/components/DatePicker/time-picker/wheel-picker/index.ts +0 -3
- package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.tsx +0 -132
- package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.ts +0 -22
- package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.tsx +0 -200
- package/src/components/DatePicker/time-picker/wheel-web.tsx +0 -180
- package/src/components/DatePicker/time-picker/wheel.tsx +0 -18
package/.turbo/turbo-build.log
CHANGED
package/.turbo/turbo-lint.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @utilitywarehouse/hearth-react-native@0.
|
|
2
|
+
> @utilitywarehouse/hearth-react-native@0.25.0 lint /home/runner/work/hearth/hearth/packages/react-native
|
|
3
3
|
> TIMING=1 eslint .
|
|
4
4
|
|
|
5
5
|
|
|
@@ -60,13 +60,13 @@
|
|
|
60
60
|
|
|
61
61
|
Rule | Time (ms) | Relative
|
|
62
62
|
:-----------------------------------------|----------:|--------:
|
|
63
|
-
@typescript-eslint/no-unused-vars |
|
|
64
|
-
react-hooks/exhaustive-deps |
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
no-
|
|
69
|
-
@typescript-eslint/
|
|
70
|
-
|
|
71
|
-
no-useless-escape |
|
|
72
|
-
no-
|
|
63
|
+
@typescript-eslint/no-unused-vars | 1535.077 | 58.6%
|
|
64
|
+
react-hooks/exhaustive-deps | 186.011 | 7.1%
|
|
65
|
+
no-global-assign | 92.645 | 3.5%
|
|
66
|
+
react-hooks/rules-of-hooks | 90.323 | 3.4%
|
|
67
|
+
no-misleading-character-class | 64.601 | 2.5%
|
|
68
|
+
no-unexpected-multiline | 47.391 | 1.8%
|
|
69
|
+
@typescript-eslint/ban-ts-comment | 39.233 | 1.5%
|
|
70
|
+
@typescript-eslint/triple-slash-reference | 36.020 | 1.4%
|
|
71
|
+
no-useless-escape | 31.051 | 1.2%
|
|
72
|
+
@typescript-eslint/no-unused-expressions | 28.768 | 1.1%
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @utilitywarehouse/hearth-react-native
|
|
2
2
|
|
|
3
|
+
## 0.25.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#982](https://github.com/utilitywarehouse/hearth/pull/982) [`506b388`](https://github.com/utilitywarehouse/hearth/commit/506b388ae1ef1065f013024a14bd9e2599a6442d) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `TimePicker` and `TimePickerInput` components with 12/24-hour support and minute intervals.
|
|
8
|
+
|
|
9
|
+
Includes a shared time picker view, updated wheel behavior for native platforms, and polished visuals like gradient fades.
|
|
10
|
+
|
|
11
|
+
**Components affected**:
|
|
12
|
+
- `TimePicker`
|
|
13
|
+
- `TimePickerInput`
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { TimePicker, TimePickerInput } from '@utilitywarehouse/hearth-react-native';
|
|
17
|
+
import { useRef, useState } from 'react';
|
|
18
|
+
import type { DateType } from '@utilitywarehouse/hearth-react-native';
|
|
19
|
+
|
|
20
|
+
const Example = () => {
|
|
21
|
+
const [value, setValue] = useState<DateType>();
|
|
22
|
+
const pickerRef = useRef(null);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<TimePickerInput
|
|
27
|
+
value={value}
|
|
28
|
+
onChange={({ date }) => setValue(date ?? undefined)}
|
|
29
|
+
onClear={() => setValue(undefined)}
|
|
30
|
+
timePickerProps={{ use12Hours: true, minuteInterval: 5 }}
|
|
31
|
+
/>
|
|
32
|
+
<TimePicker
|
|
33
|
+
ref={pickerRef}
|
|
34
|
+
date={value}
|
|
35
|
+
onChange={({ date }) => setValue(date)}
|
|
36
|
+
use12Hours
|
|
37
|
+
minuteInterval={5}
|
|
38
|
+
/>
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
3
44
|
## 0.24.0
|
|
4
45
|
|
|
5
46
|
### Minor Changes
|
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { View } from 'react-native';
|
|
3
3
|
import { StyleSheet } from 'react-native-unistyles';
|
|
4
|
+
import TimePickerView from '../TimePicker/TimePickerView';
|
|
4
5
|
import { useDatePickerContext } from './DatePicker.context';
|
|
5
6
|
import Days from './DatePickerDays';
|
|
6
7
|
import Footer from './DatePickerFooter';
|
|
7
8
|
import Header from './DatePickerHeader';
|
|
8
9
|
import Months from './DatePickerMonths';
|
|
9
10
|
import Years from './DatePickerYears';
|
|
10
|
-
import TimePicker from './TimePicker';
|
|
11
|
-
const CalendarView = {
|
|
12
|
-
year: _jsx(Years, {}),
|
|
13
|
-
month: _jsx(Months, {}),
|
|
14
|
-
day: _jsx(Days, {}),
|
|
15
|
-
time: _jsx(TimePicker, {}),
|
|
16
|
-
};
|
|
17
11
|
const Calendar = () => {
|
|
18
|
-
const { hideHeader, hideFooter, calendarView, containerHeight, navigationPosition } = useDatePickerContext();
|
|
19
|
-
|
|
12
|
+
const { hideHeader, hideFooter, calendarView, containerHeight, navigationPosition, currentDate, onSelectDate, timeZone, use12Hours, } = useDatePickerContext();
|
|
13
|
+
const calendarContent = calendarView === 'year' ? (_jsx(Years, {})) : calendarView === 'month' ? (_jsx(Months, {})) : calendarView === 'time' ? (_jsx(TimePickerView, { currentDate: currentDate, onSelectDate: onSelectDate, timeZone: timeZone, use12Hours: use12Hours, containerHeight: containerHeight })) : (_jsx(Days, {}));
|
|
14
|
+
return (_jsxs(View, { style: [styles.container], testID: "calendar", children: [!hideHeader ? _jsx(Header, { navigationPosition: navigationPosition }) : null, _jsx(View, { style: styles.containerInner(containerHeight), children: calendarContent }), !hideFooter ? _jsx(Footer, {}) : null] }));
|
|
20
15
|
};
|
|
21
16
|
const styles = StyleSheet.create(theme => ({
|
|
22
17
|
container: {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TimePickerProps } from './TimePicker.props';
|
|
2
|
+
declare const TimePicker: {
|
|
3
|
+
({ timeZone, date, onChange, use12Hours, minuteInterval, hideFooter, style, ref, onCancel, }: TimePickerProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
displayName: string;
|
|
5
|
+
};
|
|
6
|
+
export default TimePicker;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import timezone from 'dayjs/plugin/timezone';
|
|
4
|
+
import utc from 'dayjs/plugin/utc';
|
|
5
|
+
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { AccessibilityInfo, findNodeHandle, Platform, View as RNView } from 'react-native';
|
|
7
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
8
|
+
import { BottomSheetModal, BottomSheetView } from '../BottomSheet';
|
|
9
|
+
import { Button } from '../Button';
|
|
10
|
+
import TimePickerView from './TimePickerView';
|
|
11
|
+
dayjs.extend(utc);
|
|
12
|
+
dayjs.extend(timezone);
|
|
13
|
+
const Footer = ({ onCancel, onConfirm }) => {
|
|
14
|
+
return (_jsxs(RNView, { style: styles.footer, testID: "footer", children: [_jsx(Button, { variant: "ghost", colorScheme: "functional", onPress: onCancel, children: "Cancel" }), _jsx(Button, { variant: "ghost", colorScheme: "functional", onPress: onConfirm, children: "Ok" })] }));
|
|
15
|
+
};
|
|
16
|
+
const TimePicker = ({ timeZone, date, onChange, use12Hours, minuteInterval, hideFooter, style, ref, onCancel = () => { }, }) => {
|
|
17
|
+
dayjs.tz.setDefault(timeZone);
|
|
18
|
+
dayjs.locale('en');
|
|
19
|
+
const modalRef = useRef(null);
|
|
20
|
+
const pickerViewRef = useRef(null);
|
|
21
|
+
useImperativeHandle(ref, () => modalRef.current);
|
|
22
|
+
const [currentDate, setCurrentDate] = useState(() => {
|
|
23
|
+
return date ? dayjs.tz(date, timeZone) : dayjs().tz(timeZone);
|
|
24
|
+
});
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const nextDate = date ? dayjs.tz(date, timeZone) : dayjs().tz(timeZone);
|
|
27
|
+
const isSameMinute = dayjs(currentDate).isSame(nextDate, 'minute');
|
|
28
|
+
if (!isSameMinute) {
|
|
29
|
+
setCurrentDate(nextDate);
|
|
30
|
+
}
|
|
31
|
+
}, [currentDate, date, timeZone]);
|
|
32
|
+
const closeTimePicker = useCallback(() => {
|
|
33
|
+
modalRef.current?.close();
|
|
34
|
+
}, []);
|
|
35
|
+
const handleSelectDate = useCallback((selectedDate) => {
|
|
36
|
+
const newDate = dayjs.tz(selectedDate ?? currentDate, timeZone);
|
|
37
|
+
if (!dayjs(currentDate).isSame(newDate, 'minute')) {
|
|
38
|
+
setCurrentDate(newDate);
|
|
39
|
+
}
|
|
40
|
+
onChange?.({ date: newDate ? dayjs(newDate).toDate() : newDate });
|
|
41
|
+
}, [currentDate, onChange, timeZone]);
|
|
42
|
+
const handleCancel = useCallback(() => {
|
|
43
|
+
onCancel?.();
|
|
44
|
+
closeTimePicker();
|
|
45
|
+
}, [closeTimePicker, onCancel]);
|
|
46
|
+
const handleConfirm = useCallback(() => {
|
|
47
|
+
closeTimePicker();
|
|
48
|
+
}, [closeTimePicker]);
|
|
49
|
+
const handleChange = useCallback((index) => {
|
|
50
|
+
if (index > -1) {
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
AccessibilityInfo.announceForAccessibility('Time picker opened.');
|
|
53
|
+
const targetRef = pickerViewRef.current;
|
|
54
|
+
if (targetRef) {
|
|
55
|
+
const nodeHandle = findNodeHandle(targetRef);
|
|
56
|
+
if (nodeHandle) {
|
|
57
|
+
AccessibilityInfo.setAccessibilityFocus(nodeHandle);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}, 50);
|
|
61
|
+
}
|
|
62
|
+
}, []);
|
|
63
|
+
const contentStyle = useMemo(() => [styles.container, style], [style]);
|
|
64
|
+
return (_jsx(BottomSheetModal, { ref: modalRef, onChange: handleChange, accessible: false, enableContentPanningGesture: false, children: _jsx(BottomSheetView, { children: _jsxs(RNView, { ref: pickerViewRef, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Time picker' : undefined, importantForAccessibility: Platform.OS === 'android' ? 'yes' : 'auto', style: contentStyle, children: [_jsx(TimePickerView, { currentDate: currentDate, onSelectDate: handleSelectDate, timeZone: timeZone, use12Hours: use12Hours, minuteInterval: minuteInterval }), !hideFooter ? _jsx(Footer, { onCancel: handleCancel, onConfirm: handleConfirm }) : null] }) }) }));
|
|
65
|
+
};
|
|
66
|
+
TimePicker.displayName = 'TimePicker';
|
|
67
|
+
const styles = StyleSheet.create(theme => ({
|
|
68
|
+
container: {
|
|
69
|
+
backgroundColor: theme.color.background.secondary,
|
|
70
|
+
gap: theme.components.datePicker.calendar.gap,
|
|
71
|
+
},
|
|
72
|
+
footer: {
|
|
73
|
+
flexDirection: 'row',
|
|
74
|
+
gap: theme.components.datePicker.calendar.footer.gap,
|
|
75
|
+
justifyContent: 'flex-end',
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
export default TimePicker;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
|
|
2
|
+
import type { Ref } from 'react';
|
|
3
|
+
import type { ViewStyle } from 'react-native';
|
|
4
|
+
import type { DateType, PickerOption } from '../DatePicker/DatePicker.props';
|
|
5
|
+
export interface TimePickerProps {
|
|
6
|
+
/**
|
|
7
|
+
* IANA time zone identifier applied when normalising and comparing times.
|
|
8
|
+
*/
|
|
9
|
+
timeZone?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Controlled time value.
|
|
12
|
+
*/
|
|
13
|
+
date?: DateType;
|
|
14
|
+
/**
|
|
15
|
+
* Fired whenever a time is picked.
|
|
16
|
+
*/
|
|
17
|
+
onChange?: (params: {
|
|
18
|
+
date: DateType;
|
|
19
|
+
}) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Display a 12-hour clock with AM/PM selector.
|
|
22
|
+
*/
|
|
23
|
+
use12Hours?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Step interval for minutes shown in the picker.
|
|
26
|
+
*/
|
|
27
|
+
minuteInterval?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Hide the footer actions.
|
|
30
|
+
*/
|
|
31
|
+
hideFooter?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Custom container styling for the time picker surface.
|
|
34
|
+
*/
|
|
35
|
+
style?: ViewStyle;
|
|
36
|
+
/**
|
|
37
|
+
* Gives imperative access to the bottom sheet instance.
|
|
38
|
+
*/
|
|
39
|
+
ref?: Ref<BottomSheetModalMethods<any>>;
|
|
40
|
+
/**
|
|
41
|
+
* Fired when the cancel action is triggered.
|
|
42
|
+
*/
|
|
43
|
+
onCancel?: () => void;
|
|
44
|
+
}
|
|
45
|
+
export type { DateType, PickerOption };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DateType } from './TimePicker.props';
|
|
2
|
+
export type Period = 'AM' | 'PM';
|
|
3
|
+
type TimePickerViewProps = {
|
|
4
|
+
currentDate: DateType;
|
|
5
|
+
onSelectDate: (date: DateType) => void;
|
|
6
|
+
timeZone?: string;
|
|
7
|
+
use12Hours?: boolean;
|
|
8
|
+
minuteInterval?: number;
|
|
9
|
+
containerHeight?: number;
|
|
10
|
+
};
|
|
11
|
+
declare const _default: import("react").MemoExoticComponent<({ currentDate, onSelectDate, timeZone, use12Hours, minuteInterval, }: TimePickerViewProps) => import("react/jsx-runtime").JSX.Element>;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { memo, useCallback, useMemo } from 'react';
|
|
4
|
+
import { View } from 'react-native';
|
|
5
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
6
|
+
import { BodyText } from '../BodyText';
|
|
7
|
+
import { formatNumber, getParsedDate } from '../DatePicker/utils';
|
|
8
|
+
import TimePickerWheel from './TimePickerWheel';
|
|
9
|
+
const createNumberList = (num, numerals, startFrom = 0) => {
|
|
10
|
+
return Array.from({ length: num }, (_, i) => ({
|
|
11
|
+
value: i + startFrom,
|
|
12
|
+
text: i + startFrom < 10
|
|
13
|
+
? `${formatNumber(0, numerals)}${formatNumber(i + startFrom, numerals)}`
|
|
14
|
+
: `${formatNumber(i + startFrom, numerals)}`,
|
|
15
|
+
}));
|
|
16
|
+
};
|
|
17
|
+
const createMinuteList = (interval, numerals) => {
|
|
18
|
+
const safeInterval = Math.min(59, Math.max(1, Math.floor(interval)));
|
|
19
|
+
const values = Array.from({ length: Math.ceil(60 / safeInterval) }, (_, index) => Math.min(index * safeInterval, 59)).filter((value, index, array) => array.indexOf(value) === index && value < 60);
|
|
20
|
+
return values.map(value => ({
|
|
21
|
+
value,
|
|
22
|
+
text: value < 10
|
|
23
|
+
? `${formatNumber(0, numerals)}${formatNumber(value, numerals)}`
|
|
24
|
+
: `${formatNumber(value, numerals)}`,
|
|
25
|
+
}));
|
|
26
|
+
};
|
|
27
|
+
const getClosestMinute = (value, options) => {
|
|
28
|
+
if (!options.length)
|
|
29
|
+
return value;
|
|
30
|
+
const values = options.map(option => option.value);
|
|
31
|
+
if (values.includes(value))
|
|
32
|
+
return value;
|
|
33
|
+
let closest = values[0] ?? value;
|
|
34
|
+
let closestDiff = Math.abs(value - closest);
|
|
35
|
+
values.forEach(optionValue => {
|
|
36
|
+
const diff = Math.abs(value - optionValue);
|
|
37
|
+
if (diff < closestDiff) {
|
|
38
|
+
closestDiff = diff;
|
|
39
|
+
closest = optionValue;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return closest;
|
|
43
|
+
};
|
|
44
|
+
const TimePickerView = ({ currentDate, onSelectDate, timeZone, use12Hours, minuteInterval = 1, }) => {
|
|
45
|
+
const hours = useMemo(() => createNumberList(use12Hours ? 12 : 24, 'latn', use12Hours ? 1 : 0), [use12Hours]);
|
|
46
|
+
const minutes = useMemo(() => createMinuteList(minuteInterval, 'latn'), [minuteInterval]);
|
|
47
|
+
const periodOptions = useMemo(() => [
|
|
48
|
+
{ value: 'AM', text: 'AM' },
|
|
49
|
+
{ value: 'PM', text: 'PM' },
|
|
50
|
+
], []);
|
|
51
|
+
const baseDate = currentDate;
|
|
52
|
+
const { hour, hour12, minute, period } = getParsedDate(baseDate);
|
|
53
|
+
const minuteValue = useMemo(() => getClosestMinute(minute, minutes), [minute, minutes]);
|
|
54
|
+
const handleChangeHour = useCallback((value) => {
|
|
55
|
+
let hour24 = value;
|
|
56
|
+
if (use12Hours) {
|
|
57
|
+
if (period === 'AM' && value === 12) {
|
|
58
|
+
hour24 = 0;
|
|
59
|
+
}
|
|
60
|
+
else if (period === 'PM' && value < 12) {
|
|
61
|
+
hour24 = value + 12;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
hour24 = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const newDate = dayjs.tz(baseDate, timeZone).hour(hour24).minute(minuteValue);
|
|
68
|
+
onSelectDate(newDate);
|
|
69
|
+
}, [baseDate, onSelectDate, timeZone, use12Hours, period, minuteValue]);
|
|
70
|
+
const handleChangeMinute = useCallback((value) => {
|
|
71
|
+
const newDate = dayjs.tz(baseDate, timeZone).minute(value);
|
|
72
|
+
onSelectDate(newDate);
|
|
73
|
+
}, [baseDate, onSelectDate, timeZone]);
|
|
74
|
+
const handlePeriodChange = useCallback((newPeriod) => {
|
|
75
|
+
let newHour = hour12;
|
|
76
|
+
if (newPeriod === 'PM' && hour12 < 12) {
|
|
77
|
+
newHour = hour12 + 12;
|
|
78
|
+
}
|
|
79
|
+
else if (newPeriod === 'AM' && hour12 === 12) {
|
|
80
|
+
newHour = 0;
|
|
81
|
+
}
|
|
82
|
+
else if (newPeriod === 'AM' && hour >= 12) {
|
|
83
|
+
newHour = hour12;
|
|
84
|
+
}
|
|
85
|
+
const newDate = dayjs.tz(baseDate, timeZone).hour(newHour);
|
|
86
|
+
onSelectDate(newDate);
|
|
87
|
+
}, [baseDate, onSelectDate, timeZone, hour, hour12]);
|
|
88
|
+
return (_jsxs(View, { style: styles.container, testID: "time-selector", children: [_jsxs(View, { style: styles.timePickerContainer, children: [_jsx(View, { style: styles.wheelContainer, children: _jsx(TimePickerWheel, { value: use12Hours ? hour12 : hour, items: hours, setValue: handleChangeHour }) }), _jsx(BodyText, { style: styles.timeSeparator, size: "lg", children: ":" }), _jsx(View, { style: styles.wheelContainer, children: _jsx(TimePickerWheel, { value: minuteValue, items: minutes, setValue: handleChangeMinute }) })] }), use12Hours && period ? (_jsx(View, { style: styles.periodContainer, children: _jsx(TimePickerWheel, { value: period, items: periodOptions, setValue: handlePeriodChange }) })) : null] }));
|
|
89
|
+
};
|
|
90
|
+
const styles = StyleSheet.create({
|
|
91
|
+
container: {
|
|
92
|
+
flex: 1,
|
|
93
|
+
alignItems: 'center',
|
|
94
|
+
justifyContent: 'center',
|
|
95
|
+
flexDirection: 'row',
|
|
96
|
+
},
|
|
97
|
+
wheelContainer: {
|
|
98
|
+
flex: 1,
|
|
99
|
+
},
|
|
100
|
+
timePickerContainer: {
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
justifyContent: 'center',
|
|
103
|
+
width: 146,
|
|
104
|
+
height: 208,
|
|
105
|
+
flexDirection: 'row',
|
|
106
|
+
marginBottom: -30,
|
|
107
|
+
},
|
|
108
|
+
timeSeparator: {
|
|
109
|
+
marginHorizontal: 5,
|
|
110
|
+
},
|
|
111
|
+
periodContainer: {
|
|
112
|
+
marginLeft: 10,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const customComparator = (prev, next) => {
|
|
116
|
+
if (prev.onSelectDate !== next.onSelectDate) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (prev.timeZone !== next.timeZone) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
if (prev.use12Hours !== next.use12Hours) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (prev.minuteInterval !== next.minuteInterval) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return dayjs(prev.currentDate).isSame(next.currentDate, 'minute');
|
|
129
|
+
};
|
|
130
|
+
export default memo(TimePickerView, customComparator);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PickerOption } from './TimePicker.props';
|
|
2
|
+
type TimePickerWheelProps = {
|
|
3
|
+
value: number | string;
|
|
4
|
+
setValue?: (value: any) => void;
|
|
5
|
+
items: PickerOption[];
|
|
6
|
+
};
|
|
7
|
+
declare const TimePickerWheel: ({ value, setValue, items }: TimePickerWheelProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export default TimePickerWheel;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import WheelPicker from '@quidone/react-native-wheel-picker';
|
|
3
|
+
import { useCallback, useMemo } from 'react';
|
|
4
|
+
import { View } from 'react-native';
|
|
5
|
+
import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
|
|
6
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
7
|
+
import { useTheme } from '../../hooks';
|
|
8
|
+
import { BodyText } from '../BodyText';
|
|
9
|
+
const ITEM_HEIGHT = 40;
|
|
10
|
+
const VISIBLE_REST = 3;
|
|
11
|
+
const TimePickerWheel = ({ value, setValue = () => { }, items }) => {
|
|
12
|
+
const theme = useTheme();
|
|
13
|
+
const fadeHeight = ITEM_HEIGHT * 1.5;
|
|
14
|
+
const gradientId = useMemo(() => `wheel-fade-${Math.random().toString(36).slice(2)}`, []);
|
|
15
|
+
const displayCount = VISIBLE_REST * 2 + 1;
|
|
16
|
+
const pickerHeight = ITEM_HEIGHT * displayCount;
|
|
17
|
+
const data = useMemo(() => items.map(item => ({
|
|
18
|
+
value: item.value,
|
|
19
|
+
label: item.text,
|
|
20
|
+
})), [items]);
|
|
21
|
+
const handleValueChanged = useCallback(({ item }) => {
|
|
22
|
+
if (item?.value === value) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (item && item.value !== undefined) {
|
|
26
|
+
setValue(item.value);
|
|
27
|
+
}
|
|
28
|
+
}, [setValue, value]);
|
|
29
|
+
const renderOverlay = useCallback(() => (_jsxs(View, { style: [styles.overlayContainer], pointerEvents: "none", children: [_jsx(View, { pointerEvents: "none", style: [styles.fadeOverlay, { height: fadeHeight }], children: _jsxs(Svg, { width: "100%", height: "100%", preserveAspectRatio: "none", children: [_jsx(Defs, { children: _jsxs(LinearGradient, { id: `${gradientId}-top`, x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx(Stop, { offset: "0", stopColor: theme.color.background.secondary, stopOpacity: 1 }), _jsx(Stop, { offset: "1", stopColor: theme.color.background.secondary, stopOpacity: 0 })] }) }), _jsx(Rect, { width: "100%", height: "100%", fill: `url(#${gradientId}-top)` })] }) }), _jsx(View, { pointerEvents: "none", style: [styles.fadeOverlay, styles.fadeOverlayBottom, { height: fadeHeight }], children: _jsxs(Svg, { width: "100%", height: "100%", preserveAspectRatio: "none", children: [_jsx(Defs, { children: _jsxs(LinearGradient, { id: `${gradientId}-bottom`, x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx(Stop, { offset: "0", stopColor: theme.color.background.secondary, stopOpacity: 0 }), _jsx(Stop, { offset: "1", stopColor: theme.color.background.secondary, stopOpacity: 1 })] }) }), _jsx(Rect, { width: "100%", height: "100%", fill: `url(#${gradientId}-bottom)` })] }) })] })), [fadeHeight, gradientId, theme.color.background.secondary]);
|
|
30
|
+
const renderItem = useCallback(({ item }) => (_jsx(View, { style: styles.indicator, children: _jsx(BodyText, { size: "lg", children: item.label }) })), []);
|
|
31
|
+
return (_jsxs(View, { style: [styles.container, { height: pickerHeight }], children: [_jsx(View, { style: styles.overlayContainer, children: _jsx(View, { style: [styles.selection] }) }), _jsx(WheelPicker, { data: data, value: value, onValueChanged: handleValueChanged, itemHeight: ITEM_HEIGHT, visibleItemCount: displayCount, width: theme.components.timePicker.time.item.width, renderItem: renderItem, renderOverlay: renderOverlay })] }));
|
|
32
|
+
};
|
|
33
|
+
const styles = StyleSheet.create(theme => ({
|
|
34
|
+
container: {
|
|
35
|
+
minWidth: theme.components.timePicker.time.item.width,
|
|
36
|
+
overflow: 'hidden',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
justifyContent: 'center',
|
|
39
|
+
position: 'relative',
|
|
40
|
+
},
|
|
41
|
+
overlay: {
|
|
42
|
+
position: 'absolute',
|
|
43
|
+
top: 0,
|
|
44
|
+
left: 0,
|
|
45
|
+
right: 0,
|
|
46
|
+
bottom: 0,
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
},
|
|
49
|
+
indicator: {
|
|
50
|
+
width: theme.components.timePicker.time.item.width,
|
|
51
|
+
height: theme.components.timePicker.time.item.height,
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
justifyContent: 'center',
|
|
54
|
+
},
|
|
55
|
+
overlayContainer: {
|
|
56
|
+
...StyleSheet.absoluteFillObject,
|
|
57
|
+
justifyContent: 'center',
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
},
|
|
60
|
+
selection: {
|
|
61
|
+
alignSelf: 'stretch',
|
|
62
|
+
backgroundColor: theme.color.interactive.neutral.surface.subtle.active,
|
|
63
|
+
borderRadius: theme.borderRadius.md,
|
|
64
|
+
width: theme.components.timePicker.time.item.width,
|
|
65
|
+
height: theme.components.timePicker.time.item.height,
|
|
66
|
+
},
|
|
67
|
+
fadeOverlay: {
|
|
68
|
+
position: 'absolute',
|
|
69
|
+
top: 0,
|
|
70
|
+
left: 0,
|
|
71
|
+
right: 0,
|
|
72
|
+
},
|
|
73
|
+
fadeOverlayBottom: {
|
|
74
|
+
top: undefined,
|
|
75
|
+
bottom: 0,
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
export default TimePickerWheel;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { PickerOption } from '
|
|
2
|
-
|
|
1
|
+
import type { PickerOption } from './TimePicker.props';
|
|
2
|
+
type TimePickerWheelProps = {
|
|
3
3
|
value: number | string;
|
|
4
4
|
setValue?: (value: any) => void;
|
|
5
5
|
items: PickerOption[];
|
|
6
|
-
}
|
|
7
|
-
declare const _default: import("react").MemoExoticComponent<({ value, setValue, items }:
|
|
6
|
+
};
|
|
7
|
+
declare const _default: import("react").MemoExoticComponent<({ value, setValue, items }: TimePickerWheelProps) => import("react/jsx-runtime").JSX.Element>;
|
|
8
8
|
export default _default;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useCallback, useMemo } from 'react';
|
|
3
|
+
import { Platform, View } from 'react-native';
|
|
4
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
5
|
+
import Animated, { Extrapolate, interpolate, runOnJS, useAnimatedStyle, useSharedValue, } from 'react-native-reanimated';
|
|
6
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
7
|
+
import { isEqual } from '../../utils';
|
|
8
|
+
import { BodyText } from '../BodyText';
|
|
9
|
+
const ITEM_HEIGHT = 44;
|
|
10
|
+
const WheelWebItem = ({ displayValue, index, currentIndex, translateY, radius, displayCount, value, }) => {
|
|
11
|
+
const baseOpacity = displayValue?.value !== value ? 0.3 : 1;
|
|
12
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
13
|
+
const offset = (radius * 2) / displayCount;
|
|
14
|
+
const shifted = interpolate(translateY.value, [-radius, radius], [-radius + offset * (index - currentIndex), radius + offset * (index - currentIndex)], Extrapolate.EXTEND);
|
|
15
|
+
const angle = interpolate(shifted, [-radius, radius], [-Math.PI / 2, Math.PI / 2], Extrapolate.CLAMP);
|
|
16
|
+
const translate = radius * Math.sin(angle);
|
|
17
|
+
const rotateX = (angle * 180) / Math.PI;
|
|
18
|
+
return {
|
|
19
|
+
position: 'absolute',
|
|
20
|
+
height: ITEM_HEIGHT - 10,
|
|
21
|
+
opacity: baseOpacity,
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
justifyContent: 'center',
|
|
24
|
+
transform: [{ translateY: translate }, { rotateX: `${rotateX}deg` }],
|
|
25
|
+
};
|
|
26
|
+
}, [baseOpacity, currentIndex, displayCount, index, radius, translateY]);
|
|
27
|
+
return (_jsx(Animated.View, { style: animatedStyle, children: _jsx(BodyText, { size: "lg", children: displayValue?.text }) }));
|
|
28
|
+
};
|
|
29
|
+
const TimePickerWheel = ({ value, setValue = () => { }, items }) => {
|
|
30
|
+
const displayCount = 5;
|
|
31
|
+
const translateY = useSharedValue(0);
|
|
32
|
+
const renderCount = displayCount * 2 < items.length ? displayCount * 8 : displayCount * 2 - 1;
|
|
33
|
+
const circular = items.length >= displayCount;
|
|
34
|
+
const height = 140;
|
|
35
|
+
const radius = height / 2;
|
|
36
|
+
const valueIndex = useMemo(() => {
|
|
37
|
+
return Math.max(0, items.findIndex(item => item.value === value));
|
|
38
|
+
}, [items, value]);
|
|
39
|
+
const handlePanEnd = useCallback((deltaY) => {
|
|
40
|
+
let newValueIndex = valueIndex - Math.round(deltaY / ((radius * 2) / displayCount));
|
|
41
|
+
if (circular) {
|
|
42
|
+
newValueIndex = (newValueIndex + items.length) % items.length;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
if (newValueIndex < 0) {
|
|
46
|
+
newValueIndex = 0;
|
|
47
|
+
}
|
|
48
|
+
else if (newValueIndex >= items.length) {
|
|
49
|
+
newValueIndex = items.length - 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const newValue = items[newValueIndex];
|
|
53
|
+
if (newValue?.value === value) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (newValue?.value) {
|
|
57
|
+
setValue(newValue.value);
|
|
58
|
+
}
|
|
59
|
+
else if (items[0]?.value) {
|
|
60
|
+
setValue(items[0].value);
|
|
61
|
+
}
|
|
62
|
+
}, [circular, displayCount, items, radius, setValue, value, valueIndex]);
|
|
63
|
+
const panGesture = useMemo(() => Gesture.Pan()
|
|
64
|
+
.onUpdate(event => {
|
|
65
|
+
translateY.value = event.translationY;
|
|
66
|
+
})
|
|
67
|
+
.onEnd(event => {
|
|
68
|
+
runOnJS(handlePanEnd)(event.translationY);
|
|
69
|
+
translateY.value = 0;
|
|
70
|
+
}), [handlePanEnd, translateY]);
|
|
71
|
+
const displayValues = useMemo(() => {
|
|
72
|
+
const centerIndex = Math.floor(renderCount / 2);
|
|
73
|
+
return Array.from({ length: renderCount }, (_, index) => {
|
|
74
|
+
let targetIndex = valueIndex + index - centerIndex;
|
|
75
|
+
if (circular) {
|
|
76
|
+
targetIndex = ((targetIndex % items.length) + items.length) % items.length;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
targetIndex = Math.max(0, Math.min(targetIndex, items.length - 1));
|
|
80
|
+
}
|
|
81
|
+
return items[targetIndex] || items[0];
|
|
82
|
+
});
|
|
83
|
+
}, [renderCount, valueIndex, items, circular]);
|
|
84
|
+
const currentIndex = Math.max(0, displayValues.findIndex(item => item?.value === value));
|
|
85
|
+
return (_jsx(GestureDetector, { gesture: panGesture, children: _jsxs(View, { style: styles.container, children: [_jsx(View, { style: [
|
|
86
|
+
styles.selectedIndicator,
|
|
87
|
+
{
|
|
88
|
+
transform: [{ translateY: -ITEM_HEIGHT / 2 }],
|
|
89
|
+
height: ITEM_HEIGHT,
|
|
90
|
+
},
|
|
91
|
+
] }), displayValues?.map((displayValue, index) => (_jsx(WheelWebItem, { displayValue: displayValue, index: index, currentIndex: currentIndex, translateY: translateY, radius: radius, displayCount: displayCount, value: value }, `${displayValue?.text}-${index}`)))] }) }));
|
|
92
|
+
};
|
|
93
|
+
const styles = StyleSheet.create(theme => ({
|
|
94
|
+
container: {
|
|
95
|
+
minWidth: 30,
|
|
96
|
+
overflow: 'hidden',
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
justifyContent: 'center',
|
|
99
|
+
height: 208,
|
|
100
|
+
...Platform.select({
|
|
101
|
+
web: {
|
|
102
|
+
cursor: 'pointer',
|
|
103
|
+
userSelect: 'none',
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
selectedIndicator: {
|
|
108
|
+
position: 'absolute',
|
|
109
|
+
width: theme.components.timePicker.time.item.width,
|
|
110
|
+
height: theme.components.timePicker.time.item.height,
|
|
111
|
+
top: '50%',
|
|
112
|
+
backgroundColor: theme.color.interactive.neutral.surface.subtle.active,
|
|
113
|
+
borderRadius: theme.borderRadius.md,
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
justifyContent: 'center',
|
|
116
|
+
},
|
|
117
|
+
}));
|
|
118
|
+
const customComparator = (prev, next) => {
|
|
119
|
+
const areEqual = prev.value === next.value && prev.setValue === next.setValue && isEqual(prev.items, next.items);
|
|
120
|
+
return areEqual;
|
|
121
|
+
};
|
|
122
|
+
export default memo(TimePickerWheel, customComparator);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import 'dayjs/locale/en';
|
|
2
|
+
import '../DatePicker/polyfill';
|
|
3
|
+
export type { BottomSheetMethods as TimePickerMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
|
|
4
|
+
export type { DateType } from '../DatePicker/DatePicker.props';
|
|
5
|
+
export { default as TimePicker } from './TimePicker';
|
|
6
|
+
export type { TimePickerProps } from './TimePicker.props';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type TimePickerInputProps from './TimePickerInput.props';
|
|
2
|
+
declare const TimePickerInput: {
|
|
3
|
+
({ validationStatus, disabled, focused, readonly, placeholder, inBottomSheet, format, openButtonLabel, autoCloseOnSelect, label, labelVariant, helperText, helperIcon, validText, invalidText, required, onChange, onChangeText, onBlur, onFocus, value, timePickerProps, onClear, ...rest }: TimePickerInputProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
displayName: string;
|
|
5
|
+
};
|
|
6
|
+
export default TimePickerInput;
|