@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +13 -13
- package/CHANGELOG.md +77 -0
- package/build/components/DatePicker/DatePickerCalendar.js +4 -9
- package/build/components/Modal/Modal.js +5 -4
- package/build/components/Modal/Modal.props.d.ts +10 -4
- package/build/components/ProgressBar/ProgressBar.d.ts +6 -0
- package/build/components/ProgressBar/ProgressBar.js +35 -0
- package/build/components/ProgressBar/ProgressBar.props.d.ts +60 -0
- package/build/components/ProgressBar/ProgressBar.props.js +1 -0
- package/build/components/ProgressBar/ProgressBarCircular.d.ts +6 -0
- package/build/components/ProgressBar/ProgressBarCircular.js +115 -0
- package/build/components/ProgressBar/ProgressBarLinear.d.ts +6 -0
- package/build/components/ProgressBar/ProgressBarLinear.js +79 -0
- package/build/components/ProgressBar/index.d.ts +2 -0
- package/build/components/ProgressBar/index.js +1 -0
- 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 +3 -0
- package/build/components/index.js +3 -0
- package/docs/components/AllComponents.web.tsx +36 -0
- package/package.json +2 -1
- package/src/components/DatePicker/DatePickerCalendar.tsx +30 -13
- package/src/components/Modal/Modal.props.ts +13 -4
- package/src/components/Modal/Modal.stories.tsx +1 -1
- package/src/components/Modal/Modal.tsx +28 -11
- package/src/components/ProgressBar/ProgressBar.docs.mdx +90 -0
- package/src/components/ProgressBar/ProgressBar.figma.tsx +79 -0
- package/src/components/ProgressBar/ProgressBar.props.ts +60 -0
- package/src/components/ProgressBar/ProgressBar.stories.tsx +117 -0
- package/src/components/ProgressBar/ProgressBar.tsx +74 -0
- package/src/components/ProgressBar/ProgressBarCircular.tsx +181 -0
- package/src/components/ProgressBar/ProgressBarLinear.tsx +127 -0
- package/src/components/ProgressBar/index.ts +7 -0
- 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 +3 -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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { memo, useCallback, useMemo } from 'react';
|
|
2
|
+
import { Platform, View } from 'react-native';
|
|
3
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
4
|
+
import Animated, {
|
|
5
|
+
Extrapolate,
|
|
6
|
+
interpolate,
|
|
7
|
+
runOnJS,
|
|
8
|
+
type SharedValue,
|
|
9
|
+
useAnimatedStyle,
|
|
10
|
+
useSharedValue,
|
|
11
|
+
} from 'react-native-reanimated';
|
|
12
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
13
|
+
import { isEqual } from '../../utils';
|
|
14
|
+
import { BodyText } from '../BodyText';
|
|
15
|
+
import type { PickerOption } from './TimePicker.props';
|
|
16
|
+
|
|
17
|
+
type TimePickerWheelProps = {
|
|
18
|
+
value: number | string;
|
|
19
|
+
setValue?: (value: any) => void;
|
|
20
|
+
items: PickerOption[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const ITEM_HEIGHT = 44;
|
|
24
|
+
|
|
25
|
+
type WheelWebItemProps = {
|
|
26
|
+
displayValue: PickerOption | undefined;
|
|
27
|
+
index: number;
|
|
28
|
+
currentIndex: number;
|
|
29
|
+
translateY: SharedValue<number>;
|
|
30
|
+
radius: number;
|
|
31
|
+
displayCount: number;
|
|
32
|
+
value: number | string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const WheelWebItem = ({
|
|
36
|
+
displayValue,
|
|
37
|
+
index,
|
|
38
|
+
currentIndex,
|
|
39
|
+
translateY,
|
|
40
|
+
radius,
|
|
41
|
+
displayCount,
|
|
42
|
+
value,
|
|
43
|
+
}: WheelWebItemProps) => {
|
|
44
|
+
const baseOpacity = displayValue?.value !== value ? 0.3 : 1;
|
|
45
|
+
|
|
46
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
47
|
+
const offset = (radius * 2) / displayCount;
|
|
48
|
+
const shifted = interpolate(
|
|
49
|
+
translateY.value,
|
|
50
|
+
[-radius, radius],
|
|
51
|
+
[-radius + offset * (index - currentIndex), radius + offset * (index - currentIndex)],
|
|
52
|
+
Extrapolate.EXTEND
|
|
53
|
+
);
|
|
54
|
+
const angle = interpolate(
|
|
55
|
+
shifted,
|
|
56
|
+
[-radius, radius],
|
|
57
|
+
[-Math.PI / 2, Math.PI / 2],
|
|
58
|
+
Extrapolate.CLAMP
|
|
59
|
+
);
|
|
60
|
+
const translate = radius * Math.sin(angle);
|
|
61
|
+
const rotateX = (angle * 180) / Math.PI;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
position: 'absolute',
|
|
65
|
+
height: ITEM_HEIGHT - 10,
|
|
66
|
+
opacity: baseOpacity,
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
justifyContent: 'center',
|
|
69
|
+
transform: [{ translateY: translate }, { rotateX: `${rotateX}deg` }],
|
|
70
|
+
};
|
|
71
|
+
}, [baseOpacity, currentIndex, displayCount, index, radius, translateY]);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Animated.View style={animatedStyle}>
|
|
75
|
+
<BodyText size="lg">{displayValue?.text}</BodyText>
|
|
76
|
+
</Animated.View>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const TimePickerWheel = ({ value, setValue = () => {}, items }: TimePickerWheelProps) => {
|
|
81
|
+
const displayCount = 5;
|
|
82
|
+
const translateY = useSharedValue(0);
|
|
83
|
+
const renderCount = displayCount * 2 < items.length ? displayCount * 8 : displayCount * 2 - 1;
|
|
84
|
+
const circular = items.length >= displayCount;
|
|
85
|
+
const height = 140;
|
|
86
|
+
const radius = height / 2;
|
|
87
|
+
|
|
88
|
+
const valueIndex = useMemo(() => {
|
|
89
|
+
return Math.max(
|
|
90
|
+
0,
|
|
91
|
+
items.findIndex(item => item.value === value)
|
|
92
|
+
);
|
|
93
|
+
}, [items, value]);
|
|
94
|
+
|
|
95
|
+
const handlePanEnd = useCallback(
|
|
96
|
+
(deltaY: number) => {
|
|
97
|
+
let newValueIndex = valueIndex - Math.round(deltaY / ((radius * 2) / displayCount));
|
|
98
|
+
if (circular) {
|
|
99
|
+
newValueIndex = (newValueIndex + items.length) % items.length;
|
|
100
|
+
} else {
|
|
101
|
+
if (newValueIndex < 0) {
|
|
102
|
+
newValueIndex = 0;
|
|
103
|
+
} else if (newValueIndex >= items.length) {
|
|
104
|
+
newValueIndex = items.length - 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const newValue = items[newValueIndex];
|
|
108
|
+
if (newValue?.value === value) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (newValue?.value) {
|
|
112
|
+
setValue(newValue.value);
|
|
113
|
+
} else if (items[0]?.value) {
|
|
114
|
+
setValue(items[0].value);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
[circular, displayCount, items, radius, setValue, value, valueIndex]
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const panGesture = useMemo(
|
|
121
|
+
() =>
|
|
122
|
+
Gesture.Pan()
|
|
123
|
+
.onUpdate(event => {
|
|
124
|
+
translateY.value = event.translationY;
|
|
125
|
+
})
|
|
126
|
+
.onEnd(event => {
|
|
127
|
+
runOnJS(handlePanEnd)(event.translationY);
|
|
128
|
+
translateY.value = 0;
|
|
129
|
+
}),
|
|
130
|
+
[handlePanEnd, translateY]
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const displayValues = useMemo(() => {
|
|
134
|
+
const centerIndex = Math.floor(renderCount / 2);
|
|
135
|
+
|
|
136
|
+
return Array.from({ length: renderCount }, (_, index) => {
|
|
137
|
+
let targetIndex = valueIndex + index - centerIndex;
|
|
138
|
+
if (circular) {
|
|
139
|
+
targetIndex = ((targetIndex % items.length) + items.length) % items.length;
|
|
140
|
+
} else {
|
|
141
|
+
targetIndex = Math.max(0, Math.min(targetIndex, items.length - 1));
|
|
142
|
+
}
|
|
143
|
+
return items[targetIndex] || items[0];
|
|
144
|
+
});
|
|
145
|
+
}, [renderCount, valueIndex, items, circular]);
|
|
146
|
+
|
|
147
|
+
const currentIndex = Math.max(
|
|
148
|
+
0,
|
|
149
|
+
displayValues.findIndex(item => item?.value === value)
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<GestureDetector gesture={panGesture}>
|
|
154
|
+
<View style={styles.container}>
|
|
155
|
+
<View
|
|
156
|
+
style={[
|
|
157
|
+
styles.selectedIndicator,
|
|
158
|
+
{
|
|
159
|
+
transform: [{ translateY: -ITEM_HEIGHT / 2 }],
|
|
160
|
+
height: ITEM_HEIGHT,
|
|
161
|
+
},
|
|
162
|
+
]}
|
|
163
|
+
/>
|
|
164
|
+
{displayValues?.map((displayValue, index) => (
|
|
165
|
+
<WheelWebItem
|
|
166
|
+
key={`${displayValue?.text}-${index}`}
|
|
167
|
+
displayValue={displayValue}
|
|
168
|
+
index={index}
|
|
169
|
+
currentIndex={currentIndex}
|
|
170
|
+
translateY={translateY}
|
|
171
|
+
radius={radius}
|
|
172
|
+
displayCount={displayCount}
|
|
173
|
+
value={value}
|
|
174
|
+
/>
|
|
175
|
+
))}
|
|
176
|
+
</View>
|
|
177
|
+
</GestureDetector>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const styles = StyleSheet.create(theme => ({
|
|
182
|
+
container: {
|
|
183
|
+
minWidth: 30,
|
|
184
|
+
overflow: 'hidden',
|
|
185
|
+
alignItems: 'center',
|
|
186
|
+
justifyContent: 'center',
|
|
187
|
+
height: 208,
|
|
188
|
+
...Platform.select({
|
|
189
|
+
web: {
|
|
190
|
+
cursor: 'pointer',
|
|
191
|
+
userSelect: 'none',
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
},
|
|
195
|
+
selectedIndicator: {
|
|
196
|
+
position: 'absolute',
|
|
197
|
+
width: theme.components.timePicker.time.item.width,
|
|
198
|
+
height: theme.components.timePicker.time.item.height,
|
|
199
|
+
top: '50%',
|
|
200
|
+
backgroundColor: theme.color.interactive.neutral.surface.subtle.active,
|
|
201
|
+
borderRadius: theme.borderRadius.md,
|
|
202
|
+
alignItems: 'center',
|
|
203
|
+
justifyContent: 'center',
|
|
204
|
+
},
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
const customComparator = (
|
|
208
|
+
prev: Readonly<TimePickerWheelProps>,
|
|
209
|
+
next: Readonly<TimePickerWheelProps>
|
|
210
|
+
) => {
|
|
211
|
+
const areEqual =
|
|
212
|
+
prev.value === next.value && prev.setValue === next.setValue && isEqual(prev.items, next.items);
|
|
213
|
+
|
|
214
|
+
return areEqual;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export default memo(TimePickerWheel, customComparator);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import 'dayjs/locale/en';
|
|
2
|
+
import '../DatePicker/polyfill';
|
|
3
|
+
|
|
4
|
+
export type { BottomSheetMethods as TimePickerMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
|
|
5
|
+
export type { DateType } from '../DatePicker/DatePicker.props';
|
|
6
|
+
|
|
7
|
+
export { default as TimePicker } from './TimePicker';
|
|
8
|
+
export type { TimePickerProps } from './TimePicker.props';
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import { BottomSheetModalProvider, Center, FormField, TimePickerInput } from '../../';
|
|
3
|
+
import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
|
|
4
|
+
import * as Stories from './TimePickerInput.stories';
|
|
5
|
+
|
|
6
|
+
<ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=10334-6212&t=Jg2fPJPQNzOyspmQ-4" />
|
|
7
|
+
|
|
8
|
+
<Meta title="Forms / Time Picker Input" />
|
|
9
|
+
|
|
10
|
+
<BackToTopButton />
|
|
11
|
+
|
|
12
|
+
# Time Picker Input
|
|
13
|
+
|
|
14
|
+
`TimePickerInput` extends the base input to present a time picker trigger while still allowing direct text entry. It keeps the field inline and formats values with Day.js when needed.
|
|
15
|
+
|
|
16
|
+
- [Playground](#playground)
|
|
17
|
+
- [Usage](#usage)
|
|
18
|
+
- [Props](#props)
|
|
19
|
+
- [Formatting](#formatting)
|
|
20
|
+
- [Examples](#examples)
|
|
21
|
+
|
|
22
|
+
## Playground
|
|
23
|
+
|
|
24
|
+
<Canvas of={Stories.Playground} />
|
|
25
|
+
<Controls of={Stories.Playground} />
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Wrap the component with `BottomSheetModalProvider` so the underlying picker can render its modal. Provide a controlled `value` if you want to react to changes immediately or let the component manage its own display string.
|
|
30
|
+
|
|
31
|
+
<UsageWrap>
|
|
32
|
+
<BottomSheetModalProvider>
|
|
33
|
+
<Center>
|
|
34
|
+
<TimePickerInput placeholder="HH:mm" />
|
|
35
|
+
</Center>
|
|
36
|
+
</BottomSheetModalProvider>
|
|
37
|
+
</UsageWrap>
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { useState } from 'react';
|
|
41
|
+
import {
|
|
42
|
+
BottomSheetModalProvider,
|
|
43
|
+
TimePickerInput,
|
|
44
|
+
type DateType,
|
|
45
|
+
} from '@utilitywarehouse/hearth-react-native';
|
|
46
|
+
|
|
47
|
+
const BookingTimeField = () => {
|
|
48
|
+
const [time, setTime] = useState<DateType>();
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<BottomSheetModalProvider>
|
|
52
|
+
<TimePickerInput
|
|
53
|
+
value={time}
|
|
54
|
+
onChange={({ date }) => setTime(date ?? undefined)}
|
|
55
|
+
onClear={() => setTime(undefined)}
|
|
56
|
+
placeholder="HH:mm"
|
|
57
|
+
/>
|
|
58
|
+
</BottomSheetModalProvider>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Props
|
|
64
|
+
|
|
65
|
+
`TimePickerInput` inherits all React of the `Input` component props props (except `children`) and adds the following:
|
|
66
|
+
|
|
67
|
+
| Prop | Type | Default | Description |
|
|
68
|
+
| ------------------- | ------------------------------------------------------ | -------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `validationStatus` | `'initial' \| 'valid' \| 'invalid'` | `'initial'` | Renders the corresponding validation style. Inherited from `FormField` when nested. |
|
|
70
|
+
| `disabled` | `boolean` | `false` | Disables both typing and the time picker trigger button. |
|
|
71
|
+
| `readonly` | `boolean` | `false` | Prevents manual typing while keeping the picker available. |
|
|
72
|
+
| `focused` | `boolean` | `false` | Forces the focused state styling. |
|
|
73
|
+
| `label` | `string` | `-` | The label for the input. When used inside `FormField`, this is inherited from the context. |
|
|
74
|
+
| `labelVariant` | `'heading' \| 'body'` | `'body'` | The variant of the label text. |
|
|
75
|
+
| `helperText` | `string` | `-` | Helper text to display below the input. When used inside `FormField`, this is inherited from the context. |
|
|
76
|
+
| `helperIcon` | `ComponentType` | `-` | Icon to display alongside the helper |
|
|
77
|
+
| `validText` | `string` | `-` | Text to display when validation status is 'valid'. When used inside `FormField`, this is inherited. |
|
|
78
|
+
| `invalidText` | `string` | `-` | Text to display when validation status is 'invalid'. When used inside `FormField`, this is inherited. |
|
|
79
|
+
| `inBottomSheet` | `boolean` | `false` | Uses `BottomSheetTextInput` when rendering inside a bottom sheet. |
|
|
80
|
+
| `format` | `string` | `'HH:mm'` | Day.js format string used to render selected times and parse manual input. |
|
|
81
|
+
| `openButtonLabel` | `string` | `'Open time picker'` | Accessible label read by screen readers for the time trigger button. |
|
|
82
|
+
| `autoCloseOnSelect` | `boolean` | `true` | Closes the picker automatically after a time is chosen. |
|
|
83
|
+
| `timePickerProps` | `Omit<TimePickerProps, 'date' \| 'onChange' \| 'ref'>` | `-` | Forwards props to the underlying `TimePicker` (e.g. `use12Hours`, `timeZone`, `minuteInterval`). |
|
|
84
|
+
| `onChange` | `(payload: { date: DateType }) => void` | `-` | Fired whenever a valid time is parsed from typing or picked via the selector. |
|
|
85
|
+
| `onClear` | `() => void` | `-` | Called after the clear action resets the input. Also displays a trailing clear button when provided. |
|
|
86
|
+
|
|
87
|
+
## Formatting
|
|
88
|
+
|
|
89
|
+
When `format` is left as `'HH:mm'`, the input automatically inserts `:` as people type and requests a numeric keypad on supported platforms.
|
|
90
|
+
|
|
91
|
+
## Examples
|
|
92
|
+
|
|
93
|
+
### With label and helper text
|
|
94
|
+
|
|
95
|
+
<UsageWrap>
|
|
96
|
+
<BottomSheetModalProvider>
|
|
97
|
+
<Center>
|
|
98
|
+
<TimePickerInput
|
|
99
|
+
onClear={() => {}}
|
|
100
|
+
label="Meeting time"
|
|
101
|
+
helperText="Pick a time for your meeting"
|
|
102
|
+
validText="Time looks good!"
|
|
103
|
+
invalidText="Please enter a valid time"
|
|
104
|
+
/>
|
|
105
|
+
</Center>
|
|
106
|
+
</BottomSheetModalProvider>
|
|
107
|
+
</UsageWrap>
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
<TimePickerInput
|
|
111
|
+
onClear={() => {}}
|
|
112
|
+
label="Meeting time"
|
|
113
|
+
helperText="Pick a time for your meeting"
|
|
114
|
+
validText="Time looks good!"
|
|
115
|
+
invalidText="Please enter a valid time"
|
|
116
|
+
/>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### With `FormField`
|
|
120
|
+
|
|
121
|
+
<UsageWrap>
|
|
122
|
+
<BottomSheetModalProvider>
|
|
123
|
+
<Center>
|
|
124
|
+
<FormField label="Meeting time" helperText="Pick a time">
|
|
125
|
+
<TimePickerInput onClear={() => {}} />
|
|
126
|
+
</FormField>
|
|
127
|
+
</Center>
|
|
128
|
+
</BottomSheetModalProvider>
|
|
129
|
+
</UsageWrap>
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
<FormField label="Meeting time" helperText="Pick a time">
|
|
133
|
+
<TimePickerInput onClear={() => {}} />
|
|
134
|
+
</FormField>
|
|
135
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import figma from '@figma/code-connect';
|
|
2
|
+
import { TimePickerInput } from '../';
|
|
3
|
+
|
|
4
|
+
figma.connect(
|
|
5
|
+
TimePickerInput,
|
|
6
|
+
'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=10334-6212&t=Jg2fPJPQNzOyspmQ-4',
|
|
7
|
+
{
|
|
8
|
+
props: {
|
|
9
|
+
disabled: figma.enum('Variant', {
|
|
10
|
+
Disabled: true,
|
|
11
|
+
}),
|
|
12
|
+
validationStatus: figma.enum('Variant', {
|
|
13
|
+
Default: undefined,
|
|
14
|
+
Valid: 'valid',
|
|
15
|
+
Invalid: 'invalid',
|
|
16
|
+
}),
|
|
17
|
+
readonly: figma.enum('Variant', { 'Read-only': true }),
|
|
18
|
+
label: figma.string('Label'),
|
|
19
|
+
validText: figma.enum('Variant', {
|
|
20
|
+
Valid: figma.string('Validation'),
|
|
21
|
+
}),
|
|
22
|
+
invalidText: figma.enum('Variant', {
|
|
23
|
+
Invalid: figma.string('Validation'),
|
|
24
|
+
}),
|
|
25
|
+
placeholder: figma.enum('Value type', {
|
|
26
|
+
Placeholder: figma.string('Value'),
|
|
27
|
+
}),
|
|
28
|
+
value: figma.enum('Value type', {
|
|
29
|
+
Filled: figma.string('Value'),
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
example: props => <TimePickerInput {...props} />,
|
|
33
|
+
}
|
|
34
|
+
);
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
|
|
5
|
+
export interface TimePickerInputBaseProps {
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
validationStatus?: 'initial' | 'valid' | 'invalid';
|
|
8
|
+
readonly?: boolean;
|
|
9
|
+
focused?: boolean;
|
|
10
|
+
label?: string;
|
|
11
|
+
labelVariant?: 'heading' | 'body';
|
|
12
|
+
helperText?: string;
|
|
13
|
+
helperIcon?: React.ComponentType;
|
|
14
|
+
validText?: string;
|
|
15
|
+
invalidText?: string;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
inBottomSheet?: boolean;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Controls how the selected time is formatted when displayed inside the input.
|
|
21
|
+
* Accepts any Day.js format string.
|
|
22
|
+
*/
|
|
23
|
+
format?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Accessible label announced when activating the time picker trigger button.
|
|
26
|
+
*/
|
|
27
|
+
openButtonLabel?: string;
|
|
28
|
+
/**
|
|
29
|
+
* When true (default), the picker sheet is dismissed as soon as a time is picked.
|
|
30
|
+
*/
|
|
31
|
+
autoCloseOnSelect?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Additional props forwarded to the underlying TimePicker instance.
|
|
34
|
+
*/
|
|
35
|
+
timePickerProps?: Omit<TimePickerProps, 'date' | 'onChange' | 'ref'>;
|
|
36
|
+
/**
|
|
37
|
+
* Handles cleared input values.
|
|
38
|
+
*/
|
|
39
|
+
onClear?: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type TimePickerInputProps = TimePickerInputBaseProps &
|
|
43
|
+
Omit<TextInputProps, 'value' | 'onChange' | 'children'> &
|
|
44
|
+
ViewProps & {
|
|
45
|
+
/**
|
|
46
|
+
* Controlled time value. Accepts Date, string, number or Day.js instances.
|
|
47
|
+
*/
|
|
48
|
+
value?: DateType;
|
|
49
|
+
/**
|
|
50
|
+
* Fired after a valid time is parsed either from typing or the picker selection.
|
|
51
|
+
*/
|
|
52
|
+
onChange?: (params: { date: DateType }) => void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default TimePickerInputProps;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-native';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Platform } from 'react-native';
|
|
4
|
+
import { TimePickerInput, View } from '..';
|
|
5
|
+
import { VariantTitle, ViewWrap } from '../../../docs/components';
|
|
6
|
+
import type { DateType } from '../DatePicker';
|
|
7
|
+
import { Flex } from '../Flex';
|
|
8
|
+
import { FormField } from '../FormField';
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
title: 'Stories / TimePickerInput',
|
|
12
|
+
component: TimePickerInput,
|
|
13
|
+
parameters: {
|
|
14
|
+
layout: 'centered',
|
|
15
|
+
},
|
|
16
|
+
argTypes: {
|
|
17
|
+
format: {
|
|
18
|
+
control: 'text',
|
|
19
|
+
description: 'Day.js format string used to render and parse the value',
|
|
20
|
+
defaultValue: 'HH:mm',
|
|
21
|
+
},
|
|
22
|
+
validationStatus: {
|
|
23
|
+
control: 'select',
|
|
24
|
+
options: ['initial', 'valid', 'invalid'],
|
|
25
|
+
description: 'Manually set the validation status',
|
|
26
|
+
defaultValue: 'initial',
|
|
27
|
+
},
|
|
28
|
+
disabled: {
|
|
29
|
+
control: 'boolean',
|
|
30
|
+
description: 'Disable the input and trigger button',
|
|
31
|
+
defaultValue: false,
|
|
32
|
+
},
|
|
33
|
+
readonly: {
|
|
34
|
+
control: 'boolean',
|
|
35
|
+
description: 'Make the input read-only (typing disabled, picker still accessible)',
|
|
36
|
+
defaultValue: false,
|
|
37
|
+
},
|
|
38
|
+
focused: {
|
|
39
|
+
control: 'boolean',
|
|
40
|
+
description: 'Force the focused visual state',
|
|
41
|
+
defaultValue: false,
|
|
42
|
+
},
|
|
43
|
+
openButtonLabel: {
|
|
44
|
+
control: 'text',
|
|
45
|
+
description: 'Accessible label for the time trigger button',
|
|
46
|
+
defaultValue: 'Open time picker',
|
|
47
|
+
},
|
|
48
|
+
autoCloseOnSelect: {
|
|
49
|
+
control: 'boolean',
|
|
50
|
+
description: 'Automatically close the picker after selecting a time',
|
|
51
|
+
defaultValue: false,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
args: {
|
|
55
|
+
format: 'HH:mm',
|
|
56
|
+
validationStatus: 'initial',
|
|
57
|
+
disabled: false,
|
|
58
|
+
readonly: false,
|
|
59
|
+
focused: false,
|
|
60
|
+
openButtonLabel: 'Open time picker',
|
|
61
|
+
autoCloseOnSelect: false,
|
|
62
|
+
placeholder: undefined,
|
|
63
|
+
},
|
|
64
|
+
} satisfies Meta<typeof TimePickerInput>;
|
|
65
|
+
|
|
66
|
+
export default meta;
|
|
67
|
+
|
|
68
|
+
type Story = StoryObj<typeof meta>;
|
|
69
|
+
|
|
70
|
+
export const Playground: Story = {
|
|
71
|
+
render: (args: typeof meta.args) => {
|
|
72
|
+
const [selected, setSelected] = useState<DateType>();
|
|
73
|
+
|
|
74
|
+
const picker = (
|
|
75
|
+
<TimePickerInput
|
|
76
|
+
{...args}
|
|
77
|
+
value={selected}
|
|
78
|
+
onChange={({ date }) => setSelected(date ?? undefined)}
|
|
79
|
+
onClear={() => setSelected(undefined)}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (Platform.OS !== 'web') {
|
|
84
|
+
return picker;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
|
|
89
|
+
<ViewWrap>{picker}</ViewWrap>
|
|
90
|
+
</View>
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const States: Story = {
|
|
96
|
+
parameters: {
|
|
97
|
+
controls: { include: [] },
|
|
98
|
+
},
|
|
99
|
+
render: () => {
|
|
100
|
+
const [withValue, setWithValue] = useState<DateType>(new Date());
|
|
101
|
+
const [clearableTime, setClearableTime] = useState<DateType>(new Date());
|
|
102
|
+
const [formFieldTime, setFormFieldTime] = useState<DateType>();
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Flex direction="column" spacing="lg">
|
|
106
|
+
<VariantTitle title="Default">
|
|
107
|
+
<TimePickerInput />
|
|
108
|
+
</VariantTitle>
|
|
109
|
+
<VariantTitle title="With value">
|
|
110
|
+
<TimePickerInput
|
|
111
|
+
value={withValue}
|
|
112
|
+
onChange={({ date }) => setWithValue(date ?? undefined)}
|
|
113
|
+
onClear={() => setWithValue(undefined)}
|
|
114
|
+
/>
|
|
115
|
+
</VariantTitle>
|
|
116
|
+
<VariantTitle title="Disabled">
|
|
117
|
+
<TimePickerInput disabled />
|
|
118
|
+
</VariantTitle>
|
|
119
|
+
<VariantTitle title="Readonly">
|
|
120
|
+
<TimePickerInput readonly value={withValue} />
|
|
121
|
+
</VariantTitle>
|
|
122
|
+
<VariantTitle title="Invalid">
|
|
123
|
+
<TimePickerInput validationStatus="invalid" />
|
|
124
|
+
</VariantTitle>
|
|
125
|
+
<VariantTitle title="Valid">
|
|
126
|
+
<TimePickerInput validationStatus="valid" />
|
|
127
|
+
</VariantTitle>
|
|
128
|
+
<VariantTitle title="With clear action">
|
|
129
|
+
<TimePickerInput
|
|
130
|
+
value={clearableTime}
|
|
131
|
+
onChange={({ date }) => setClearableTime(date ?? undefined)}
|
|
132
|
+
onClear={() => setClearableTime(undefined)}
|
|
133
|
+
/>
|
|
134
|
+
</VariantTitle>
|
|
135
|
+
<VariantTitle title="Inside FormField">
|
|
136
|
+
<FormField label="Meeting time" helperText="Pick a time">
|
|
137
|
+
<TimePickerInput
|
|
138
|
+
value={formFieldTime}
|
|
139
|
+
onChange={({ date }) => setFormFieldTime(date ?? undefined)}
|
|
140
|
+
onClear={() => setFormFieldTime(undefined)}
|
|
141
|
+
/>
|
|
142
|
+
</FormField>
|
|
143
|
+
</VariantTitle>
|
|
144
|
+
</Flex>
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const MinuteIntervals: Story = {
|
|
150
|
+
parameters: {
|
|
151
|
+
controls: { include: [] },
|
|
152
|
+
},
|
|
153
|
+
render: () => {
|
|
154
|
+
const [intervalTime, setIntervalTime] = useState<DateType>();
|
|
155
|
+
|
|
156
|
+
const picker = (
|
|
157
|
+
<TimePickerInput
|
|
158
|
+
value={intervalTime}
|
|
159
|
+
onChange={({ date }) => setIntervalTime(date ?? undefined)}
|
|
160
|
+
onClear={() => setIntervalTime(undefined)}
|
|
161
|
+
timePickerProps={{ minuteInterval: 5 }}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (Platform.OS !== 'web') {
|
|
166
|
+
return picker;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
|
|
171
|
+
<ViewWrap>{picker}</ViewWrap>
|
|
172
|
+
</View>
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
};
|