react-native-molecules 0.5.0-beta.16 → 0.5.0-beta.17
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/components/DateField/DateField.tsx +110 -0
- package/components/DateField/index.tsx +6 -0
- package/components/{DatePickerInput/inputUtils.ts → DateField/useDateFieldState.ts} +17 -49
- package/components/DatePicker/DateCalendar.tsx +83 -0
- package/components/DatePicker/DatePickerActions.tsx +73 -0
- package/components/DatePicker/DatePickerModal.tsx +234 -0
- package/components/DatePicker/DatePickerPopover.tsx +79 -0
- package/components/DatePicker/DatePickerProvider.tsx +152 -0
- package/components/DatePicker/DatePickerTrigger.tsx +23 -0
- package/components/DatePicker/context.tsx +82 -0
- package/components/DatePicker/index.tsx +44 -0
- package/components/DatePicker/utils.ts +292 -0
- package/components/DatePickerInline/DatePickerContext.tsx +1 -0
- package/components/DatePickerInline/DatePickerDockedHeader.tsx +113 -0
- package/components/DatePickerInline/DatePickerInline.tsx +16 -15
- package/components/DatePickerInline/DatePickerInlineBase.tsx +7 -1
- package/components/DatePickerInline/Day.tsx +25 -1
- package/components/DatePickerInline/DayRange.tsx +2 -4
- package/components/DatePickerInline/HeaderItem.tsx +42 -27
- package/components/DatePickerInline/Month.tsx +45 -65
- package/components/DatePickerInline/MonthPicker.tsx +25 -41
- package/components/DatePickerInline/Swiper.native.tsx +21 -4
- package/components/DatePickerInline/Swiper.tsx +168 -13
- package/components/DatePickerInline/Week.tsx +6 -1
- package/components/DatePickerInline/YearPicker.tsx +206 -53
- package/components/DatePickerInline/dateUtils.tsx +17 -12
- package/components/DatePickerInline/types.ts +3 -0
- package/components/DatePickerInline/utils.ts +66 -29
- package/components/ListItem/ListItem.tsx +3 -1
- package/components/ListItem/utils.ts +1 -1
- package/components/LoadingIndicator/index.tsx +1 -1
- package/components/Popover/Popover.native.tsx +4 -25
- package/components/Popover/Popover.tsx +4 -26
- package/components/Popover/utils.ts +41 -0
- package/components/Select/Select.tsx +7 -8
- package/components/Select/context.tsx +72 -0
- package/components/Select/index.ts +1 -0
- package/components/Select/utils.ts +0 -71
- package/components/Slot/compose-refs.tsx +2 -0
- package/components/TimeField/TimeField.tsx +75 -0
- package/components/TimeField/index.tsx +6 -0
- package/components/TimeField/useTimeFieldState.ts +70 -0
- package/components/{TimePickerField/sanitizeTime.ts → TimeField/utils.ts} +77 -10
- package/components/TimePicker/TimePicker.tsx +53 -9
- package/components/TimePicker/TimePickerModal.tsx +186 -0
- package/components/TimePicker/context.tsx +17 -0
- package/components/TimePicker/index.tsx +15 -3
- package/components/TimePicker/utils.ts +50 -0
- package/hooks/useActionState.tsx +19 -8
- package/package.json +6 -1
- package/components/DatePickerDocked/DatePickerDocked.tsx +0 -30
- package/components/DatePickerDocked/DatePickerDockedHeader.tsx +0 -129
- package/components/DatePickerDocked/index.tsx +0 -17
- package/components/DatePickerDocked/types.ts +0 -11
- package/components/DatePickerDocked/utils.ts +0 -157
- package/components/DatePickerInput/DatePickerInput.tsx +0 -130
- package/components/DatePickerInput/DatePickerInputModal.tsx +0 -48
- package/components/DatePickerInput/DatePickerInputWithoutModal.tsx +0 -73
- package/components/DatePickerInput/DateRangeInput.tsx +0 -88
- package/components/DatePickerInput/index.tsx +0 -11
- package/components/DatePickerInput/types.ts +0 -26
- package/components/DatePickerInput/utils.ts +0 -24
- package/components/DatePickerModal/AnimatedCrossView.tsx +0 -94
- package/components/DatePickerModal/CalendarEdit.tsx +0 -140
- package/components/DatePickerModal/DatePickerModal.tsx +0 -85
- package/components/DatePickerModal/DatePickerModalContent.tsx +0 -155
- package/components/DatePickerModal/DatePickerModalContentHeader.tsx +0 -213
- package/components/DatePickerModal/DatePickerModalHeader.tsx +0 -74
- package/components/DatePickerModal/DatePickerModalHeaderBackground.tsx +0 -13
- package/components/DatePickerModal/index.tsx +0 -16
- package/components/DatePickerModal/types.ts +0 -92
- package/components/DatePickerModal/utils.ts +0 -122
- package/components/DateTimePicker/DateTimePicker.tsx +0 -172
- package/components/DateTimePicker/index.tsx +0 -10
- package/components/DateTimePicker/utils.ts +0 -12
- package/components/TimePickerField/TimePickerField.tsx +0 -154
- package/components/TimePickerField/index.tsx +0 -10
- package/components/TimePickerField/utils.ts +0 -94
- package/components/TimePickerModal/TimePickerModal.tsx +0 -119
- package/components/TimePickerModal/index.tsx +0 -10
- package/components/TimePickerModal/utils.ts +0 -47
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment, memo, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
|
2
|
-
import { AppState, Dimensions, Platform, Pressable,
|
|
2
|
+
import { AppState, Dimensions, Platform, Pressable, View } from 'react-native';
|
|
3
3
|
import { ScopedTheme, UnistylesRuntime } from 'react-native-unistyles';
|
|
4
4
|
|
|
5
5
|
import { Portal } from '../Portal';
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
useArrowStyles,
|
|
11
11
|
usePopover,
|
|
12
12
|
} from './common';
|
|
13
|
+
import { popoverStyles } from './utils';
|
|
13
14
|
|
|
14
15
|
const Popover = ({
|
|
15
16
|
triggerRef,
|
|
@@ -145,12 +146,12 @@ const Popover = ({
|
|
|
145
146
|
{...(inverted
|
|
146
147
|
? { name: UnistylesRuntime.themeName === 'dark' ? 'light' : 'dark' }
|
|
147
148
|
: ({} as { name: 'light' }))}>
|
|
148
|
-
<Pressable onPress={handleOutsidePress} style={
|
|
149
|
+
<Pressable onPress={handleOutsidePress} style={popoverStyles.overlay} />
|
|
149
150
|
|
|
150
151
|
<View
|
|
151
152
|
ref={popoverRef}
|
|
152
153
|
onLayout={handlePopoverLayout}
|
|
153
|
-
style={[
|
|
154
|
+
style={[popoverStyles.popoverContainer, style, popoverStyle]}
|
|
154
155
|
{...rest}>
|
|
155
156
|
{children}
|
|
156
157
|
{showArrow && popoverStyle.opacity === 1 && <View style={arrowStyles} />}
|
|
@@ -160,26 +161,4 @@ const Popover = ({
|
|
|
160
161
|
);
|
|
161
162
|
};
|
|
162
163
|
|
|
163
|
-
const styles = StyleSheet.create({
|
|
164
|
-
overlay: {
|
|
165
|
-
position: 'absolute',
|
|
166
|
-
top: 0,
|
|
167
|
-
bottom: 0,
|
|
168
|
-
left: 0,
|
|
169
|
-
right: 0,
|
|
170
|
-
backgroundColor: 'transparent',
|
|
171
|
-
},
|
|
172
|
-
popoverContainer: {
|
|
173
|
-
...popoverDefaultStyles,
|
|
174
|
-
backgroundColor: 'white',
|
|
175
|
-
borderRadius: 4,
|
|
176
|
-
shadowColor: 'rgba(0, 0, 0, 1)',
|
|
177
|
-
shadowOffset: { width: 0, height: 2 },
|
|
178
|
-
shadowOpacity: 0.3,
|
|
179
|
-
shadowRadius: 3.84,
|
|
180
|
-
elevation: 5,
|
|
181
|
-
zIndex: 100,
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
|
|
185
164
|
export default memo(Popover);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { Pressable, View } from 'react-native';
|
|
3
|
-
import { ScopedTheme,
|
|
3
|
+
import { ScopedTheme, UnistylesRuntime } from 'react-native-unistyles';
|
|
4
4
|
|
|
5
5
|
import { Portal } from '../Portal';
|
|
6
6
|
import {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
useArrowStyles,
|
|
11
11
|
usePopover,
|
|
12
12
|
} from './common';
|
|
13
|
+
import { popoverStyles } from './utils';
|
|
13
14
|
|
|
14
15
|
const Popover = ({
|
|
15
16
|
triggerRef,
|
|
@@ -156,11 +157,11 @@ const Popover = ({
|
|
|
156
157
|
<Portal>
|
|
157
158
|
<Wrapper {...(WrapperProps as any)}>
|
|
158
159
|
{withBackdropDismiss && (
|
|
159
|
-
<Pressable style={[
|
|
160
|
+
<Pressable style={[popoverStyles.backdrop, backdropStyles]} onPress={onClose} />
|
|
160
161
|
)}
|
|
161
162
|
<View
|
|
162
163
|
onLayout={handlePopoverLayout}
|
|
163
|
-
style={[
|
|
164
|
+
style={[popoverStyles.popoverContainer, style, popoverStyle]}
|
|
164
165
|
{...{ dataSet }}
|
|
165
166
|
{...rest}
|
|
166
167
|
ref={popoverRef}>
|
|
@@ -172,27 +173,4 @@ const Popover = ({
|
|
|
172
173
|
);
|
|
173
174
|
};
|
|
174
175
|
|
|
175
|
-
const styles = StyleSheet.create(theme => ({
|
|
176
|
-
popoverContainer: {
|
|
177
|
-
...popoverDefaultStyles,
|
|
178
|
-
backgroundColor: theme.colors.surface,
|
|
179
|
-
borderRadius: 4,
|
|
180
|
-
shadowColor: 'rgba(0, 0, 0, 1)',
|
|
181
|
-
shadowOffset: { width: 0, height: 2 },
|
|
182
|
-
shadowOpacity: theme.dark ? 0.7 : 0.3,
|
|
183
|
-
shadowRadius: 10,
|
|
184
|
-
zIndex: 100,
|
|
185
|
-
},
|
|
186
|
-
backdrop: {
|
|
187
|
-
position: 'absolute',
|
|
188
|
-
top: 0,
|
|
189
|
-
left: 0,
|
|
190
|
-
right: 0,
|
|
191
|
-
bottom: 0,
|
|
192
|
-
_web: {
|
|
193
|
-
cursor: 'default',
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
}));
|
|
197
|
-
|
|
198
176
|
export default memo(Popover);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
|
|
3
|
+
import { getRegisteredComponentStylesWithFallback } from '../../core';
|
|
4
|
+
import { popoverDefaultStyles } from './common';
|
|
5
|
+
|
|
6
|
+
const popoverStylesDefault = StyleSheet.create(theme => ({
|
|
7
|
+
popoverContainer: {
|
|
8
|
+
...popoverDefaultStyles,
|
|
9
|
+
backgroundColor: theme.colors.surface,
|
|
10
|
+
borderRadius: 4,
|
|
11
|
+
shadowColor: 'rgba(0, 0, 0, 1)',
|
|
12
|
+
shadowOffset: { width: 0, height: 2 },
|
|
13
|
+
shadowOpacity: theme.dark ? 0.7 : 0.3,
|
|
14
|
+
shadowRadius: 10,
|
|
15
|
+
elevation: 5,
|
|
16
|
+
zIndex: 100,
|
|
17
|
+
},
|
|
18
|
+
backdrop: {
|
|
19
|
+
position: 'absolute',
|
|
20
|
+
top: 0,
|
|
21
|
+
left: 0,
|
|
22
|
+
right: 0,
|
|
23
|
+
bottom: 0,
|
|
24
|
+
_web: {
|
|
25
|
+
cursor: 'default',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
overlay: {
|
|
29
|
+
position: 'absolute',
|
|
30
|
+
top: 0,
|
|
31
|
+
bottom: 0,
|
|
32
|
+
left: 0,
|
|
33
|
+
right: 0,
|
|
34
|
+
backgroundColor: 'transparent',
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
export const popoverStyles = getRegisteredComponentStylesWithFallback(
|
|
39
|
+
'Popover',
|
|
40
|
+
popoverStylesDefault,
|
|
41
|
+
);
|
|
@@ -19,6 +19,12 @@ import { IconButton } from '../IconButton';
|
|
|
19
19
|
import { Popover } from '../Popover';
|
|
20
20
|
import { Text } from '../Text';
|
|
21
21
|
import { TextInput, type TextInputHandles, type TextInputProps } from '../TextInput';
|
|
22
|
+
import {
|
|
23
|
+
SelectContextProvider,
|
|
24
|
+
SelectDropdownContextProvider,
|
|
25
|
+
useSelectContextValue,
|
|
26
|
+
useSelectDropdownContextValue,
|
|
27
|
+
} from './context';
|
|
22
28
|
import type {
|
|
23
29
|
DefaultItemT,
|
|
24
30
|
SelectContentProps,
|
|
@@ -31,14 +37,7 @@ import type {
|
|
|
31
37
|
SelectTriggerProps,
|
|
32
38
|
SelectValueProps,
|
|
33
39
|
} from './types';
|
|
34
|
-
import {
|
|
35
|
-
SelectContextProvider,
|
|
36
|
-
SelectDropdownContextProvider,
|
|
37
|
-
styles,
|
|
38
|
-
triggerStyles,
|
|
39
|
-
useSelectContextValue,
|
|
40
|
-
useSelectDropdownContextValue,
|
|
41
|
-
} from './utils';
|
|
40
|
+
import { styles, triggerStyles } from './utils';
|
|
42
41
|
|
|
43
42
|
const emptyArr: unknown[] = [];
|
|
44
43
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { View } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { createFastContext } from '../../fast-context';
|
|
4
|
+
import { registerPortalContext } from '../Portal';
|
|
5
|
+
import type { DefaultItemT, SelectContextValue, SelectDropdownContextValue } from './types';
|
|
6
|
+
|
|
7
|
+
// SelectContext - holds value, onAdd, onRemove with fast-context for optimized rendering
|
|
8
|
+
const selectContextDefaultValue: SelectContextValue<DefaultItemT> = {
|
|
9
|
+
value: null,
|
|
10
|
+
multiple: false,
|
|
11
|
+
onAdd: () => {},
|
|
12
|
+
onRemove: () => {},
|
|
13
|
+
disabled: false,
|
|
14
|
+
error: false,
|
|
15
|
+
labelKey: 'label',
|
|
16
|
+
options: [],
|
|
17
|
+
searchQuery: '',
|
|
18
|
+
setSearchQuery: () => {},
|
|
19
|
+
filteredOptions: [],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
useStoreRef: useSelectStoreRef,
|
|
24
|
+
Provider: SelectContextProvider,
|
|
25
|
+
useContext: useSelectContext,
|
|
26
|
+
useContextValue: useSelectContextValue,
|
|
27
|
+
Context: SelectContext,
|
|
28
|
+
} = createFastContext<SelectContextValue<DefaultItemT>>(selectContextDefaultValue, true);
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
SelectContext,
|
|
32
|
+
SelectContextProvider,
|
|
33
|
+
useSelectContext,
|
|
34
|
+
useSelectContextValue,
|
|
35
|
+
useSelectStoreRef,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// SelectDropdownContext - holds isOpen, onClose, triggerRef with fast-context
|
|
39
|
+
export type SelectDropdownContextType = SelectDropdownContextValue & {
|
|
40
|
+
triggerRef: React.RefObject<View> | null;
|
|
41
|
+
contentRef: React.RefObject<any> | null;
|
|
42
|
+
triggerLayout: { width: number; height: number } | null;
|
|
43
|
+
setTriggerLayout: (layout: { width: number; height: number }) => void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const selectDropdownContextDefaultValue: SelectDropdownContextType = {
|
|
47
|
+
isOpen: false,
|
|
48
|
+
onClose: () => {},
|
|
49
|
+
onOpen: () => {},
|
|
50
|
+
triggerRef: null,
|
|
51
|
+
contentRef: null,
|
|
52
|
+
triggerLayout: null,
|
|
53
|
+
setTriggerLayout: () => {},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const {
|
|
57
|
+
useStoreRef: useSelectDropdownStoreRef,
|
|
58
|
+
Provider: SelectDropdownContextProvider,
|
|
59
|
+
useContext: useSelectDropdownContext,
|
|
60
|
+
useContextValue: useSelectDropdownContextValue,
|
|
61
|
+
Context: SelectDropdownContext,
|
|
62
|
+
} = createFastContext<SelectDropdownContextType>(selectDropdownContextDefaultValue, true);
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
SelectDropdownContext,
|
|
66
|
+
SelectDropdownContextProvider,
|
|
67
|
+
useSelectDropdownContext,
|
|
68
|
+
useSelectDropdownContextValue,
|
|
69
|
+
useSelectDropdownStoreRef,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
registerPortalContext([SelectContext, SelectDropdownContext]);
|
|
@@ -1,77 +1,6 @@
|
|
|
1
|
-
import type { View } from 'react-native';
|
|
2
1
|
import { StyleSheet } from 'react-native-unistyles';
|
|
3
2
|
|
|
4
3
|
import { getRegisteredComponentStylesWithFallback } from '../../core';
|
|
5
|
-
import { createFastContext } from '../../fast-context';
|
|
6
|
-
import { registerPortalContext } from '../Portal';
|
|
7
|
-
import type { DefaultItemT, SelectContextValue, SelectDropdownContextValue } from './types';
|
|
8
|
-
|
|
9
|
-
// SelectContext - holds value, onAdd, onRemove with fast-context for optimized rendering
|
|
10
|
-
const selectContextDefaultValue: SelectContextValue<DefaultItemT> = {
|
|
11
|
-
value: null,
|
|
12
|
-
multiple: false,
|
|
13
|
-
onAdd: () => {},
|
|
14
|
-
onRemove: () => {},
|
|
15
|
-
disabled: false,
|
|
16
|
-
error: false,
|
|
17
|
-
labelKey: 'label',
|
|
18
|
-
options: [],
|
|
19
|
-
searchQuery: '',
|
|
20
|
-
setSearchQuery: () => {},
|
|
21
|
-
filteredOptions: [],
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const {
|
|
25
|
-
useStoreRef: useSelectStoreRef,
|
|
26
|
-
Provider: SelectContextProvider,
|
|
27
|
-
useContext: useSelectContext,
|
|
28
|
-
useContextValue: useSelectContextValue,
|
|
29
|
-
Context: SelectContext,
|
|
30
|
-
} = createFastContext<SelectContextValue<DefaultItemT>>(selectContextDefaultValue, true);
|
|
31
|
-
|
|
32
|
-
export {
|
|
33
|
-
SelectContext,
|
|
34
|
-
SelectContextProvider,
|
|
35
|
-
useSelectContext,
|
|
36
|
-
useSelectContextValue,
|
|
37
|
-
useSelectStoreRef,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// SelectDropdownContext - holds isOpen, onClose, triggerRef with fast-context
|
|
41
|
-
export type SelectDropdownContextType = SelectDropdownContextValue & {
|
|
42
|
-
triggerRef: React.RefObject<View> | null;
|
|
43
|
-
contentRef: React.RefObject<any> | null;
|
|
44
|
-
triggerLayout: { width: number; height: number } | null;
|
|
45
|
-
setTriggerLayout: (layout: { width: number; height: number }) => void;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const selectDropdownContextDefaultValue: SelectDropdownContextType = {
|
|
49
|
-
isOpen: false,
|
|
50
|
-
onClose: () => {},
|
|
51
|
-
onOpen: () => {},
|
|
52
|
-
triggerRef: null,
|
|
53
|
-
contentRef: null,
|
|
54
|
-
triggerLayout: null,
|
|
55
|
-
setTriggerLayout: () => {},
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const {
|
|
59
|
-
useStoreRef: useSelectDropdownStoreRef,
|
|
60
|
-
Provider: SelectDropdownContextProvider,
|
|
61
|
-
useContext: useSelectDropdownContext,
|
|
62
|
-
useContextValue: useSelectDropdownContextValue,
|
|
63
|
-
Context: SelectDropdownContext,
|
|
64
|
-
} = createFastContext<SelectDropdownContextType>(selectDropdownContextDefaultValue, true);
|
|
65
|
-
|
|
66
|
-
export {
|
|
67
|
-
SelectDropdownContext,
|
|
68
|
-
SelectDropdownContextProvider,
|
|
69
|
-
useSelectDropdownContext,
|
|
70
|
-
useSelectDropdownContextValue,
|
|
71
|
-
useSelectDropdownStoreRef,
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
registerPortalContext([SelectContext, SelectDropdownContext]);
|
|
75
4
|
|
|
76
5
|
const triggerDefaultStyles = StyleSheet.create(theme => ({
|
|
77
6
|
trigger: {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { memo, useCallback, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { DatePickerValue, RangeValue } from '../DatePicker/context';
|
|
4
|
+
import { useDatePickerContext } from '../DatePicker/context';
|
|
5
|
+
import { TextInput } from '../TextInput';
|
|
6
|
+
import type { Props as TextInputProps } from '../TextInput/TextInput';
|
|
7
|
+
import { useTimeFieldState } from './useTimeFieldState';
|
|
8
|
+
import { timeFormat } from './utils';
|
|
9
|
+
|
|
10
|
+
export type TimeFieldProps = Omit<TextInputProps, 'value' | 'defaultValue' | 'onChangeText'>;
|
|
11
|
+
|
|
12
|
+
const isRange = (value: DatePickerValue): value is RangeValue =>
|
|
13
|
+
value !== null && typeof value === 'object' && 'start' in value && 'end' in value;
|
|
14
|
+
|
|
15
|
+
const toTimeString = (value: Date | null): string => {
|
|
16
|
+
if (!value) return '';
|
|
17
|
+
const h = value.getHours().toString().padStart(2, '0');
|
|
18
|
+
const m = value.getMinutes().toString().padStart(2, '0');
|
|
19
|
+
return `${h}:${m}`;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const applyTimeToDate = (base: Date | null, time: string): Date | null => {
|
|
23
|
+
if (!time) return null;
|
|
24
|
+
const [h, m] = time.split(':').map(n => parseInt(n, 10));
|
|
25
|
+
if (Number.isNaN(h) || Number.isNaN(m)) return base;
|
|
26
|
+
const next = base ? new Date(base) : new Date();
|
|
27
|
+
next.setHours(h, m, 0, 0);
|
|
28
|
+
return next;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function TimeField({ ref, disabled: disabledProp, onBlur, onFocus, ...rest }: TimeFieldProps) {
|
|
32
|
+
const { value, commitValue, is24Hour, disabled: providerDisabled } = useDatePickerContext();
|
|
33
|
+
|
|
34
|
+
const disabled = disabledProp ?? providerDisabled;
|
|
35
|
+
|
|
36
|
+
const dateValue = useMemo<Date | null>(() => (isRange(value) ? null : value), [value]);
|
|
37
|
+
|
|
38
|
+
const timeString = useMemo(() => toTimeString(dateValue), [dateValue]);
|
|
39
|
+
|
|
40
|
+
const onChange = useCallback(
|
|
41
|
+
(next: string) => {
|
|
42
|
+
commitValue(applyTimeToDate(dateValue, next));
|
|
43
|
+
},
|
|
44
|
+
[dateValue, commitValue],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
timeString: formatted,
|
|
49
|
+
onChangeText,
|
|
50
|
+
onBlur: onInnerBlur,
|
|
51
|
+
onFocus: onInnerFocus,
|
|
52
|
+
} = useTimeFieldState({
|
|
53
|
+
time: timeString,
|
|
54
|
+
is24Hour,
|
|
55
|
+
disabled,
|
|
56
|
+
onChange,
|
|
57
|
+
onBlur,
|
|
58
|
+
onFocus,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<TextInput
|
|
63
|
+
{...rest}
|
|
64
|
+
ref={ref}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
value={formatted}
|
|
67
|
+
placeholder={timeFormat[is24Hour ? '24' : '12'].format}
|
|
68
|
+
onChangeText={onChangeText}
|
|
69
|
+
onBlur={onInnerBlur}
|
|
70
|
+
onFocus={onInnerFocus}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default memo(TimeField);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { BlurEvent, FocusEvent } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { getFormattedTime, getOutputTime, sanitizeTime } from './utils';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
time: string;
|
|
8
|
+
is24Hour: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
onChange?: (time: string) => void;
|
|
11
|
+
onBlur?: (e: BlurEvent) => void;
|
|
12
|
+
onFocus?: (e: FocusEvent) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function useTimeFieldState({
|
|
16
|
+
time,
|
|
17
|
+
is24Hour,
|
|
18
|
+
disabled,
|
|
19
|
+
onChange,
|
|
20
|
+
onBlur: onBlurProp,
|
|
21
|
+
onFocus: onFocusProp,
|
|
22
|
+
}: Props) {
|
|
23
|
+
const [timeString, setTimeString] = useState(() => getFormattedTime({ time, is24Hour }));
|
|
24
|
+
const isBlurredRef = useRef(true);
|
|
25
|
+
|
|
26
|
+
const onChangeText = useCallback(
|
|
27
|
+
(_text: string) => {
|
|
28
|
+
const text = sanitizeTime(_text, is24Hour);
|
|
29
|
+
setTimeString(_text);
|
|
30
|
+
|
|
31
|
+
if (disabled || !text) return;
|
|
32
|
+
|
|
33
|
+
onChange?.(getOutputTime({ time: text || time, is24Hour }));
|
|
34
|
+
},
|
|
35
|
+
[disabled, is24Hour, onChange, time],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const onBlur = useCallback(
|
|
39
|
+
(e: BlurEvent) => {
|
|
40
|
+
isBlurredRef.current = true;
|
|
41
|
+
onBlurProp?.(e);
|
|
42
|
+
|
|
43
|
+
if (disabled) return;
|
|
44
|
+
|
|
45
|
+
setTimeString(sanitizeTime(getFormattedTime({ time, is24Hour }), is24Hour));
|
|
46
|
+
},
|
|
47
|
+
[disabled, is24Hour, onBlurProp, time],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const onFocus = useCallback(
|
|
51
|
+
(e: FocusEvent) => {
|
|
52
|
+
isBlurredRef.current = false;
|
|
53
|
+
onFocusProp?.(e);
|
|
54
|
+
},
|
|
55
|
+
[onFocusProp],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!isBlurredRef.current) return;
|
|
60
|
+
|
|
61
|
+
setTimeString(getFormattedTime({ time, is24Hour }));
|
|
62
|
+
}, [is24Hour, time]);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
timeString,
|
|
66
|
+
onChangeText,
|
|
67
|
+
onBlur,
|
|
68
|
+
onFocus,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -1,19 +1,61 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { format, parse, set } from 'date-fns';
|
|
2
|
+
|
|
3
|
+
export const timeMask24Hour = (text: string = '') => {
|
|
4
|
+
const cleanTime = text.replace(/\D+/g, '');
|
|
5
|
+
|
|
6
|
+
const hourFirstDigit = /[012]/;
|
|
7
|
+
let hourSecondDigit = /\d/;
|
|
8
|
+
|
|
9
|
+
if (cleanTime.charAt(0) === '2') {
|
|
10
|
+
hourSecondDigit = /[0123]/;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return [hourFirstDigit, hourSecondDigit, ':', /[012345]/, /\d/];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const timeMask12Hour = (text: string = '') => {
|
|
17
|
+
const cleanTime = text.replace(/\D+/g, '');
|
|
18
|
+
|
|
19
|
+
const hourFirstDigit = /[01]/;
|
|
20
|
+
let hourSecondDigit = /\d/;
|
|
21
|
+
|
|
22
|
+
if (cleanTime.charAt(0) === '1') {
|
|
23
|
+
hourSecondDigit = /[012]/;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return [hourFirstDigit, hourSecondDigit, ':', /[012345]/, /\d/, /[ap]/, 'm'];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const timeFormat = {
|
|
30
|
+
'24': {
|
|
31
|
+
format: 'HH:mm',
|
|
32
|
+
mask: timeMask24Hour,
|
|
33
|
+
},
|
|
34
|
+
'12': {
|
|
35
|
+
format: 'hh:mmaaa',
|
|
36
|
+
mask: timeMask12Hour,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const referenceDate = new Date('2022-01-01T00:00:00.000Z');
|
|
41
|
+
|
|
42
|
+
export const sanitizeTimeString = (time: string): string => time.replace(/[^\d:]/g, '');
|
|
43
|
+
|
|
44
|
+
export const sanitizeTime = (value: string, is24Hour = false) => {
|
|
3
45
|
const sanitizedValue = value.replace(/[^0-9:apm]/gi, '');
|
|
4
46
|
|
|
5
47
|
const singleDigitHour = sanitizedValue.match(/^(\d{1,2})$/);
|
|
6
48
|
if (singleDigitHour) {
|
|
7
49
|
const hours = parseInt(singleDigitHour[1], 10);
|
|
8
|
-
if (!
|
|
50
|
+
if (!is24Hour && hours >= 1 && hours <= 12) {
|
|
9
51
|
return `${hours}:00am`;
|
|
10
|
-
}
|
|
52
|
+
}
|
|
53
|
+
if (is24Hour && hours >= 0 && hours < 24) {
|
|
11
54
|
return `${hours.toString().padStart(2, '0')}:00`;
|
|
12
55
|
}
|
|
13
56
|
}
|
|
14
57
|
|
|
15
|
-
if (
|
|
16
|
-
// Check if it's a valid 24-hour time format (HH:MM)
|
|
58
|
+
if (is24Hour) {
|
|
17
59
|
const match24Hour = sanitizedValue.match(/^(\d{1,2}):?(\d{2})$/);
|
|
18
60
|
|
|
19
61
|
if (match24Hour) {
|
|
@@ -27,7 +69,6 @@ export const sanitizeTime = (value: string, is24hour = false) => {
|
|
|
27
69
|
}
|
|
28
70
|
}
|
|
29
71
|
|
|
30
|
-
// Convert 12-hour time to 24-hour time format if necessary
|
|
31
72
|
const match12Hour = sanitizedValue.match(/^(\d{1,2}):?(\d{2})\s*([ap]m)$/i);
|
|
32
73
|
if (match12Hour) {
|
|
33
74
|
let hours = parseInt(match12Hour[1], 10);
|
|
@@ -40,15 +81,16 @@ export const sanitizeTime = (value: string, is24hour = false) => {
|
|
|
40
81
|
} else if (period === 'am' && hours === 12) {
|
|
41
82
|
hours = 0;
|
|
42
83
|
}
|
|
84
|
+
|
|
43
85
|
return `${hours.toString().padStart(2, '0')}:${minutes
|
|
44
86
|
.toString()
|
|
45
87
|
.padStart(2, '0')}`;
|
|
46
88
|
}
|
|
47
89
|
}
|
|
90
|
+
|
|
48
91
|
return '';
|
|
49
92
|
}
|
|
50
93
|
|
|
51
|
-
// Convert 24-hour time to 12-hour time format if necessary
|
|
52
94
|
const match24Hour = sanitizedValue.match(/^(\d{1,2}):?(\d{2})$/);
|
|
53
95
|
if (match24Hour) {
|
|
54
96
|
let hours = parseInt(match24Hour[1], 10);
|
|
@@ -68,7 +110,6 @@ export const sanitizeTime = (value: string, is24hour = false) => {
|
|
|
68
110
|
}
|
|
69
111
|
}
|
|
70
112
|
|
|
71
|
-
// Check if it's a valid 12-hour time format (HH:MM am/pm)
|
|
72
113
|
const match12Hour = sanitizedValue.match(/^(\d{1,2}):?(\d{2})\s*([ap]m)$/i);
|
|
73
114
|
if (match12Hour) {
|
|
74
115
|
const hours = parseInt(match12Hour[1], 10);
|
|
@@ -80,6 +121,32 @@ export const sanitizeTime = (value: string, is24hour = false) => {
|
|
|
80
121
|
}
|
|
81
122
|
}
|
|
82
123
|
|
|
83
|
-
// If no match, return empty string as invalid input
|
|
84
124
|
return '';
|
|
85
125
|
};
|
|
126
|
+
|
|
127
|
+
export const getFormattedTime = ({ time, is24Hour }: { time: string; is24Hour: boolean }) => {
|
|
128
|
+
if (!time) return '';
|
|
129
|
+
|
|
130
|
+
const [hour = '0', minute = '0'] = sanitizeTimeString(time).split(':');
|
|
131
|
+
|
|
132
|
+
return format(
|
|
133
|
+
set(referenceDate, { hours: +hour.padStart(2, '0'), minutes: +minute.padStart(2, '0') }),
|
|
134
|
+
timeFormat[is24Hour ? '24' : '12'].format,
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const getOutputTime = ({ time, is24Hour }: { time: string; is24Hour: boolean }) => {
|
|
139
|
+
if (!time) return '';
|
|
140
|
+
|
|
141
|
+
const formattedTime = sanitizeTimeString(getFormattedTime({ time, is24Hour }));
|
|
142
|
+
const isPM = time.replace(/[\d:]/g, '').includes('p');
|
|
143
|
+
|
|
144
|
+
return format(
|
|
145
|
+
parse(
|
|
146
|
+
formattedTime + (is24Hour ? '' : isPM ? 'pm' : 'am'),
|
|
147
|
+
timeFormat[is24Hour ? '24' : '12'].format,
|
|
148
|
+
referenceDate,
|
|
149
|
+
),
|
|
150
|
+
'HH:mm',
|
|
151
|
+
);
|
|
152
|
+
};
|