react-native-molecules 0.5.0-beta.2 → 0.5.0-beta.20

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 (157) hide show
  1. package/README.md +1 -1
  2. package/components/Accordion/Accordion.tsx +2 -6
  3. package/components/Accordion/AccordionItem.tsx +16 -12
  4. package/components/Accordion/AccordionItemContent.tsx +6 -1
  5. package/components/Accordion/AccordionItemHeader.tsx +1 -1
  6. package/components/Accordion/utils.ts +6 -0
  7. package/components/ActivityIndicator/ActivityIndicator.tsx +6 -15
  8. package/components/Appbar/AppbarBase.tsx +18 -13
  9. package/components/Button/Button.tsx +209 -264
  10. package/components/Button/index.tsx +9 -3
  11. package/components/Button/types.ts +16 -2
  12. package/components/Button/utils.ts +230 -208
  13. package/components/Checkbox/CheckboxBase.tsx +23 -128
  14. package/components/Checkbox/utils.ts +0 -25
  15. package/components/Chip/Chip.tsx +40 -52
  16. package/components/Chip/utils.ts +3 -7
  17. package/components/DateField/DateField.tsx +110 -0
  18. package/components/DateField/index.tsx +6 -0
  19. package/components/{DatePickerInput/inputUtils.ts → DateField/useDateFieldState.ts} +17 -49
  20. package/components/DatePicker/DateCalendar.tsx +83 -0
  21. package/components/DatePicker/DatePickerActions.tsx +73 -0
  22. package/components/DatePicker/DatePickerModal.tsx +234 -0
  23. package/components/DatePicker/DatePickerPopover.tsx +79 -0
  24. package/components/DatePicker/DatePickerProvider.tsx +152 -0
  25. package/components/DatePicker/DatePickerTrigger.tsx +23 -0
  26. package/components/DatePicker/context.tsx +82 -0
  27. package/components/DatePicker/index.tsx +44 -0
  28. package/components/DatePicker/utils.ts +293 -0
  29. package/components/DatePickerInline/DatePickerContext.tsx +1 -0
  30. package/components/DatePickerInline/DatePickerDockedHeader.tsx +113 -0
  31. package/components/DatePickerInline/DatePickerInline.tsx +16 -15
  32. package/components/DatePickerInline/DatePickerInlineBase.tsx +7 -1
  33. package/components/DatePickerInline/Day.tsx +25 -1
  34. package/components/DatePickerInline/DayRange.tsx +2 -4
  35. package/components/DatePickerInline/HeaderItem.tsx +42 -27
  36. package/components/DatePickerInline/Month.tsx +45 -65
  37. package/components/DatePickerInline/MonthPicker.tsx +25 -41
  38. package/components/DatePickerInline/Swiper.native.tsx +21 -4
  39. package/components/DatePickerInline/Swiper.tsx +168 -13
  40. package/components/DatePickerInline/Week.tsx +6 -1
  41. package/components/DatePickerInline/YearPicker.tsx +206 -53
  42. package/components/DatePickerInline/dateUtils.tsx +17 -12
  43. package/components/DatePickerInline/types.ts +3 -0
  44. package/components/DatePickerInline/utils.ts +66 -29
  45. package/components/Drawer/Drawer.tsx +17 -6
  46. package/components/ElementGroup/ElementGroup.tsx +16 -14
  47. package/components/FilePicker/FilePicker.tsx +48 -78
  48. package/components/FilePicker/index.tsx +2 -1
  49. package/components/FilePicker/utils.ts +9 -0
  50. package/components/HelperText/HelperText.tsx +0 -35
  51. package/components/Icon/iconFactory.tsx +3 -3
  52. package/components/Icon/index.tsx +1 -1
  53. package/components/Icon/types.ts +17 -6
  54. package/components/IconButton/IconButton.tsx +42 -57
  55. package/components/IconButton/utils.ts +142 -33
  56. package/components/ListItem/ListItem.tsx +3 -1
  57. package/components/ListItem/utils.ts +1 -1
  58. package/components/LoadingIndicator/LoadingIndicator.tsx +253 -0
  59. package/components/LoadingIndicator/LoadingIndicator.web.tsx +136 -0
  60. package/components/LoadingIndicator/index.tsx +13 -0
  61. package/components/LoadingIndicator/utils.ts +117 -0
  62. package/components/Menu/Menu.tsx +3 -18
  63. package/components/NavigationRail/NavigationRail.tsx +15 -9
  64. package/components/Popover/Popover.tsx +122 -145
  65. package/components/Popover/PopoverRoot.tsx +74 -0
  66. package/components/Popover/common.ts +50 -34
  67. package/components/Popover/index.ts +18 -1
  68. package/components/Popover/usePlatformMeasure.native.ts +90 -0
  69. package/components/Popover/usePlatformMeasure.ts +118 -0
  70. package/components/Popover/utils.ts +34 -0
  71. package/components/Select/Select.tsx +368 -507
  72. package/components/Select/context.tsx +72 -0
  73. package/components/Select/index.ts +8 -14
  74. package/components/Select/types.ts +2 -4
  75. package/components/Select/utils.ts +144 -0
  76. package/components/Slot/Slot.tsx +244 -0
  77. package/components/Slot/compose-refs.tsx +62 -0
  78. package/components/Slot/index.tsx +8 -0
  79. package/components/Surface/Surface.android.tsx +34 -8
  80. package/components/Surface/Surface.ios.tsx +36 -29
  81. package/components/Surface/Surface.tsx +31 -4
  82. package/components/Surface/utils.ts +44 -30
  83. package/components/Switch/Switch.tsx +8 -2
  84. package/components/Tabs/TabItem.tsx +35 -58
  85. package/components/Tabs/TabLabel.tsx +5 -9
  86. package/components/Tabs/Tabs.tsx +154 -148
  87. package/components/Tabs/utils.ts +15 -2
  88. package/components/TextInput/TextInput.tsx +658 -575
  89. package/components/TextInput/index.tsx +19 -3
  90. package/components/TextInput/types.ts +76 -27
  91. package/components/TextInput/utils.ts +225 -145
  92. package/components/TimeField/TimeField.tsx +75 -0
  93. package/components/TimeField/index.tsx +6 -0
  94. package/components/TimeField/useTimeFieldState.ts +70 -0
  95. package/components/{TimePickerField/sanitizeTime.ts → TimeField/utils.ts} +77 -10
  96. package/components/TimePicker/TimeInput.tsx +87 -37
  97. package/components/TimePicker/TimeInputs.tsx +137 -49
  98. package/components/TimePicker/TimePicker.tsx +73 -10
  99. package/components/TimePicker/TimePickerModal.tsx +186 -0
  100. package/components/TimePicker/context.tsx +17 -0
  101. package/components/TimePicker/index.tsx +15 -3
  102. package/components/TimePicker/utils.ts +93 -0
  103. package/components/Tooltip/Tooltip.tsx +42 -67
  104. package/components/Tooltip/TooltipContent.tsx +32 -5
  105. package/components/Tooltip/TooltipTrigger.tsx +20 -20
  106. package/components/Tooltip/index.tsx +1 -1
  107. package/components/TouchableRipple/TouchableRipple.native.tsx +50 -14
  108. package/components/TouchableRipple/TouchableRipple.tsx +137 -47
  109. package/hocs/withPortal.tsx +1 -1
  110. package/hooks/index.tsx +0 -6
  111. package/hooks/useActionState.tsx +19 -8
  112. package/hooks/useControlledValue.tsx +20 -4
  113. package/hooks/useFilePicker.tsx +6 -16
  114. package/hooks/useWhatHasUpdated.tsx +48 -0
  115. package/package.json +17 -13
  116. package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +5 -2
  117. package/styles/shadow.ts +2 -1
  118. package/styles/themes/LightTheme.tsx +1 -1
  119. package/utils/DocumentPicker/documentPicker.ts +78 -27
  120. package/utils/DocumentPicker/types.ts +0 -1
  121. package/utils/extractPropertiesFromStyles.ts +25 -0
  122. package/utils/extractSubcomponents.ts +89 -0
  123. package/utils/lodash.ts +77 -5
  124. package/components/DatePickerDocked/DatePickerDocked.tsx +0 -30
  125. package/components/DatePickerDocked/DatePickerDockedHeader.tsx +0 -129
  126. package/components/DatePickerDocked/index.tsx +0 -17
  127. package/components/DatePickerDocked/types.ts +0 -11
  128. package/components/DatePickerDocked/utils.ts +0 -157
  129. package/components/DatePickerInput/DatePickerInput.tsx +0 -139
  130. package/components/DatePickerInput/DatePickerInputModal.tsx +0 -48
  131. package/components/DatePickerInput/DatePickerInputWithoutModal.tsx +0 -77
  132. package/components/DatePickerInput/DateRangeInput.tsx +0 -88
  133. package/components/DatePickerInput/index.tsx +0 -10
  134. package/components/DatePickerInput/types.ts +0 -28
  135. package/components/DatePickerInput/utils.ts +0 -15
  136. package/components/DatePickerModal/AnimatedCrossView.tsx +0 -94
  137. package/components/DatePickerModal/CalendarEdit.tsx +0 -139
  138. package/components/DatePickerModal/DatePickerModal.tsx +0 -85
  139. package/components/DatePickerModal/DatePickerModalContent.tsx +0 -155
  140. package/components/DatePickerModal/DatePickerModalContentHeader.tsx +0 -213
  141. package/components/DatePickerModal/DatePickerModalHeader.tsx +0 -74
  142. package/components/DatePickerModal/DatePickerModalHeaderBackground.tsx +0 -13
  143. package/components/DatePickerModal/index.tsx +0 -16
  144. package/components/DatePickerModal/types.ts +0 -92
  145. package/components/DatePickerModal/utils.ts +0 -122
  146. package/components/DateTimePicker/DateTimePicker.tsx +0 -172
  147. package/components/DateTimePicker/index.tsx +0 -10
  148. package/components/DateTimePicker/utils.ts +0 -12
  149. package/components/Popover/Popover.native.tsx +0 -185
  150. package/components/TimePickerField/TimePickerField.tsx +0 -152
  151. package/components/TimePickerField/index.tsx +0 -10
  152. package/components/TimePickerField/utils.ts +0 -94
  153. package/components/TimePickerModal/TimePickerModal.tsx +0 -115
  154. package/components/TimePickerModal/index.tsx +0 -10
  155. package/components/TimePickerModal/utils.ts +0 -47
  156. package/hooks/useSearchable.tsx +0 -74
  157. package/hooks/useSubcomponents.tsx +0 -59
@@ -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 { memo, useCallback, useEffect, useRef, useState } from 'react';
5
5
  import { TextInput as TextInputNative, useWindowDimensions, View } from 'react-native';
6
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
1
  import { memo, useCallback, useMemo, useState } from 'react';
2
2
  import { type StyleProp, View, type ViewStyle } from 'react-native';
3
3
 
4
+ import { getRegisteredComponentWithFallback } from '../../core';
4
5
  import { useControlledValue } from '../../hooks';
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 type { ReactNode } from 'react';
2
+ import { memo, useMemo } from 'react';
3
+ import { KeyboardAvoidingView, Platform, View } from 'react-native';
4
+
5
+ import { getRegisteredComponentWithFallback } from '../../core';
6
+ import { useControlledValue } from '../../hooks';
7
+ import { DatePickerActions, DatePickerProvider } from '../DatePicker';
8
+ import type { DatePickerContextType, DatePickerValue } from '../DatePicker/context';
9
+ import {
10
+ DatePickerContext,
11
+ useDatePickerContext,
12
+ useOptionalDatePickerContext,
13
+ withDraftLayer,
14
+ } from '../DatePicker/context';
15
+ import { IconButton } from '../IconButton';
16
+ import { Modal, type ModalProps } from '../Modal';
17
+ import { Portal } from '../Portal';
18
+ import { Text } from '../Text';
19
+ import { TimePickerContext } from './context';
20
+ import { TimePickerClock } from './TimePicker';
21
+ import {
22
+ getTimeInputTypeIcon,
23
+ inputTypes,
24
+ type PossibleInputTypes,
25
+ reverseInputTypes,
26
+ } from './timeUtils';
27
+ import { timePickerModalStyles as styles } from './utils';
28
+
29
+ export type TimePickerModalProps = Omit<ModalProps, 'children' | 'isOpen' | 'onClose'> & {
30
+ children?: ReactNode;
31
+ isOpen?: boolean;
32
+ onClose?: () => void;
33
+ value?: DatePickerValue;
34
+ onChange?: (value: DatePickerValue) => void;
35
+ is24Hour?: boolean;
36
+ inputType?: PossibleInputTypes;
37
+ defaultInputType?: PossibleInputTypes;
38
+ onInputTypeChange?: (next: PossibleInputTypes) => void;
39
+ label?: string;
40
+ uppercase?: boolean;
41
+ cancelLabel?: string;
42
+ confirmLabel?: string;
43
+ keyboardIcon?: string;
44
+ clockIcon?: string;
45
+ /** Override the surface default draft mode. Modal defaults to `true` (staged commit). */
46
+ draft?: boolean;
47
+ };
48
+
49
+ type BodyProps = Omit<
50
+ TimePickerModalProps,
51
+ 'isOpen' | 'onClose' | 'value' | 'onChange' | 'is24Hour' | 'draft'
52
+ >;
53
+
54
+ function TimePickerModalBody({
55
+ children,
56
+ style,
57
+ label = 'Select time',
58
+ uppercase = false,
59
+ cancelLabel = 'Cancel',
60
+ confirmLabel = 'OK',
61
+ keyboardIcon = 'keyboard-outline',
62
+ clockIcon = 'clock-outline',
63
+ inputType: inputTypeProp,
64
+ defaultInputType = inputTypes.picker,
65
+ onInputTypeChange,
66
+ ...rest
67
+ }: BodyProps) {
68
+ const ctx = useDatePickerContext();
69
+ const [inputType, setInputType] = useControlledValue<PossibleInputTypes>({
70
+ value: inputTypeProp,
71
+ defaultValue: defaultInputType,
72
+ onChange: onInputTypeChange,
73
+ });
74
+
75
+ const tpContextValue = useMemo(() => ({ inputType, setInputType }), [inputType, setInputType]);
76
+
77
+ const modalStyle = useMemo(() => [styles.modalContent, style], [style]);
78
+
79
+ return (
80
+ <Portal>
81
+ <TimePickerContext value={tpContextValue}>
82
+ <Modal {...rest} isOpen={ctx.open} onClose={ctx.cancel} style={modalStyle}>
83
+ <KeyboardAvoidingView
84
+ style={styles.keyboardView}
85
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
86
+ <View style={styles.frame}>
87
+ <View style={styles.labelContainer}>
88
+ <Text style={styles.label}>
89
+ {uppercase ? label.toUpperCase() : label}
90
+ </Text>
91
+ </View>
92
+ <View style={styles.timePickerContainer}>
93
+ {children ?? <TimePickerClock />}
94
+ </View>
95
+ <View style={styles.footer}>
96
+ <IconButton
97
+ name={getTimeInputTypeIcon(inputType, {
98
+ keyboard: keyboardIcon,
99
+ picker: clockIcon,
100
+ })}
101
+ onPress={() => setInputType(reverseInputTypes[inputType])}
102
+ style={styles.inputTypeToggle}
103
+ accessibilityLabel="toggle keyboard"
104
+ />
105
+ <View style={styles.fill} />
106
+ <DatePickerActions
107
+ cancelLabel={cancelLabel}
108
+ confirmLabel={confirmLabel}
109
+ />
110
+ </View>
111
+ </View>
112
+ </KeyboardAvoidingView>
113
+ </Modal>
114
+ </TimePickerContext>
115
+ </Portal>
116
+ );
117
+ }
118
+
119
+ function TimePickerModalLayer({
120
+ base,
121
+ draft: draftProp,
122
+ bodyProps,
123
+ }: {
124
+ base: DatePickerContextType;
125
+ draft: boolean | undefined;
126
+ bodyProps: BodyProps;
127
+ }) {
128
+ const effectiveDraft = draftProp ?? base.providerDraft ?? true;
129
+ const ctx = useMemo(() => withDraftLayer(base, effectiveDraft), [base, effectiveDraft]);
130
+ if (!base.open) return null;
131
+ return (
132
+ <DatePickerContext value={ctx}>
133
+ <TimePickerModalBody {...bodyProps} />
134
+ </DatePickerContext>
135
+ );
136
+ }
137
+
138
+ function TimePickerModalAdapter({
139
+ draft,
140
+ bodyProps,
141
+ }: {
142
+ draft: boolean | undefined;
143
+ bodyProps: BodyProps;
144
+ }) {
145
+ const base = useDatePickerContext();
146
+ return <TimePickerModalLayer base={base} draft={draft} bodyProps={bodyProps} />;
147
+ }
148
+
149
+ const TimePickerModalDefault = memo(
150
+ ({
151
+ isOpen: isOpenProp,
152
+ onClose: onCloseProp,
153
+ value: valueProp,
154
+ onChange: onChangeProp,
155
+ is24Hour,
156
+ draft: draftProp,
157
+ ...rest
158
+ }: TimePickerModalProps) => {
159
+ const outer = useOptionalDatePickerContext();
160
+
161
+ if (outer) {
162
+ return <TimePickerModalLayer base={outer} draft={draftProp} bodyProps={rest} />;
163
+ }
164
+
165
+ return (
166
+ <DatePickerProvider
167
+ mode="time"
168
+ value={valueProp}
169
+ onChange={onChangeProp}
170
+ open={isOpenProp}
171
+ onOpenChange={next => {
172
+ if (!next) onCloseProp?.();
173
+ }}
174
+ is24Hour={is24Hour}>
175
+ <TimePickerModalAdapter draft={draftProp} bodyProps={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';