@utilitywarehouse/hearth-react-native 0.24.0 → 0.26.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 (90) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +13 -13
  3. package/CHANGELOG.md +72 -0
  4. package/build/components/DatePicker/DatePickerCalendar.js +4 -9
  5. package/build/components/Modal/Modal.d.ts +1 -1
  6. package/build/components/Modal/Modal.js +30 -7
  7. package/build/components/Modal/Modal.props.d.ts +4 -2
  8. package/build/components/TimePicker/TimePicker.d.ts +6 -0
  9. package/build/components/TimePicker/TimePicker.js +78 -0
  10. package/build/components/TimePicker/TimePicker.props.d.ts +45 -0
  11. package/build/components/TimePicker/TimePicker.props.js +1 -0
  12. package/build/components/TimePicker/TimePickerView.d.ts +12 -0
  13. package/build/components/TimePicker/TimePickerView.js +130 -0
  14. package/build/components/TimePicker/TimePickerWheel.d.ts +8 -0
  15. package/build/components/TimePicker/TimePickerWheel.js +78 -0
  16. package/build/components/{DatePicker/time-picker/wheel-web.d.ts → TimePicker/TimePickerWheel.web.d.ts} +4 -4
  17. package/build/components/TimePicker/TimePickerWheel.web.js +122 -0
  18. package/build/components/TimePicker/index.d.ts +6 -0
  19. package/build/components/TimePicker/index.js +3 -0
  20. package/build/components/TimePickerInput/TimePickerInput.d.ts +6 -0
  21. package/build/components/TimePickerInput/TimePickerInput.js +127 -0
  22. package/build/components/TimePickerInput/TimePickerInput.props.d.ts +52 -0
  23. package/build/components/TimePickerInput/TimePickerInput.props.js +1 -0
  24. package/build/components/TimePickerInput/TimePickerInputDoneButton.d.ts +8 -0
  25. package/build/components/TimePickerInput/TimePickerInputDoneButton.js +19 -0
  26. package/build/components/TimePickerInput/TimePickerInputDoneButton.web.d.ts +5 -0
  27. package/build/components/TimePickerInput/TimePickerInputDoneButton.web.js +5 -0
  28. package/build/components/TimePickerInput/index.d.ts +2 -0
  29. package/build/components/TimePickerInput/index.js +1 -0
  30. package/build/components/index.d.ts +2 -0
  31. package/build/components/index.js +2 -0
  32. package/docs/components/AllComponents.web.tsx +30 -0
  33. package/package.json +3 -2
  34. package/src/components/DatePicker/DatePickerCalendar.tsx +30 -13
  35. package/src/components/Modal/Modal.docs.mdx +9 -3
  36. package/src/components/Modal/Modal.props.ts +4 -2
  37. package/src/components/Modal/Modal.tsx +44 -7
  38. package/src/components/TimePicker/TimePicker.docs.mdx +84 -0
  39. package/src/components/TimePicker/TimePicker.figma.tsx +29 -0
  40. package/src/components/TimePicker/TimePicker.props.ts +45 -0
  41. package/src/components/TimePicker/TimePicker.stories.tsx +85 -0
  42. package/src/components/TimePicker/TimePicker.tsx +150 -0
  43. package/src/components/TimePicker/TimePickerView.tsx +216 -0
  44. package/src/components/TimePicker/TimePickerWheel.tsx +154 -0
  45. package/src/components/TimePicker/TimePickerWheel.web.tsx +217 -0
  46. package/src/components/TimePicker/index.ts +8 -0
  47. package/src/components/TimePickerInput/TimePickerInput.docs.mdx +135 -0
  48. package/src/components/TimePickerInput/TimePickerInput.figma.tsx +34 -0
  49. package/src/components/TimePickerInput/TimePickerInput.props.ts +55 -0
  50. package/src/components/TimePickerInput/TimePickerInput.stories.tsx +175 -0
  51. package/src/components/TimePickerInput/TimePickerInput.tsx +283 -0
  52. package/src/components/TimePickerInput/TimePickerInputDoneButton.tsx +42 -0
  53. package/src/components/TimePickerInput/TimePickerInputDoneButton.web.tsx +7 -0
  54. package/src/components/TimePickerInput/index.ts +2 -0
  55. package/src/components/index.ts +2 -0
  56. package/build/components/DatePicker/TimePicker.d.ts +0 -3
  57. package/build/components/DatePicker/TimePicker.js +0 -84
  58. package/build/components/DatePicker/time-picker/animated-math.d.ts +0 -4
  59. package/build/components/DatePicker/time-picker/animated-math.js +0 -19
  60. package/build/components/DatePicker/time-picker/period-native.d.ts +0 -6
  61. package/build/components/DatePicker/time-picker/period-native.js +0 -17
  62. package/build/components/DatePicker/time-picker/period-picker.d.ts +0 -6
  63. package/build/components/DatePicker/time-picker/period-picker.js +0 -10
  64. package/build/components/DatePicker/time-picker/period-web.d.ts +0 -6
  65. package/build/components/DatePicker/time-picker/period-web.js +0 -21
  66. package/build/components/DatePicker/time-picker/wheel-native.d.ts +0 -8
  67. package/build/components/DatePicker/time-picker/wheel-native.js +0 -19
  68. package/build/components/DatePicker/time-picker/wheel-picker/index.d.ts +0 -2
  69. package/build/components/DatePicker/time-picker/wheel-picker/index.js +0 -2
  70. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.d.ts +0 -16
  71. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.js +0 -97
  72. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.d.ts +0 -21
  73. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.js +0 -88
  74. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.d.ts +0 -23
  75. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.js +0 -21
  76. package/build/components/DatePicker/time-picker/wheel-web.js +0 -146
  77. package/build/components/DatePicker/time-picker/wheel.d.ts +0 -8
  78. package/build/components/DatePicker/time-picker/wheel.js +0 -10
  79. package/src/components/DatePicker/TimePicker.tsx +0 -141
  80. package/src/components/DatePicker/time-picker/animated-math.ts +0 -33
  81. package/src/components/DatePicker/time-picker/period-native.tsx +0 -34
  82. package/src/components/DatePicker/time-picker/period-picker.tsx +0 -16
  83. package/src/components/DatePicker/time-picker/period-web.tsx +0 -36
  84. package/src/components/DatePicker/time-picker/wheel-native.tsx +0 -37
  85. package/src/components/DatePicker/time-picker/wheel-picker/index.ts +0 -3
  86. package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.tsx +0 -132
  87. package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.ts +0 -22
  88. package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.tsx +0 -200
  89. package/src/components/DatePicker/time-picker/wheel-web.tsx +0 -180
  90. package/src/components/DatePicker/time-picker/wheel.tsx +0 -18
@@ -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 '../DatePicker.props';
2
- interface WheelProps {
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 }: WheelProps) => import("react/jsx-runtime").JSX.Element>;
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,3 @@
1
+ import 'dayjs/locale/en';
2
+ import '../DatePicker/polyfill';
3
+ export { default as TimePicker } from './TimePicker';
@@ -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;
@@ -0,0 +1,127 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { CloseSmallIcon, TimeSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
+ import dayjs from 'dayjs';
4
+ import customParseFormat from 'dayjs/plugin/customParseFormat';
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import { Keyboard, Platform } from 'react-native';
7
+ import { StyleSheet } from 'react-native-unistyles';
8
+ import { useFormFieldContext } from '../FormField';
9
+ import { Input, InputField, InputSlot } from '../Input';
10
+ import { TimePicker } from '../TimePicker';
11
+ import { UnstyledIconButton } from '../UnstyledIconButton';
12
+ import TimePickerInputDoneButton from './TimePickerInputDoneButton';
13
+ dayjs.extend(customParseFormat);
14
+ const DEFAULT_FORMAT_24 = 'HH:mm';
15
+ const DEFAULT_FORMAT_12 = 'hh:mm A';
16
+ const maskDefaultFormat = (value) => {
17
+ const digitsOnly = value.replace(/\D/g, '').slice(0, 4);
18
+ const hours = digitsOnly.slice(0, 2);
19
+ const minutes = digitsOnly.slice(2, 4);
20
+ return [hours, minutes].filter(Boolean).join(':');
21
+ };
22
+ const TimePickerInput = ({ validationStatus = 'initial', disabled, focused, readonly, placeholder = '--:--', inBottomSheet = false, format, openButtonLabel = 'Open time picker', autoCloseOnSelect = true, label, labelVariant, helperText, helperIcon, validText, invalidText, required = true, onChange, onChangeText, onBlur, onFocus, value, timePickerProps, onClear, ...rest }) => {
23
+ const formFieldContext = useFormFieldContext();
24
+ const isDisabled = formFieldContext?.disabled ?? disabled;
25
+ const isReadonly = formFieldContext?.readonly ?? readonly;
26
+ const pickerRef = useRef(null);
27
+ const accessoryViewID = useMemo(() => {
28
+ if (Platform.OS !== 'ios')
29
+ return undefined;
30
+ return `timepicker-input-${Math.random().toString(36).slice(2)}`;
31
+ }, []);
32
+ const use12Hours = timePickerProps?.use12Hours ?? false;
33
+ const resolvedFormat = useMemo(() => format ?? (use12Hours ? DEFAULT_FORMAT_12 : DEFAULT_FORMAT_24), [format, use12Hours]);
34
+ const formatTime = useCallback((dateValue) => {
35
+ if (!dateValue)
36
+ return '';
37
+ const parsed = dayjs(dateValue);
38
+ return parsed.isValid() ? parsed.format(resolvedFormat) : '';
39
+ }, [resolvedFormat]);
40
+ const isDefaultFormat = resolvedFormat === DEFAULT_FORMAT_24;
41
+ const [inputValue, setInputValue] = useState(() => formatTime(value));
42
+ useEffect(() => {
43
+ setInputValue(formatTime(value));
44
+ }, [value, formatTime]);
45
+ const handleTextChange = useCallback((text) => {
46
+ const nextValue = isDefaultFormat ? maskDefaultFormat(text) : text;
47
+ setInputValue(nextValue);
48
+ onChangeText?.(nextValue);
49
+ if (!nextValue) {
50
+ onChange?.({ date: undefined });
51
+ return;
52
+ }
53
+ const parsed = dayjs(nextValue, resolvedFormat, true);
54
+ if (parsed.isValid()) {
55
+ onChange?.({ date: parsed.toDate() });
56
+ }
57
+ else {
58
+ onChange?.({ date: undefined });
59
+ }
60
+ }, [isDefaultFormat, onChange, onChangeText, resolvedFormat]);
61
+ const handleClear = useCallback(() => {
62
+ setInputValue('');
63
+ onChange?.({ date: undefined });
64
+ onClear?.();
65
+ }, [onChange, onClear]);
66
+ const closeKeyboard = useCallback(() => {
67
+ Keyboard.dismiss();
68
+ }, []);
69
+ const openPicker = useCallback(() => {
70
+ if (isDisabled || isReadonly)
71
+ return;
72
+ closeKeyboard();
73
+ pickerRef.current?.present();
74
+ }, [closeKeyboard, isDisabled, isReadonly]);
75
+ const selectedDate = useMemo(() => {
76
+ if (value) {
77
+ return value;
78
+ }
79
+ const parsed = dayjs(inputValue, resolvedFormat, true);
80
+ return parsed.isValid() ? parsed.toDate() : undefined;
81
+ }, [value, inputValue, resolvedFormat]);
82
+ const handlePickerChange = useCallback(({ date }) => {
83
+ if (!date) {
84
+ handleClear();
85
+ return;
86
+ }
87
+ const formatted = formatTime(date);
88
+ setInputValue(formatted);
89
+ onChange?.({ date });
90
+ if (autoCloseOnSelect) {
91
+ pickerRef.current?.close?.();
92
+ }
93
+ }, [autoCloseOnSelect, formatTime, handleClear, onChange]);
94
+ const handleBlur = useCallback((event) => {
95
+ onBlur?.(event);
96
+ }, [onBlur]);
97
+ const handleFocus = useCallback((event) => {
98
+ onFocus?.(event);
99
+ }, [onFocus]);
100
+ const { onCancel: pickerOnCancel, ...restTimePickerProps } = timePickerProps ?? {};
101
+ const { style: inputStyle, keyboardType: keyboardTypeProp, inputMode: inputModeProp, accessibilityHint: accessibilityHintProp, accessibilityLabel: accessibilityLabelProp, accessible: accessibleProp, importantForAccessibility: importantForAccessibilityProp, ...textInputProps } = rest;
102
+ const resolvedKeyboardType = keyboardTypeProp ?? (isDefaultFormat ? 'number-pad' : undefined);
103
+ const resolvedInputMode = inputModeProp ?? (isDefaultFormat ? 'numeric' : undefined);
104
+ const resolvedAccessibilityHint = accessibilityHintProp ?? `Enter the time in ${resolvedFormat} format`;
105
+ const resolvedAccessibilityLabel = accessibilityLabelProp ?? 'Time input';
106
+ const resolvedAccessible = accessibleProp ?? true;
107
+ const resolvedImportantForAccessibility = importantForAccessibilityProp ?? 'yes';
108
+ const handleCancel = useCallback(() => {
109
+ pickerOnCancel?.();
110
+ pickerRef.current?.close?.();
111
+ }, [pickerOnCancel]);
112
+ const placeholderValue = placeholder;
113
+ return (_jsxs(_Fragment, { children: [_jsxs(Input, { validationStatus: validationStatus, disabled: disabled, readonly: readonly, focused: focused, label: label, labelVariant: labelVariant, helperText: helperText, helperIcon: helperIcon, validText: validText, invalidText: invalidText, required: required, style: styles.wrap, accessible: false, children: [_jsx(InputField, { editable: !isReadonly && !isDisabled, value: inputValue, placeholder: placeholderValue, onChangeText: handleTextChange, onBlur: handleBlur, onFocus: handleFocus, inBottomSheet: inBottomSheet, keyboardType: resolvedKeyboardType, inputMode: resolvedInputMode, accessibilityHint: resolvedAccessibilityHint, "aria-label": "Time input", accessibilityLabel: resolvedAccessibilityLabel, accessible: resolvedAccessible, accessibilityState: {
114
+ disabled: isDisabled || isReadonly,
115
+ }, importantForAccessibility: resolvedImportantForAccessibility, inputAccessoryViewID: Platform.OS === 'ios' ? accessoryViewID : undefined, ...textInputProps, style: [styles.input, inputStyle] }), !!inputValue && onClear && !isReadonly && !isDisabled && (_jsx(InputSlot, { accessibilityElementsHidden: false, children: _jsx(UnstyledIconButton, { accessibilityLabel: "Clear time", accessibilityHint: "Removes the current time", icon: CloseSmallIcon, onPress: handleClear }) })), _jsx(InputSlot, { accessibilityElementsHidden: false, children: _jsx(UnstyledIconButton, { accessibilityLabel: openButtonLabel, accessibilityHint: "Opens the time picker", icon: TimeSmallIcon, onPress: openPicker, disabled: isDisabled || isReadonly }) })] }), _jsx(TimePicker, { ref: pickerRef, date: selectedDate, onChange: handlePickerChange, onCancel: handleCancel, ...restTimePickerProps }), Platform.OS === 'ios' && accessoryViewID && (_jsx(TimePickerInputDoneButton, { accessoryViewID: accessoryViewID, closeKeyboard: closeKeyboard }))] }));
116
+ };
117
+ TimePickerInput.displayName = 'TimePickerInput';
118
+ const styles = StyleSheet.create(theme => ({
119
+ wrap: {
120
+ gap: theme.components.input.date.gap,
121
+ },
122
+ input: {
123
+ paddingLeft: 0,
124
+ paddingRight: 0,
125
+ },
126
+ }));
127
+ export default TimePickerInput;
@@ -0,0 +1,52 @@
1
+ import type { TextInputProps, ViewProps } from 'react-native';
2
+ import type { DateType } from '../DatePicker/DatePicker.props';
3
+ import type { TimePickerProps } from '../TimePicker/TimePicker.props';
4
+ export interface TimePickerInputBaseProps {
5
+ disabled?: boolean;
6
+ validationStatus?: 'initial' | 'valid' | 'invalid';
7
+ readonly?: boolean;
8
+ focused?: boolean;
9
+ label?: string;
10
+ labelVariant?: 'heading' | 'body';
11
+ helperText?: string;
12
+ helperIcon?: React.ComponentType;
13
+ validText?: string;
14
+ invalidText?: string;
15
+ placeholder?: string;
16
+ inBottomSheet?: boolean;
17
+ required?: boolean;
18
+ /**
19
+ * Controls how the selected time is formatted when displayed inside the input.
20
+ * Accepts any Day.js format string.
21
+ */
22
+ format?: string;
23
+ /**
24
+ * Accessible label announced when activating the time picker trigger button.
25
+ */
26
+ openButtonLabel?: string;
27
+ /**
28
+ * When true (default), the picker sheet is dismissed as soon as a time is picked.
29
+ */
30
+ autoCloseOnSelect?: boolean;
31
+ /**
32
+ * Additional props forwarded to the underlying TimePicker instance.
33
+ */
34
+ timePickerProps?: Omit<TimePickerProps, 'date' | 'onChange' | 'ref'>;
35
+ /**
36
+ * Handles cleared input values.
37
+ */
38
+ onClear?: () => void;
39
+ }
40
+ export type TimePickerInputProps = TimePickerInputBaseProps & Omit<TextInputProps, 'value' | 'onChange' | 'children'> & ViewProps & {
41
+ /**
42
+ * Controlled time value. Accepts Date, string, number or Day.js instances.
43
+ */
44
+ value?: DateType;
45
+ /**
46
+ * Fired after a valid time is parsed either from typing or the picker selection.
47
+ */
48
+ onChange?: (params: {
49
+ date: DateType;
50
+ }) => void;
51
+ };
52
+ export default TimePickerInputProps;
@@ -0,0 +1,8 @@
1
+ declare const TimePickerInputDoneButton: {
2
+ ({ accessoryViewID, closeKeyboard, }: {
3
+ accessoryViewID: string;
4
+ closeKeyboard: () => void;
5
+ }): import("react/jsx-runtime").JSX.Element;
6
+ DisplayName: string;
7
+ };
8
+ export default TimePickerInputDoneButton;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { InputAccessoryView, View } from 'react-native';
3
+ import { StyleSheet } from 'react-native-unistyles';
4
+ import { Button } from '../Button';
5
+ const TimePickerInputDoneButton = ({ accessoryViewID, closeKeyboard, }) => {
6
+ return (_jsx(InputAccessoryView, { nativeID: accessoryViewID, children: _jsx(View, { style: styles.accessory, children: _jsx(Button, { accessibilityRole: "button", accessibilityLabel: "Done", onPress: closeKeyboard, variant: "ghost", colorScheme: "functional", children: "Done" }) }) }));
7
+ };
8
+ const styles = StyleSheet.create(theme => ({
9
+ accessory: {
10
+ paddingHorizontal: 16,
11
+ paddingVertical: 2,
12
+ alignItems: 'flex-end',
13
+ backgroundColor: theme.color.surface.neutral.strong,
14
+ borderTopWidth: theme.borderWidth[1],
15
+ borderTopColor: theme.color.border.subtle,
16
+ },
17
+ }));
18
+ TimePickerInputDoneButton.DisplayName = 'TimePickerInputDoneButton';
19
+ export default TimePickerInputDoneButton;
@@ -0,0 +1,5 @@
1
+ declare const TimePickerInputDoneButton: {
2
+ (): null;
3
+ DisplayName: string;
4
+ };
5
+ export default TimePickerInputDoneButton;
@@ -0,0 +1,5 @@
1
+ const TimePickerInputDoneButton = () => {
2
+ return null;
3
+ };
4
+ TimePickerInputDoneButton.DisplayName = 'TimePickerInputDoneButton';
5
+ export default TimePickerInputDoneButton;
@@ -0,0 +1,2 @@
1
+ export { default as TimePickerInput } from './TimePickerInput';
2
+ export type { default as TimePickerInputProps } from './TimePickerInput.props';
@@ -0,0 +1 @@
1
+ export { default as TimePickerInput } from './TimePickerInput';
@@ -52,6 +52,8 @@ export * from './Switch';
52
52
  export * from './Tabs';
53
53
  export * from './Textarea';
54
54
  export * from './ThemedImage';
55
+ export * from './TimePicker';
56
+ export * from './TimePickerInput';
55
57
  export * from './Toast';
56
58
  export * from './ToggleButtonCard';
57
59
  export * from './VerificationInput';
@@ -53,6 +53,8 @@ export * from './Switch';
53
53
  export * from './Tabs';
54
54
  export * from './Textarea';
55
55
  export * from './ThemedImage';
56
+ export * from './TimePicker';
57
+ export * from './TimePickerInput';
56
58
  export * from './Toast';
57
59
  export * from './ToggleButtonCard';
58
60
  export * from './VerificationInput';
@@ -92,6 +92,8 @@ import {
92
92
  TabsList,
93
93
  Textarea,
94
94
  ThemedImage,
95
+ TimePicker,
96
+ TimePickerInput,
95
97
  ToastItem,
96
98
  ToggleButtonCard,
97
99
  ToggleButtonCardGroup,
@@ -151,6 +153,11 @@ const AllComponents: React.FC = () => {
151
153
  const handleDatePickerOpenPress = useCallback(() => {
152
154
  datePickerRef.current?.present();
153
155
  }, []);
156
+ const [selectedTime, setSelectedTime] = useState<DateType>();
157
+ const timePickerRef = useRef<BottomSheetModal>(null);
158
+ const handleTimePickerOpenPress = useCallback(() => {
159
+ timePickerRef.current?.present();
160
+ }, []);
154
161
  useEffect(() => {
155
162
  if (bottomSheetRef.current) {
156
163
  setTimeout(() => {
@@ -381,6 +388,29 @@ const AllComponents: React.FC = () => {
381
388
  </BottomSheetModalProvider>
382
389
  </Center>
383
390
  </ComponentWrapper>
391
+ <ComponentWrapper name="Time Picker" link="/?path=/docs/components-time-picker--docs">
392
+ <Center flex={1}>
393
+ <Button onPress={handleTimePickerOpenPress}>Open Time Picker</Button>
394
+ <BottomSheetModalProvider>
395
+ <TimePicker
396
+ ref={timePickerRef}
397
+ date={selectedTime}
398
+ onChange={({ date }) => setSelectedTime(date)}
399
+ onCancel={() => setSelectedTime(undefined)}
400
+ />
401
+ </BottomSheetModalProvider>
402
+ </Center>
403
+ </ComponentWrapper>
404
+ <ComponentWrapper
405
+ name="Time Picker Input"
406
+ link="/?path=/docs/forms-time-picker-input--docs"
407
+ >
408
+ <Center flex={1} padding="200">
409
+ <BottomSheetModalProvider>
410
+ <TimePickerInput placeholder="HH:mm" />
411
+ </BottomSheetModalProvider>
412
+ </Center>
413
+ </ComponentWrapper>
384
414
  <ComponentWrapper
385
415
  name="Description List"
386
416
  link="/?path=/docs/components-description-list--docs"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilitywarehouse/hearth-react-native",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "Utility Warehouse React Native UI library",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -20,6 +20,7 @@
20
20
  "@gluestack-ui/spinner": "0.1.14",
21
21
  "@gluestack-ui/switch": "0.1.22",
22
22
  "@gluestack-ui/textarea": "0.1.23",
23
+ "@quidone/react-native-wheel-picker": "^1.6.1",
23
24
  "dayjs": "^1.11.13"
24
25
  },
25
26
  "devDependencies": {
@@ -57,8 +58,8 @@
57
58
  "vitest": "^3.2.4",
58
59
  "@utilitywarehouse/hearth-fonts": "^0.0.4",
59
60
  "@utilitywarehouse/hearth-react-icons": "^0.8.0",
60
- "@utilitywarehouse/hearth-react-native-icons": "^0.8.0",
61
61
  "@utilitywarehouse/hearth-svg-assets": "^0.5.0",
62
+ "@utilitywarehouse/hearth-react-native-icons": "^0.8.0",
62
63
  "@utilitywarehouse/hearth-tokens": "^0.2.3"
63
64
  },
64
65
  "peerDependencies": {
@@ -1,30 +1,47 @@
1
- import { ReactNode } from 'react';
2
1
  import { View } from 'react-native';
3
2
  import { StyleSheet } from 'react-native-unistyles';
3
+ import TimePickerView from '../TimePicker/TimePickerView';
4
4
  import { useDatePickerContext } from './DatePicker.context';
5
5
  import Days from './DatePickerDays';
6
6
  import Footer from './DatePickerFooter';
7
7
  import Header from './DatePickerHeader';
8
8
  import Months from './DatePickerMonths';
9
9
  import Years from './DatePickerYears';
10
- import type { CalendarViews } from './enums';
11
- import TimePicker from './TimePicker';
12
-
13
- const CalendarView: Record<CalendarViews, ReactNode> = {
14
- year: <Years />,
15
- month: <Months />,
16
- day: <Days />,
17
- time: <TimePicker />,
18
- };
19
10
 
20
11
  const Calendar = () => {
21
- const { hideHeader, hideFooter, calendarView, containerHeight, navigationPosition } =
22
- useDatePickerContext();
12
+ const {
13
+ hideHeader,
14
+ hideFooter,
15
+ calendarView,
16
+ containerHeight,
17
+ navigationPosition,
18
+ currentDate,
19
+ onSelectDate,
20
+ timeZone,
21
+ use12Hours,
22
+ } = useDatePickerContext();
23
+
24
+ const calendarContent =
25
+ calendarView === 'year' ? (
26
+ <Years />
27
+ ) : calendarView === 'month' ? (
28
+ <Months />
29
+ ) : calendarView === 'time' ? (
30
+ <TimePickerView
31
+ currentDate={currentDate}
32
+ onSelectDate={onSelectDate}
33
+ timeZone={timeZone}
34
+ use12Hours={use12Hours}
35
+ containerHeight={containerHeight}
36
+ />
37
+ ) : (
38
+ <Days />
39
+ );
23
40
 
24
41
  return (
25
42
  <View style={[styles.container]} testID="calendar">
26
43
  {!hideHeader ? <Header navigationPosition={navigationPosition} /> : null}
27
- <View style={styles.containerInner(containerHeight)}>{CalendarView[calendarView]}</View>
44
+ <View style={styles.containerInner(containerHeight)}>{calendarContent}</View>
28
45
  {!hideFooter ? <Footer /> : null}
29
46
  </View>
30
47
  );