@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
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import type { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
|
|
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, TextInputFocusEvent } from 'react-native';
|
|
7
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
8
|
+
import type { DateType } from '../DatePicker/DatePicker.props';
|
|
9
|
+
import { useFormFieldContext } from '../FormField';
|
|
10
|
+
import { Input, InputField, InputSlot } from '../Input';
|
|
11
|
+
import { TimePicker } from '../TimePicker';
|
|
12
|
+
import { UnstyledIconButton } from '../UnstyledIconButton';
|
|
13
|
+
import type TimePickerInputProps from './TimePickerInput.props';
|
|
14
|
+
import TimePickerInputDoneButton from './TimePickerInputDoneButton';
|
|
15
|
+
|
|
16
|
+
dayjs.extend(customParseFormat);
|
|
17
|
+
|
|
18
|
+
const DEFAULT_FORMAT_24 = 'HH:mm';
|
|
19
|
+
const DEFAULT_FORMAT_12 = 'hh:mm A';
|
|
20
|
+
|
|
21
|
+
const maskDefaultFormat = (value: string) => {
|
|
22
|
+
const digitsOnly = value.replace(/\D/g, '').slice(0, 4);
|
|
23
|
+
const hours = digitsOnly.slice(0, 2);
|
|
24
|
+
const minutes = digitsOnly.slice(2, 4);
|
|
25
|
+
|
|
26
|
+
return [hours, minutes].filter(Boolean).join(':');
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const TimePickerInput = ({
|
|
30
|
+
validationStatus = 'initial',
|
|
31
|
+
disabled,
|
|
32
|
+
focused,
|
|
33
|
+
readonly,
|
|
34
|
+
placeholder = '--:--',
|
|
35
|
+
inBottomSheet = false,
|
|
36
|
+
format,
|
|
37
|
+
openButtonLabel = 'Open time picker',
|
|
38
|
+
autoCloseOnSelect = true,
|
|
39
|
+
label,
|
|
40
|
+
labelVariant,
|
|
41
|
+
helperText,
|
|
42
|
+
helperIcon,
|
|
43
|
+
validText,
|
|
44
|
+
invalidText,
|
|
45
|
+
required = true,
|
|
46
|
+
onChange,
|
|
47
|
+
onChangeText,
|
|
48
|
+
onBlur,
|
|
49
|
+
onFocus,
|
|
50
|
+
value,
|
|
51
|
+
timePickerProps,
|
|
52
|
+
onClear,
|
|
53
|
+
...rest
|
|
54
|
+
}: TimePickerInputProps) => {
|
|
55
|
+
const formFieldContext = useFormFieldContext();
|
|
56
|
+
const isDisabled = formFieldContext?.disabled ?? disabled;
|
|
57
|
+
const isReadonly = formFieldContext?.readonly ?? readonly;
|
|
58
|
+
|
|
59
|
+
const pickerRef = useRef<BottomSheetModalMethods | null>(null);
|
|
60
|
+
const accessoryViewID = useMemo(() => {
|
|
61
|
+
if (Platform.OS !== 'ios') return undefined;
|
|
62
|
+
return `timepicker-input-${Math.random().toString(36).slice(2)}`;
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const use12Hours = timePickerProps?.use12Hours ?? false;
|
|
66
|
+
const resolvedFormat = useMemo(
|
|
67
|
+
() => format ?? (use12Hours ? DEFAULT_FORMAT_12 : DEFAULT_FORMAT_24),
|
|
68
|
+
[format, use12Hours]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const formatTime = useCallback(
|
|
72
|
+
(dateValue?: DateType) => {
|
|
73
|
+
if (!dateValue) return '';
|
|
74
|
+
const parsed = dayjs(dateValue);
|
|
75
|
+
return parsed.isValid() ? parsed.format(resolvedFormat) : '';
|
|
76
|
+
},
|
|
77
|
+
[resolvedFormat]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const isDefaultFormat = resolvedFormat === DEFAULT_FORMAT_24;
|
|
81
|
+
|
|
82
|
+
const [inputValue, setInputValue] = useState(() => formatTime(value));
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
setInputValue(formatTime(value));
|
|
86
|
+
}, [value, formatTime]);
|
|
87
|
+
|
|
88
|
+
const handleTextChange = useCallback(
|
|
89
|
+
(text: string) => {
|
|
90
|
+
const nextValue = isDefaultFormat ? maskDefaultFormat(text) : text;
|
|
91
|
+
|
|
92
|
+
setInputValue(nextValue);
|
|
93
|
+
onChangeText?.(nextValue);
|
|
94
|
+
|
|
95
|
+
if (!nextValue) {
|
|
96
|
+
onChange?.({ date: undefined });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const parsed = dayjs(nextValue, resolvedFormat, true);
|
|
101
|
+
|
|
102
|
+
if (parsed.isValid()) {
|
|
103
|
+
onChange?.({ date: parsed.toDate() });
|
|
104
|
+
} else {
|
|
105
|
+
onChange?.({ date: undefined });
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
[isDefaultFormat, onChange, onChangeText, resolvedFormat]
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const handleClear = useCallback(() => {
|
|
112
|
+
setInputValue('');
|
|
113
|
+
onChange?.({ date: undefined });
|
|
114
|
+
onClear?.();
|
|
115
|
+
}, [onChange, onClear]);
|
|
116
|
+
|
|
117
|
+
const closeKeyboard = useCallback(() => {
|
|
118
|
+
Keyboard.dismiss();
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
const openPicker = useCallback(() => {
|
|
122
|
+
if (isDisabled || isReadonly) return;
|
|
123
|
+
closeKeyboard();
|
|
124
|
+
pickerRef.current?.present();
|
|
125
|
+
}, [closeKeyboard, isDisabled, isReadonly]);
|
|
126
|
+
|
|
127
|
+
const selectedDate = useMemo(() => {
|
|
128
|
+
if (value) {
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const parsed = dayjs(inputValue, resolvedFormat, true);
|
|
133
|
+
return parsed.isValid() ? parsed.toDate() : undefined;
|
|
134
|
+
}, [value, inputValue, resolvedFormat]);
|
|
135
|
+
|
|
136
|
+
const handlePickerChange = useCallback(
|
|
137
|
+
({ date }: { date: DateType }) => {
|
|
138
|
+
if (!date) {
|
|
139
|
+
handleClear();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const formatted = formatTime(date);
|
|
144
|
+
setInputValue(formatted);
|
|
145
|
+
onChange?.({ date });
|
|
146
|
+
|
|
147
|
+
if (autoCloseOnSelect) {
|
|
148
|
+
pickerRef.current?.close?.();
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
[autoCloseOnSelect, formatTime, handleClear, onChange]
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const handleBlur = useCallback(
|
|
155
|
+
(event: TextInputFocusEvent) => {
|
|
156
|
+
onBlur?.(event);
|
|
157
|
+
},
|
|
158
|
+
[onBlur]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const handleFocus = useCallback(
|
|
162
|
+
(event: TextInputFocusEvent) => {
|
|
163
|
+
onFocus?.(event);
|
|
164
|
+
},
|
|
165
|
+
[onFocus]
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const { onCancel: pickerOnCancel, ...restTimePickerProps } = timePickerProps ?? {};
|
|
169
|
+
const {
|
|
170
|
+
style: inputStyle,
|
|
171
|
+
keyboardType: keyboardTypeProp,
|
|
172
|
+
inputMode: inputModeProp,
|
|
173
|
+
accessibilityHint: accessibilityHintProp,
|
|
174
|
+
accessibilityLabel: accessibilityLabelProp,
|
|
175
|
+
accessible: accessibleProp,
|
|
176
|
+
importantForAccessibility: importantForAccessibilityProp,
|
|
177
|
+
...textInputProps
|
|
178
|
+
} = rest;
|
|
179
|
+
|
|
180
|
+
const resolvedKeyboardType = keyboardTypeProp ?? (isDefaultFormat ? 'number-pad' : undefined);
|
|
181
|
+
const resolvedInputMode = inputModeProp ?? (isDefaultFormat ? 'numeric' : undefined);
|
|
182
|
+
const resolvedAccessibilityHint =
|
|
183
|
+
accessibilityHintProp ?? `Enter the time in ${resolvedFormat} format`;
|
|
184
|
+
const resolvedAccessibilityLabel = accessibilityLabelProp ?? 'Time input';
|
|
185
|
+
const resolvedAccessible = accessibleProp ?? true;
|
|
186
|
+
const resolvedImportantForAccessibility = importantForAccessibilityProp ?? 'yes';
|
|
187
|
+
|
|
188
|
+
const handleCancel = useCallback(() => {
|
|
189
|
+
pickerOnCancel?.();
|
|
190
|
+
pickerRef.current?.close?.();
|
|
191
|
+
}, [pickerOnCancel]);
|
|
192
|
+
|
|
193
|
+
const placeholderValue = placeholder;
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<>
|
|
197
|
+
<Input
|
|
198
|
+
validationStatus={validationStatus}
|
|
199
|
+
disabled={disabled}
|
|
200
|
+
readonly={readonly}
|
|
201
|
+
focused={focused}
|
|
202
|
+
label={label}
|
|
203
|
+
labelVariant={labelVariant}
|
|
204
|
+
helperText={helperText}
|
|
205
|
+
helperIcon={helperIcon}
|
|
206
|
+
validText={validText}
|
|
207
|
+
invalidText={invalidText}
|
|
208
|
+
required={required}
|
|
209
|
+
style={styles.wrap}
|
|
210
|
+
accessible={false}
|
|
211
|
+
>
|
|
212
|
+
<InputField
|
|
213
|
+
editable={!isReadonly && !isDisabled}
|
|
214
|
+
value={inputValue}
|
|
215
|
+
placeholder={placeholderValue}
|
|
216
|
+
onChangeText={handleTextChange}
|
|
217
|
+
onBlur={handleBlur}
|
|
218
|
+
onFocus={handleFocus}
|
|
219
|
+
inBottomSheet={inBottomSheet}
|
|
220
|
+
keyboardType={resolvedKeyboardType}
|
|
221
|
+
inputMode={resolvedInputMode}
|
|
222
|
+
accessibilityHint={resolvedAccessibilityHint}
|
|
223
|
+
aria-label="Time input"
|
|
224
|
+
accessibilityLabel={resolvedAccessibilityLabel}
|
|
225
|
+
accessible={resolvedAccessible}
|
|
226
|
+
accessibilityState={{
|
|
227
|
+
disabled: isDisabled || isReadonly,
|
|
228
|
+
}}
|
|
229
|
+
importantForAccessibility={resolvedImportantForAccessibility}
|
|
230
|
+
inputAccessoryViewID={Platform.OS === 'ios' ? accessoryViewID : undefined}
|
|
231
|
+
{...textInputProps}
|
|
232
|
+
style={[styles.input, inputStyle]}
|
|
233
|
+
/>
|
|
234
|
+
{!!inputValue && onClear && !isReadonly && !isDisabled && (
|
|
235
|
+
<InputSlot accessibilityElementsHidden={false}>
|
|
236
|
+
<UnstyledIconButton
|
|
237
|
+
accessibilityLabel="Clear time"
|
|
238
|
+
accessibilityHint="Removes the current time"
|
|
239
|
+
icon={CloseSmallIcon}
|
|
240
|
+
onPress={handleClear}
|
|
241
|
+
/>
|
|
242
|
+
</InputSlot>
|
|
243
|
+
)}
|
|
244
|
+
<InputSlot accessibilityElementsHidden={false}>
|
|
245
|
+
<UnstyledIconButton
|
|
246
|
+
accessibilityLabel={openButtonLabel}
|
|
247
|
+
accessibilityHint="Opens the time picker"
|
|
248
|
+
icon={TimeSmallIcon}
|
|
249
|
+
onPress={openPicker}
|
|
250
|
+
disabled={isDisabled || isReadonly}
|
|
251
|
+
/>
|
|
252
|
+
</InputSlot>
|
|
253
|
+
</Input>
|
|
254
|
+
<TimePicker
|
|
255
|
+
ref={pickerRef}
|
|
256
|
+
date={selectedDate}
|
|
257
|
+
onChange={handlePickerChange}
|
|
258
|
+
onCancel={handleCancel}
|
|
259
|
+
{...restTimePickerProps}
|
|
260
|
+
/>
|
|
261
|
+
{Platform.OS === 'ios' && accessoryViewID && (
|
|
262
|
+
<TimePickerInputDoneButton
|
|
263
|
+
accessoryViewID={accessoryViewID}
|
|
264
|
+
closeKeyboard={closeKeyboard}
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
</>
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
TimePickerInput.displayName = 'TimePickerInput';
|
|
272
|
+
|
|
273
|
+
const styles = StyleSheet.create(theme => ({
|
|
274
|
+
wrap: {
|
|
275
|
+
gap: theme.components.input.date.gap,
|
|
276
|
+
},
|
|
277
|
+
input: {
|
|
278
|
+
paddingLeft: 0,
|
|
279
|
+
paddingRight: 0,
|
|
280
|
+
},
|
|
281
|
+
}));
|
|
282
|
+
|
|
283
|
+
export default TimePickerInput;
|