@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.
Files changed (84) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +11 -11
  3. package/CHANGELOG.md +41 -0
  4. package/build/components/DatePicker/DatePickerCalendar.js +4 -9
  5. package/build/components/TimePicker/TimePicker.d.ts +6 -0
  6. package/build/components/TimePicker/TimePicker.js +78 -0
  7. package/build/components/TimePicker/TimePicker.props.d.ts +45 -0
  8. package/build/components/TimePicker/TimePicker.props.js +1 -0
  9. package/build/components/TimePicker/TimePickerView.d.ts +12 -0
  10. package/build/components/TimePicker/TimePickerView.js +130 -0
  11. package/build/components/TimePicker/TimePickerWheel.d.ts +8 -0
  12. package/build/components/TimePicker/TimePickerWheel.js +78 -0
  13. package/build/components/{DatePicker/time-picker/wheel-web.d.ts → TimePicker/TimePickerWheel.web.d.ts} +4 -4
  14. package/build/components/TimePicker/TimePickerWheel.web.js +122 -0
  15. package/build/components/TimePicker/index.d.ts +6 -0
  16. package/build/components/TimePicker/index.js +3 -0
  17. package/build/components/TimePickerInput/TimePickerInput.d.ts +6 -0
  18. package/build/components/TimePickerInput/TimePickerInput.js +127 -0
  19. package/build/components/TimePickerInput/TimePickerInput.props.d.ts +52 -0
  20. package/build/components/TimePickerInput/TimePickerInput.props.js +1 -0
  21. package/build/components/TimePickerInput/TimePickerInputDoneButton.d.ts +8 -0
  22. package/build/components/TimePickerInput/TimePickerInputDoneButton.js +19 -0
  23. package/build/components/TimePickerInput/TimePickerInputDoneButton.web.d.ts +5 -0
  24. package/build/components/TimePickerInput/TimePickerInputDoneButton.web.js +5 -0
  25. package/build/components/TimePickerInput/index.d.ts +2 -0
  26. package/build/components/TimePickerInput/index.js +1 -0
  27. package/build/components/index.d.ts +2 -0
  28. package/build/components/index.js +2 -0
  29. package/docs/components/AllComponents.web.tsx +30 -0
  30. package/package.json +2 -1
  31. package/src/components/DatePicker/DatePickerCalendar.tsx +30 -13
  32. package/src/components/TimePicker/TimePicker.docs.mdx +84 -0
  33. package/src/components/TimePicker/TimePicker.figma.tsx +29 -0
  34. package/src/components/TimePicker/TimePicker.props.ts +45 -0
  35. package/src/components/TimePicker/TimePicker.stories.tsx +85 -0
  36. package/src/components/TimePicker/TimePicker.tsx +150 -0
  37. package/src/components/TimePicker/TimePickerView.tsx +216 -0
  38. package/src/components/TimePicker/TimePickerWheel.tsx +154 -0
  39. package/src/components/TimePicker/TimePickerWheel.web.tsx +217 -0
  40. package/src/components/TimePicker/index.ts +8 -0
  41. package/src/components/TimePickerInput/TimePickerInput.docs.mdx +135 -0
  42. package/src/components/TimePickerInput/TimePickerInput.figma.tsx +34 -0
  43. package/src/components/TimePickerInput/TimePickerInput.props.ts +55 -0
  44. package/src/components/TimePickerInput/TimePickerInput.stories.tsx +175 -0
  45. package/src/components/TimePickerInput/TimePickerInput.tsx +283 -0
  46. package/src/components/TimePickerInput/TimePickerInputDoneButton.tsx +42 -0
  47. package/src/components/TimePickerInput/TimePickerInputDoneButton.web.tsx +7 -0
  48. package/src/components/TimePickerInput/index.ts +2 -0
  49. package/src/components/index.ts +2 -0
  50. package/build/components/DatePicker/TimePicker.d.ts +0 -3
  51. package/build/components/DatePicker/TimePicker.js +0 -84
  52. package/build/components/DatePicker/time-picker/animated-math.d.ts +0 -4
  53. package/build/components/DatePicker/time-picker/animated-math.js +0 -19
  54. package/build/components/DatePicker/time-picker/period-native.d.ts +0 -6
  55. package/build/components/DatePicker/time-picker/period-native.js +0 -17
  56. package/build/components/DatePicker/time-picker/period-picker.d.ts +0 -6
  57. package/build/components/DatePicker/time-picker/period-picker.js +0 -10
  58. package/build/components/DatePicker/time-picker/period-web.d.ts +0 -6
  59. package/build/components/DatePicker/time-picker/period-web.js +0 -21
  60. package/build/components/DatePicker/time-picker/wheel-native.d.ts +0 -8
  61. package/build/components/DatePicker/time-picker/wheel-native.js +0 -19
  62. package/build/components/DatePicker/time-picker/wheel-picker/index.d.ts +0 -2
  63. package/build/components/DatePicker/time-picker/wheel-picker/index.js +0 -2
  64. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.d.ts +0 -16
  65. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.js +0 -97
  66. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.d.ts +0 -21
  67. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.js +0 -88
  68. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.d.ts +0 -23
  69. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.js +0 -21
  70. package/build/components/DatePicker/time-picker/wheel-web.js +0 -146
  71. package/build/components/DatePicker/time-picker/wheel.d.ts +0 -8
  72. package/build/components/DatePicker/time-picker/wheel.js +0 -10
  73. package/src/components/DatePicker/TimePicker.tsx +0 -141
  74. package/src/components/DatePicker/time-picker/animated-math.ts +0 -33
  75. package/src/components/DatePicker/time-picker/period-native.tsx +0 -34
  76. package/src/components/DatePicker/time-picker/period-picker.tsx +0 -16
  77. package/src/components/DatePicker/time-picker/period-web.tsx +0 -36
  78. package/src/components/DatePicker/time-picker/wheel-native.tsx +0 -37
  79. package/src/components/DatePicker/time-picker/wheel-picker/index.ts +0 -3
  80. package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.tsx +0 -132
  81. package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.ts +0 -22
  82. package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.tsx +0 -200
  83. package/src/components/DatePicker/time-picker/wheel-web.tsx +0 -180
  84. package/src/components/DatePicker/time-picker/wheel.tsx +0 -18
@@ -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.25.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": {
@@ -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
  );
@@ -0,0 +1,84 @@
1
+ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
2
+ import { Button, Center } from '../../';
3
+ import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
4
+ import * as Stories from './TimePicker.stories';
5
+
6
+ <ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=10334-16770&t=Jg2fPJPQNzOyspmQ-4" />
7
+
8
+ <Meta title="Components / Time Picker" />
9
+
10
+ <BackToTopButton />
11
+
12
+ # Time Picker
13
+
14
+ `TimePicker` presents a wheel-based time selector inside a bottom sheet, letting people pick hours and minutes without leaving the current context. It supports 12-hour and 24-hour clocks and returns a JavaScript `Date` whenever the selection changes.
15
+
16
+ - [Playground](#playground)
17
+ - [Usage](#usage)
18
+ - [Props](#props)
19
+ - [Accessibility](#accessibility)
20
+
21
+ ## Playground
22
+
23
+ <Canvas of={Stories.Playground} />
24
+
25
+ <Controls of={Stories.Playground} />
26
+
27
+ ## Usage
28
+
29
+ Use the `TimePicker` with a ref to present the bottom sheet when people tap a trigger button.
30
+
31
+ <UsageWrap>
32
+ <Center>
33
+ <Button onPress={() => {}}>Pick a time</Button>
34
+ </Center>
35
+ </UsageWrap>
36
+
37
+ ```tsx
38
+ import { useRef, useState } from 'react';
39
+ import {
40
+ BottomSheetModalProvider,
41
+ Button,
42
+ TimePicker,
43
+ type DateType,
44
+ } from '@utilitywarehouse/hearth-react-native';
45
+
46
+ const BookingTime = () => {
47
+ const pickerRef = useRef(null);
48
+ const [time, setTime] = useState<DateType>();
49
+
50
+ return (
51
+ <BottomSheetModalProvider>
52
+ <Button onPress={() => pickerRef.current?.present()}>Choose time</Button>
53
+
54
+ <TimePicker
55
+ ref={pickerRef}
56
+ date={time}
57
+ onChange={({ date }) => setTime(date)}
58
+ onCancel={() => setTime(undefined)}
59
+ use12Hours={false}
60
+ />
61
+ </BottomSheetModalProvider>
62
+ );
63
+ };
64
+ ```
65
+
66
+ ## Props
67
+
68
+ `TimePicker` extends the `BottomSheetModal` component. The table below highlights the main props.
69
+
70
+ | Prop | Type | Default | Description |
71
+ | ---------------- | --------------------------------------- | ---------- | --------------------------------------------------------------- |
72
+ | `date` | `DateType` | `-` | Selected time value. |
73
+ | `timeZone` | `string` | `-` | IANA time zone identifier applied to the selected time. |
74
+ | `use12Hours` | `boolean` | `false` | Displays an AM/PM selector and formats hours from 1 to 12. |
75
+ | `minuteInterval` | `number` | `1` | Step interval for minutes shown in the picker. |
76
+ | `hideFooter` | `boolean` | `false` | Hides the Cancel/Ok actions. |
77
+ | `onChange` | `(payload: { date: DateType }) => void` | `-` | Fired whenever the selected time changes. |
78
+ | `onCancel` | `() => void` | `() => {}` | Fired when the cancel action is triggered. |
79
+ | `ref` | `Ref<BottomSheetModalMethods>` | `-` | Gives imperative access to present or dismiss the bottom sheet. |
80
+
81
+ ## Accessibility
82
+
83
+ - Screen readers announce the picker when the sheet opens and focus the wheel area on Android.
84
+ - Action buttons are exposed as standard buttons with clear labels.
@@ -0,0 +1,29 @@
1
+ import figma from '@figma/code-connect';
2
+ import { useRef, useState } from 'react';
3
+ import { BottomSheetModalProvider, Button, DateType, TimePicker } from '../';
4
+
5
+ figma.connect(
6
+ TimePicker,
7
+ 'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=10334-16770&t=Jg2fPJPQNzOyspmQ-4',
8
+ {
9
+ props: {},
10
+ example: props => {
11
+ const pickerRef = useRef(null);
12
+ const [time, setTime] = useState<DateType>();
13
+
14
+ return (
15
+ <BottomSheetModalProvider>
16
+ <Button onPress={() => pickerRef.current?.present()}>Choose time</Button>
17
+
18
+ <TimePicker
19
+ ref={pickerRef}
20
+ date={time}
21
+ onChange={({ date }) => setTime(date)}
22
+ onCancel={() => setTime(undefined)}
23
+ use12Hours={false}
24
+ />
25
+ </BottomSheetModalProvider>
26
+ );
27
+ },
28
+ }
29
+ );
@@ -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
+
6
+ export interface TimePickerProps {
7
+ /**
8
+ * IANA time zone identifier applied when normalising and comparing times.
9
+ */
10
+ timeZone?: string;
11
+ /**
12
+ * Controlled time value.
13
+ */
14
+ date?: DateType;
15
+ /**
16
+ * Fired whenever a time is picked.
17
+ */
18
+ onChange?: (params: { date: DateType }) => void;
19
+ /**
20
+ * Display a 12-hour clock with AM/PM selector.
21
+ */
22
+ use12Hours?: boolean;
23
+ /**
24
+ * Step interval for minutes shown in the picker.
25
+ */
26
+ minuteInterval?: number;
27
+ /**
28
+ * Hide the footer actions.
29
+ */
30
+ hideFooter?: boolean;
31
+ /**
32
+ * Custom container styling for the time picker surface.
33
+ */
34
+ style?: ViewStyle;
35
+ /**
36
+ * Gives imperative access to the bottom sheet instance.
37
+ */
38
+ ref?: Ref<BottomSheetModalMethods<any>>;
39
+ /**
40
+ * Fired when the cancel action is triggered.
41
+ */
42
+ onCancel?: () => void;
43
+ }
44
+
45
+ export type { DateType, PickerOption };
@@ -0,0 +1,85 @@
1
+ import { Meta, StoryObj } from '@storybook/react-native';
2
+ import { useRef, useState } from 'react';
3
+ import { Platform, View } from 'react-native';
4
+ import { DateType, TimePicker } from '.';
5
+ import { ViewWrap } from '../../../docs/components';
6
+ import { BottomSheetModal } from '../BottomSheet';
7
+ import { Button } from '../Button';
8
+
9
+ const meta = {
10
+ title: 'Stories / TimePicker',
11
+ component: TimePicker,
12
+ parameters: {
13
+ layout: 'centered',
14
+ },
15
+ argTypes: {
16
+ use12Hours: {
17
+ control: 'boolean',
18
+ description: 'Display a 12-hour clock with AM/PM selector',
19
+ defaultValue: false,
20
+ },
21
+ minuteInterval: {
22
+ control: 'number',
23
+ description: 'Step interval for minutes shown in the picker',
24
+ defaultValue: 1,
25
+ },
26
+ },
27
+ args: {
28
+ use12Hours: false,
29
+ minuteInterval: 1,
30
+ },
31
+ } satisfies Meta<typeof TimePicker>;
32
+
33
+ export default meta;
34
+
35
+ type Story = StoryObj<typeof meta>;
36
+ type StoryArgs = Story['args'];
37
+
38
+ export const Playground: Story = {
39
+ render: (args: StoryArgs) => {
40
+ const [selected, setSelected] = useState<DateType>();
41
+ const modalRef = useRef<BottomSheetModal>(null);
42
+
43
+ return (
44
+ <View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
45
+ <ViewWrap>
46
+ <Button onPress={() => modalRef.current?.present()}>Show Time Picker</Button>
47
+ <TimePicker
48
+ ref={modalRef}
49
+ date={selected}
50
+ use12Hours={args.use12Hours}
51
+ minuteInterval={args.minuteInterval}
52
+ onChange={({ date }) => setSelected(date)}
53
+ onCancel={() => setSelected(undefined)}
54
+ />
55
+ </ViewWrap>
56
+ </View>
57
+ );
58
+ },
59
+ };
60
+
61
+ export const TwelveHour: Story = {
62
+ args: {
63
+ use12Hours: true,
64
+ },
65
+ render: (args: StoryArgs) => {
66
+ const [selected, setSelected] = useState<DateType>();
67
+ const modalRef = useRef<BottomSheetModal>(null);
68
+
69
+ return (
70
+ <View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
71
+ <ViewWrap>
72
+ <Button onPress={() => modalRef.current?.present()}>Show 12-hour Time Picker</Button>
73
+ <TimePicker
74
+ ref={modalRef}
75
+ date={selected}
76
+ use12Hours={args.use12Hours}
77
+ minuteInterval={args.minuteInterval}
78
+ onChange={({ date }) => setSelected(date)}
79
+ onCancel={() => setSelected(undefined)}
80
+ />
81
+ </ViewWrap>
82
+ </View>
83
+ );
84
+ },
85
+ };