@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +13 -13
- package/CHANGELOG.md +72 -0
- package/build/components/DatePicker/DatePickerCalendar.js +4 -9
- package/build/components/Modal/Modal.d.ts +1 -1
- package/build/components/Modal/Modal.js +30 -7
- package/build/components/Modal/Modal.props.d.ts +4 -2
- package/build/components/TimePicker/TimePicker.d.ts +6 -0
- package/build/components/TimePicker/TimePicker.js +78 -0
- package/build/components/TimePicker/TimePicker.props.d.ts +45 -0
- package/build/components/TimePicker/TimePicker.props.js +1 -0
- package/build/components/TimePicker/TimePickerView.d.ts +12 -0
- package/build/components/TimePicker/TimePickerView.js +130 -0
- package/build/components/TimePicker/TimePickerWheel.d.ts +8 -0
- package/build/components/TimePicker/TimePickerWheel.js +78 -0
- package/build/components/{DatePicker/time-picker/wheel-web.d.ts → TimePicker/TimePickerWheel.web.d.ts} +4 -4
- package/build/components/TimePicker/TimePickerWheel.web.js +122 -0
- package/build/components/TimePicker/index.d.ts +6 -0
- package/build/components/TimePicker/index.js +3 -0
- package/build/components/TimePickerInput/TimePickerInput.d.ts +6 -0
- package/build/components/TimePickerInput/TimePickerInput.js +127 -0
- package/build/components/TimePickerInput/TimePickerInput.props.d.ts +52 -0
- package/build/components/TimePickerInput/TimePickerInput.props.js +1 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.d.ts +8 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.js +19 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.web.d.ts +5 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.web.js +5 -0
- package/build/components/TimePickerInput/index.d.ts +2 -0
- package/build/components/TimePickerInput/index.js +1 -0
- package/build/components/index.d.ts +2 -0
- package/build/components/index.js +2 -0
- package/docs/components/AllComponents.web.tsx +30 -0
- package/package.json +3 -2
- package/src/components/DatePicker/DatePickerCalendar.tsx +30 -13
- package/src/components/Modal/Modal.docs.mdx +9 -3
- package/src/components/Modal/Modal.props.ts +4 -2
- package/src/components/Modal/Modal.tsx +44 -7
- package/src/components/TimePicker/TimePicker.docs.mdx +84 -0
- package/src/components/TimePicker/TimePicker.figma.tsx +29 -0
- package/src/components/TimePicker/TimePicker.props.ts +45 -0
- package/src/components/TimePicker/TimePicker.stories.tsx +85 -0
- package/src/components/TimePicker/TimePicker.tsx +150 -0
- package/src/components/TimePicker/TimePickerView.tsx +216 -0
- package/src/components/TimePicker/TimePickerWheel.tsx +154 -0
- package/src/components/TimePicker/TimePickerWheel.web.tsx +217 -0
- package/src/components/TimePicker/index.ts +8 -0
- package/src/components/TimePickerInput/TimePickerInput.docs.mdx +135 -0
- package/src/components/TimePickerInput/TimePickerInput.figma.tsx +34 -0
- package/src/components/TimePickerInput/TimePickerInput.props.ts +55 -0
- package/src/components/TimePickerInput/TimePickerInput.stories.tsx +175 -0
- package/src/components/TimePickerInput/TimePickerInput.tsx +283 -0
- package/src/components/TimePickerInput/TimePickerInputDoneButton.tsx +42 -0
- package/src/components/TimePickerInput/TimePickerInputDoneButton.web.tsx +7 -0
- package/src/components/TimePickerInput/index.ts +2 -0
- package/src/components/index.ts +2 -0
- package/build/components/DatePicker/TimePicker.d.ts +0 -3
- package/build/components/DatePicker/TimePicker.js +0 -84
- package/build/components/DatePicker/time-picker/animated-math.d.ts +0 -4
- package/build/components/DatePicker/time-picker/animated-math.js +0 -19
- package/build/components/DatePicker/time-picker/period-native.d.ts +0 -6
- package/build/components/DatePicker/time-picker/period-native.js +0 -17
- package/build/components/DatePicker/time-picker/period-picker.d.ts +0 -6
- package/build/components/DatePicker/time-picker/period-picker.js +0 -10
- package/build/components/DatePicker/time-picker/period-web.d.ts +0 -6
- package/build/components/DatePicker/time-picker/period-web.js +0 -21
- package/build/components/DatePicker/time-picker/wheel-native.d.ts +0 -8
- package/build/components/DatePicker/time-picker/wheel-native.js +0 -19
- package/build/components/DatePicker/time-picker/wheel-picker/index.d.ts +0 -2
- package/build/components/DatePicker/time-picker/wheel-picker/index.js +0 -2
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.d.ts +0 -16
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.js +0 -97
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.d.ts +0 -21
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.js +0 -88
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.d.ts +0 -23
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.js +0 -21
- package/build/components/DatePicker/time-picker/wheel-web.js +0 -146
- package/build/components/DatePicker/time-picker/wheel.d.ts +0 -8
- package/build/components/DatePicker/time-picker/wheel.js +0 -10
- package/src/components/DatePicker/TimePicker.tsx +0 -141
- package/src/components/DatePicker/time-picker/animated-math.ts +0 -33
- package/src/components/DatePicker/time-picker/period-native.tsx +0 -34
- package/src/components/DatePicker/time-picker/period-picker.tsx +0 -16
- package/src/components/DatePicker/time-picker/period-web.tsx +0 -36
- package/src/components/DatePicker/time-picker/wheel-native.tsx +0 -37
- package/src/components/DatePicker/time-picker/wheel-picker/index.ts +0 -3
- package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.tsx +0 -132
- package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.ts +0 -22
- package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.tsx +0 -200
- package/src/components/DatePicker/time-picker/wheel-web.tsx +0 -180
- package/src/components/DatePicker/time-picker/wheel.tsx +0 -18
|
@@ -107,9 +107,10 @@ The Modal component extends the `BottomSheetModal` component and accepts all of
|
|
|
107
107
|
| `primaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the primary button (colorScheme defaults to 'highlight', variant to 'solid') | - |
|
|
108
108
|
| `secondaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the secondary button (colorScheme defaults to 'functional', variant to 'outline') | - |
|
|
109
109
|
| `closeButtonProps` | `Omit<UnstyledIconButtonProps, 'children'>` | Additional props to pass to the close button | - |
|
|
110
|
-
| `fullscreen` | `boolean` | Whether the modal should take up the full screen height
|
|
110
|
+
| `fullscreen` | `boolean` | Whether the modal should take up the full screen height. Only applies when `inNavModal` is `false` | `false` |
|
|
111
111
|
| `inNavModal` | `boolean` | Renders the modal correctly when used inside a navigation modal | `false` |
|
|
112
112
|
| `background` | `'default' \| 'brand'` | Sets the modal background. Only applies when `inNavModal` is `true` | `'default'` |
|
|
113
|
+
| `scrollable` | `boolean` | Whether the modal's content should be placed in a `ScrollView`. Only applies when `inNavModal` is `true` | `true` |
|
|
113
114
|
|
|
114
115
|
\* use this to detect if the modal has been opened or closed, index 0 indicates open state and -1 indicates closed state
|
|
115
116
|
|
|
@@ -462,11 +463,16 @@ const AlertModal = () => {
|
|
|
462
463
|
|
|
463
464
|
### Modal In Navigation Modal
|
|
464
465
|
|
|
465
|
-
When
|
|
466
|
+
When wanting to use the Modal component in a navigation context using [React Navigation](https://reactnavigation.org/docs/modal), you can set `inNavModal` to `true` to make it behave like a standard modal screen.
|
|
467
|
+
|
|
468
|
+
Within React Navigation, you can set `presentation: 'modal'` in the screen's settings to have the Modal look and behave like a standard modal/bottom sheet, or you can set `presentation: 'fullScreenModal'` to have the Modal fill the entire screen.
|
|
469
|
+
|
|
470
|
+
When using `inNavModal`, by default the content will be rendered inside a `ScrollView` to ensure it is scrollable, especially on smaller devices or smaller modals. You can disable this by setting `scrollable={false}` if, for example, you need to center your content or add some custom content.
|
|
471
|
+
|
|
466
472
|
Here's an example of how to implement this with custom close animations for Android:
|
|
467
473
|
|
|
468
474
|
```tsx
|
|
469
|
-
import { useNavigation } from 'react-navigation/native';
|
|
475
|
+
import { useNavigation } from '@react-navigation/native';
|
|
470
476
|
import { useCallback, useEffect, useRef } from 'react';
|
|
471
477
|
import { Platform, StyleSheet, View } from 'react-native';
|
|
472
478
|
|
|
@@ -12,6 +12,7 @@ interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
|
|
|
12
12
|
loadingHeading?: string;
|
|
13
13
|
description?: string;
|
|
14
14
|
fullscreen?: boolean;
|
|
15
|
+
stickyFooter?: boolean;
|
|
15
16
|
children?: ViewProps['children'];
|
|
16
17
|
onPressPrimaryButton?: () => void;
|
|
17
18
|
primaryButtonText?: string;
|
|
@@ -28,12 +29,13 @@ interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
|
|
|
28
29
|
type ModalProps =
|
|
29
30
|
| (ModalPropsBase & {
|
|
30
31
|
inNavModal?: false | undefined;
|
|
31
|
-
|
|
32
|
+
scrollable?: never;
|
|
32
33
|
background?: never;
|
|
33
34
|
})
|
|
34
35
|
| (ModalPropsBase & {
|
|
35
36
|
inNavModal: true;
|
|
36
|
-
|
|
37
|
+
fullscreen?: never;
|
|
38
|
+
scrollable?: boolean;
|
|
37
39
|
background?: 'default' | 'brand';
|
|
38
40
|
});
|
|
39
41
|
|
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
} from '@gorhom/bottom-sheet';
|
|
7
7
|
import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
|
|
8
8
|
import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
9
|
-
import { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
|
10
|
-
import { AccessibilityInfo, Platform, ScrollView, View, findNodeHandle } from 'react-native';
|
|
9
|
+
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
10
|
+
import { AccessibilityInfo, Dimensions, Platform, ScrollView, View, findNodeHandle } from 'react-native';
|
|
11
11
|
import Animated, {
|
|
12
12
|
Easing,
|
|
13
13
|
useAnimatedStyle,
|
|
@@ -51,6 +51,7 @@ const Modal = ({
|
|
|
51
51
|
inNavModal = false,
|
|
52
52
|
stickyFooter = true,
|
|
53
53
|
background = 'default',
|
|
54
|
+
scrollable = true,
|
|
54
55
|
...props
|
|
55
56
|
}: ModalProps) => {
|
|
56
57
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
|
@@ -61,6 +62,16 @@ const Modal = ({
|
|
|
61
62
|
const pretendContentTranslateY = useSharedValue(20);
|
|
62
63
|
const isBrandBackground = background === 'brand';
|
|
63
64
|
|
|
65
|
+
const [inNavModalHeight, setInNavModalHeight] = useState<number>();
|
|
66
|
+
|
|
67
|
+
const isNavModalFullScreen = useMemo(() => {
|
|
68
|
+
if (!inNavModalHeight || !inNavModal) return false;
|
|
69
|
+
|
|
70
|
+
const screenHeight = Dimensions.get('window').height;
|
|
71
|
+
|
|
72
|
+
return inNavModalHeight >= screenHeight;
|
|
73
|
+
}, [inNavModalHeight, inNavModal]);
|
|
74
|
+
|
|
64
75
|
const triggerCloseAnimation = useCallback(() => {
|
|
65
76
|
if (Platform.OS === 'android' && inNavModal) {
|
|
66
77
|
pretendContentTranslateY.value = withTiming(20, {
|
|
@@ -173,6 +184,9 @@ const Modal = ({
|
|
|
173
184
|
stickyFooter,
|
|
174
185
|
showHandle: props.showHandle,
|
|
175
186
|
background: isBrandBackground ? 'brand' : 'primary',
|
|
187
|
+
...(inNavModal && {
|
|
188
|
+
fullscreen: isNavModalFullScreen,
|
|
189
|
+
}),
|
|
176
190
|
});
|
|
177
191
|
|
|
178
192
|
const footer = (
|
|
@@ -200,6 +214,8 @@ const Modal = ({
|
|
|
200
214
|
</View>
|
|
201
215
|
);
|
|
202
216
|
|
|
217
|
+
const InNavModalContainer = scrollable ? ScrollView : View;
|
|
218
|
+
|
|
203
219
|
const content = (
|
|
204
220
|
<>
|
|
205
221
|
{loading ? (
|
|
@@ -275,8 +291,14 @@ const Modal = ({
|
|
|
275
291
|
</View>
|
|
276
292
|
</View>
|
|
277
293
|
) : null}
|
|
278
|
-
{inNavModal
|
|
279
|
-
|
|
294
|
+
{inNavModal && (
|
|
295
|
+
<InNavModalContainer style={{ flexGrow: stickyFooter ? 1 : 0 }}>
|
|
296
|
+
{children}
|
|
297
|
+
{!stickyFooter ? <View style={styles.inNavModalFooterContainer}>{footer}</View> : null}
|
|
298
|
+
</InNavModalContainer>
|
|
299
|
+
)}
|
|
300
|
+
{!inNavModal && children}
|
|
301
|
+
{((!stickyFooter && !inNavModal) || (inNavModal && stickyFooter)) && !noButtons ? footer : null}
|
|
280
302
|
</View>
|
|
281
303
|
)}
|
|
282
304
|
</>
|
|
@@ -300,6 +322,9 @@ const Modal = ({
|
|
|
300
322
|
|
|
301
323
|
return inNavModal ? (
|
|
302
324
|
<View
|
|
325
|
+
onLayout={(e) => {
|
|
326
|
+
setInNavModalHeight(e.nativeEvent.layout.height);
|
|
327
|
+
}}
|
|
303
328
|
style={{
|
|
304
329
|
flex: 1,
|
|
305
330
|
backgroundColor: theme.color.background[isBrandBackground ? 'brand' : 'primary'],
|
|
@@ -313,7 +338,9 @@ const Modal = ({
|
|
|
313
338
|
<Animated.View
|
|
314
339
|
style={[styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle]}
|
|
315
340
|
>
|
|
316
|
-
<View style={styles.inNavModalContent}>
|
|
341
|
+
<View style={styles.inNavModalContent}>
|
|
342
|
+
{content}
|
|
343
|
+
</View>
|
|
317
344
|
</Animated.View>
|
|
318
345
|
</View>
|
|
319
346
|
) : (
|
|
@@ -444,8 +471,6 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
444
471
|
borderTopLeftRadius: theme.components.modal.borderRadius,
|
|
445
472
|
borderTopRightRadius: theme.components.modal.borderRadius,
|
|
446
473
|
backgroundColor: theme.color.surface.neutral.strong,
|
|
447
|
-
gap: theme.components.modal.gap,
|
|
448
|
-
padding: theme.components.modal.padding,
|
|
449
474
|
paddingBottom: theme.components.modal.padding + rt.insets.bottom,
|
|
450
475
|
variants: {
|
|
451
476
|
background: {
|
|
@@ -454,8 +479,20 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
454
479
|
backgroundColor: theme.color.background.brand,
|
|
455
480
|
},
|
|
456
481
|
},
|
|
482
|
+
fullscreen: {
|
|
483
|
+
true: {
|
|
484
|
+
padding: theme.components.modal.padding,
|
|
485
|
+
paddingTop: rt.insets.top,
|
|
486
|
+
},
|
|
487
|
+
false: {
|
|
488
|
+
padding: theme.components.modal.padding,
|
|
489
|
+
}
|
|
490
|
+
}
|
|
457
491
|
},
|
|
458
492
|
},
|
|
493
|
+
inNavModalFooterContainer: {
|
|
494
|
+
paddingTop: theme.components.modal.padding,
|
|
495
|
+
},
|
|
459
496
|
androidContainer: {
|
|
460
497
|
height: rt.insets.top + 18,
|
|
461
498
|
paddingLeft: theme.components.modal.padding,
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import timezone from 'dayjs/plugin/timezone';
|
|
3
|
+
import utc from 'dayjs/plugin/utc';
|
|
4
|
+
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { AccessibilityInfo, findNodeHandle, Platform, View as RNView } from 'react-native';
|
|
6
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
7
|
+
import { BottomSheetModal, BottomSheetView } from '../BottomSheet';
|
|
8
|
+
import { Button } from '../Button';
|
|
9
|
+
import type { DateType, TimePickerProps } from './TimePicker.props';
|
|
10
|
+
import TimePickerView from './TimePickerView';
|
|
11
|
+
|
|
12
|
+
dayjs.extend(utc);
|
|
13
|
+
dayjs.extend(timezone);
|
|
14
|
+
|
|
15
|
+
type FooterProps = {
|
|
16
|
+
onCancel: () => void;
|
|
17
|
+
onConfirm: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const Footer = ({ onCancel, onConfirm }: FooterProps) => {
|
|
21
|
+
return (
|
|
22
|
+
<RNView style={styles.footer} testID="footer">
|
|
23
|
+
<Button variant="ghost" colorScheme="functional" onPress={onCancel}>
|
|
24
|
+
Cancel
|
|
25
|
+
</Button>
|
|
26
|
+
<Button variant="ghost" colorScheme="functional" onPress={onConfirm}>
|
|
27
|
+
Ok
|
|
28
|
+
</Button>
|
|
29
|
+
</RNView>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const TimePicker = ({
|
|
34
|
+
timeZone,
|
|
35
|
+
date,
|
|
36
|
+
onChange,
|
|
37
|
+
use12Hours,
|
|
38
|
+
minuteInterval,
|
|
39
|
+
hideFooter,
|
|
40
|
+
style,
|
|
41
|
+
ref,
|
|
42
|
+
onCancel = () => {},
|
|
43
|
+
}: TimePickerProps) => {
|
|
44
|
+
dayjs.tz.setDefault(timeZone);
|
|
45
|
+
dayjs.locale('en');
|
|
46
|
+
|
|
47
|
+
const modalRef = useRef<BottomSheetModal>(null);
|
|
48
|
+
const pickerViewRef = useRef<RNView>(null);
|
|
49
|
+
|
|
50
|
+
useImperativeHandle(ref, () => modalRef.current as BottomSheetModal);
|
|
51
|
+
|
|
52
|
+
const [currentDate, setCurrentDate] = useState<DateType>(() => {
|
|
53
|
+
return date ? dayjs.tz(date, timeZone) : dayjs().tz(timeZone);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const nextDate = date ? dayjs.tz(date, timeZone) : dayjs().tz(timeZone);
|
|
58
|
+
const isSameMinute = dayjs(currentDate).isSame(nextDate, 'minute');
|
|
59
|
+
|
|
60
|
+
if (!isSameMinute) {
|
|
61
|
+
setCurrentDate(nextDate);
|
|
62
|
+
}
|
|
63
|
+
}, [currentDate, date, timeZone]);
|
|
64
|
+
|
|
65
|
+
const closeTimePicker = useCallback(() => {
|
|
66
|
+
modalRef.current?.close();
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const handleSelectDate = useCallback(
|
|
70
|
+
(selectedDate: DateType) => {
|
|
71
|
+
const newDate = dayjs.tz(selectedDate ?? currentDate, timeZone);
|
|
72
|
+
if (!dayjs(currentDate).isSame(newDate, 'minute')) {
|
|
73
|
+
setCurrentDate(newDate);
|
|
74
|
+
}
|
|
75
|
+
onChange?.({ date: newDate ? dayjs(newDate).toDate() : newDate });
|
|
76
|
+
},
|
|
77
|
+
[currentDate, onChange, timeZone]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const handleCancel = useCallback(() => {
|
|
81
|
+
onCancel?.();
|
|
82
|
+
closeTimePicker();
|
|
83
|
+
}, [closeTimePicker, onCancel]);
|
|
84
|
+
|
|
85
|
+
const handleConfirm = useCallback(() => {
|
|
86
|
+
closeTimePicker();
|
|
87
|
+
}, [closeTimePicker]);
|
|
88
|
+
|
|
89
|
+
const handleChange = useCallback((index: number) => {
|
|
90
|
+
if (index > -1) {
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
AccessibilityInfo.announceForAccessibility('Time picker opened.');
|
|
93
|
+
|
|
94
|
+
const targetRef = pickerViewRef.current;
|
|
95
|
+
if (targetRef) {
|
|
96
|
+
const nodeHandle = findNodeHandle(targetRef);
|
|
97
|
+
if (nodeHandle) {
|
|
98
|
+
AccessibilityInfo.setAccessibilityFocus(nodeHandle);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, 50);
|
|
102
|
+
}
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const contentStyle = useMemo(() => [styles.container, style], [style]);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<BottomSheetModal
|
|
109
|
+
ref={modalRef}
|
|
110
|
+
onChange={handleChange}
|
|
111
|
+
accessible={false}
|
|
112
|
+
enableContentPanningGesture={false}
|
|
113
|
+
>
|
|
114
|
+
<BottomSheetView>
|
|
115
|
+
<RNView
|
|
116
|
+
ref={pickerViewRef}
|
|
117
|
+
accessible={Platform.OS === 'android' ? true : undefined}
|
|
118
|
+
accessibilityLabel={Platform.OS === 'android' ? 'Time picker' : undefined}
|
|
119
|
+
importantForAccessibility={Platform.OS === 'android' ? 'yes' : 'auto'}
|
|
120
|
+
style={contentStyle}
|
|
121
|
+
>
|
|
122
|
+
<TimePickerView
|
|
123
|
+
currentDate={currentDate}
|
|
124
|
+
onSelectDate={handleSelectDate}
|
|
125
|
+
timeZone={timeZone}
|
|
126
|
+
use12Hours={use12Hours}
|
|
127
|
+
minuteInterval={minuteInterval}
|
|
128
|
+
/>
|
|
129
|
+
{!hideFooter ? <Footer onCancel={handleCancel} onConfirm={handleConfirm} /> : null}
|
|
130
|
+
</RNView>
|
|
131
|
+
</BottomSheetView>
|
|
132
|
+
</BottomSheetModal>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
TimePicker.displayName = 'TimePicker';
|
|
137
|
+
|
|
138
|
+
const styles = StyleSheet.create(theme => ({
|
|
139
|
+
container: {
|
|
140
|
+
backgroundColor: theme.color.background.secondary,
|
|
141
|
+
gap: theme.components.datePicker.calendar.gap,
|
|
142
|
+
},
|
|
143
|
+
footer: {
|
|
144
|
+
flexDirection: 'row',
|
|
145
|
+
gap: theme.components.datePicker.calendar.footer.gap,
|
|
146
|
+
justifyContent: 'flex-end',
|
|
147
|
+
},
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
export default TimePicker;
|