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

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 +224 -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 +21 -24
  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
@@ -1,11 +1,12 @@
1
1
  // @typescript-eslint/no-unused-vars
2
2
  // WORK IN PROGRESS
3
3
 
4
- import { memo, useCallback, useRef } from 'react';
4
+ import { useLatest } from '@react-native-molecules/utils/hooks';
5
+ import { memo, useCallback, useEffect, useRef, useState } from 'react';
5
6
  import { TextInput as TextInputNative, useWindowDimensions, View } from 'react-native';
6
7
 
7
- import { useLatest } from '../../hooks';
8
8
  import { resolveStateVariant } from '../../utils';
9
+ import { Text } from '../Text';
9
10
  import AmPmSwitcher from './AmPmSwitcher';
10
11
  import TimeInput from './TimeInput';
11
12
  import {
@@ -21,6 +22,10 @@ type Props = {
21
22
  focused: PossibleClockTypes;
22
23
  hours: number;
23
24
  minutes: number;
25
+ hourLabel: string;
26
+ minuteLabel: string;
27
+ hourErrorText: string;
28
+ minuteErrorText: string;
24
29
  onFocusInput: (type: PossibleClockTypes) => any;
25
30
  onChange: (hoursMinutesAndFocused: {
26
31
  hours: number;
@@ -33,6 +38,10 @@ type Props = {
33
38
  function TimeInputs({
34
39
  hours,
35
40
  minutes,
41
+ hourLabel,
42
+ minuteLabel,
43
+ hourErrorText,
44
+ minuteErrorText,
36
45
  onFocusInput,
37
46
  focused,
38
47
  inputType,
@@ -41,6 +50,8 @@ function TimeInputs({
41
50
  }: Props) {
42
51
  const dimensions = useWindowDimensions();
43
52
  const isLandscape = dimensions.width > dimensions.height;
53
+ const [hourError, setHourError] = useState(false);
54
+ const [minuteError, setMinuteError] = useState(false);
44
55
 
45
56
  timePickerInputsStyles.useVariants({
46
57
  state: resolveStateVariant({
@@ -50,6 +61,17 @@ function TimeInputs({
50
61
  const startInput = useRef<TextInputNative | null>(null);
51
62
  const endInput = useRef<TextInputNative | null>(null);
52
63
 
64
+ useEffect(() => {
65
+ if (inputType !== 'keyboard') {
66
+ setHourError(false);
67
+ setMinuteError(false);
68
+ return;
69
+ }
70
+
71
+ const id = setTimeout(() => startInput.current?.focus(), 0);
72
+ return () => clearTimeout(id);
73
+ }, [inputType]);
74
+
53
75
  const onSubmitStartInput = useCallback(() => {
54
76
  if (endInput.current) {
55
77
  endInput.current.focus();
@@ -61,6 +83,7 @@ function TimeInputs({
61
83
  }, []);
62
84
 
63
85
  const minutesRef = useLatest(minutes);
86
+ const isPm = hours >= 12;
64
87
  const onChangeHours = useCallback(
65
88
  (newHours: number) => {
66
89
  onChange({
@@ -73,74 +96,139 @@ function TimeInputs({
73
96
  );
74
97
 
75
98
  const onHourChange = useCallback(
76
- (newHoursFromInput: number) => {
99
+ (newHoursFromInput: number, text: string) => {
100
+ if (text.length === 0) {
101
+ setHourError(false);
102
+ return;
103
+ }
104
+
105
+ const isNumeric = /^\d+$/.test(text);
106
+ const minHours = is24Hour ? 0 : 1;
107
+ const maxHours = is24Hour ? 23 : 12;
108
+ const isValid =
109
+ isNumeric && newHoursFromInput >= minHours && newHoursFromInput <= maxHours;
110
+
111
+ setHourError(!isValid);
112
+
113
+ if (!isValid) {
114
+ return;
115
+ }
116
+
77
117
  let newHours = newHoursFromInput;
78
- if (newHoursFromInput >= 24) {
79
- newHours = 0;
118
+ if (!is24Hour) {
119
+ if (isPm) {
120
+ newHours = newHoursFromInput === 12 ? 12 : newHoursFromInput + 12;
121
+ } else {
122
+ newHours = newHoursFromInput === 12 ? 0 : newHoursFromInput;
123
+ }
80
124
  }
125
+
81
126
  onChange({
82
127
  hours: newHours,
83
- minutes,
128
+ minutes: minutesRef.current,
84
129
  });
130
+
131
+ const maxStartDigit = is24Hour ? 2 : 1;
132
+ const shouldAdvance = text.length >= 2 || newHoursFromInput > maxStartDigit;
133
+ if (shouldAdvance) endInput.current?.focus();
85
134
  },
86
- [minutes, onChange],
135
+ [is24Hour, isPm, minutesRef, onChange],
87
136
  );
88
137
 
89
138
  const onMinuteChange = useCallback(
90
- (newMinutesFromInput: number) => {
91
- let newMinutes = newMinutesFromInput;
139
+ (newMinutesFromInput: number, text: string) => {
140
+ if (text.length === 0) {
141
+ setMinuteError(false);
142
+ return;
143
+ }
144
+
145
+ const isNumeric = /^\d+$/.test(text);
146
+ const isValid = isNumeric && newMinutesFromInput >= 0 && newMinutesFromInput <= 59;
92
147
 
93
- if (newMinutesFromInput > 59) {
94
- newMinutes = 0;
148
+ setMinuteError(!isValid);
149
+
150
+ if (!isValid) {
151
+ return;
95
152
  }
153
+
96
154
  onChange({
97
155
  hours: hours,
98
- minutes: newMinutes,
156
+ minutes: newMinutesFromInput,
99
157
  });
100
158
  },
101
159
  [hours, onChange],
102
160
  );
103
161
 
104
162
  return (
105
- <View style={timePickerInputsStyles.inputContainer}>
106
- <TimeInput
107
- ref={startInput}
108
- placeholder={'00'}
109
- value={toHourInputFormat(hours, is24Hour)}
110
- clockType={clockTypes.hours}
111
- pressed={focused === clockTypes.hours}
112
- onPress={onFocusInput}
113
- inputType={inputType}
114
- returnKeyType={'next'}
115
- onSubmitEditing={onSubmitStartInput}
116
- blurOnSubmit={false}
117
- onChanged={onHourChange}
118
- // onChangeText={onChangeStartInput}
119
- />
120
- <View style={timePickerInputsStyles.hoursAndMinutesSeparator}>
121
- <View style={timePickerInputsStyles.spaceDot} />
122
- <View style={timePickerInputsStyles.dot} />
123
- <View style={timePickerInputsStyles.betweenDot} />
124
- <View style={timePickerInputsStyles.dot} />
125
- <View style={timePickerInputsStyles.spaceDot} />
163
+ <View style={timePickerInputsStyles.wrapper}>
164
+ <View style={timePickerInputsStyles.inputContainer}>
165
+ <TimeInput
166
+ ref={startInput}
167
+ placeholder={''}
168
+ value={toHourInputFormat(hours, is24Hour)}
169
+ clockType={clockTypes.hours}
170
+ pressed={focused === clockTypes.hours}
171
+ onPress={onFocusInput}
172
+ inputType={inputType}
173
+ returnKeyType={'next'}
174
+ onSubmitEditing={onSubmitStartInput}
175
+ blurOnSubmit={false}
176
+ error={hourError}
177
+ onChanged={onHourChange}
178
+ // onChangeText={onChangeStartInput}
179
+ />
180
+ <View style={timePickerInputsStyles.hoursAndMinutesSeparator}>
181
+ <View style={timePickerInputsStyles.spaceDot} />
182
+ <View style={timePickerInputsStyles.dot} />
183
+ <View style={timePickerInputsStyles.betweenDot} />
184
+ <View style={timePickerInputsStyles.dot} />
185
+ <View style={timePickerInputsStyles.spaceDot} />
186
+ </View>
187
+ <TimeInput
188
+ ref={endInput}
189
+ placeholder={'00'}
190
+ value={minutes}
191
+ clockType={clockTypes.minutes}
192
+ pressed={focused === clockTypes.minutes}
193
+ onPress={onFocusInput}
194
+ inputType={inputType}
195
+ error={minuteError}
196
+ onSubmitEditing={onSubmitEndInput}
197
+ onChanged={onMinuteChange}
198
+ />
199
+ {!is24Hour && (
200
+ <>
201
+ <View style={timePickerInputsStyles.spaceBetweenInputsAndSwitcher} />
202
+ <AmPmSwitcher hours={hours} onChange={onChangeHours} />
203
+ </>
204
+ )}
126
205
  </View>
127
- <TimeInput
128
- ref={endInput}
129
- placeholder={'00'}
130
- value={minutes}
131
- clockType={clockTypes.minutes}
132
- pressed={focused === clockTypes.minutes}
133
- onPress={onFocusInput}
134
- inputType={inputType}
135
- onSubmitEditing={onSubmitEndInput}
136
- onChanged={onMinuteChange}
137
- />
138
- {!is24Hour && (
139
- <>
140
- <View style={timePickerInputsStyles.spaceBetweenInputsAndSwitcher} />
141
- <AmPmSwitcher hours={hours} onChange={onChangeHours} />
142
- </>
143
- )}
206
+ {inputType === 'keyboard' ? (
207
+ <View style={timePickerInputsStyles.supportingRow}>
208
+ <View style={timePickerInputsStyles.supportingSlot}>
209
+ <Text
210
+ style={[
211
+ timePickerInputsStyles.supportingText,
212
+ hourError ? timePickerInputsStyles.supportingTextError : null,
213
+ ]}>
214
+ {hourError ? hourErrorText : hourLabel}
215
+ </Text>
216
+ </View>
217
+ <View style={timePickerInputsStyles.hoursAndMinutesSeparator} />
218
+ <View style={timePickerInputsStyles.supportingSlot}>
219
+ <Text
220
+ style={[
221
+ timePickerInputsStyles.supportingText,
222
+ minuteError ? timePickerInputsStyles.supportingTextError : null,
223
+ ]}>
224
+ {minuteError ? minuteErrorText : minuteLabel}
225
+ </Text>
226
+ </View>
227
+ {!is24Hour && (
228
+ <View style={timePickerInputsStyles.spaceBetweenInputsAndSwitcher} />
229
+ )}
230
+ </View>
231
+ ) : null}
144
232
  </View>
145
233
  );
146
234
  }
@@ -1,9 +1,12 @@
1
+ import { useControlledValue } from '@react-native-molecules/utils/hooks';
1
2
  import { memo, useCallback, useMemo, useState } from 'react';
2
3
  import { type StyleProp, View, type ViewStyle } from 'react-native';
3
4
 
4
- import { useControlledValue } from '../../hooks';
5
+ import { getRegisteredComponentWithFallback } from '../../core';
5
6
  import { format, parse } from '../../utils/date-fns';
7
+ import { useOptionalDatePickerContext } from '../DatePicker/context';
6
8
  import AnalogClock from './AnalogClock';
9
+ import { useOptionalTimePickerContext } from './context';
7
10
  import { DisplayModeContext } from './DisplayModeContext';
8
11
  import TimeInputs from './TimeInputs';
9
12
  import {
@@ -26,10 +29,10 @@ type onChangeFunc = ({
26
29
 
27
30
  export type Props = {
28
31
  /**
29
- * hh:mm format
32
+ * hh:mm format. Optional when mounted inside a DatePickerProvider — the provider's Date is read instead.
30
33
  * */
31
- time: string;
32
- onTimeChange: (params: { time: string; focused?: undefined | PossibleClockTypes }) => any;
34
+ time?: string;
35
+ onTimeChange?: (params: { time: string; focused?: undefined | PossibleClockTypes }) => any;
33
36
 
34
37
  is24Hour?: boolean;
35
38
  inputType?: PossibleInputTypes;
@@ -38,18 +41,64 @@ export type Props = {
38
41
  onFocusInput?: (type: PossibleClockTypes) => any;
39
42
  isLandscape?: boolean;
40
43
  style?: StyleProp<ViewStyle>;
44
+ hourLabel?: string;
45
+ minuteLabel?: string;
46
+ hourErrorText?: string;
47
+ minuteErrorText?: string;
48
+ };
49
+
50
+ const toTimeString = (value: Date | null | undefined): string => {
51
+ if (!value) return '';
52
+ const h = value.getHours().toString().padStart(2, '0');
53
+ const m = value.getMinutes().toString().padStart(2, '0');
54
+ return `${h}:${m}`;
55
+ };
56
+
57
+ const applyTimeToDate = (base: Date | null | undefined, time: string): Date | null => {
58
+ if (!time) return null;
59
+ const [h, m] = time.split(':').map(n => parseInt(n, 10));
60
+ if (Number.isNaN(h) || Number.isNaN(m)) return base ?? null;
61
+ const next = base ? new Date(base) : new Date();
62
+ next.setHours(h, m, 0, 0);
63
+ return next;
41
64
  };
42
65
 
43
66
  function TimePicker({
44
- is24Hour = false,
45
- time,
67
+ time: timeProp,
68
+ onTimeChange: onTimeChangeProp,
69
+ is24Hour: is24HourProp,
46
70
  focused: focusedProp,
47
71
  onFocusInput: onFocusInputProp,
48
- inputType = 'keyboard',
49
- onTimeChange,
72
+ inputType: inputTypeProp,
50
73
  isLandscape = false,
51
74
  style,
75
+ hourLabel = 'Hour',
76
+ minuteLabel = 'Minute',
77
+ hourErrorText,
78
+ minuteErrorText = 'Minute must be 0-59',
52
79
  }: Props) {
80
+ const ctx = useOptionalDatePickerContext();
81
+ const tpCtx = useOptionalTimePickerContext();
82
+
83
+ const ctxDate =
84
+ ctx && ctx.draftValue && typeof ctx.draftValue === 'object' && 'start' in ctx.draftValue
85
+ ? null
86
+ : (ctx?.draftValue as Date | null | undefined);
87
+
88
+ const time = timeProp ?? toTimeString(ctxDate);
89
+ const is24Hour = is24HourProp ?? ctx?.is24Hour ?? false;
90
+ const inputType = inputTypeProp ?? tpCtx?.inputType ?? (ctx ? 'picker' : 'keyboard');
91
+
92
+ const onTimeChange = useMemo(
93
+ () =>
94
+ onTimeChangeProp ??
95
+ ((params: { time: string; focused?: undefined | PossibleClockTypes }) => {
96
+ if (!ctx) return;
97
+ ctx.setValue(applyTimeToDate(ctxDate, params.time));
98
+ }),
99
+ [onTimeChangeProp, ctx, ctxDate],
100
+ );
101
+
53
102
  const { hours, minutes } = useMemo(() => {
54
103
  const date = time ? parse(time, 'HH:mm', new Date()) : new Date();
55
104
 
@@ -62,7 +111,6 @@ function TimePicker({
62
111
  onChange: onFocusInputProp,
63
112
  });
64
113
 
65
- // Initialize display Mode according the hours value
66
114
  const [displayMode, setDisplayMode] = useState<'AM' | 'PM' | undefined>(() =>
67
115
  !is24Hour ? (hours >= 12 ? 'PM' : 'AM') : undefined,
68
116
  );
@@ -81,6 +129,11 @@ function TimePicker({
81
129
 
82
130
  if (newDisplayMode !== displayMode) setDisplayMode(newDisplayMode);
83
131
 
132
+ if (params.focused) {
133
+ const nextFocused = params.focused;
134
+ setTimeout(() => onFocusInput(nextFocused), 300);
135
+ }
136
+
84
137
  onTimeChange?.({
85
138
  time: `${`${params.hours}`.padStart(2, '0')}:${`${params.minutes}`.padStart(
86
139
  2,
@@ -89,7 +142,7 @@ function TimePicker({
89
142
  focused: params.focused,
90
143
  });
91
144
  },
92
- [displayMode, onTimeChange],
145
+ [displayMode, onFocusInput, onTimeChange],
93
146
  );
94
147
 
95
148
  const memoizedValue = useMemo(
@@ -108,6 +161,12 @@ function TimePicker({
108
161
  onChange={onChange}
109
162
  onFocusInput={onFocusInput}
110
163
  focused={focused}
164
+ hourLabel={hourLabel}
165
+ minuteLabel={minuteLabel}
166
+ hourErrorText={
167
+ hourErrorText ?? (is24Hour ? 'Hour must be 0-23' : 'Hour must be 1-12')
168
+ }
169
+ minuteErrorText={minuteErrorText}
111
170
  />
112
171
  <>
113
172
  {inputType === inputTypes.picker ? (
@@ -127,4 +186,8 @@ function TimePicker({
127
186
  );
128
187
  }
129
188
 
130
- export default memo(TimePicker);
189
+ const TimePickerDefault = memo(TimePicker);
190
+
191
+ export default TimePickerDefault;
192
+
193
+ export const TimePickerClock = getRegisteredComponentWithFallback('TimePicker', TimePickerDefault);
@@ -0,0 +1,186 @@
1
+ import { useControlledValue } from '@react-native-molecules/utils/hooks';
2
+ import type { ReactNode } from 'react';
3
+ import { memo, useMemo } from 'react';
4
+ import { KeyboardAvoidingView, Platform, View } from 'react-native';
5
+
6
+ import { getRegisteredComponentWithFallback } from '../../core';
7
+ import { DatePickerActions, DatePickerProvider } from '../DatePicker';
8
+ import type {
9
+ DatePickerContextType,
10
+ DatePickerLocale,
11
+ DatePickerValue,
12
+ } from '../DatePicker/context';
13
+ import {
14
+ DatePickerContext,
15
+ useDatePickerContext,
16
+ useOptionalDatePickerContext,
17
+ withDraftLayer,
18
+ } from '../DatePicker/context';
19
+ import { IconButton } from '../IconButton';
20
+ import { Modal, type ModalProps } from '../Modal';
21
+ import { Portal } from '../Portal';
22
+ import { Text } from '../Text';
23
+ import { TimePickerContext } from './context';
24
+ import { TimePickerClock } from './TimePicker';
25
+ import {
26
+ getTimeInputTypeIcon,
27
+ inputTypes,
28
+ type PossibleInputTypes,
29
+ reverseInputTypes,
30
+ } from './timeUtils';
31
+ import { timePickerModalStyles as styles } from './utils';
32
+
33
+ export type TimePickerModalProps = Omit<ModalProps, 'children' | 'isOpen' | 'onClose'> & {
34
+ children?: ReactNode;
35
+ isOpen?: boolean;
36
+ onClose?: () => void;
37
+ value?: DatePickerValue;
38
+ onChange?: (value: DatePickerValue) => void;
39
+ locale?: DatePickerLocale;
40
+ is24Hour?: boolean;
41
+ inputType?: PossibleInputTypes;
42
+ defaultInputType?: PossibleInputTypes;
43
+ onInputTypeChange?: (next: PossibleInputTypes) => void;
44
+ label?: string;
45
+ uppercase?: boolean;
46
+ cancelLabel?: string;
47
+ confirmLabel?: string;
48
+ keyboardIcon?: string;
49
+ clockIcon?: string;
50
+ /** Override the surface default draft mode. Modal defaults to `true` (staged commit). */
51
+ draft?: boolean;
52
+ };
53
+
54
+ type BodyProps = Omit<
55
+ TimePickerModalProps,
56
+ 'isOpen' | 'onClose' | 'value' | 'onChange' | 'is24Hour' | 'draft'
57
+ >;
58
+
59
+ function TimePickerModalBody({
60
+ children,
61
+ style,
62
+ label = 'Select time',
63
+ uppercase = false,
64
+ cancelLabel = 'Cancel',
65
+ confirmLabel = 'OK',
66
+ keyboardIcon = 'keyboard-outline',
67
+ clockIcon = 'clock-outline',
68
+ inputType: inputTypeProp,
69
+ defaultInputType = inputTypes.picker,
70
+ onInputTypeChange,
71
+ ...rest
72
+ }: BodyProps) {
73
+ const ctx = useDatePickerContext();
74
+ const [inputType, setInputType] = useControlledValue<PossibleInputTypes>({
75
+ value: inputTypeProp,
76
+ defaultValue: defaultInputType,
77
+ onChange: onInputTypeChange,
78
+ });
79
+
80
+ const tpContextValue = useMemo(() => ({ inputType, setInputType }), [inputType, setInputType]);
81
+
82
+ const modalStyle = useMemo(() => [styles.modalContent, style], [style]);
83
+
84
+ return (
85
+ <Portal>
86
+ <TimePickerContext value={tpContextValue}>
87
+ <Modal {...rest} isOpen={ctx.open} onClose={ctx.cancel} style={modalStyle}>
88
+ <KeyboardAvoidingView
89
+ style={styles.keyboardView}
90
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
91
+ <View style={styles.frame}>
92
+ <View style={styles.labelContainer}>
93
+ <Text style={styles.label}>
94
+ {uppercase ? label.toUpperCase() : label}
95
+ </Text>
96
+ </View>
97
+ <View style={styles.timePickerContainer}>
98
+ {children ?? <TimePickerClock />}
99
+ </View>
100
+ <View style={styles.footer}>
101
+ <IconButton
102
+ name={getTimeInputTypeIcon(inputType, {
103
+ keyboard: keyboardIcon,
104
+ picker: clockIcon,
105
+ })}
106
+ onPress={() => setInputType(reverseInputTypes[inputType])}
107
+ style={styles.inputTypeToggle}
108
+ accessibilityLabel="toggle keyboard"
109
+ />
110
+ <View style={styles.fill} />
111
+ <DatePickerActions
112
+ cancelLabel={cancelLabel}
113
+ confirmLabel={confirmLabel}
114
+ />
115
+ </View>
116
+ </View>
117
+ </KeyboardAvoidingView>
118
+ </Modal>
119
+ </TimePickerContext>
120
+ </Portal>
121
+ );
122
+ }
123
+
124
+ function TimePickerModalLayer({
125
+ base,
126
+ draft: draftProp,
127
+ ...rest
128
+ }: BodyProps & {
129
+ base: DatePickerContextType;
130
+ draft: boolean | undefined;
131
+ }) {
132
+ const effectiveDraft = draftProp ?? base.providerDraft ?? true;
133
+ const ctx = useMemo(() => withDraftLayer(base, effectiveDraft), [base, effectiveDraft]);
134
+ if (!base.open) return null;
135
+ return (
136
+ <DatePickerContext value={ctx}>
137
+ <TimePickerModalBody {...rest} />
138
+ </DatePickerContext>
139
+ );
140
+ }
141
+
142
+ function TimePickerModalAdapter({ draft, ...rest }: BodyProps & { draft: boolean | undefined }) {
143
+ const base = useDatePickerContext();
144
+ return <TimePickerModalLayer base={base} draft={draft} {...rest} />;
145
+ }
146
+
147
+ const TimePickerModalDefault = memo(
148
+ ({
149
+ isOpen: isOpenProp,
150
+ onClose: onCloseProp,
151
+ value: valueProp,
152
+ onChange: onChangeProp,
153
+ locale,
154
+ is24Hour,
155
+ draft: draftProp,
156
+ ...rest
157
+ }: TimePickerModalProps) => {
158
+ const outer = useOptionalDatePickerContext();
159
+
160
+ if (outer) {
161
+ return <TimePickerModalLayer base={outer} draft={draftProp} {...rest} />;
162
+ }
163
+
164
+ return (
165
+ <DatePickerProvider
166
+ mode="time"
167
+ value={valueProp}
168
+ onChange={onChangeProp}
169
+ open={isOpenProp}
170
+ locale={locale}
171
+ onOpenChange={next => {
172
+ if (!next) onCloseProp?.();
173
+ }}
174
+ is24Hour={is24Hour}>
175
+ <TimePickerModalAdapter draft={draftProp} {...rest} />
176
+ </DatePickerProvider>
177
+ );
178
+ },
179
+ );
180
+
181
+ export const TimePickerModal = getRegisteredComponentWithFallback(
182
+ 'TimePickerModal',
183
+ TimePickerModalDefault,
184
+ );
185
+
186
+ export default TimePickerModal;
@@ -0,0 +1,17 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ import { registerPortalContext } from '../Portal';
4
+ import type { PossibleInputTypes } from './timeUtils';
5
+
6
+ export type TimePickerContextType = {
7
+ inputType: PossibleInputTypes;
8
+ setInputType: (next: PossibleInputTypes) => void;
9
+ };
10
+
11
+ export const TimePickerContext = createContext<TimePickerContextType | null>(null);
12
+
13
+ export function useOptionalTimePickerContext(): TimePickerContextType | null {
14
+ return useContext(TimePickerContext);
15
+ }
16
+
17
+ registerPortalContext(TimePickerContext);
@@ -1,9 +1,20 @@
1
- import { getRegisteredComponentWithFallback } from '../../core';
2
- import TimePickerDefault from './TimePicker';
1
+ import { DatePickerProvider, DatePickerTrigger } from '../DatePicker';
2
+ import { TimePickerClock } from './TimePicker';
3
+ import { TimePickerModal } from './TimePickerModal';
3
4
 
4
- export const TimePicker = getRegisteredComponentWithFallback('TimePicker', TimePickerDefault);
5
+ export const TimePicker = {
6
+ Provider: DatePickerProvider,
7
+ Trigger: DatePickerTrigger,
8
+ Clock: TimePickerClock,
9
+ Modal: TimePickerModal,
10
+ };
5
11
 
12
+ export type { TimePickerContextType } from './context';
13
+ export { TimePickerContext, useOptionalTimePickerContext } from './context';
6
14
  export type { Props as TimePickerProps } from './TimePicker';
15
+ export { TimePickerClock } from './TimePicker';
16
+ export type { TimePickerModalProps } from './TimePickerModal';
17
+ export { TimePickerModal } from './TimePickerModal';
7
18
  export {
8
19
  timePickerAmPmSwitcherStyles,
9
20
  timePickerClockHoursStyles,
@@ -11,5 +22,6 @@ export {
11
22
  timePickerClockStyles,
12
23
  timePickerInputsStyles,
13
24
  timePickerInputStyles,
25
+ timePickerModalStyles,
14
26
  timePickerStyles,
15
27
  } from './utils';