@momo-kits/foundation 0.79.6 → 0.80.2-beta.1

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.
@@ -3,11 +3,13 @@ import {
3
3
  Alert,
4
4
  Animated,
5
5
  DeviceEventEmitter,
6
+ Platform,
6
7
  StatusBar,
7
8
  StyleSheet,
8
- TouchableOpacity,
9
9
  View,
10
+ TouchableOpacity as TouchableRN,
10
11
  } from 'react-native';
12
+ import {TouchableOpacity as TouchableGesture} from 'react-native-gesture-handler';
11
13
  import {ApplicationContext} from './index';
12
14
  import {Colors, Radius, Spacing, Styles} from '../Consts';
13
15
  import {Image} from '../Image';
@@ -22,6 +24,8 @@ import {scaleSize, Text} from '../Text';
22
24
  import {Icon} from '../Icon';
23
25
  import {PopupNotify} from '../Popup';
24
26
 
27
+ const Touchable = Platform.OS === 'android' ? TouchableGesture : TouchableRN;
28
+
25
29
  const styles = StyleSheet.create({
26
30
  navigationButton: {
27
31
  height: 28,
@@ -101,7 +105,7 @@ const NavigationButton: React.FC<NavigationButtonProps> = ({
101
105
  }
102
106
 
103
107
  return (
104
- <TouchableOpacity
108
+ <Touchable
105
109
  accessibilityLabel={`NavigationButton/${accessibilityLabel}`}
106
110
  style={[
107
111
  styles.navigationButton,
@@ -116,7 +120,7 @@ const NavigationButton: React.FC<NavigationButtonProps> = ({
116
120
  color={tintColor ?? theme.colors.text.default}
117
121
  size={20}
118
122
  />
119
- </TouchableOpacity>
123
+ </Touchable>
120
124
  );
121
125
  };
122
126
 
@@ -225,7 +229,11 @@ const HeaderBackground: React.FC<HeaderBackgroundProps> = ({
225
229
  barStyle={headerImage || theme.dark ? 'light-content' : 'dark-content'}
226
230
  />
227
231
  {!!headerImage && (
228
- <Image style={styles.headerBackground} source={{uri: headerImage}} />
232
+ <Image
233
+ style={styles.headerBackground}
234
+ source={{uri: headerImage}}
235
+ loading={false}
236
+ />
229
237
  )}
230
238
  </Animated.View>
231
239
  );
@@ -388,21 +396,17 @@ const HeaderToolkitAction: React.FC<any> = ({tintColor}) => {
388
396
  borderWidth: backgroundColor ? 0.5 : 0,
389
397
  },
390
398
  ]}>
391
- <TouchableOpacity
392
- accessibilityLabel={'Toolkit/More'}
393
- onPress={onMore}>
399
+ <Touchable accessibilityLabel={'Toolkit/More'} onPress={onMore}>
394
400
  <Icon color={tintColor} source="navigation_more_horiz" size={20} />
395
- </TouchableOpacity>
401
+ </Touchable>
396
402
  <View style={[styles.divider, {backgroundColor: tintColor}]} />
397
- <TouchableOpacity
398
- accessibilityLabel={`Toolkit/Close`}
399
- onPress={onClose}>
403
+ <Touchable accessibilityLabel={`Toolkit/Close`} onPress={onClose}>
400
404
  <Icon
401
405
  color={tintColor}
402
406
  source="16_navigation_close_circle"
403
407
  size={20}
404
408
  />
405
- </TouchableOpacity>
409
+ </Touchable>
406
410
  </View>
407
411
  </View>
408
412
  );
@@ -1,20 +1,18 @@
1
- import React, {
2
- useCallback,
3
- useContext,
4
- useEffect,
5
- useMemo,
6
- useRef,
7
- useState,
8
- } from 'react';
1
+ import React, {useContext, useEffect, useMemo, useRef} from 'react';
9
2
  import {
10
3
  KeyboardAvoidingView,
11
4
  Platform,
12
5
  Pressable,
13
6
  StyleSheet,
14
7
  View,
8
+ Keyboard,
15
9
  } from 'react-native';
16
- import {SafeAreaView} from 'react-native-safe-area-context';
17
- import {BottomSheetBackdrop, BottomSheetModal} from '@gorhom/bottom-sheet';
10
+ import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
11
+ import {
12
+ BottomSheetBackdrop,
13
+ BottomSheetModal,
14
+ BottomSheetView,
15
+ } from '@gorhom/bottom-sheet';
18
16
  import Navigation from './Navigation';
19
17
  import {BottomSheetParams, ModalParams} from './types';
20
18
  import {Radius, Spacing, Styles} from '../Consts';
@@ -90,12 +88,9 @@ const Modal: React.FC<ModalParams> = props => {
90
88
  };
91
89
 
92
90
  const BottomSheet: React.FC<BottomSheetParams> = props => {
91
+ const insets = useSafeAreaInsets();
93
92
  const {theme, navigator} = useContext(ApplicationContext);
94
- const {
95
- screen,
96
- options,
97
- useSnap = true,
98
- }: BottomSheetParams = props.route.params;
93
+ const {screen, options, snapPoint}: BottomSheetParams = props.route.params;
99
94
  const Component = useRef(screen).current;
100
95
  const navigation = new Navigation(props.navigation, theme);
101
96
  const params = {
@@ -105,40 +100,30 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
105
100
 
106
101
  delete params.screen;
107
102
 
108
- const [contentHeight, setContentHeight] = useState(undefined);
109
103
  const snapPoints = useMemo(() => {
110
- if (useSnap) {
111
- return ['50%', '90%'];
104
+ if (snapPoint) {
105
+ return [snapPoint];
112
106
  }
113
- return [contentHeight ?? '50%'];
114
- }, [contentHeight]);
107
+ return ['50%', '70%', '90%'];
108
+ }, []);
115
109
 
116
110
  const bottomSheetRef = useRef<any>(null);
117
111
  const mountedRef = useRef(true);
118
112
 
119
- let handleOnLayout;
120
- if (!useSnap) {
121
- handleOnLayout = useCallback(
122
- ({
123
- nativeEvent: {
124
- layout: {height},
125
- },
126
- }) => {
127
- setContentHeight(height);
128
- },
129
- [],
130
- );
131
- }
132
-
133
113
  let backgroundColor = theme.colors.background.default;
134
114
  if (options?.surface) {
135
115
  backgroundColor = theme.colors.background.surface;
136
116
  }
137
117
 
138
118
  useEffect(() => {
119
+ const keyboardDidShow = Keyboard.addListener('keyboardDidShow', () =>
120
+ bottomSheetRef.current?.expand(),
121
+ );
122
+
139
123
  bottomSheetRef.current?.present();
140
124
  return () => {
141
125
  mountedRef.current = false;
126
+ keyboardDidShow.remove();
142
127
  };
143
128
  }, []);
144
129
 
@@ -162,8 +147,15 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
162
147
  }}
163
148
  handleComponent={null}
164
149
  backdropComponent={backdropComponent}>
165
- <View onLayout={handleOnLayout} accessibilityLabel={'BottomSheet'}>
166
- <View style={[styles.sheetContainer, {backgroundColor}]}>
150
+ <BottomSheetView accessibilityLabel={'BottomSheet'}>
151
+ <View
152
+ style={[
153
+ styles.sheetContainer,
154
+ {
155
+ backgroundColor,
156
+ paddingBottom: Math.min(insets.bottom, 20) + Spacing.S,
157
+ },
158
+ ]}>
167
159
  <View style={styles.indicatorContainer}>
168
160
  <View
169
161
  style={[
@@ -222,7 +214,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
222
214
  }}
223
215
  />
224
216
  </View>
225
- </View>
217
+ </BottomSheetView>
226
218
  </BottomSheetModal>
227
219
  </SafeAreaView>
228
220
  );
@@ -255,11 +247,12 @@ const styles = StyleSheet.create({
255
247
  indicatorContainer: {
256
248
  width: '100%',
257
249
  paddingTop: Spacing.S,
250
+ paddingBottom: Spacing.XS,
258
251
  alignItems: 'center',
259
252
  justifyContent: 'center',
260
253
  },
261
254
  indicator: {
262
- width: 64,
255
+ width: 40,
263
256
  height: 4,
264
257
  borderRadius: Radius.S,
265
258
  },
@@ -10,9 +10,10 @@ import StackScreen from './StackScreen';
10
10
  import ModalScreen from './ModalScreen';
11
11
  import Navigator from './Navigator';
12
12
  import {getDialogOptions, getModalOptions, getStackOptions} from './utils';
13
- import {NavigationContainerProps} from './types';
13
+ import {LocalizationObject, NavigationContainerProps} from './types';
14
14
  import {ApplicationContext} from './index';
15
15
  import {DeviceEventEmitter} from 'react-native';
16
+ import kitLocalization from '../Assets/language.json';
16
17
 
17
18
  const Stack = createStackNavigator();
18
19
 
@@ -41,6 +42,9 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
41
42
  }, []);
42
43
 
43
44
  useEffect(() => {
45
+ maxApi?.getLanguage?.((lang: 'vi' | 'en') => {
46
+ setLanguage(lang);
47
+ });
44
48
  const listener = maxApi?.observer?.('language', (lang: 'vi' | 'en') => {
45
49
  setLanguage(lang);
46
50
  });
@@ -52,7 +56,11 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
52
56
  navigator.current.maxApi = maxApi;
53
57
 
54
58
  const translate = (key: string) => {
55
- return localization?.[language]?.[key] || key;
59
+ const mergedLocalization: LocalizationObject = {
60
+ vi: {...localization?.vi, ...kitLocalization.vi},
61
+ en: {...localization?.en, ...kitLocalization.en},
62
+ };
63
+ return mergedLocalization?.[language]?.[key] || key;
56
64
  };
57
65
 
58
66
  return (
@@ -1,4 +1,4 @@
1
- import React, {useContext, useEffect} from 'react';
1
+ import React, {useContext, useLayoutEffect} from 'react';
2
2
  import {ScreenParams} from './types';
3
3
  import Navigation from './Navigation';
4
4
  import {ApplicationContext} from './index';
@@ -16,7 +16,7 @@ const StackScreen: React.FC<ScreenParams> = props => {
16
16
 
17
17
  delete params.screen;
18
18
 
19
- useEffect(() => {
19
+ useLayoutEffect(() => {
20
20
  if (options) {
21
21
  navigation.setOptions(options);
22
22
  }
@@ -107,7 +107,7 @@ export interface ModalParams extends ScreenParams {
107
107
  export interface BottomSheetParams extends ScreenParams {
108
108
  [key: string]: any;
109
109
  onDismiss?: () => void;
110
- useSnap?: boolean;
110
+ snapPoint?: '30%' | '50%' | '70%' | '90%';
111
111
  }
112
112
 
113
113
  export type NavigationButtonProps = {
package/Consts/theme.ts CHANGED
@@ -117,6 +117,9 @@ const defaultContext: Context = {
117
117
  theme: defaultTheme,
118
118
  navigator: undefined,
119
119
  showGrid: false,
120
+ translate: (key: string) => {
121
+ return key;
122
+ },
120
123
  };
121
124
 
122
125
  export {defaultContext, defaultTheme, defaultDarkTheme};
package/Input/Input.tsx CHANGED
@@ -13,7 +13,6 @@ import {InputProps} from './index';
13
13
  import {Icon} from '../Icon';
14
14
  import {checkTyping} from './utils';
15
15
  import {Loader} from '../Loader';
16
- import {Image} from '../Image';
17
16
 
18
17
  const Input = forwardRef(
19
18
  (
@@ -34,7 +33,8 @@ const Input = forwardRef(
34
33
  required,
35
34
  errorSpacing,
36
35
  loading,
37
- image,
36
+ leadingIcon,
37
+ leadingIconColor,
38
38
  ...props
39
39
  }: InputProps,
40
40
  ref,
@@ -78,7 +78,6 @@ const Input = forwardRef(
78
78
  let textColor = theme.colors.text.default;
79
79
  let placeholderColor = theme.colors.text.hint;
80
80
  let iconTintColor = iconColor;
81
- const imageStyle = size === 'small' ? styles.imageSmall : styles.image;
82
81
 
83
82
  if (disabled) {
84
83
  textColor = disabledColor;
@@ -102,7 +101,15 @@ const Input = forwardRef(
102
101
  floatingIcon={floatingIcon}
103
102
  />
104
103
  <View style={styles.inputView}>
105
- {!!image && <Image style={imageStyle} source={{uri: image}} />}
104
+ {!!leadingIcon && (
105
+ <View style={styles.leadingIconContainer}>
106
+ <Icon
107
+ color={leadingIconColor}
108
+ source={leadingIcon}
109
+ size={size === 'small' ? 24 : 32}
110
+ />
111
+ </View>
112
+ )}
106
113
  <TextInput
107
114
  {...props}
108
115
  accessibilityLabel={`TextInput`}
@@ -0,0 +1,127 @@
1
+ import React, {forwardRef, useContext, useRef} from 'react';
2
+ import {TextInput, TouchableOpacity, View} from 'react-native';
3
+ import {ApplicationContext} from '../Application';
4
+ import styles from './styles';
5
+ import {ErrorView, FloatingView, getBorderColor, getSizeStyle} from './common';
6
+ import {InputDropDownProps} from './index';
7
+ import {Icon} from '../Icon';
8
+ import {Loader} from '../Loader';
9
+ import {Styles} from '../Consts';
10
+
11
+ const InputDropDown = forwardRef(
12
+ (
13
+ {
14
+ value,
15
+ floatingValue,
16
+ floatingIcon,
17
+ size,
18
+ onPress,
19
+ placeholder,
20
+ errorMessage,
21
+ icon = 'arrow_chevron_down_small',
22
+ disabled,
23
+ floatingIconColor,
24
+ iconColor,
25
+ required,
26
+ errorSpacing,
27
+ loading,
28
+ leadingIcon,
29
+ leadingIconColor,
30
+ ...props
31
+ }: InputDropDownProps,
32
+ ref,
33
+ ) => {
34
+ const {theme} = useContext(ApplicationContext);
35
+ const inputRef = (ref as React.RefObject<TextInput>) ?? useRef<TextInput>();
36
+ const full = !!value || !!placeholder;
37
+ const renderIcon = (color: string | undefined) => {
38
+ if (loading) {
39
+ return <Loader type={'spinner'} color={color} />;
40
+ }
41
+ if (icon) {
42
+ return <Icon color={color} source={icon} size={24} />;
43
+ }
44
+ };
45
+
46
+ const renderInputView = () => {
47
+ const disabledColor = theme.colors.text.disable;
48
+ let textColor = theme.colors.text.default;
49
+ let placeholderColor = theme.colors.text.hint;
50
+ let iconTintColor = iconColor;
51
+
52
+ if (disabled) {
53
+ textColor = disabledColor;
54
+ placeholderColor = disabledColor;
55
+ iconTintColor = disabledColor;
56
+ }
57
+
58
+ return (
59
+ <View
60
+ style={[
61
+ styles.inputWrapper,
62
+ {backgroundColor: theme.colors.background.surface},
63
+ getSizeStyle(size),
64
+ getBorderColor(theme, false, errorMessage, disabled),
65
+ ]}>
66
+ <FloatingView
67
+ floatingValue={floatingValue}
68
+ floatingIconColor={floatingIconColor}
69
+ disabled={disabled}
70
+ required={required}
71
+ floatingIcon={floatingIcon}
72
+ />
73
+ <View style={full ? styles.inputView : Styles.row}>
74
+ {!!leadingIcon && (
75
+ <View style={styles.leadingIconContainer}>
76
+ <Icon
77
+ color={leadingIconColor}
78
+ source={leadingIcon}
79
+ size={size === 'small' ? 24 : 32}
80
+ />
81
+ </View>
82
+ )}
83
+ {full && (
84
+ <TextInput
85
+ {...props}
86
+ onPressIn={onPress}
87
+ accessibilityLabel={`TextInput`}
88
+ editable={false}
89
+ textAlignVertical="top"
90
+ ref={inputRef}
91
+ style={[
92
+ styles.input,
93
+ {
94
+ color: textColor,
95
+ fontFamily: `${theme.font}-Regular`,
96
+ },
97
+ ]}
98
+ textBreakStrategy="highQuality"
99
+ value={value}
100
+ placeholder={placeholder}
101
+ selectionColor={theme.colors.primary}
102
+ placeholderTextColor={placeholderColor}
103
+ />
104
+ )}
105
+ </View>
106
+ <View style={styles.iconView}>{renderIcon(iconTintColor)}</View>
107
+ </View>
108
+ );
109
+ };
110
+
111
+ return (
112
+ <TouchableOpacity
113
+ onPress={onPress}
114
+ style={full && styles.wrapper}
115
+ accessibilityLabel={`InputDropDown/${floatingValue}`}>
116
+ {renderInputView()}
117
+ <ErrorView errorMessage={errorMessage} errorSpacing={errorSpacing} />
118
+ </TouchableOpacity>
119
+ );
120
+ },
121
+ );
122
+
123
+ InputDropDown.defaultProps = {
124
+ size: 'small',
125
+ };
126
+
127
+ export default InputDropDown;
@@ -0,0 +1,213 @@
1
+ import React, {FC, useContext, useEffect, useRef, useState} from 'react';
2
+ import {
3
+ Animated,
4
+ KeyboardType,
5
+ NativeSyntheticEvent,
6
+ TextInput,
7
+ TextInputFocusEventData,
8
+ TouchableOpacity,
9
+ View,
10
+ } from 'react-native';
11
+ import {CaretProps, InputOTPProps} from './index';
12
+ import {scaleSize, Text} from '../Text';
13
+ import styles from './styles';
14
+ import {ErrorView, getBorderColor} from './common';
15
+ import {ApplicationContext} from '../Application';
16
+ import {Spacing} from '../Consts';
17
+
18
+ const OTPCaret: FC<CaretProps> = ({index, length}) => {
19
+ const DURATION = 300;
20
+ const {theme} = useContext(ApplicationContext);
21
+ const opacity = useRef(new Animated.Value(0)).current;
22
+
23
+ useEffect(() => {
24
+ Animated.loop(
25
+ Animated.sequence([
26
+ Animated.timing(opacity, {
27
+ toValue: 1,
28
+ duration: DURATION,
29
+ useNativeDriver: true,
30
+ }),
31
+ Animated.delay(DURATION * 2),
32
+ Animated.timing(opacity, {
33
+ toValue: 0,
34
+ duration: DURATION,
35
+ useNativeDriver: true,
36
+ }),
37
+ ]),
38
+ ).start();
39
+ }, []);
40
+ const spacingStyle = !isNaN(Number(length)) &&
41
+ index !== Number(length) - 1 && {marginRight: Spacing.L};
42
+
43
+ return (
44
+ <View style={[{flexDirection: 'row', alignItems: 'center'}, spacingStyle]}>
45
+ <Animated.View
46
+ style={{
47
+ height: scaleSize(12),
48
+ width: 1,
49
+ backgroundColor: theme.colors.primary,
50
+ opacity,
51
+ }}
52
+ />
53
+ <Text color={theme.colors.text.hint} typography={'paragraph_default'}>
54
+ -
55
+ </Text>
56
+ </View>
57
+ );
58
+ };
59
+
60
+ const InputOTP: FC<InputOTPProps> = ({
61
+ length,
62
+ errorMessage,
63
+ errorSpacing,
64
+ floatingValue,
65
+ floatingIconColor,
66
+ floatingIcon,
67
+ placeholder,
68
+ onChangeText,
69
+ onFocus,
70
+ onBlur,
71
+ dataType = 'number',
72
+ ...props
73
+ }) => {
74
+ const MAX_LENGTH = 10;
75
+ const [value, setValue] = useState('');
76
+ const [focused, setFocused] = useState(false);
77
+ const inputRef = useRef<TextInput>(null);
78
+ const {theme} = useContext(ApplicationContext);
79
+
80
+ const onFocusInput = () => {
81
+ setFocused(true);
82
+ inputRef.current?.focus();
83
+ };
84
+
85
+ const onBlurInput = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
86
+ setFocused(false);
87
+ onBlur?.(e);
88
+ };
89
+
90
+ const _onChangeText = (text: string) => {
91
+ if (text.length > MAX_LENGTH) return;
92
+ if (dataType === 'number' && isNaN(Number(text))) return;
93
+ if (
94
+ !isNaN(Number(text)) &&
95
+ value.length >= Number(length) &&
96
+ text.length >= value.length
97
+ )
98
+ return;
99
+
100
+ setValue(text);
101
+ onChangeText?.(text);
102
+ };
103
+
104
+ const renderInputs = (length: number) => {
105
+ const TextInputs = [];
106
+ for (let i = 0; i < length; i++) {
107
+ TextInputs.push(
108
+ <View>
109
+ {focused && value.length === i ? (
110
+ <OTPCaret index={i} length={length} />
111
+ ) : (
112
+ <Text
113
+ style={i !== length - 1 && {marginRight: Spacing.L}}
114
+ color={
115
+ !!value[i] ? theme.colors.text.default : theme.colors.text.hint
116
+ }
117
+ typography={!!value[i] ? 'title_s' : 'paragraph_default'}>
118
+ {value[i] || '-'}
119
+ </Text>
120
+ )}
121
+ </View>,
122
+ );
123
+ }
124
+ return TextInputs;
125
+ };
126
+
127
+ const renderUnidentifiedInputs = () => {
128
+ const splitValue = value.split('');
129
+
130
+ if (!focused && value.length === 0 && !!placeholder) {
131
+ return (
132
+ <Text color={theme.colors.text.hint} typography={'description_s'}>
133
+ {placeholder}
134
+ </Text>
135
+ );
136
+ }
137
+
138
+ return (
139
+ <View style={{flexDirection: 'row'}}>
140
+ {splitValue.map((letter, index) => {
141
+ return (
142
+ <Text
143
+ style={
144
+ (index !== value.length - 1 ||
145
+ (focused && value.length !== MAX_LENGTH)) && {
146
+ marginRight: Spacing.L,
147
+ }
148
+ }
149
+ typography={'title_s'}>
150
+ {letter}
151
+ </Text>
152
+ );
153
+ })}
154
+ {focused && value.length !== MAX_LENGTH && <OTPCaret />}
155
+ </View>
156
+ );
157
+ };
158
+
159
+ const renderInputView = () => {
160
+ let placeholderColor = theme.colors.text.hint;
161
+ const keyboardType: KeyboardType =
162
+ dataType === 'number' ? 'number-pad' : 'default';
163
+ return (
164
+ <View
165
+ style={[
166
+ styles.otpInput,
167
+ {backgroundColor: theme.colors.background.surface},
168
+ getBorderColor(theme, focused, errorMessage),
169
+ ]}>
170
+ {!!floatingValue && (
171
+ <View
172
+ style={[
173
+ styles.otpFloatingView,
174
+ {backgroundColor: theme.colors.background.surface},
175
+ ]}>
176
+ <Text typography={'description_s'}>{floatingValue}</Text>
177
+ </View>
178
+ )}
179
+ <View style={styles.otpInputsView}>
180
+ {length ? renderInputs(length) : renderUnidentifiedInputs()}
181
+ </View>
182
+ <View style={styles.inputView}>
183
+ <TextInput
184
+ {...props}
185
+ accessibilityLabel={`TextInput`}
186
+ ref={inputRef}
187
+ value={value}
188
+ onChangeText={_onChangeText}
189
+ keyboardType={keyboardType}
190
+ onFocus={onFocusInput}
191
+ onBlur={onBlurInput}
192
+ style={{opacity: 0}}
193
+ selectionColor={theme.colors.primary}
194
+ placeholderTextColor={placeholderColor}
195
+ />
196
+ </View>
197
+ </View>
198
+ );
199
+ };
200
+
201
+ return (
202
+ <TouchableOpacity
203
+ activeOpacity={1}
204
+ onPress={onFocusInput}
205
+ style={styles.otpWrapper}
206
+ accessibilityLabel={`Input/${floatingValue}`}>
207
+ {renderInputView()}
208
+ <ErrorView errorMessage={errorMessage} errorSpacing={errorSpacing} />
209
+ </TouchableOpacity>
210
+ );
211
+ };
212
+
213
+ export default InputOTP;
package/Input/common.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import {View} from 'react-native';
1
+ import {View, ViewStyle} from 'react-native';
2
2
  import styles from './styles';
3
3
  import {Text} from '../Text';
4
4
  import React, {FC, useContext} from 'react';
@@ -13,6 +13,7 @@ type FloatingViewProps = {
13
13
  disabled?: boolean;
14
14
  floatingIcon?: string;
15
15
  required?: boolean;
16
+ style?: ViewStyle;
16
17
  };
17
18
 
18
19
  export const DEFAULT_HEIGHT = 112;
@@ -81,6 +82,7 @@ export const FloatingView: FC<FloatingViewProps> = ({
81
82
  disabled,
82
83
  floatingIcon,
83
84
  required,
85
+ style,
84
86
  }) => {
85
87
  const {theme} = useContext(ApplicationContext);
86
88
 
@@ -100,6 +102,7 @@ export const FloatingView: FC<FloatingViewProps> = ({
100
102
  backgroundColor: theme.colors.background.surface,
101
103
  zIndex: 10,
102
104
  },
105
+ style,
103
106
  ]}>
104
107
  <Text color={floatingTextColor} typography={'label_s'}>
105
108
  {floatingValue}
package/Input/index.tsx CHANGED
@@ -2,6 +2,8 @@ import Input from './Input';
2
2
  import InputTextArea from './InputTextArea';
3
3
  import InputSearch from './InputSearch';
4
4
  import InputMoney from './InputMoney';
5
+ import InputOTP from './InputOTP';
6
+ import InputDropDown from './InputDropDown';
5
7
  import {TextInputProps} from 'react-native';
6
8
 
7
9
  export interface InputProps extends TextInputProps {
@@ -30,6 +32,11 @@ export interface InputProps extends TextInputProps {
30
32
  */
31
33
  icon?: string;
32
34
 
35
+ /**
36
+ * Optional. Represents the color of the icon in the Input component.
37
+ */
38
+ iconColor?: string;
39
+
33
40
  /**
34
41
  * Optional. If `true`, the user won't be able to interact with the Input component.
35
42
  * Defaults to `false` if not provided.
@@ -41,11 +48,6 @@ export interface InputProps extends TextInputProps {
41
48
  */
42
49
  floatingIconColor?: string;
43
50
 
44
- /**
45
- * Optional. Represents the color of the icon in the Input component.
46
- */
47
- iconColor?: string;
48
-
49
51
  /**
50
52
  * Optional. If `true`, the Input component is marked as required,
51
53
  * indicating that the user must provide a value before submitting a form.
@@ -65,13 +67,22 @@ export interface InputProps extends TextInputProps {
65
67
  */
66
68
  loading?: boolean;
67
69
 
68
- image?: string;
70
+ /**
71
+ * Optional. Represents the leading icon in the Input component.
72
+ */
73
+ leadingIcon?: string;
74
+
75
+ /**
76
+ * Optional. Represents the color of the leading icon in the Input component.
77
+ */
78
+ leadingIconColor?: string;
69
79
  }
70
80
 
71
81
  type InputPropsWithoutSizeAndIcon = Omit<
72
82
  InputProps,
73
83
  'size' | 'icon' | 'iconColor'
74
84
  >;
85
+
75
86
  type InputPropsOmitForSearch = Omit<
76
87
  InputProps,
77
88
  'required' | 'size' | 'errorMessage' | 'disabled'
@@ -109,4 +120,102 @@ export interface InputSearchProps extends InputPropsOmitForSearch {
109
120
 
110
121
  export interface InputMoneyProps extends InputPropsWithoutPlaceholder {}
111
122
 
112
- export {Input, InputTextArea, InputSearch, InputMoney};
123
+ export type OTPInputLength = 2 | 4 | 6 | 8 | 10;
124
+
125
+ export interface InputOTPProps extends InputPropsWithoutSizeAndIcon {
126
+ length?: OTPInputLength;
127
+ dataType?: 'string' | 'number';
128
+ }
129
+
130
+ export type CaretProps = {
131
+ index?: number;
132
+ length?: number;
133
+ };
134
+
135
+ export type InputDropDownProps = {
136
+ /**
137
+ * Optional. Defines the size of the Input component.
138
+ */
139
+ size?: 'small' | 'large';
140
+
141
+ /**
142
+ * Optional. Defines the value of InputDropDown.
143
+ */
144
+ value?: string;
145
+
146
+ /**
147
+ * Optional. Defines the placeholder of InputDropDown.
148
+ */
149
+ placeholder?: string;
150
+
151
+ /**
152
+ * Optional. Defines the onPress of InputDropDown.
153
+ */
154
+ onPress?: () => void;
155
+ /**
156
+ * Optional. Represents the value for the floating title in the Input component.
157
+ */
158
+ floatingValue?: string;
159
+
160
+ /**
161
+ * Optional. Represents the name or key of the floating icon to be displayed in the Input component.
162
+ */
163
+ floatingIcon?: string;
164
+
165
+ /**
166
+ * Optional. Represents the error message to be displayed below the Input component when there is an error.
167
+ */
168
+ errorMessage?: string;
169
+
170
+ /**
171
+ * Optional. Represents the name or key of the icon to be displayed in the Input component.
172
+ */
173
+ icon?: string;
174
+
175
+ /**
176
+ * Optional. Represents the color of the icon in the Input component.
177
+ */
178
+ iconColor?: string;
179
+
180
+ /**
181
+ * Optional. If `true`, the user won't be able to interact with the Input component.
182
+ * Defaults to `false` if not provided.
183
+ */
184
+ disabled?: boolean;
185
+
186
+ /**
187
+ * Optional. Represents the color of the floating icon in the Input component.
188
+ */
189
+ floatingIconColor?: string;
190
+
191
+ /**
192
+ * Optional. If `true`, the Input component is marked as required,
193
+ * indicating that the user must provide a value before submitting a form.
194
+ * Defaults to `false` if not provided.
195
+ */
196
+ required?: boolean;
197
+
198
+ /**
199
+ * Optional. If `true`,
200
+ * include spacing when Input not have error message.
201
+ * Defaults to `false` if not provided.
202
+ */
203
+ errorSpacing?: boolean;
204
+
205
+ /**
206
+ * If true, the icon of input will use be show indicator loading.
207
+ */
208
+ loading?: boolean;
209
+
210
+ /**
211
+ * Optional. Represents the leading icon in the Input component.
212
+ */
213
+ leadingIcon?: string;
214
+
215
+ /**
216
+ * Optional. Represents the color of the leading icon in the Input component.
217
+ */
218
+ leadingIconColor?: string;
219
+ };
220
+
221
+ export {Input, InputTextArea, InputSearch, InputMoney, InputOTP, InputDropDown};
package/Input/styles.ts CHANGED
@@ -42,17 +42,10 @@ export default StyleSheet.create({
42
42
  flexDirection: 'row',
43
43
  alignItems: 'center',
44
44
  },
45
- image: {
46
- width: 32,
47
- height: 32,
48
- marginLeft: Spacing.M,
49
- borderRadius: Radius.XS,
50
- },
51
- imageSmall: {
52
- width: 24,
53
- height: 24,
45
+ leadingIconContainer: {
54
46
  marginLeft: Spacing.M,
55
47
  borderRadius: Radius.XS,
48
+ overflow: 'hidden',
56
49
  },
57
50
  iconView: {
58
51
  flexDirection: 'row',
@@ -142,4 +135,28 @@ export default StyleSheet.create({
142
135
  paddingVertical: Spacing.S,
143
136
  fontWeight: 'bold',
144
137
  },
138
+
139
+ //OTP
140
+ otpWrapper: {
141
+ width: '100%',
142
+ marginTop: Spacing.M,
143
+ },
144
+ otpInput: {
145
+ height: scaleSize(56),
146
+ borderRadius: Radius.S,
147
+ borderWidth: 1,
148
+ },
149
+ otpFloatingView: {
150
+ position: 'absolute',
151
+ top: -Spacing.S,
152
+ alignSelf: 'center',
153
+ paddingHorizontal: Spacing.S,
154
+ },
155
+ otpInputsView: {
156
+ width: '100%',
157
+ height: '100%',
158
+ flexDirection: 'row',
159
+ alignItems: 'center',
160
+ justifyContent: 'center',
161
+ },
145
162
  });
package/Layout/Screen.tsx CHANGED
@@ -107,12 +107,10 @@ const Screen: React.FC<ScreenProps> = ({
107
107
  if (useExtendedHeader) {
108
108
  title = {headerTitle: HeaderTitle};
109
109
  }
110
- setImmediate(() => {
111
- navigation?.setOptions({
112
- animatedValue: animatedValue.current,
113
- headerTransparent: headerTransparent,
114
- ...title,
115
- });
110
+ navigation?.setOptions({
111
+ animatedValue: animatedValue.current,
112
+ headerTransparent: headerTransparent,
113
+ ...title,
116
114
  });
117
115
  }
118
116
  }, [useExtendedHeader, useAnimatedHeader]);
@@ -1,4 +1,4 @@
1
- import React, {useContext} from 'react';
1
+ import React, {Fragment, useContext} from 'react';
2
2
  import {Dimensions, View, ViewProps} from 'react-native';
3
3
  import {useGridSystem, validateChildren} from './utils';
4
4
  import {GridContextProps} from './types';
@@ -135,6 +135,9 @@ const Section: React.FC<SectionProps> = ({
135
135
  }
136
136
  } else {
137
137
  element.push(children);
138
+ if (children?.type == Fragment) {
139
+ return renderView(children?.props.children);
140
+ }
138
141
  }
139
142
  return element;
140
143
  };
package/Layout/utils.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import {Alert, Dimensions} from 'react-native';
2
- import React, {useEffect, useState} from 'react';
2
+ import React, {Fragment, ReactElement, useEffect, useState} from 'react';
3
3
 
4
4
  /**
5
5
  * validate children type
@@ -7,53 +7,36 @@ import React, {useEffect, useState} from 'react';
7
7
  * @param childrenTypes
8
8
  */
9
9
  const validateChildren = (
10
- children:
11
- | React.ReactElement<any, string | React.JSXElementConstructor<any>>
12
- | string
13
- | number
14
- | {}
15
- | Iterable<React.ReactNode>
16
- | React.ReactPortal
17
- | null
18
- | undefined
19
- | boolean,
20
- childrenTypes: Array<React.FC<any>>,
10
+ children: React.ReactNode | React.ReactNode[],
11
+ childrenTypes: Array<React.FC>,
21
12
  ) => {
22
13
  if (!__DEV__) return children;
23
14
 
24
- let results: React.ReactElement[] = [];
25
15
  const validateType = (child: React.ReactElement) => {
26
- return childrenTypes.some(childrenType => child?.type === childrenType);
27
- };
28
-
29
- const alertWrongType = () => {
30
- Alert.alert(
31
- 'Component Kits',
32
- `Wrong children type for this component, expected are ${childrenTypes
33
- .map(type => type.displayName)
34
- .join(', ')}.`,
16
+ const valid = childrenTypes.some(
17
+ childrenType => child?.type === childrenType || child?.type == Fragment,
35
18
  );
19
+ if (!valid) {
20
+ Alert.alert(
21
+ 'Component Kits',
22
+ `Wrong children type for this component, expected are ${childrenTypes
23
+ .map(type => type.displayName)
24
+ .join(', ')}.`,
25
+ );
26
+ }
27
+ return valid;
36
28
  };
37
29
 
38
30
  if (Array.isArray(children)) {
39
31
  const flattenChildren: React.ReactElement[] = children.flat();
40
- results = flattenChildren.filter((child: React.ReactElement) => {
41
- if (validateType(child)) {
42
- return child;
43
- } else {
44
- alertWrongType();
45
- }
46
- });
47
- } else {
48
- const singleChild: any = children;
49
- if (validateType(singleChild)) {
50
- results = [singleChild];
51
- } else {
52
- alertWrongType();
32
+ for (const child of flattenChildren) {
33
+ validateType(child);
53
34
  }
35
+ return flattenChildren;
36
+ } else {
37
+ validateType(children as ReactElement);
38
+ return children;
54
39
  }
55
-
56
- return results;
57
40
  };
58
41
 
59
42
  const useGridSystem = () => {
package/Switch/index.tsx CHANGED
@@ -1,19 +1,17 @@
1
- import React, {FC, useContext} from 'react';
1
+ import React, {FC} from 'react';
2
2
  import {TouchableOpacity, View} from 'react-native';
3
3
  import {SwitchProps} from './types';
4
4
  import styles from './styles';
5
- import {ApplicationContext} from '../Application';
6
5
  import {Colors} from '../Consts';
7
6
 
8
7
  const Switch: FC<SwitchProps> = props => {
9
- const {theme} = useContext(ApplicationContext);
10
8
  const {value, onChange, disabled, style} = props;
11
9
  const circleBackgroundColor = value ? Colors.black_01 : Colors.black_03;
12
10
  const circleAlign = value ? 'flex-end' : 'flex-start';
13
11
 
14
- let backgroundColor = value ? theme.colors.primary : Colors.black_07;
12
+ let backgroundColor = value ? Colors.green_03 : Colors.black_07;
15
13
  if (disabled) {
16
- backgroundColor = value ? theme.colors.background.tonal : Colors.black_05;
14
+ backgroundColor = value ? Colors.green_09 : Colors.black_05;
17
15
  }
18
16
 
19
17
  return (
@@ -0,0 +1,89 @@
1
+ import React, {FC, useContext} from 'react';
2
+ import {Text as RNText, TouchableOpacity, View} from 'react-native';
3
+ import {TitleProps} from './types';
4
+ import {Icon} from '../Icon';
5
+ import styles from './styles';
6
+ import {ApplicationContext} from '../Application';
7
+ import {Colors, Spacing} from '../Consts';
8
+ import {Button} from '../Button';
9
+ import {scaleSize, Text} from '../Text';
10
+
11
+ const Title: FC<TitleProps> = ({
12
+ size = 'large',
13
+ type = 'section',
14
+ onPressAction,
15
+ actionTitle,
16
+ title = 'Title',
17
+ titleFlex = true,
18
+ showAction = true,
19
+ icon,
20
+ description,
21
+ }) => {
22
+ const {theme} = useContext(ApplicationContext);
23
+ const flexStyle = titleFlex ? {flex: 1} : {};
24
+ const typeStyle =
25
+ type === 'screen' ? styles.screen : styles[`${type}_${size}`];
26
+ const actionColor = titleFlex
27
+ ? theme.colors.primary
28
+ : theme.colors.text.default;
29
+ const actionBackground = titleFlex ? Colors.pink_08 + '4D' : Colors.black_06;
30
+ const actionSize = titleFlex ? scaleSize(22) : scaleSize(18);
31
+ const titleWeight = type === 'screen' ? 'Semibold' : 'Bold';
32
+ const marginStyle = type === 'section' ? {marginTop: Spacing.M} : {};
33
+
34
+ const renderAction = () => {
35
+ return !!actionTitle && titleFlex ? (
36
+ <Button
37
+ onPress={() => onPressAction?.()}
38
+ type={'text'}
39
+ full={false}
40
+ size={'small'}
41
+ title={actionTitle}
42
+ />
43
+ ) : (
44
+ <TouchableOpacity
45
+ style={[
46
+ {backgroundColor: actionBackground},
47
+ styles.actionIcon,
48
+ {width: actionSize, height: actionSize},
49
+ ]}
50
+ onPress={onPressAction}>
51
+ <Icon
52
+ size={actionSize}
53
+ color={actionColor}
54
+ source={'arrow_chevron_right_small'}
55
+ />
56
+ </TouchableOpacity>
57
+ );
58
+ };
59
+
60
+ return (
61
+ <View style={styles.wrapper}>
62
+ <View style={[styles.container, marginStyle]}>
63
+ {!!icon && type !== 'screen' && (
64
+ <Icon style={styles.icon} source={icon} />
65
+ )}
66
+ <RNText
67
+ style={[
68
+ typeStyle,
69
+ flexStyle,
70
+ styles.title,
71
+ {
72
+ fontFamily: `${theme.font}-${titleWeight}`,
73
+ },
74
+ ]}>
75
+ {title}
76
+ </RNText>
77
+ {showAction && type !== 'screen' && renderAction()}
78
+ </View>
79
+ {!!description && !!icon && type !== 'screen' && (
80
+ <Text typography={'description_s'} color={theme.colors.text.secondary}>
81
+ {description}
82
+ </Text>
83
+ )}
84
+ </View>
85
+ );
86
+ };
87
+
88
+ export {Title};
89
+ export type {TitleProps};
@@ -0,0 +1,46 @@
1
+ import {StyleSheet} from 'react-native';
2
+ import {scaleSize} from '../Text';
3
+ import {Radius, Spacing} from '../Consts';
4
+
5
+ export default StyleSheet.create({
6
+ card_small: {
7
+ fontSize: scaleSize(16),
8
+ lineHeight: scaleSize(22),
9
+ },
10
+ card_large: {
11
+ fontSize: scaleSize(18),
12
+ lineHeight: scaleSize(26),
13
+ },
14
+ section_small: {
15
+ fontSize: scaleSize(20),
16
+ lineHeight: scaleSize(28),
17
+ },
18
+ section_large: {
19
+ fontSize: scaleSize(24),
20
+ lineHeight: scaleSize(34),
21
+ },
22
+ screen: {
23
+ fontSize: scaleSize(36),
24
+ lineHeight: scaleSize(44),
25
+ },
26
+ title: {
27
+ marginRight: Spacing.XS,
28
+ },
29
+ container: {
30
+ flexDirection: 'row',
31
+ alignItems: 'center',
32
+ width: '100%',
33
+ },
34
+ actionIcon: {
35
+ borderRadius: Radius.M,
36
+ alignItems: 'center',
37
+ justifyContent: 'center',
38
+ },
39
+ icon: {
40
+ marginRight: Spacing.XS,
41
+ borderRadius: Radius.XS,
42
+ },
43
+ wrapper: {
44
+ width: '100%',
45
+ },
46
+ });
package/Title/types.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type TitleProps = {
2
+ type?: 'screen' | 'section' | 'card';
3
+ title: string;
4
+ description?: string;
5
+ size?: 'large' | 'small';
6
+ icon?: string;
7
+ titleFlex?: boolean;
8
+ actionTitle?: string;
9
+ onPressAction?: () => void;
10
+ showAction?: boolean;
11
+ };
package/index.ts CHANGED
@@ -45,3 +45,5 @@ export * from './Pagination';
45
45
  export * from './Pagination/types';
46
46
  export * from './Loader';
47
47
  export * from './Loader/types';
48
+ export * from './Title';
49
+ export * from './Title/types';
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.79.6",
3
+ "version": "0.80.2-beta.1",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "postinstall": "node verify.js"
8
8
  },
9
9
  "keywords": [
10
10
  "@momo-kits/foundation"
@@ -42,4 +42,4 @@
42
42
  },
43
43
  "author": "@momo-kits/foundation",
44
44
  "license": "ISC"
45
- }
45
+ }
package/verify.js ADDED
@@ -0,0 +1,24 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ try {
5
+ const package = path.join(`${__dirname}`, 'package.json');
6
+ const packageInfo = JSON.parse(fs.readFileSync(package, 'utf8'));
7
+ const packageVersion = parseInt(packageInfo?.version.split('.')?.[1]);
8
+
9
+ const app = path.join(`${__dirname}/../../../`, 'app.json');
10
+ const appInfo = JSON.parse(fs.readFileSync(app, 'utf8'));
11
+ const iOSAppTarget = appInfo?.client?.ios.deploymentTarget;
12
+ const androidAppTarget = appInfo?.client?.android.deploymentTarget;
13
+
14
+ if (appInfo.name === 'MoMoPlatform') {
15
+ return;
16
+ }
17
+
18
+ if (packageVersion > iOSAppTarget || packageVersion > androidAppTarget) {
19
+ throw `Package ${packageInfo.name} version: ${packageInfo.version} require deploymentTarget ${packageVersion}`;
20
+ }
21
+ } catch (error) {
22
+ console.error(`Installation failed: ${error.message}`);
23
+ process.exit(1);
24
+ }
Binary file