react-native-molecules 0.5.0-beta.18 → 0.5.0-beta.19

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.
@@ -171,6 +171,7 @@ const datePickerModalEditStylesDefault = StyleSheet.create(theme => ({
171
171
  const datePickerPopoverHeaderStylesDefault = StyleSheet.create(theme => ({
172
172
  buttonContainer: {
173
173
  height: 46,
174
+ gap: theme.spacings['2'],
174
175
  // width: '50%',
175
176
  flexDirection: 'row',
176
177
  alignItems: 'center',
@@ -96,7 +96,7 @@ function HeaderItem({
96
96
  <Icon
97
97
  onPress={handlePressDropDown}
98
98
  name={selecting && type === pickerType ? 'menu-up' : 'menu-down'}
99
- size={16}
99
+ size={18}
100
100
  />
101
101
  )}
102
102
  </View>
@@ -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,46 +60,88 @@ 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 { rippleColor, 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
+ rippleColor: timePickerInputStyles.root?._rippleColor,
83
+ containerStyle: container,
84
+ textInputStyle: [
85
+ input,
86
+ isKeyboardInput ? keyboardInput : null,
87
+ isKeyboardInput && highlighted ? keyboardInputHighlighted : null,
88
+ error ? inputError : null,
89
+ isKeyboardInput && error ? keyboardInputError : null,
90
+ style,
91
+ inputStyle,
92
+ ],
93
+ buttonStyle: [StyleSheet.absoluteFill, button],
94
+ };
95
+ // eslint-disable-next-line react-hooks/exhaustive-deps
96
+ }, [error, highlighted, inputStyle, inputType, style, state]);
77
97
 
78
98
  const onFocus = useCallback(() => setInputFocused(true), []);
79
- const onBlur = useCallback(() => setInputFocused(false), []);
99
+
100
+ const onBlur = useCallback(() => {
101
+ setInputFocused(false);
102
+ setSelection(undefined);
103
+ if (!error) {
104
+ setRawText(null);
105
+ }
106
+ }, [error]);
107
+
80
108
  const onPressInput = useCallback(() => onPress?.(clockType), [clockType, onPress]);
81
109
 
110
+ const onSelectionChange = useCallback(
111
+ (e: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
112
+ if (selection) {
113
+ setSelection(undefined);
114
+ }
115
+
116
+ rest.onSelectionChange?.(e);
117
+ },
118
+ [rest, selection],
119
+ );
120
+
121
+ useEffect(() => {
122
+ if (error && inputFocused && rawText?.length) {
123
+ setSelection({ start: 0, end: rawText.length });
124
+ return;
125
+ }
126
+
127
+ setSelection(undefined);
128
+ }, [error, inputFocused, rawText]);
129
+
82
130
  return (
83
131
  <View style={containerStyle}>
84
- <TextInput
85
- variant="plain"
132
+ <NativeTextInput
86
133
  ref={ref}
87
- inputStyle={textInputStyle}
88
- style={textInputContainerStyle}
89
134
  onFocus={onFocus}
90
135
  onBlur={onBlur}
91
136
  keyboardAppearance={theme.dark ? 'dark' : 'default'}
92
137
  value={formattedValue}
93
138
  maxLength={2}
139
+ placeholderTextColor={theme.colors.onSurfaceVariant}
140
+ selectTextOnFocus={inputType === inputTypes.picker || error}
141
+ selection={selection}
142
+ onSelectionChange={onSelectionChange}
94
143
  onChangeText={onInnerChange}
144
+ style={textInputStyle}
95
145
  {...rest}
96
146
  />
97
147
  <>
@@ -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 {
@@ -41,6 +42,8 @@ function TimeInputs({
41
42
  }: Props) {
42
43
  const dimensions = useWindowDimensions();
43
44
  const isLandscape = dimensions.width > dimensions.height;
45
+ const [hourError, setHourError] = useState(false);
46
+ const [minuteError, setMinuteError] = useState(false);
44
47
 
45
48
  timePickerInputsStyles.useVariants({
46
49
  state: resolveStateVariant({
@@ -50,6 +53,17 @@ function TimeInputs({
50
53
  const startInput = useRef<TextInputNative | null>(null);
51
54
  const endInput = useRef<TextInputNative | null>(null);
52
55
 
56
+ useEffect(() => {
57
+ if (inputType !== 'keyboard') {
58
+ setHourError(false);
59
+ setMinuteError(false);
60
+ return;
61
+ }
62
+
63
+ const id = setTimeout(() => startInput.current?.focus(), 0);
64
+ return () => clearTimeout(id);
65
+ }, [inputType]);
66
+
53
67
  const onSubmitStartInput = useCallback(() => {
54
68
  if (endInput.current) {
55
69
  endInput.current.focus();
@@ -61,6 +75,9 @@ function TimeInputs({
61
75
  }, []);
62
76
 
63
77
  const minutesRef = useLatest(minutes);
78
+ const isPm = hours >= 12;
79
+ const hourErrorText = is24Hour ? 'Hour must be 0-23' : 'Hour must be 1-12';
80
+ const minuteErrorText = 'Minute must be 0-59';
64
81
  const onChangeHours = useCallback(
65
82
  (newHours: number) => {
66
83
  onChange({
@@ -73,74 +90,139 @@ function TimeInputs({
73
90
  );
74
91
 
75
92
  const onHourChange = useCallback(
76
- (newHoursFromInput: number) => {
93
+ (newHoursFromInput: number, text: string) => {
94
+ if (text.length === 0) {
95
+ setHourError(false);
96
+ return;
97
+ }
98
+
99
+ const isNumeric = /^\d+$/.test(text);
100
+ const minHours = is24Hour ? 0 : 1;
101
+ const maxHours = is24Hour ? 23 : 12;
102
+ const isValid =
103
+ isNumeric && newHoursFromInput >= minHours && newHoursFromInput <= maxHours;
104
+
105
+ setHourError(!isValid);
106
+
107
+ if (!isValid) {
108
+ return;
109
+ }
110
+
77
111
  let newHours = newHoursFromInput;
78
- if (newHoursFromInput >= 24) {
79
- newHours = 0;
112
+ if (!is24Hour) {
113
+ if (isPm) {
114
+ newHours = newHoursFromInput === 12 ? 12 : newHoursFromInput + 12;
115
+ } else {
116
+ newHours = newHoursFromInput === 12 ? 0 : newHoursFromInput;
117
+ }
80
118
  }
119
+
81
120
  onChange({
82
121
  hours: newHours,
83
- minutes,
122
+ minutes: minutesRef.current,
84
123
  });
124
+
125
+ const maxStartDigit = is24Hour ? 2 : 1;
126
+ const shouldAdvance = text.length >= 2 || newHoursFromInput > maxStartDigit;
127
+ if (shouldAdvance) endInput.current?.focus();
85
128
  },
86
- [minutes, onChange],
129
+ [is24Hour, isPm, minutesRef, onChange],
87
130
  );
88
131
 
89
132
  const onMinuteChange = useCallback(
90
- (newMinutesFromInput: number) => {
91
- let newMinutes = newMinutesFromInput;
133
+ (newMinutesFromInput: number, text: string) => {
134
+ if (text.length === 0) {
135
+ setMinuteError(false);
136
+ return;
137
+ }
138
+
139
+ const isNumeric = /^\d+$/.test(text);
140
+ const isValid = isNumeric && newMinutesFromInput >= 0 && newMinutesFromInput <= 59;
92
141
 
93
- if (newMinutesFromInput > 59) {
94
- newMinutes = 0;
142
+ setMinuteError(!isValid);
143
+
144
+ if (!isValid) {
145
+ return;
95
146
  }
147
+
96
148
  onChange({
97
149
  hours: hours,
98
- minutes: newMinutes,
150
+ minutes: newMinutesFromInput,
99
151
  });
100
152
  },
101
153
  [hours, onChange],
102
154
  );
103
155
 
104
156
  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} />
157
+ <View style={timePickerInputsStyles.wrapper}>
158
+ <View style={timePickerInputsStyles.inputContainer}>
159
+ <TimeInput
160
+ ref={startInput}
161
+ placeholder={''}
162
+ value={toHourInputFormat(hours, is24Hour)}
163
+ clockType={clockTypes.hours}
164
+ pressed={focused === clockTypes.hours}
165
+ onPress={onFocusInput}
166
+ inputType={inputType}
167
+ returnKeyType={'next'}
168
+ onSubmitEditing={onSubmitStartInput}
169
+ blurOnSubmit={false}
170
+ error={hourError}
171
+ onChanged={onHourChange}
172
+ // onChangeText={onChangeStartInput}
173
+ />
174
+ <View style={timePickerInputsStyles.hoursAndMinutesSeparator}>
175
+ <View style={timePickerInputsStyles.spaceDot} />
176
+ <View style={timePickerInputsStyles.dot} />
177
+ <View style={timePickerInputsStyles.betweenDot} />
178
+ <View style={timePickerInputsStyles.dot} />
179
+ <View style={timePickerInputsStyles.spaceDot} />
180
+ </View>
181
+ <TimeInput
182
+ ref={endInput}
183
+ placeholder={'00'}
184
+ value={minutes}
185
+ clockType={clockTypes.minutes}
186
+ pressed={focused === clockTypes.minutes}
187
+ onPress={onFocusInput}
188
+ inputType={inputType}
189
+ error={minuteError}
190
+ onSubmitEditing={onSubmitEndInput}
191
+ onChanged={onMinuteChange}
192
+ />
193
+ {!is24Hour && (
194
+ <>
195
+ <View style={timePickerInputsStyles.spaceBetweenInputsAndSwitcher} />
196
+ <AmPmSwitcher hours={hours} onChange={onChangeHours} />
197
+ </>
198
+ )}
126
199
  </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
- )}
200
+ {inputType === 'keyboard' ? (
201
+ <View style={timePickerInputsStyles.supportingRow}>
202
+ <View style={timePickerInputsStyles.supportingSlot}>
203
+ <Text
204
+ style={[
205
+ timePickerInputsStyles.supportingText,
206
+ hourError ? timePickerInputsStyles.supportingTextError : null,
207
+ ]}>
208
+ {hourError ? hourErrorText : 'Hour'}
209
+ </Text>
210
+ </View>
211
+ <View style={timePickerInputsStyles.hoursAndMinutesSeparator} />
212
+ <View style={timePickerInputsStyles.supportingSlot}>
213
+ <Text
214
+ style={[
215
+ timePickerInputsStyles.supportingText,
216
+ minuteError ? timePickerInputsStyles.supportingTextError : null,
217
+ ]}>
218
+ {minuteError ? minuteErrorText : 'Minute'}
219
+ </Text>
220
+ </View>
221
+ {!is24Hour && (
222
+ <View style={timePickerInputsStyles.spaceBetweenInputsAndSwitcher} />
223
+ )}
224
+ </View>
225
+ ) : null}
144
226
  </View>
145
227
  );
146
228
  }
@@ -121,6 +121,11 @@ function TimePicker({
121
121
 
122
122
  if (newDisplayMode !== displayMode) setDisplayMode(newDisplayMode);
123
123
 
124
+ if (params.focused) {
125
+ const nextFocused = params.focused;
126
+ setTimeout(() => onFocusInput(nextFocused), 300);
127
+ }
128
+
124
129
  onTimeChange?.({
125
130
  time: `${`${params.hours}`.padStart(2, '0')}:${`${params.minutes}`.padStart(
126
131
  2,
@@ -129,7 +134,7 @@ function TimePicker({
129
134
  focused: params.focused,
130
135
  });
131
136
  },
132
- [displayMode, onTimeChange],
137
+ [displayMode, onFocusInput, onTimeChange],
133
138
  );
134
139
 
135
140
  const memoizedValue = useMemo(
@@ -41,6 +41,9 @@ const timePickerStylesDefault = StyleSheet.create(theme => ({
41
41
 
42
42
  const timePickerInputsStylesDefault = StyleSheet.create(theme => ({
43
43
  spaceBetweenInputsAndSwitcher: { width: 12 },
44
+ wrapper: {
45
+ alignItems: 'center',
46
+ },
44
47
  inputContainer: {
45
48
  flexDirection: 'row',
46
49
  alignItems: 'center',
@@ -70,6 +73,27 @@ const timePickerInputsStylesDefault = StyleSheet.create(theme => ({
70
73
  betweenDot: {
71
74
  height: 12,
72
75
  },
76
+ supportingRow: {
77
+ flexDirection: 'row',
78
+ alignItems: 'flex-start',
79
+ width: '100%',
80
+ marginTop: 2,
81
+ },
82
+ supportingSlot: {
83
+ width: 96,
84
+ minHeight: theme.typescale.bodyMedium.lineHeight * 2,
85
+ },
86
+ supportingText: {
87
+ ...theme.typescale.bodyMedium,
88
+ fontSize: 12,
89
+ lineHeight: 16,
90
+ color: theme.colors.onSurfaceVariant,
91
+ textAlign: 'left',
92
+ paddingHorizontal: theme.spacings['1'],
93
+ },
94
+ supportingTextError: {
95
+ color: theme.colors.error,
96
+ },
73
97
  }));
74
98
 
75
99
  const timePickerInputStylesDefault = StyleSheet.create(theme => ({
@@ -91,6 +115,10 @@ const timePickerInputStylesDefault = StyleSheet.create(theme => ({
91
115
  color: theme.colors.onSurface,
92
116
  borderRadius: theme.shapes.corner.small,
93
117
 
118
+ _web: {
119
+ outline: 'none',
120
+ },
121
+
94
122
  variants: {
95
123
  state: {
96
124
  highlighted: {
@@ -100,6 +128,20 @@ const timePickerInputStylesDefault = StyleSheet.create(theme => ({
100
128
  },
101
129
  },
102
130
  },
131
+ keyboardInput: {
132
+ borderWidth: 2,
133
+ borderColor: 'transparent',
134
+ },
135
+ keyboardInputHighlighted: {
136
+ borderColor: theme.colors.primary,
137
+ },
138
+ inputError: {
139
+ backgroundColor: theme.colors.errorContainer,
140
+ color: theme.colors.onErrorContainer,
141
+ },
142
+ keyboardInputError: {
143
+ borderColor: theme.colors.error,
144
+ },
103
145
  button: {
104
146
  overflow: 'hidden',
105
147
  borderRadius: theme.shapes.corner.small,
@@ -253,6 +295,7 @@ const timePickerModalStylesDefault = StyleSheet.create(theme => ({
253
295
  timePickerContainer: {
254
296
  padding: theme.spacings['6'],
255
297
  paddingTop: theme.spacings['2'],
298
+ paddingBottom: 0,
256
299
  },
257
300
  footer: {
258
301
  flexDirection: 'row',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-molecules",
3
- "version": "0.5.0-beta.18",
3
+ "version": "0.5.0-beta.19",
4
4
  "author": "Thet Aung <thetaung.dev@gmail.com>",
5
5
  "license": "MIT",
6
6
  "main": "index.ts",