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.
Files changed (81) hide show
  1. package/components/DateField/DateField.tsx +110 -0
  2. package/components/DateField/index.tsx +6 -0
  3. package/components/{DatePickerInput/inputUtils.ts → DateField/useDateFieldState.ts} +17 -49
  4. package/components/DatePicker/DateCalendar.tsx +83 -0
  5. package/components/DatePicker/DatePickerActions.tsx +73 -0
  6. package/components/DatePicker/DatePickerModal.tsx +234 -0
  7. package/components/DatePicker/DatePickerPopover.tsx +79 -0
  8. package/components/DatePicker/DatePickerProvider.tsx +152 -0
  9. package/components/DatePicker/DatePickerTrigger.tsx +23 -0
  10. package/components/DatePicker/context.tsx +82 -0
  11. package/components/DatePicker/index.tsx +44 -0
  12. package/components/DatePicker/utils.ts +292 -0
  13. package/components/DatePickerInline/DatePickerContext.tsx +1 -0
  14. package/components/DatePickerInline/DatePickerDockedHeader.tsx +113 -0
  15. package/components/DatePickerInline/DatePickerInline.tsx +16 -15
  16. package/components/DatePickerInline/DatePickerInlineBase.tsx +7 -1
  17. package/components/DatePickerInline/Day.tsx +25 -1
  18. package/components/DatePickerInline/DayRange.tsx +2 -4
  19. package/components/DatePickerInline/HeaderItem.tsx +42 -27
  20. package/components/DatePickerInline/Month.tsx +45 -65
  21. package/components/DatePickerInline/MonthPicker.tsx +25 -41
  22. package/components/DatePickerInline/Swiper.native.tsx +21 -4
  23. package/components/DatePickerInline/Swiper.tsx +168 -13
  24. package/components/DatePickerInline/Week.tsx +6 -1
  25. package/components/DatePickerInline/YearPicker.tsx +206 -53
  26. package/components/DatePickerInline/dateUtils.tsx +17 -12
  27. package/components/DatePickerInline/types.ts +3 -0
  28. package/components/DatePickerInline/utils.ts +66 -29
  29. package/components/ListItem/ListItem.tsx +3 -1
  30. package/components/ListItem/utils.ts +1 -1
  31. package/components/LoadingIndicator/index.tsx +1 -1
  32. package/components/Popover/Popover.native.tsx +4 -25
  33. package/components/Popover/Popover.tsx +4 -26
  34. package/components/Popover/utils.ts +41 -0
  35. package/components/Select/Select.tsx +7 -8
  36. package/components/Select/context.tsx +72 -0
  37. package/components/Select/index.ts +1 -0
  38. package/components/Select/utils.ts +0 -71
  39. package/components/Slot/compose-refs.tsx +2 -0
  40. package/components/TimeField/TimeField.tsx +75 -0
  41. package/components/TimeField/index.tsx +6 -0
  42. package/components/TimeField/useTimeFieldState.ts +70 -0
  43. package/components/{TimePickerField/sanitizeTime.ts → TimeField/utils.ts} +77 -10
  44. package/components/TimePicker/TimePicker.tsx +53 -9
  45. package/components/TimePicker/TimePickerModal.tsx +186 -0
  46. package/components/TimePicker/context.tsx +17 -0
  47. package/components/TimePicker/index.tsx +15 -3
  48. package/components/TimePicker/utils.ts +50 -0
  49. package/hooks/useActionState.tsx +19 -8
  50. package/package.json +6 -1
  51. package/components/DatePickerDocked/DatePickerDocked.tsx +0 -30
  52. package/components/DatePickerDocked/DatePickerDockedHeader.tsx +0 -129
  53. package/components/DatePickerDocked/index.tsx +0 -17
  54. package/components/DatePickerDocked/types.ts +0 -11
  55. package/components/DatePickerDocked/utils.ts +0 -157
  56. package/components/DatePickerInput/DatePickerInput.tsx +0 -130
  57. package/components/DatePickerInput/DatePickerInputModal.tsx +0 -48
  58. package/components/DatePickerInput/DatePickerInputWithoutModal.tsx +0 -73
  59. package/components/DatePickerInput/DateRangeInput.tsx +0 -88
  60. package/components/DatePickerInput/index.tsx +0 -11
  61. package/components/DatePickerInput/types.ts +0 -26
  62. package/components/DatePickerInput/utils.ts +0 -24
  63. package/components/DatePickerModal/AnimatedCrossView.tsx +0 -94
  64. package/components/DatePickerModal/CalendarEdit.tsx +0 -140
  65. package/components/DatePickerModal/DatePickerModal.tsx +0 -85
  66. package/components/DatePickerModal/DatePickerModalContent.tsx +0 -155
  67. package/components/DatePickerModal/DatePickerModalContentHeader.tsx +0 -213
  68. package/components/DatePickerModal/DatePickerModalHeader.tsx +0 -74
  69. package/components/DatePickerModal/DatePickerModalHeaderBackground.tsx +0 -13
  70. package/components/DatePickerModal/index.tsx +0 -16
  71. package/components/DatePickerModal/types.ts +0 -92
  72. package/components/DatePickerModal/utils.ts +0 -122
  73. package/components/DateTimePicker/DateTimePicker.tsx +0 -172
  74. package/components/DateTimePicker/index.tsx +0 -10
  75. package/components/DateTimePicker/utils.ts +0 -12
  76. package/components/TimePickerField/TimePickerField.tsx +0 -154
  77. package/components/TimePickerField/index.tsx +0 -10
  78. package/components/TimePickerField/utils.ts +0 -94
  79. package/components/TimePickerModal/TimePickerModal.tsx +0 -119
  80. package/components/TimePickerModal/index.tsx +0 -10
  81. 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, StyleSheet, View } from 'react-native';
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={styles.overlay} />
149
+ <Pressable onPress={handleOutsidePress} style={popoverStyles.overlay} />
149
150
 
150
151
  <View
151
152
  ref={popoverRef}
152
153
  onLayout={handlePopoverLayout}
153
- style={[styles.popoverContainer, style, popoverStyle]}
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, StyleSheet, UnistylesRuntime } from 'react-native-unistyles';
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={[styles.backdrop, backdropStyles]} onPress={onClose} />
160
+ <Pressable style={[popoverStyles.backdrop, backdropStyles]} onPress={onClose} />
160
161
  )}
161
162
  <View
162
163
  onLayout={handlePopoverLayout}
163
- style={[styles.popoverContainer, style, popoverStyle]}
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]);
@@ -3,5 +3,6 @@ import SelectDefault from './Select';
3
3
 
4
4
  export const Select = getRegisteredComponentWithFallback('Select', SelectDefault);
5
5
 
6
+ export * from './context';
6
7
  export type * from './types';
7
8
  export * from './utils';
@@ -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: {
@@ -45,6 +45,8 @@ function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
45
45
  }
46
46
  };
47
47
  }
48
+
49
+ return undefined;
48
50
  };
49
51
  }
50
52
 
@@ -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,6 @@
1
+ import { getRegisteredComponentWithFallback } from '../../core';
2
+ import TimeFieldDefault from './TimeField';
3
+
4
+ export const TimeField = getRegisteredComponentWithFallback('TimeField', TimeFieldDefault);
5
+
6
+ export type { TimeFieldProps } from './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
- export const sanitizeTime = (value: string, is24hour = false) => {
2
- // Remove all non-numeric and non-colon characters except am/pm
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 (!is24hour && hours >= 1 && hours <= 12) {
50
+ if (!is24Hour && hours >= 1 && hours <= 12) {
9
51
  return `${hours}:00am`;
10
- } else if (is24hour && hours >= 0 && hours < 24) {
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 (is24hour) {
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
+ };