react-native-molecules 0.5.0-beta.3 → 0.5.0-beta.30

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 (227) hide show
  1. package/components/Accordion/Accordion.tsx +2 -6
  2. package/components/Accordion/AccordionItem.tsx +16 -12
  3. package/components/Accordion/AccordionItemContent.tsx +6 -1
  4. package/components/Accordion/AccordionItemHeader.tsx +1 -1
  5. package/components/Accordion/utils.ts +6 -0
  6. package/components/ActivityIndicator/ActivityIndicator.tsx +6 -15
  7. package/components/Appbar/AppbarBase.tsx +18 -13
  8. package/components/Button/Button.tsx +211 -264
  9. package/components/Button/index.tsx +9 -3
  10. package/components/Button/types.ts +16 -2
  11. package/components/Button/utils.ts +230 -208
  12. package/components/Card/Card.tsx +1 -1
  13. package/components/Checkbox/Checkbox.tsx +125 -88
  14. package/components/Checkbox/CheckboxBase.ios.tsx +14 -23
  15. package/components/Checkbox/CheckboxBase.tsx +21 -137
  16. package/components/Checkbox/context.tsx +14 -0
  17. package/components/Checkbox/index.tsx +11 -4
  18. package/components/Checkbox/types.ts +63 -29
  19. package/components/Checkbox/utils.ts +25 -108
  20. package/components/Chip/Chip.tsx +41 -52
  21. package/components/Chip/utils.ts +3 -7
  22. package/components/DateField/DateField.tsx +111 -0
  23. package/components/DateField/index.tsx +6 -0
  24. package/components/{DatePickerInput/inputUtils.ts → DateField/useDateFieldState.ts} +19 -51
  25. package/components/DatePicker/DateCalendar.tsx +83 -0
  26. package/components/DatePicker/DatePickerActions.tsx +73 -0
  27. package/components/DatePicker/DatePickerModal.tsx +246 -0
  28. package/components/DatePicker/DatePickerPopover.tsx +79 -0
  29. package/components/DatePicker/DatePickerProvider.tsx +158 -0
  30. package/components/DatePicker/DatePickerTrigger.tsx +23 -0
  31. package/components/DatePicker/context.tsx +83 -0
  32. package/components/DatePicker/index.tsx +45 -0
  33. package/components/DatePicker/utils.ts +295 -0
  34. package/components/DatePickerInline/DatePickerDockedHeader.tsx +117 -0
  35. package/components/DatePickerInline/DatePickerInline.tsx +17 -16
  36. package/components/DatePickerInline/DatePickerInlineBase.tsx +11 -5
  37. package/components/DatePickerInline/DatePickerInlineHeader.tsx +50 -20
  38. package/components/DatePickerInline/Day.tsx +25 -1
  39. package/components/DatePickerInline/DayNames.tsx +13 -10
  40. package/components/DatePickerInline/DayRange.tsx +2 -4
  41. package/components/DatePickerInline/HeaderItem.tsx +44 -29
  42. package/components/DatePickerInline/Month.tsx +48 -67
  43. package/components/DatePickerInline/MonthPicker.tsx +80 -92
  44. package/components/DatePickerInline/Swiper.native.tsx +21 -4
  45. package/components/DatePickerInline/Swiper.tsx +169 -14
  46. package/components/DatePickerInline/SwiperUtils.ts +1 -1
  47. package/components/DatePickerInline/Week.tsx +6 -1
  48. package/components/DatePickerInline/YearPicker.tsx +220 -78
  49. package/components/DatePickerInline/dateUtils.tsx +18 -13
  50. package/components/DatePickerInline/store.tsx +27 -0
  51. package/components/DatePickerInline/types.ts +6 -2
  52. package/components/DatePickerInline/utils.ts +66 -29
  53. package/components/Divider/Divider.tsx +192 -0
  54. package/components/Divider/index.tsx +10 -0
  55. package/components/Drawer/Drawer.tsx +17 -6
  56. package/components/Drawer/DrawerItemGroup.tsx +3 -7
  57. package/components/ElementGroup/ElementGroup.tsx +1 -1
  58. package/components/FilePicker/FilePicker.tsx +48 -78
  59. package/components/FilePicker/index.tsx +2 -1
  60. package/components/FilePicker/utils.ts +9 -0
  61. package/components/HelperText/HelperText.tsx +0 -35
  62. package/components/Icon/iconFactory.tsx +5 -4
  63. package/components/Icon/index.tsx +1 -1
  64. package/components/Icon/types.ts +17 -6
  65. package/components/IconButton/IconButton.tsx +84 -84
  66. package/components/IconButton/index.tsx +1 -0
  67. package/components/IconButton/types.ts +10 -0
  68. package/components/IconButton/utils.ts +167 -33
  69. package/components/List/List.tsx +276 -0
  70. package/components/List/context.tsx +27 -0
  71. package/components/List/index.ts +8 -0
  72. package/components/List/types.ts +117 -0
  73. package/components/List/utils.ts +79 -0
  74. package/components/LoadingIndicator/LoadingIndicator.tsx +253 -0
  75. package/components/LoadingIndicator/LoadingIndicator.web.tsx +136 -0
  76. package/components/LoadingIndicator/index.tsx +13 -0
  77. package/components/LoadingIndicator/utils.ts +117 -0
  78. package/components/Menu/Menu.tsx +162 -39
  79. package/components/Menu/index.tsx +10 -7
  80. package/components/Menu/utils.ts +21 -70
  81. package/components/NavigationRail/NavigationRail.tsx +15 -9
  82. package/components/Popover/Popover.tsx +119 -145
  83. package/components/Popover/PopoverRoot.tsx +60 -0
  84. package/components/Popover/common.ts +54 -34
  85. package/components/Popover/index.ts +12 -1
  86. package/components/Popover/usePlatformMeasure.native.ts +90 -0
  87. package/components/Popover/usePlatformMeasure.ts +120 -0
  88. package/components/Popover/utils.ts +34 -0
  89. package/components/Portal/Portal.tsx +1 -2
  90. package/components/Radio/Radio.tsx +188 -0
  91. package/components/Radio/RadioBase.ios.tsx +69 -0
  92. package/components/Radio/RadioBase.tsx +136 -0
  93. package/components/Radio/context.tsx +23 -0
  94. package/components/Radio/index.tsx +20 -0
  95. package/components/Radio/types.ts +101 -0
  96. package/components/Radio/utils.ts +115 -0
  97. package/components/Rating/Rating.tsx +1 -1
  98. package/components/Select/Select.tsx +521 -785
  99. package/components/Select/context.tsx +81 -0
  100. package/components/Select/index.ts +26 -14
  101. package/components/Select/types.ts +65 -58
  102. package/components/Select/utils.ts +126 -0
  103. package/components/Slot/Slot.tsx +244 -0
  104. package/components/Slot/compose-refs.tsx +62 -0
  105. package/components/Slot/index.tsx +8 -0
  106. package/components/Surface/Surface.android.tsx +32 -7
  107. package/components/Surface/Surface.ios.tsx +34 -29
  108. package/components/Surface/Surface.tsx +31 -4
  109. package/components/Surface/utils.ts +44 -6
  110. package/components/Switch/Switch.ios.tsx +1 -1
  111. package/components/Switch/Switch.tsx +10 -3
  112. package/components/Tabs/TabItem.tsx +35 -58
  113. package/components/Tabs/TabLabel.tsx +5 -9
  114. package/components/Tabs/Tabs.tsx +156 -150
  115. package/components/Tabs/utils.ts +15 -2
  116. package/components/Text/textFactory.tsx +17 -5
  117. package/components/TextInput/TextInput.tsx +663 -579
  118. package/components/TextInput/index.tsx +19 -3
  119. package/components/TextInput/types.ts +77 -28
  120. package/components/TextInput/utils.ts +235 -145
  121. package/components/TimeField/TimeField.tsx +75 -0
  122. package/components/TimeField/index.tsx +6 -0
  123. package/components/TimeField/useTimeFieldState.ts +70 -0
  124. package/components/{TimePickerField/sanitizeTime.ts → TimeField/utils.ts} +77 -10
  125. package/components/TimePicker/AnalogClock.tsx +1 -1
  126. package/components/TimePicker/TimeInput.tsx +87 -42
  127. package/components/TimePicker/TimeInputs.tsx +138 -50
  128. package/components/TimePicker/TimePicker.tsx +74 -11
  129. package/components/TimePicker/TimePickerModal.tsx +186 -0
  130. package/components/TimePicker/context.tsx +17 -0
  131. package/components/TimePicker/index.tsx +15 -3
  132. package/components/TimePicker/utils.ts +93 -4
  133. package/components/Tooltip/Tooltip.tsx +42 -67
  134. package/components/Tooltip/TooltipContent.tsx +32 -5
  135. package/components/Tooltip/TooltipTrigger.tsx +20 -20
  136. package/components/Tooltip/index.tsx +1 -1
  137. package/components/TouchableRipple/TouchableRipple.native.tsx +83 -16
  138. package/components/TouchableRipple/TouchableRipple.tsx +150 -102
  139. package/components/TouchableRipple/rippleFromForegroundColor.ts +21 -0
  140. package/hocs/index.tsx +1 -1
  141. package/hocs/withKeyboardAccessibility.tsx +2 -3
  142. package/hocs/withPortal.tsx +1 -1
  143. package/hooks/index.tsx +2 -12
  144. package/hooks/useActionState.tsx +19 -8
  145. package/hooks/useContrastColor.ts +1 -2
  146. package/hooks/useFilePicker.tsx +7 -17
  147. package/hooks/useHandleNumberFormat.tsx +2 -2
  148. package/hooks/useMediaQuery.tsx +1 -2
  149. package/package.json +95 -111
  150. package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +6 -3
  151. package/shortcuts-manager/ShortcutsManager/utils.tsx +1 -1
  152. package/shortcuts-manager/useSetScopes/useSetScopes.tsx +1 -1
  153. package/shortcuts-manager/useShortcut/useShortcut.tsx +1 -1
  154. package/styles/shadow.ts +2 -1
  155. package/styles/themes/LightTheme.tsx +1 -1
  156. package/utils/DocumentPicker/documentPicker.ts +78 -27
  157. package/utils/DocumentPicker/types.ts +0 -1
  158. package/utils/extractSubcomponents.ts +89 -0
  159. package/utils/extractTextStyles.ts +1 -2
  160. package/utils/formatNumberWithMask/formatNumberWithMask.ts +2 -1
  161. package/utils/index.ts +0 -3
  162. package/utils/normalizeToNumberString/normalizeToNumberString.ts +1 -1
  163. package/components/DatePickerDocked/DatePickerDocked.tsx +0 -30
  164. package/components/DatePickerDocked/DatePickerDockedHeader.tsx +0 -129
  165. package/components/DatePickerDocked/index.tsx +0 -17
  166. package/components/DatePickerDocked/types.ts +0 -11
  167. package/components/DatePickerDocked/utils.ts +0 -157
  168. package/components/DatePickerInline/DatePickerContext.tsx +0 -21
  169. package/components/DatePickerInput/DatePickerInput.tsx +0 -139
  170. package/components/DatePickerInput/DatePickerInputModal.tsx +0 -48
  171. package/components/DatePickerInput/DatePickerInputWithoutModal.tsx +0 -77
  172. package/components/DatePickerInput/DateRangeInput.tsx +0 -88
  173. package/components/DatePickerInput/index.tsx +0 -10
  174. package/components/DatePickerInput/types.ts +0 -28
  175. package/components/DatePickerInput/utils.ts +0 -15
  176. package/components/DatePickerModal/AnimatedCrossView.tsx +0 -94
  177. package/components/DatePickerModal/CalendarEdit.tsx +0 -139
  178. package/components/DatePickerModal/DatePickerModal.tsx +0 -85
  179. package/components/DatePickerModal/DatePickerModalContent.tsx +0 -155
  180. package/components/DatePickerModal/DatePickerModalContentHeader.tsx +0 -213
  181. package/components/DatePickerModal/DatePickerModalHeader.tsx +0 -74
  182. package/components/DatePickerModal/DatePickerModalHeaderBackground.tsx +0 -13
  183. package/components/DatePickerModal/index.tsx +0 -16
  184. package/components/DatePickerModal/types.ts +0 -92
  185. package/components/DatePickerModal/utils.ts +0 -122
  186. package/components/DateTimePicker/DateTimePicker.tsx +0 -172
  187. package/components/DateTimePicker/index.tsx +0 -10
  188. package/components/DateTimePicker/utils.ts +0 -12
  189. package/components/HorizontalDivider/HorizontalDivider.tsx +0 -103
  190. package/components/HorizontalDivider/index.tsx +0 -9
  191. package/components/ListItem/ListItem.tsx +0 -136
  192. package/components/ListItem/ListItemDescription.tsx +0 -25
  193. package/components/ListItem/ListItemTitle.tsx +0 -25
  194. package/components/ListItem/index.tsx +0 -14
  195. package/components/ListItem/utils.ts +0 -115
  196. package/components/Menu/MenuDivider.tsx +0 -13
  197. package/components/Menu/MenuItem.tsx +0 -128
  198. package/components/Popover/Popover.native.tsx +0 -185
  199. package/components/RadioButton/RadioButton.tsx +0 -138
  200. package/components/RadioButton/RadioButtonAndroid.tsx +0 -188
  201. package/components/RadioButton/RadioButtonGroup.tsx +0 -98
  202. package/components/RadioButton/RadioButtonIOS.tsx +0 -106
  203. package/components/RadioButton/RadioButtonItem.tsx +0 -232
  204. package/components/RadioButton/index.ts +0 -22
  205. package/components/RadioButton/utils.ts +0 -165
  206. package/components/TimePickerField/TimePickerField.tsx +0 -152
  207. package/components/TimePickerField/index.tsx +0 -10
  208. package/components/TimePickerField/utils.ts +0 -94
  209. package/components/TimePickerModal/TimePickerModal.tsx +0 -115
  210. package/components/TimePickerModal/index.tsx +0 -10
  211. package/components/TimePickerModal/utils.ts +0 -47
  212. package/components/VerticalDivider/VerticalDivider.tsx +0 -100
  213. package/components/VerticalDivider/index.tsx +0 -9
  214. package/context-bridge/index.tsx +0 -87
  215. package/fast-context/index.tsx +0 -190
  216. package/hocs/typedMemo.tsx +0 -5
  217. package/hooks/useControlledValue.tsx +0 -68
  218. package/hooks/useLatest.tsx +0 -9
  219. package/hooks/useMergedRefs.ts +0 -14
  220. package/hooks/usePrevious.ts +0 -13
  221. package/hooks/useSearchable.tsx +0 -74
  222. package/hooks/useSubcomponents.tsx +0 -59
  223. package/hooks/useToggle.tsx +0 -24
  224. package/utils/color.ts +0 -22
  225. package/utils/compare/index.ts +0 -54
  226. package/utils/lodash.ts +0 -49
  227. package/utils/repository.ts +0 -53
@@ -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
+ placeholder={timeFormat[is24Hour ? '24' : '12'].format}
64
+ {...rest}
65
+ ref={ref}
66
+ disabled={disabled}
67
+ value={formatted}
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
+ };
@@ -1,3 +1,4 @@
1
+ import { useLatest } from '@react-native-molecules/utils/hooks';
1
2
  import { memo, useCallback, useMemo, useRef } from 'react';
2
3
  import {
3
4
  type GestureResponderEvent,
@@ -7,7 +8,6 @@ import {
7
8
  type ViewProps,
8
9
  } from 'react-native';
9
10
 
10
- import { useLatest } from '../../hooks';
11
11
  import AnalogClockHours from './AnalogClockHours';
12
12
  import AnalogClockMinutes from './AnalogClockMinutes';
13
13
  import AnimatedClockSwitcher from './AnimatedClockSwitcher';
@@ -1,24 +1,28 @@
1
- import { forwardRef, memo, useCallback, useMemo, useState } from 'react';
2
- import { StyleSheet, View } from 'react-native';
1
+ import { forwardRef, memo, useCallback, useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ type NativeSyntheticEvent,
4
+ StyleSheet,
5
+ TextInput as NativeTextInput,
6
+ type TextInputProps as NativeTextInputProps,
7
+ type TextInputSelectionChangeEventData,
8
+ View,
9
+ } from 'react-native';
3
10
 
4
11
  import { useTheme } from '../../hooks';
5
12
  import { resolveStateVariant } from '../../utils';
6
- import { TextInput, type TextInputProps } from '../TextInput';
7
13
  import { TouchableRipple } from '../TouchableRipple';
8
14
  import { inputTypes, type PossibleClockTypes, type PossibleInputTypes } from './timeUtils';
9
15
  import { timePickerInputStyles } from './utils';
10
16
 
11
- interface TimeInputProps
12
- extends Omit<
13
- Omit<TextInputProps, 'value' | 'variant' | 'onChangeText' | 'onPress'>,
14
- 'onFocus'
15
- > {
17
+ interface TimeInputProps extends Omit<NativeTextInputProps, 'value' | 'onChangeText' | 'onPress'> {
16
18
  value: number;
17
19
  clockType: PossibleClockTypes;
18
20
  onPress?: (type: PossibleClockTypes) => any;
19
21
  pressed: boolean;
20
- onChanged: (n: number) => any;
22
+ onChanged: (n: number, text: string) => any;
21
23
  inputType: PossibleInputTypes;
24
+ error?: boolean;
25
+ inputStyle?: NativeTextInputProps['style'];
22
26
  }
23
27
 
24
28
  function TimeInput(
@@ -29,18 +33,22 @@ function TimeInput(
29
33
  onPress,
30
34
  onChanged,
31
35
  inputType,
36
+ error = false,
32
37
  inputStyle,
33
38
  style,
34
39
  ...rest
35
40
  }: TimeInputProps,
36
41
  ref: any,
37
42
  ) {
38
- const onInnerChange = (text: string) => {
39
- onChanged(Number(text));
40
- };
41
-
42
43
  const theme = useTheme();
43
44
  const [inputFocused, setInputFocused] = useState<boolean>(false);
45
+ const [rawText, setRawText] = useState<string | null>(null);
46
+ const [selection, setSelection] = useState<{ start: number; end: number } | undefined>();
47
+
48
+ const onInnerChange = (text: string) => {
49
+ setRawText(text);
50
+ onChanged(Number(text), text);
51
+ };
44
52
 
45
53
  const highlighted = inputType === inputTypes.picker ? pressed : inputFocused;
46
54
 
@@ -52,55 +60,92 @@ function TimeInput(
52
60
  });
53
61
 
54
62
  const formattedValue = useMemo(() => {
55
- let _formattedValue = `${value}`;
56
-
57
- if (!inputFocused) {
58
- _formattedValue = `${value}`.length === 1 ? `0${value}` : `${value}`;
59
- }
63
+ if (rawText !== null && (inputFocused || error)) return rawText;
60
64
 
61
- return _formattedValue;
62
- }, [value, inputFocused]);
65
+ const str = `${value}`;
66
+ return str.length === 1 ? `0${str}` : str;
67
+ }, [value, inputFocused, rawText, error]);
63
68
 
64
- const { rippleColor, containerStyle, textInputContainerStyle, textInputStyle, buttonStyle } =
65
- useMemo(() => {
66
- const { container, input, button } = timePickerInputStyles;
69
+ const { containerStyle, textInputStyle, buttonStyle } = useMemo(() => {
70
+ const {
71
+ container,
72
+ input,
73
+ keyboardInput,
74
+ keyboardInputHighlighted,
75
+ inputError,
76
+ keyboardInputError,
77
+ button,
78
+ } = timePickerInputStyles;
79
+ const isKeyboardInput = inputType === inputTypes.keyboard;
67
80
 
68
- return {
69
- rippleColor: timePickerInputStyles.root?._rippleColor,
70
- containerStyle: container,
71
- textInputContainerStyle: [{ paddingHorizontal: 0 }, style],
72
- textInputStyle: [input, inputStyle],
73
- buttonStyle: [StyleSheet.absoluteFill, button],
74
- };
75
- // eslint-disable-next-line react-hooks/exhaustive-deps
76
- }, [inputStyle, style, state]);
81
+ return {
82
+ containerStyle: container,
83
+ textInputStyle: [
84
+ input,
85
+ isKeyboardInput ? keyboardInput : null,
86
+ isKeyboardInput && highlighted ? keyboardInputHighlighted : null,
87
+ error ? inputError : null,
88
+ isKeyboardInput && error ? keyboardInputError : null,
89
+ style,
90
+ inputStyle,
91
+ ],
92
+ buttonStyle: [StyleSheet.absoluteFill, button],
93
+ };
94
+ // eslint-disable-next-line react-hooks/exhaustive-deps
95
+ }, [error, highlighted, inputStyle, inputType, style, state]);
77
96
 
78
97
  const onFocus = useCallback(() => setInputFocused(true), []);
79
- const onBlur = useCallback(() => setInputFocused(false), []);
98
+
99
+ const onBlur = useCallback(() => {
100
+ setInputFocused(false);
101
+ setSelection(undefined);
102
+ if (!error) {
103
+ setRawText(null);
104
+ }
105
+ }, [error]);
106
+
80
107
  const onPressInput = useCallback(() => onPress?.(clockType), [clockType, onPress]);
81
108
 
109
+ const onSelectionChange = useCallback(
110
+ (e: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
111
+ if (selection) {
112
+ setSelection(undefined);
113
+ }
114
+
115
+ rest.onSelectionChange?.(e);
116
+ },
117
+ [rest, selection],
118
+ );
119
+
120
+ useEffect(() => {
121
+ if (error && inputFocused && rawText?.length) {
122
+ setSelection({ start: 0, end: rawText.length });
123
+ return;
124
+ }
125
+
126
+ setSelection(undefined);
127
+ }, [error, inputFocused, rawText]);
128
+
82
129
  return (
83
130
  <View style={containerStyle}>
84
- <TextInput
85
- variant="plain"
131
+ <NativeTextInput
86
132
  ref={ref}
87
- inputStyle={textInputStyle}
88
- style={textInputContainerStyle}
89
133
  onFocus={onFocus}
90
134
  onBlur={onBlur}
91
135
  keyboardAppearance={theme.dark ? 'dark' : 'default'}
92
136
  value={formattedValue}
93
137
  maxLength={2}
138
+ placeholderTextColor={theme.colors.onSurfaceVariant}
139
+ selectTextOnFocus={inputType === inputTypes.picker || error}
140
+ selection={selection}
141
+ onSelectionChange={onSelectionChange}
94
142
  onChangeText={onInnerChange}
143
+ style={textInputStyle}
95
144
  {...rest}
96
145
  />
97
146
  <>
98
147
  {onPress && inputType === inputTypes.picker ? (
99
- <TouchableRipple
100
- style={buttonStyle}
101
- rippleColor={rippleColor}
102
- onPress={onPressInput}
103
- borderless={true}>
148
+ <TouchableRipple style={buttonStyle} onPress={onPressInput} borderless={true}>
104
149
  <View />
105
150
  </TouchableRipple>
106
151
  ) : null}