@momo-kits/foundation 0.151.1-beta.1 → 0.151.1-beta.2
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.
- package/Application/BottomSheet.tsx +15 -1
- package/Application/types.ts +4 -0
- package/Assets/language.json +6 -2
- package/CheckBox/index.tsx +1 -1
- package/CheckBox/styles.ts +1 -0
- package/Input/InputPhoneNumber.tsx +274 -0
- package/Input/index.tsx +21 -1
- package/Input/styles.ts +4 -0
- package/Input/utils.ts +42 -1
- package/package.json +1 -1
|
@@ -49,6 +49,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
49
49
|
keyboardVerticalOffset,
|
|
50
50
|
useDivider = true,
|
|
51
51
|
footerComponent,
|
|
52
|
+
leftOptions,
|
|
52
53
|
}: BottomSheetParams = props.route.params;
|
|
53
54
|
|
|
54
55
|
const translateY = useSharedValue(heightDevice);
|
|
@@ -167,11 +168,23 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
167
168
|
},
|
|
168
169
|
]}
|
|
169
170
|
>
|
|
171
|
+
{leftOptions?.onPressIconLeft ? (
|
|
172
|
+
<TouchableOpacity
|
|
173
|
+
style={styles.iconButton}
|
|
174
|
+
onPress={() => {
|
|
175
|
+
onDismiss(true, leftOptions.onPressIconLeft);
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
<Icon source={leftOptions.iconLeft ?? 'ic_back'} />
|
|
179
|
+
</TouchableOpacity>
|
|
180
|
+
) : (
|
|
181
|
+
<View style={styles.iconButton} />
|
|
182
|
+
)}
|
|
183
|
+
|
|
170
184
|
{options.header ? (
|
|
171
185
|
<View style={Styles.flex}>{options.header}</View>
|
|
172
186
|
) : (
|
|
173
187
|
<>
|
|
174
|
-
<View style={styles.iconButton} />
|
|
175
188
|
<Text
|
|
176
189
|
style={[Styles.flex, { textAlign: 'center' }]}
|
|
177
190
|
numberOfLines={1}
|
|
@@ -195,6 +208,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
195
208
|
);
|
|
196
209
|
}, [
|
|
197
210
|
onDismiss,
|
|
211
|
+
leftOptions,
|
|
198
212
|
options.header,
|
|
199
213
|
options.title,
|
|
200
214
|
panResponder.panHandlers,
|
package/Application/types.ts
CHANGED
|
@@ -167,6 +167,10 @@ export type BottomSheetParams = {
|
|
|
167
167
|
keyboardVerticalOffset?: number;
|
|
168
168
|
useDivider?: boolean;
|
|
169
169
|
footerComponent?: React.ReactNode;
|
|
170
|
+
leftOptions?: {
|
|
171
|
+
iconLeft?: string;
|
|
172
|
+
onPressIconLeft?: () => void;
|
|
173
|
+
};
|
|
170
174
|
};
|
|
171
175
|
|
|
172
176
|
export interface NavigationButtonProps extends TouchableOpacityProps {
|
package/Assets/language.json
CHANGED
|
@@ -98,7 +98,9 @@
|
|
|
98
98
|
"tutorial": "Hướng dẫn sử dụng",
|
|
99
99
|
"question": "Câu hỏi thường gặp",
|
|
100
100
|
"support": "Trung tâm hỗ trợ",
|
|
101
|
-
"skip": "Bỏ qua"
|
|
101
|
+
"skip": "Bỏ qua",
|
|
102
|
+
"enterPhoneNumber": "Vui lòng nhập số điện thoại",
|
|
103
|
+
"invalidPhoneNumber": "Số điện thoại không đúng"
|
|
102
104
|
},
|
|
103
105
|
"en": {
|
|
104
106
|
"seeMore": "See more",
|
|
@@ -199,6 +201,8 @@
|
|
|
199
201
|
"question": "FAQ",
|
|
200
202
|
"support": "Support center",
|
|
201
203
|
"errorCode": "Error code: ",
|
|
202
|
-
"skip": "Skip"
|
|
204
|
+
"skip": "Skip",
|
|
205
|
+
"enterPhoneNumber": "Please enter your phone number",
|
|
206
|
+
"invalidPhoneNumber": "Invalid phone number"
|
|
203
207
|
}
|
|
204
208
|
}
|
package/CheckBox/index.tsx
CHANGED
|
@@ -80,7 +80,7 @@ const CheckBox: FC<CheckBoxProps> = ({
|
|
|
80
80
|
/>
|
|
81
81
|
)}
|
|
82
82
|
</View>
|
|
83
|
-
{!!label && <Text typography={'body_default_regular'}>{label}</Text>}
|
|
83
|
+
{!!label && <Text typography={'body_default_regular'} style={styles.label}>{label}</Text>}
|
|
84
84
|
</TouchableOpacity>
|
|
85
85
|
</ComponentContext.Provider>
|
|
86
86
|
);
|
package/CheckBox/styles.ts
CHANGED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useContext,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import {
|
|
9
|
+
NativeSyntheticEvent,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
TextInput,
|
|
12
|
+
TextInputFocusEventData,
|
|
13
|
+
TouchableOpacity,
|
|
14
|
+
View,
|
|
15
|
+
} from 'react-native';
|
|
16
|
+
import {
|
|
17
|
+
ApplicationContext,
|
|
18
|
+
ComponentContext,
|
|
19
|
+
useComponentId,
|
|
20
|
+
} from '../Application';
|
|
21
|
+
import { Colors, Spacing, Styles } from '../Consts';
|
|
22
|
+
import { Icon } from '../Icon';
|
|
23
|
+
import { scaleSize, Text } from '../Text';
|
|
24
|
+
import {
|
|
25
|
+
ErrorView,
|
|
26
|
+
getBorderColor,
|
|
27
|
+
getSizeStyle,
|
|
28
|
+
RenderTrailing,
|
|
29
|
+
} from './common';
|
|
30
|
+
import { InputPhoneNumberProps } from './index';
|
|
31
|
+
import styles from './styles';
|
|
32
|
+
import SystemTextInput from './SystemTextInput';
|
|
33
|
+
import { checkTyping } from './utils';
|
|
34
|
+
import { Image } from '../Image';
|
|
35
|
+
import { Typography } from '../Text/types';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Input default component
|
|
39
|
+
*/
|
|
40
|
+
const InputPhoneNumber = forwardRef(
|
|
41
|
+
(
|
|
42
|
+
{
|
|
43
|
+
value,
|
|
44
|
+
onChangeText,
|
|
45
|
+
size = 'small',
|
|
46
|
+
placeholder = '0123456789',
|
|
47
|
+
onBlur,
|
|
48
|
+
onFocus,
|
|
49
|
+
errorMessage,
|
|
50
|
+
trailing,
|
|
51
|
+
trailingColor,
|
|
52
|
+
onPressTrailing,
|
|
53
|
+
disabled = false,
|
|
54
|
+
errorSpacing,
|
|
55
|
+
loading = false,
|
|
56
|
+
secureTextEntry,
|
|
57
|
+
keyboardType,
|
|
58
|
+
style,
|
|
59
|
+
params,
|
|
60
|
+
hintText,
|
|
61
|
+
editable = true,
|
|
62
|
+
showClearIcon = true,
|
|
63
|
+
...props
|
|
64
|
+
}: InputPhoneNumberProps,
|
|
65
|
+
ref,
|
|
66
|
+
) => {
|
|
67
|
+
const { theme } = useContext(ApplicationContext);
|
|
68
|
+
const [focused, setFocused] = useState(false);
|
|
69
|
+
const [haveValue, setHaveValue] = useState(!!value || !!props.defaultValue);
|
|
70
|
+
const inputRef = useRef<TextInput | null>(null);
|
|
71
|
+
const componentName = 'InputPhoneNumber';
|
|
72
|
+
|
|
73
|
+
const { componentId } = useComponentId(
|
|
74
|
+
`${componentName}/${placeholder}`,
|
|
75
|
+
props.accessibilityLabel,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const onClearText = () => {
|
|
79
|
+
inputRef?.current?.clear();
|
|
80
|
+
_onChangeText('');
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const _onChangeText = (text: string) => {
|
|
84
|
+
checkTyping(text, haveValue, setHaveValue);
|
|
85
|
+
onChangeText?.(text);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
|
89
|
+
setFocused(true);
|
|
90
|
+
onFocus?.(e);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const _onBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
|
94
|
+
setFocused(false);
|
|
95
|
+
onBlur?.(e);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const _setText = (text: string) => {
|
|
99
|
+
inputRef?.current?.setNativeProps({ text });
|
|
100
|
+
_onChangeText(text);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
useImperativeHandle(ref, () => {
|
|
104
|
+
return {
|
|
105
|
+
clear: onClearText,
|
|
106
|
+
focus: () => inputRef.current?.focus(),
|
|
107
|
+
blur: () => inputRef.current?.blur(),
|
|
108
|
+
setText: _setText,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let inputTextStyles = {};
|
|
113
|
+
let dividerHeight = 24;
|
|
114
|
+
let typography: Typography;
|
|
115
|
+
if (size === 'small') {
|
|
116
|
+
inputTextStyles = { fontSize: scaleSize(14), fontWeight: '600' };
|
|
117
|
+
dividerHeight = 24;
|
|
118
|
+
typography = 'header_s_semibold';
|
|
119
|
+
} else {
|
|
120
|
+
inputTextStyles = { fontSize: scaleSize(18), fontWeight: '700' };
|
|
121
|
+
dividerHeight = 32;
|
|
122
|
+
typography = 'header_m_bold';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Render input view
|
|
127
|
+
*/
|
|
128
|
+
const renderInputView = () => {
|
|
129
|
+
const disabledColor = theme.colors.text.disable;
|
|
130
|
+
const secure = secureTextEntry;
|
|
131
|
+
let textColor = theme.colors.text.default;
|
|
132
|
+
let placeholderColor = theme.colors.text.hint;
|
|
133
|
+
let iconTintColor = trailingColor ?? theme.colors.text.default;
|
|
134
|
+
const borderWidth = focused ? 1.5 : 1;
|
|
135
|
+
|
|
136
|
+
if (disabled) {
|
|
137
|
+
textColor = disabledColor;
|
|
138
|
+
placeholderColor = disabledColor;
|
|
139
|
+
iconTintColor = disabledColor;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<View
|
|
144
|
+
style={[
|
|
145
|
+
styles.inputWrapper,
|
|
146
|
+
{ backgroundColor: theme.colors.background.surface },
|
|
147
|
+
getSizeStyle(size),
|
|
148
|
+
{ borderWidth },
|
|
149
|
+
getBorderColor(theme, focused, errorMessage, disabled),
|
|
150
|
+
]}
|
|
151
|
+
>
|
|
152
|
+
<View style={styles.inputView}>
|
|
153
|
+
<View style={[styles.leadingIconContainer, Styles.row]}>
|
|
154
|
+
<Image
|
|
155
|
+
source={{
|
|
156
|
+
uri: 'https://static.momocdn.net/app/img/icon/ic-qrcode-package/ic_vn_flag.png',
|
|
157
|
+
}}
|
|
158
|
+
style={innerStyle.iconFlag}
|
|
159
|
+
resizeMode="contain"
|
|
160
|
+
accessibilityLabel={`${componentId}|leading-icon`}
|
|
161
|
+
/>
|
|
162
|
+
<View style={{ width: Spacing.XS }} />
|
|
163
|
+
<Text typography={typography} color={textColor}>
|
|
164
|
+
{'+84'}
|
|
165
|
+
</Text>
|
|
166
|
+
<View
|
|
167
|
+
style={[
|
|
168
|
+
styles.phoneNumberDivider,
|
|
169
|
+
{
|
|
170
|
+
height: dividerHeight,
|
|
171
|
+
backgroundColor: Colors.black_04,
|
|
172
|
+
},
|
|
173
|
+
]}
|
|
174
|
+
/>
|
|
175
|
+
</View>
|
|
176
|
+
<SystemTextInput
|
|
177
|
+
{...props}
|
|
178
|
+
accessibilityLabel={`${componentId}`}
|
|
179
|
+
editable={editable && !disabled}
|
|
180
|
+
secureTextEntry={secure}
|
|
181
|
+
textAlignVertical="center"
|
|
182
|
+
ref={inputRef}
|
|
183
|
+
accessibilityState={{
|
|
184
|
+
disabled,
|
|
185
|
+
...props.accessibilityState,
|
|
186
|
+
}}
|
|
187
|
+
style={[
|
|
188
|
+
styles.input,
|
|
189
|
+
{
|
|
190
|
+
color: textColor,
|
|
191
|
+
},
|
|
192
|
+
inputTextStyles,
|
|
193
|
+
secure && haveValue && { fontSize: size === 'small' ? 18 : 22 },
|
|
194
|
+
]}
|
|
195
|
+
allowFontScaling={false}
|
|
196
|
+
textBreakStrategy="highQuality"
|
|
197
|
+
value={value}
|
|
198
|
+
onChangeText={_onChangeText}
|
|
199
|
+
onFocus={_onFocus}
|
|
200
|
+
onBlur={_onBlur}
|
|
201
|
+
placeholder={placeholder}
|
|
202
|
+
selectionColor={theme.colors.primary}
|
|
203
|
+
placeholderTextColor={placeholderColor}
|
|
204
|
+
keyboardType={keyboardType ?? 'number-pad'}
|
|
205
|
+
/>
|
|
206
|
+
</View>
|
|
207
|
+
<View style={styles.iconView}>
|
|
208
|
+
{showClearIcon && focused && haveValue && (
|
|
209
|
+
<TouchableOpacity
|
|
210
|
+
accessibilityLabel={`${componentId}|clear-icon-touch`}
|
|
211
|
+
accessible={false}
|
|
212
|
+
style={styles.iconWrapper}
|
|
213
|
+
onPress={onClearText}
|
|
214
|
+
>
|
|
215
|
+
<Icon
|
|
216
|
+
source="24_navigation_close_circle_full"
|
|
217
|
+
size={16}
|
|
218
|
+
color={theme.colors.text.hint}
|
|
219
|
+
accessibilityLabel={`${componentId}|clear-icon`}
|
|
220
|
+
/>
|
|
221
|
+
</TouchableOpacity>
|
|
222
|
+
)}
|
|
223
|
+
<RenderTrailing
|
|
224
|
+
color={iconTintColor}
|
|
225
|
+
icon={trailing}
|
|
226
|
+
onPressIcon={onPressTrailing}
|
|
227
|
+
loading={loading}
|
|
228
|
+
componentId={componentId}
|
|
229
|
+
/>
|
|
230
|
+
</View>
|
|
231
|
+
</View>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
let inputState = 'active';
|
|
236
|
+
if (value && value?.length > 0) {
|
|
237
|
+
inputState = 'filled';
|
|
238
|
+
}
|
|
239
|
+
if (errorMessage && errorMessage?.length > 0) {
|
|
240
|
+
inputState = 'error';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<ComponentContext.Provider
|
|
245
|
+
value={{
|
|
246
|
+
componentName,
|
|
247
|
+
componentId,
|
|
248
|
+
params,
|
|
249
|
+
state: inputState,
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
<View style={[style, styles.wrapper]}>
|
|
253
|
+
{renderInputView()}
|
|
254
|
+
<ErrorView
|
|
255
|
+
errorMessage={errorMessage}
|
|
256
|
+
errorSpacing={errorSpacing}
|
|
257
|
+
hintText={hintText}
|
|
258
|
+
componentId={componentId}
|
|
259
|
+
/>
|
|
260
|
+
</View>
|
|
261
|
+
</ComponentContext.Provider>
|
|
262
|
+
);
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const innerStyle = StyleSheet.create({
|
|
267
|
+
iconFlag: {
|
|
268
|
+
width: 24,
|
|
269
|
+
height: 24,
|
|
270
|
+
marginHorizontal: 2,
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
export default InputPhoneNumber;
|
package/Input/index.tsx
CHANGED
|
@@ -9,6 +9,8 @@ import InputMoney from './InputMoney';
|
|
|
9
9
|
import InputOTP from './InputOTP';
|
|
10
10
|
import InputSearch from './InputSearch';
|
|
11
11
|
import InputTextArea from './InputTextArea';
|
|
12
|
+
import InputPhoneNumber from './InputPhoneNumber';
|
|
13
|
+
import { checkValidPhoneNumber } from './utils';
|
|
12
14
|
|
|
13
15
|
export type OTPInputLength = 2 | 4 | 6 | 8 | 10;
|
|
14
16
|
|
|
@@ -343,6 +345,14 @@ export interface InputDropDownProps extends TouchableOpacityProps {
|
|
|
343
345
|
onPressFloatingIcon?: () => void;
|
|
344
346
|
}
|
|
345
347
|
|
|
348
|
+
export interface InputPhoneNumberProps extends InputProps {
|
|
349
|
+
/**
|
|
350
|
+
* Optional. Represents the country code to be displayed in the InputPhoneNumber component.
|
|
351
|
+
* It should be in the format of a string, e.g., "+1" for the United States.
|
|
352
|
+
*/
|
|
353
|
+
countryCode?: string;
|
|
354
|
+
}
|
|
355
|
+
|
|
346
356
|
export type InputRef = {
|
|
347
357
|
clear: () => void;
|
|
348
358
|
focus: () => void | undefined;
|
|
@@ -350,4 +360,14 @@ export type InputRef = {
|
|
|
350
360
|
setText: (text: string) => void;
|
|
351
361
|
};
|
|
352
362
|
|
|
353
|
-
export {
|
|
363
|
+
export {
|
|
364
|
+
Input,
|
|
365
|
+
InputDropDown,
|
|
366
|
+
InputMoney,
|
|
367
|
+
InputOTP,
|
|
368
|
+
InputSearch,
|
|
369
|
+
InputTextArea,
|
|
370
|
+
InputPhoneNumber,
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
export {checkValidPhoneNumber};
|
package/Input/styles.ts
CHANGED
package/Input/utils.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Localize } from '../Application';
|
|
2
|
+
|
|
1
3
|
const formatNumberToMoney = (number: number, currency = '') => {
|
|
2
4
|
if (!number || isNaN(number) || Number(number) === 0) {
|
|
3
5
|
return `0${currency}`;
|
|
@@ -64,4 +66,43 @@ const checkTyping = (text: string, value: boolean, setValue: any) => {
|
|
|
64
66
|
}
|
|
65
67
|
};
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
// --- PHONE VALIDATION ---
|
|
70
|
+
function formatPhoneNumber(phone: string): string {
|
|
71
|
+
return phone.replace(/\D/g, '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checkValidPhoneNumber(phone: string): {
|
|
75
|
+
phoneFormatted: string;
|
|
76
|
+
error?: string;
|
|
77
|
+
} {
|
|
78
|
+
let phoneNumber = phone;
|
|
79
|
+
|
|
80
|
+
// if not starting with 0 and has 9 digits => prepend 0
|
|
81
|
+
if (
|
|
82
|
+
phoneNumber.length > 0 &&
|
|
83
|
+
phoneNumber[0] !== '0' &&
|
|
84
|
+
/^\d$/.test(phoneNumber[0]) &&
|
|
85
|
+
phoneNumber.length === 9
|
|
86
|
+
) {
|
|
87
|
+
phoneNumber = '0' + phoneNumber;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const phoneFormatted = formatPhoneNumber(phoneNumber);
|
|
91
|
+
const localize = new Localize({ vi: {}, en: {} });
|
|
92
|
+
let error: string | undefined;
|
|
93
|
+
|
|
94
|
+
if (phoneFormatted.length === 0) {
|
|
95
|
+
error = localize.translate('enterPhoneNumber');
|
|
96
|
+
} else if (phoneFormatted.length < 10 || phoneFormatted.length > 11) {
|
|
97
|
+
error = localize.translate('invalidPhoneNumber');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { phoneFormatted: phoneFormatted, error };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
formatMoneyToNumber,
|
|
105
|
+
formatNumberToMoney,
|
|
106
|
+
checkTyping,
|
|
107
|
+
checkValidPhoneNumber,
|
|
108
|
+
};
|