@momo-kits/foundation 0.92.5 → 0.92.6-beta.10

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.
@@ -120,7 +120,7 @@ export interface BottomSheetParams extends ScreenParams {
120
120
  [key: string]: any;
121
121
  onDismiss?: () => void;
122
122
  footerComponent?: React.ReactNode;
123
- flatListProps: FlatListProps<any>;
123
+ flatListProps?: FlatListProps<any>;
124
124
  }
125
125
 
126
126
  export type NavigationButtonProps = {
@@ -0,0 +1,154 @@
1
+ import {
2
+ default as React,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import {
10
+ Animated,
11
+ LayoutChangeEvent,
12
+ StyleSheet,
13
+ TouchableOpacity,
14
+ } from 'react-native';
15
+ import {useSafeAreaInsets} from 'react-native-safe-area-context';
16
+ import {ApplicationContext} from '../Application';
17
+ import {Spacing} from '../Consts';
18
+ import {Icon} from '../Icon';
19
+ import {Text} from '../Text';
20
+
21
+ export interface FloatingButtonProps {
22
+ label?: string;
23
+ onPress?: () => void;
24
+ position?: 'center' | 'right';
25
+ icon?: string;
26
+ iconColor?: string;
27
+ size?: 'small' | 'large';
28
+ animatedValue: Animated.Value;
29
+ isBottomTab?: boolean;
30
+ isFooter?: boolean;
31
+ }
32
+
33
+ export const FloatingButton: React.FC<FloatingButtonProps> = ({
34
+ label = '',
35
+ onPress = () => {},
36
+ position = 'right',
37
+ icon = '',
38
+ iconColor = '',
39
+ size = 'small',
40
+ animatedValue,
41
+ isBottomTab = false,
42
+ isFooter = false,
43
+ }: FloatingButtonProps) => {
44
+ const {theme} = useContext(ApplicationContext);
45
+ const insets = useSafeAreaInsets();
46
+ const [isExpanded, setIsExpanded] = React.useState(true);
47
+ const [init, setInit] = useState(true);
48
+ const width = size == 'small' ? 36 : 48;
49
+ const widthAnim = useRef(new Animated.Value(width)).current;
50
+ const [textWidth, setTextWidth] = useState(0);
51
+
52
+ useEffect(() => {
53
+ if (!label) return;
54
+
55
+ const listener = animatedValue.addListener(({value}) => {
56
+ if (value === 1) {
57
+ setIsExpanded(false);
58
+ collapseFab();
59
+ } else {
60
+ setIsExpanded(true);
61
+ expandFab();
62
+ }
63
+ });
64
+
65
+ return () => {
66
+ animatedValue?.removeListener(listener);
67
+ };
68
+ }, [textWidth, label, init]);
69
+
70
+ // If the text width is greater than 0 and it's not initialized then expand the fab
71
+ useEffect(() => {
72
+ if (textWidth > 0 && !init) {
73
+ expandFab();
74
+ }
75
+ }, [textWidth]);
76
+
77
+ const handleLayout = (event: LayoutChangeEvent) => {
78
+ const {width} = event.nativeEvent.layout;
79
+
80
+ if (init) {
81
+ setTextWidth(width);
82
+ setInit(false);
83
+ }
84
+ };
85
+
86
+ const expandFab = useCallback(() => {
87
+ Animated.timing(widthAnim, {
88
+ toValue: width + textWidth + 8,
89
+ duration: 200,
90
+ useNativeDriver: false,
91
+ }).start();
92
+ }, [textWidth, widthAnim]);
93
+
94
+ const collapseFab = useCallback(() => {
95
+ Animated.timing(widthAnim, {
96
+ toValue: width,
97
+ duration: 200,
98
+ useNativeDriver: false,
99
+ }).start();
100
+ }, [widthAnim]);
101
+
102
+ return (
103
+ <Animated.View
104
+ style={[
105
+ styles.container,
106
+ {
107
+ right: position === 'right' ? 12 : undefined,
108
+ alignSelf: position === 'center' ? 'center' : 'flex-end',
109
+ width: init ? undefined : widthAnim,
110
+ height: width,
111
+ backgroundColor: theme.colors.primary,
112
+ bottom:
113
+ isFooter || isBottomTab
114
+ ? 12
115
+ : Math.min(insets.bottom, 21) + Spacing.S,
116
+ },
117
+ ]}>
118
+ <TouchableOpacity
119
+ style={{flexDirection: 'row', alignItems: 'center'}}
120
+ onPress={onPress}>
121
+ <Icon
122
+ source={icon}
123
+ size={size === 'small' ? 12 : 24}
124
+ color={iconColor}
125
+ />
126
+ {isExpanded && !!label && (
127
+ <Text
128
+ onLayout={handleLayout}
129
+ style={styles.label}
130
+ color="white"
131
+ typography="action_default_bold">
132
+ Hello
133
+ </Text>
134
+ )}
135
+ </TouchableOpacity>
136
+ </Animated.View>
137
+ );
138
+ };
139
+
140
+ const styles = StyleSheet.create({
141
+ container: {
142
+ flexDirection: 'row',
143
+ alignContent: 'center',
144
+ alignItems: 'center',
145
+ position: 'absolute',
146
+ borderRadius: 100,
147
+ paddingHorizontal: 12,
148
+ flexWrap: 'wrap',
149
+ alignSelf: 'flex-end',
150
+ },
151
+ label: {
152
+ marginLeft: 8,
153
+ },
154
+ });
package/Icon/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {ImageStyle} from 'react-native-fast-image';
2
+ import {StyleProp} from "react-native";
2
3
 
3
4
  export type IconProps = {
4
5
  /**
@@ -23,5 +24,5 @@ export type IconProps = {
23
24
  * Optional. Used to apply custom styling to the Icon component.
24
25
  * Accepts an object of style properties.
25
26
  */
26
- style?: ImageStyle;
27
+ style?: StyleProp<ImageStyle>;
27
28
  };
package/Input/Input.tsx CHANGED
@@ -1,4 +1,10 @@
1
- import React, {forwardRef, useContext, useRef, useState} from 'react';
1
+ import React, {
2
+ forwardRef,
3
+ useContext,
4
+ useImperativeHandle,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
2
8
  import {
3
9
  NativeSyntheticEvent,
4
10
  Platform,
@@ -8,12 +14,12 @@ import {
8
14
  View,
9
15
  } from 'react-native';
10
16
  import {ApplicationContext, ComponentContext} from '../Application';
11
- import styles from './styles';
17
+ import {Icon} from '../Icon';
18
+ import {Loader} from '../Loader';
12
19
  import {ErrorView, FloatingView, getBorderColor, getSizeStyle} from './common';
13
20
  import {InputProps} from './index';
14
- import {Icon} from '../Icon';
21
+ import styles from './styles';
15
22
  import {checkTyping} from './utils';
16
- import {Loader} from '../Loader';
17
23
 
18
24
  const Input = forwardRef(
19
25
  (
@@ -42,6 +48,7 @@ const Input = forwardRef(
42
48
  keyboardType,
43
49
  style,
44
50
  params,
51
+ hintText,
45
52
  accessibilityLabel,
46
53
  ...props
47
54
  }: InputProps,
@@ -51,7 +58,7 @@ const Input = forwardRef(
51
58
  const [focused, setFocused] = useState(false);
52
59
  const [haveValue, setHaveValue] = useState(!!value || !!props.defaultValue);
53
60
  const [secureTextInput, setSecureTextInput] = useState(secureTextEntry);
54
- const inputRef = (ref as React.RefObject<TextInput>) ?? useRef<TextInput>();
61
+ const inputRef = useRef<TextInput | null>(null);
55
62
 
56
63
  const onClearText = () => {
57
64
  inputRef?.current?.clear();
@@ -73,6 +80,15 @@ const Input = forwardRef(
73
80
  onBlur?.(e);
74
81
  };
75
82
 
83
+ useImperativeHandle(ref, () => {
84
+ return {
85
+ clear: onClearText,
86
+ focus: () => inputRef.current?.focus(),
87
+ blur: () => inputRef.current?.blur(),
88
+ setText: (text: string) => _onChangeText(text),
89
+ };
90
+ });
91
+
76
92
  const renderIcon = (color: string | undefined) => {
77
93
  if (loading) {
78
94
  return <Loader type={'spinner'} color={color} style={styles.icon} />;
@@ -217,7 +233,11 @@ const Input = forwardRef(
217
233
  }}>
218
234
  <View style={[style, styles.wrapper]}>
219
235
  {renderInputView()}
220
- <ErrorView errorMessage={errorMessage} errorSpacing={errorSpacing} />
236
+ <ErrorView
237
+ errorMessage={errorMessage}
238
+ errorSpacing={errorSpacing}
239
+ hintText={hintText}
240
+ />
221
241
  </View>
222
242
  </ComponentContext.Provider>
223
243
  );
@@ -1,12 +1,12 @@
1
1
  import React, {useContext} from 'react';
2
2
  import {TextInput, TouchableOpacity, View} from 'react-native';
3
3
  import {ApplicationContext, ComponentContext} from '../Application';
4
- import styles from './styles';
5
- import {ErrorView, FloatingView, getBorderColor, getSizeStyle} from './common';
6
- import {InputDropDownProps} from './index';
4
+ import {Styles} from '../Consts';
7
5
  import {Icon} from '../Icon';
8
6
  import {Loader} from '../Loader';
9
- import {Styles} from '../Consts';
7
+ import {ErrorView, FloatingView, getBorderColor, getSizeStyle} from './common';
8
+ import {InputDropDownProps} from './index';
9
+ import styles from './styles';
10
10
 
11
11
  const InputDropDown = ({
12
12
  value,
@@ -28,6 +28,7 @@ const InputDropDown = ({
28
28
  style,
29
29
  params,
30
30
  accessibilityLabel,
31
+ hintText,
31
32
  ...props
32
33
  }: InputDropDownProps) => {
33
34
  const {theme} = useContext(ApplicationContext);
@@ -118,7 +119,11 @@ const InputDropDown = ({
118
119
  onPress={onPress}
119
120
  style={[style, full && styles.wrapper]}>
120
121
  {renderInputView()}
121
- <ErrorView errorMessage={errorMessage} errorSpacing={errorSpacing} />
122
+ <ErrorView
123
+ errorMessage={errorMessage}
124
+ errorSpacing={errorSpacing}
125
+ hintText={hintText}
126
+ />
122
127
  </TouchableOpacity>
123
128
  </ComponentContext.Provider>
124
129
  );
@@ -13,10 +13,10 @@ import {
13
13
  View,
14
14
  } from 'react-native';
15
15
  import {ApplicationContext, ComponentContext} from '../Application';
16
- import styles from './styles';
16
+ import {Icon} from '../Icon';
17
17
  import {ErrorView, FloatingView, getBorderColor, getSizeStyle} from './common';
18
18
  import {InputMoneyProps} from './index';
19
- import {Icon} from '../Icon';
19
+ import styles from './styles';
20
20
  import {formatMoneyToNumber, formatNumberToMoney} from './utils';
21
21
 
22
22
  const InputMoney = forwardRef(
@@ -39,13 +39,14 @@ const InputMoney = forwardRef(
39
39
  params,
40
40
  defaultValue = '',
41
41
  accessibilityLabel,
42
+ hintText,
42
43
  ...props
43
44
  }: InputMoneyProps,
44
45
  ref,
45
46
  ) => {
46
47
  const {theme} = useContext(ApplicationContext);
47
48
  const [focused, setFocused] = useState(false);
48
- const inputRef = (ref as React.RefObject<TextInput>) ?? useRef<TextInput>();
49
+ const inputRef = useRef<TextInput>(null);
49
50
 
50
51
  const validateText = (text: string) => {
51
52
  const valueFormat = formatMoneyToNumber(text, 'đ');
@@ -72,6 +73,10 @@ const InputMoney = forwardRef(
72
73
 
73
74
  useImperativeHandle(ref, () => {
74
75
  return {
76
+ clear: onClearText,
77
+ focus: () => inputRef.current?.focus(),
78
+ blur: () => inputRef.current?.blur(),
79
+ changeText: (text: string) => _onChangeText(text),
75
80
  setText: _onChangeText,
76
81
  };
77
82
  });
@@ -177,7 +182,11 @@ const InputMoney = forwardRef(
177
182
  }}>
178
183
  <View style={[style, styles.wrapper]}>
179
184
  {renderInputView()}
180
- <ErrorView errorMessage={errorMessage} errorSpacing={errorSpacing} />
185
+ <ErrorView
186
+ errorMessage={errorMessage}
187
+ errorSpacing={errorSpacing}
188
+ hintText={hintText}
189
+ />
181
190
  </View>
182
191
  </ComponentContext.Provider>
183
192
  );
@@ -15,12 +15,12 @@ import {
15
15
  TouchableOpacity,
16
16
  View,
17
17
  } from 'react-native';
18
- import {CaretProps, InputOTPProps} from './index';
19
- import {scaleSize, Text} from '../Text';
20
- import styles from './styles';
21
- import {ErrorView, getBorderColor} from './common';
22
18
  import {ApplicationContext, ComponentContext} from '../Application';
23
19
  import {Spacing} from '../Consts';
20
+ import {scaleSize, Text} from '../Text';
21
+ import {ErrorView, getBorderColor} from './common';
22
+ import {CaretProps, InputOTPProps} from './index';
23
+ import styles from './styles';
24
24
 
25
25
  const OTPCaret: FC<CaretProps> = ({index, length}) => {
26
26
  const DURATION = 300;
@@ -81,6 +81,7 @@ const InputOTP = forwardRef(
81
81
  params,
82
82
  style,
83
83
  accessibilityLabel,
84
+ hintText,
84
85
  ...props
85
86
  }: InputOTPProps,
86
87
  ref,
@@ -88,13 +89,16 @@ const InputOTP = forwardRef(
88
89
  const MAX_LENGTH = 10;
89
90
  const [value, setValue] = useState('');
90
91
  const [focused, setFocused] = useState(false);
91
- const inputRef = useRef<TextInput>(null);
92
+ const inputRef = useRef<TextInput | null>(null);
92
93
  const {theme} = useContext(ApplicationContext);
93
94
 
94
95
  useImperativeHandle(ref, () => ({
95
96
  onChangeText: (text: string) => {
96
97
  _onChangeText(text);
97
98
  },
99
+ focus: () => inputRef.current?.focus(),
100
+ blur: () => inputRef.current?.blur(),
101
+ setText: (text: string) => _onChangeText(text),
98
102
  }));
99
103
 
100
104
  const onFocusInput = () => {
@@ -239,7 +243,11 @@ const InputOTP = forwardRef(
239
243
  onPress={onFocusInput}
240
244
  style={[style, styles.otpWrapper]}>
241
245
  {renderInputView()}
242
- <ErrorView errorMessage={errorMessage} errorSpacing={errorSpacing} />
246
+ <ErrorView
247
+ errorMessage={errorMessage}
248
+ errorSpacing={errorSpacing}
249
+ hintText={hintText}
250
+ />
243
251
  </TouchableOpacity>
244
252
  </ComponentContext.Provider>
245
253
  );
@@ -1,4 +1,10 @@
1
- import React, {forwardRef, useContext, useRef, useState} from 'react';
1
+ import React, {
2
+ forwardRef,
3
+ useContext,
4
+ useImperativeHandle,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
2
8
  import {
3
9
  NativeSyntheticEvent,
4
10
  TextInput,
@@ -6,11 +12,11 @@ import {
6
12
  TouchableOpacity,
7
13
  View,
8
14
  } from 'react-native';
9
- import {InputSearchProps} from './index';
10
15
  import {ApplicationContext, ComponentContext} from '../Application';
11
- import styles from './styles';
12
16
  import {Icon} from '../Icon';
13
17
  import {Text} from '../Text';
18
+ import {InputSearchProps} from './index';
19
+ import styles from './styles';
14
20
  import {checkTyping} from './utils';
15
21
 
16
22
  const InputSearch = forwardRef(
@@ -38,7 +44,7 @@ const InputSearch = forwardRef(
38
44
  const {theme} = useContext(ApplicationContext);
39
45
  const [focused, setFocused] = useState(false);
40
46
  const [haveValue, setHaveValue] = useState(!!value || !!defaultValue);
41
- const inputRef = (ref as React.RefObject<TextInput>) ?? useRef<TextInput>();
47
+ const inputRef = useRef<TextInput | null>(null);
42
48
 
43
49
  const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
44
50
  setFocused(true);
@@ -60,6 +66,15 @@ const InputSearch = forwardRef(
60
66
  onChangeText?.(text);
61
67
  };
62
68
 
69
+ useImperativeHandle(ref, () => {
70
+ return {
71
+ clear: onClearText,
72
+ focus: () => inputRef.current?.focus(),
73
+ blur: () => inputRef.current?.blur(),
74
+ setText: (text: string) => _onChangeText(text),
75
+ };
76
+ });
77
+
63
78
  const renderInputView = () => {
64
79
  return (
65
80
  <TextInput
@@ -1,4 +1,10 @@
1
- import React, {forwardRef, useContext, useRef, useState} from 'react';
1
+ import React, {
2
+ forwardRef,
3
+ useContext,
4
+ useImperativeHandle,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
2
8
  import {
3
9
  NativeSyntheticEvent,
4
10
  TextInput,
@@ -6,7 +12,8 @@ import {
6
12
  TouchableOpacity,
7
13
  View,
8
14
  } from 'react-native';
9
- import styles from './styles';
15
+ import {ApplicationContext} from '../Application';
16
+ import {Icon} from '../Icon';
10
17
  import {Text} from '../Text';
11
18
  import {
12
19
  DEFAULT_HEIGHT,
@@ -16,8 +23,7 @@ import {
16
23
  MAX_LENGTH,
17
24
  } from './common';
18
25
  import {InputTextAreaProps} from './index';
19
- import {Icon} from '../Icon';
20
- import {ApplicationContext} from '../Application';
26
+ import styles from './styles';
21
27
 
22
28
  const InputTextArea = forwardRef(
23
29
  (
@@ -39,6 +45,7 @@ const InputTextArea = forwardRef(
39
45
  errorSpacing,
40
46
  style,
41
47
  accessibilityLabel,
48
+ hintText,
42
49
  ...props
43
50
  }: InputTextAreaProps,
44
51
  ref,
@@ -46,7 +53,7 @@ const InputTextArea = forwardRef(
46
53
  const {theme} = useContext(ApplicationContext);
47
54
  const [focused, setFocused] = useState(false);
48
55
  const [inputValue, setInputValue] = useState(defaultValue ?? '');
49
- const inputRef = (ref as React.RefObject<TextInput>) ?? useRef<TextInput>();
56
+ const inputRef = useRef<TextInput | null>(null);
50
57
 
51
58
  const onClearText = () => {
52
59
  inputRef?.current?.clear();
@@ -68,6 +75,15 @@ const InputTextArea = forwardRef(
68
75
  onBlur?.(e);
69
76
  };
70
77
 
78
+ useImperativeHandle(ref, () => {
79
+ return {
80
+ clear: onClearText,
81
+ focus: () => inputRef.current?.focus(),
82
+ blur: () => inputRef.current?.blur(),
83
+ setText: (text: string) => _onChangeText(text),
84
+ };
85
+ });
86
+
71
87
  const renderCountingView = () => {
72
88
  return (
73
89
  <View style={styles.countingView}>
@@ -151,7 +167,11 @@ const InputTextArea = forwardRef(
151
167
  return (
152
168
  <View style={[style, styles.wrapper]}>
153
169
  {renderInputView()}
154
- <ErrorView errorMessage={errorMessage} errorSpacing={errorSpacing} />
170
+ <ErrorView
171
+ errorMessage={errorMessage}
172
+ errorSpacing={errorSpacing}
173
+ hintText={hintText}
174
+ />
155
175
  </View>
156
176
  );
157
177
  },
package/Input/common.tsx CHANGED
@@ -1,11 +1,11 @@
1
- import {View, ViewStyle} from 'react-native';
2
- import styles from './styles';
3
- import {scaleSize, Text} from '../Text';
4
1
  import React, {FC, useContext} from 'react';
2
+ import {View, ViewStyle} from 'react-native';
5
3
  import {ApplicationContext} from '../Application';
6
- import {Icon} from '../Icon';
7
- import {Styles} from '../Consts';
8
4
  import {Theme} from '../Application/types';
5
+ import {Styles} from '../Consts';
6
+ import {Icon} from '../Icon';
7
+ import {scaleSize, Text} from '../Text';
8
+ import styles from './styles';
9
9
 
10
10
  type FloatingViewProps = {
11
11
  floatingValue?: string;
@@ -16,6 +16,13 @@ type FloatingViewProps = {
16
16
  style?: ViewStyle;
17
17
  };
18
18
 
19
+ export interface InputRef {
20
+ focus: () => void;
21
+ blur: () => void;
22
+ clear: () => void;
23
+ setText: (text: string) => void;
24
+ }
25
+
19
26
  export const DEFAULT_HEIGHT = scaleSize(104);
20
27
  export const MAX_LENGTH = 300;
21
28
 
@@ -48,24 +55,31 @@ export const getSizeStyle = (size?: 'small' | 'large') => {
48
55
  }
49
56
  return styles.container;
50
57
  };
51
- export const ErrorView: FC<{errorMessage?: string; errorSpacing?: boolean}> = ({
52
- errorMessage,
53
- errorSpacing,
54
- }) => {
58
+ export const ErrorView: FC<{
59
+ errorMessage?: string;
60
+ errorSpacing?: boolean;
61
+ hintText?: string;
62
+ }> = ({errorMessage, errorSpacing, hintText}) => {
55
63
  const {theme} = useContext(ApplicationContext);
56
64
  const errorColor = theme.colors.error.primary;
57
- if (errorMessage) {
65
+ const hintColor = theme.colors.text.hint;
66
+ const hintTextDefault = hintText ?? 'Không thể chỉnh sửa';
67
+ if (errorMessage || hintText) {
58
68
  return (
59
69
  <View style={styles.errorView}>
60
70
  <View style={styles.errorIcon}>
61
- <Icon color={errorColor} source="ic_error" size={16} />
71
+ <Icon
72
+ color={errorMessage ? errorColor : hintColor}
73
+ source="ic_error"
74
+ size={16}
75
+ />
62
76
  </View>
63
77
  <Text
64
78
  style={Styles.flex}
65
- color={errorColor}
79
+ color={errorMessage ? errorColor : hintColor}
66
80
  numberOfLines={2}
67
81
  typography={'description_default_regular'}>
68
- {errorMessage}
82
+ {errorMessage ?? hintTextDefault}
69
83
  </Text>
70
84
  </View>
71
85
  );
package/Input/index.tsx CHANGED
@@ -1,10 +1,10 @@
1
+ import {TextInputProps, ViewStyle} from 'react-native';
1
2
  import Input from './Input';
2
- import InputTextArea from './InputTextArea';
3
- import InputSearch from './InputSearch';
3
+ import InputDropDown from './InputDropDown';
4
4
  import InputMoney from './InputMoney';
5
5
  import InputOTP from './InputOTP';
6
- import InputDropDown from './InputDropDown';
7
- import {TextInputProps, ViewStyle} from 'react-native';
6
+ import InputSearch from './InputSearch';
7
+ import InputTextArea from './InputTextArea';
8
8
 
9
9
  export interface InputProps extends TextInputProps {
10
10
  /**
@@ -77,11 +77,25 @@ export interface InputProps extends TextInputProps {
77
77
  */
78
78
  leadingIconColor?: string;
79
79
 
80
+ /**
81
+ * Optional. Represents the font weight of the text in the Input component.
82
+ */
80
83
  fontWeight?: 'Regular' | 'Bold';
81
84
 
85
+ /**
86
+ * Optional. callback function to be called when the icon is pressed.
87
+ */
82
88
  onPressIcon?: () => void;
83
89
 
90
+ /**
91
+ * Optional. params for element tracking.
92
+ */
84
93
  params?: any;
94
+
95
+ /**
96
+ * Optional. Represents text below the Input component.
97
+ */
98
+ hintText?: string;
85
99
  }
86
100
 
87
101
  type InputPropsWithoutSizeAndIcon = Omit<
@@ -267,4 +281,4 @@ export interface InputDropDownProps extends InputProps {
267
281
  style?: ViewStyle | ViewStyle[];
268
282
  }
269
283
 
270
- export {Input, InputTextArea, InputSearch, InputMoney, InputOTP, InputDropDown};
284
+ export {Input, InputDropDown, InputMoney, InputOTP, InputSearch, InputTextArea};
package/Layout/Screen.tsx CHANGED
@@ -1,6 +1,18 @@
1
+ import {useHeaderHeight} from '@react-navigation/stack';
2
+ import React, {
3
+ forwardRef,
4
+ Fragment,
5
+ ReactNode,
6
+ useCallback,
7
+ useContext,
8
+ useEffect,
9
+ useRef,
10
+ } from 'react';
1
11
  import {
2
12
  Animated,
13
+ Easing,
3
14
  KeyboardAvoidingView,
15
+ LayoutChangeEvent,
4
16
  NativeScrollEvent,
5
17
  NativeSyntheticEvent,
6
18
  Platform,
@@ -8,28 +20,24 @@ import {
8
20
  View,
9
21
  ViewProps,
10
22
  } from 'react-native';
11
- import {useHeaderHeight} from '@react-navigation/stack';
12
23
  import {useSafeAreaInsets} from 'react-native-safe-area-context';
13
- import React, {
14
- forwardRef,
15
- Fragment,
16
- ReactNode,
17
- useContext,
18
- useEffect,
19
- useRef,
20
- } from 'react';
21
- import {Colors, Spacing, Styles} from '../Consts';
22
- import {Card, Section, validateChildren} from './index';
23
24
  import {
24
25
  ApplicationContext,
25
26
  HeaderBackground,
26
27
  HeaderTitle,
27
28
  } from '../Application';
28
29
  import Navigation from '../Application/Navigation';
29
- import styles from './styles';
30
30
  import {AnimatedHeader, NavigationOptions} from '../Application/types';
31
+ import {Colors, Spacing, Styles} from '../Consts';
32
+ import {
33
+ FloatingButton,
34
+ FloatingButtonProps,
35
+ } from '../FloatingButton/FloatingButton';
31
36
  import {Image} from '../Image';
37
+ import {Card, Section, validateChildren} from './index';
38
+ import styles from './styles';
32
39
  import {HeaderType} from './types';
40
+ import {debounce} from './utils';
33
41
 
34
42
  export interface ScreenProps extends ViewProps {
35
43
  children: React.ReactNode;
@@ -82,6 +90,11 @@ export interface ScreenProps extends ViewProps {
82
90
  * Optional. layout card offset overlap header banner.
83
91
  */
84
92
  layoutOffset?: -8 | -24 | -56;
93
+
94
+ /**
95
+ * Optional. Props for the floating action button.
96
+ */
97
+ floatingButtonProps?: Omit<FloatingButtonProps, 'animatedValue'>;
85
98
  }
86
99
 
87
100
  const Screen = forwardRef(
@@ -98,13 +111,18 @@ const Screen = forwardRef(
98
111
  layoutOffset = -56,
99
112
  backgroundColor,
100
113
  headerType = 'default',
114
+ floatingButtonProps,
101
115
  }: ScreenProps,
102
116
  ref,
103
117
  ) => {
104
118
  const animatedValue = useRef<Animated.Value | undefined>(undefined);
119
+ const lastScrollPosition = useRef(0);
120
+ const animatedFABValue = useRef(new Animated.Value(0)).current;
105
121
  const {theme} = useContext(ApplicationContext);
106
122
  const insets = useSafeAreaInsets();
107
123
  const heightHeader = useHeaderHeight();
124
+ const layoutHeight = useRef(0);
125
+ const contentHeight = useRef(0);
108
126
  const currentTint = useRef(Colors.black_01);
109
127
  let styleAnimatedHeader: NavigationOptions;
110
128
  let handleScroll;
@@ -155,7 +173,7 @@ const Screen = forwardRef(
155
173
 
156
174
  useEffect(() => {
157
175
  if (!animatedHeader) {
158
- let headerOptions: NavigationOptions = {};
176
+ let headerOptions: NavigationOptions;
159
177
  switch (headerType) {
160
178
  case 'none':
161
179
  headerOptions = {
@@ -213,8 +231,9 @@ const Screen = forwardRef(
213
231
  if (scrollable) {
214
232
  Component = Animated.ScrollView;
215
233
  handleScroll = scrollViewProps?.onScroll;
216
- if (animatedHeader) {
234
+ if (animatedHeader || floatingButtonProps) {
217
235
  animatedValue.current = new Animated.Value(0);
236
+
218
237
  handleScroll = Animated.event(
219
238
  [
220
239
  {
@@ -224,9 +243,9 @@ const Screen = forwardRef(
224
243
  },
225
244
  ],
226
245
  {
227
- useNativeDriver: true,
246
+ useNativeDriver: false,
228
247
  listener: (e: NativeSyntheticEvent<NativeScrollEvent>) => {
229
- if (animatedHeader.type == 'surface') {
248
+ if (animatedHeader?.type == 'surface') {
230
249
  const offsetY = e.nativeEvent.contentOffset.y;
231
250
  let color = Colors.black_01;
232
251
  if (offsetY > 50) {
@@ -239,13 +258,46 @@ const Screen = forwardRef(
239
258
  });
240
259
  }
241
260
  }
242
- scrollViewProps?.onScroll?.(e);
261
+ if (floatingButtonProps) {
262
+ const currentScrollPosition = e.nativeEvent.contentOffset.y;
263
+ const isScrollingDown =
264
+ currentScrollPosition > lastScrollPosition.current;
265
+ scrollViewProps?.onScroll?.(e);
266
+
267
+ if (currentScrollPosition < 0) {
268
+ return;
269
+ }
270
+ const isAtBottom =
271
+ currentScrollPosition +
272
+ layoutHeight.current +
273
+ Math.min(insets.bottom, 21) +
274
+ Spacing.S >=
275
+ contentHeight.current;
276
+ if (isAtBottom) {
277
+ return;
278
+ }
279
+ animateFAB(isScrollingDown);
280
+
281
+ lastScrollPosition.current = currentScrollPosition;
282
+ }
243
283
  },
244
284
  },
245
285
  );
246
286
  }
247
287
  }
248
288
 
289
+ const animateFAB = useCallback(
290
+ debounce((isScrollingDown: boolean) => {
291
+ Animated.timing(animatedFABValue, {
292
+ toValue: isScrollingDown ? 1 : 0,
293
+ duration: 100,
294
+ useNativeDriver: false,
295
+ easing: Easing.linear,
296
+ }).start();
297
+ }, 300),
298
+ [],
299
+ );
300
+
249
301
  /**
250
302
  * render top navigation banner
251
303
  */
@@ -299,7 +351,6 @@ const Screen = forwardRef(
299
351
  );
300
352
  }
301
353
  };
302
-
303
354
  return (
304
355
  <View style={[Styles.flex, {backgroundColor}]}>
305
356
  {!!headerBackground && (
@@ -327,6 +378,12 @@ const Screen = forwardRef(
327
378
  ref={ref}
328
379
  showsVerticalScrollIndicator={false}
329
380
  onScroll={handleScroll}
381
+ onContentSizeChange={(_: number, height: number) => {
382
+ contentHeight.current = height;
383
+ }}
384
+ onLayout={(event: LayoutChangeEvent) => {
385
+ layoutHeight.current = event.nativeEvent.layout.height;
386
+ }}
330
387
  style={Styles.flex}>
331
388
  <View
332
389
  style={[styles.screenBanner, {maxHeight: 210 + layoutOffset}]}>
@@ -334,10 +391,25 @@ const Screen = forwardRef(
334
391
  </View>
335
392
  {renderContent(children)}
336
393
  </Component>
394
+ <View>
395
+ {floatingButtonProps && (
396
+ <FloatingButton
397
+ label={floatingButtonProps?.label}
398
+ onPress={floatingButtonProps?.onPress}
399
+ size={floatingButtonProps?.size}
400
+ position={floatingButtonProps?.position}
401
+ icon={floatingButtonProps?.icon}
402
+ iconColor={floatingButtonProps?.iconColor}
403
+ isBottomTab={floatingButtonProps?.isBottomTab}
404
+ animatedValue={animatedFABValue}
405
+ isFooter={!!Footer}
406
+ />
407
+ )}
408
+ </View>
337
409
  {Footer && (
338
410
  <View
339
411
  style={[
340
- styles.footerContainer,
412
+ styles.shadow,
341
413
  {
342
414
  paddingBottom: Math.min(insets.bottom, 21) + Spacing.S,
343
415
  backgroundColor: theme.colors.background.surface,
package/Layout/styles.ts CHANGED
@@ -80,17 +80,21 @@ export default StyleSheet.create({
80
80
  paddingHorizontal: undefined,
81
81
  paddingVertical: undefined,
82
82
  },
83
- footerContainer: {
83
+ shadow: {
84
84
  paddingTop: Spacing.S,
85
85
  ...Platform.select({
86
- android: {
87
- elevation: 4,
88
- },
89
86
  ios: {
87
+ shadowColor: Colors.black_17,
88
+ shadowOffset: {
89
+ width: 2,
90
+ height: 2,
91
+ },
92
+ shadowOpacity: 0.25,
93
+ shadowRadius: 10,
94
+ },
95
+ android: {
90
96
  shadowColor: Colors.black_20,
91
- shadowOffset: {width: 0, height: -2},
92
- shadowOpacity: 0.1,
93
- shadowRadius: 3,
97
+ elevation: 24,
94
98
  },
95
99
  }),
96
100
  },
package/Layout/utils.ts CHANGED
@@ -1,5 +1,5 @@
1
- import {Dimensions} from 'react-native';
2
1
  import React, {Fragment, ReactElement, useEffect, useState} from 'react';
2
+ import {Dimensions} from 'react-native';
3
3
 
4
4
  /**
5
5
  * validate children type
@@ -51,5 +51,20 @@ const useGridSystem = () => {
51
51
 
52
52
  return {numberOfColumns};
53
53
  };
54
+ function debounce<T extends (...args: any[]) => void>(
55
+ func: T,
56
+ wait: number,
57
+ ): (...args: Parameters<T>) => void {
58
+ let timeout: NodeJS.Timeout | null = null;
59
+
60
+ return function (...args: Parameters<T>) {
61
+ if (timeout !== null) {
62
+ clearTimeout(timeout);
63
+ }
64
+ timeout = setTimeout(() => {
65
+ func(...args);
66
+ }, wait);
67
+ };
68
+ }
54
69
 
55
- export {validateChildren, useGridSystem};
70
+ export {debounce, useGridSystem, validateChildren};
@@ -1,12 +1,12 @@
1
1
  import React, {useContext, useEffect, useRef, useState} from 'react';
2
2
  import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native';
3
- import {PopupNotifyProps} from './types';
4
- import {Radius, Spacing, Styles} from '../Consts';
5
- import {Image} from '../Image';
6
- import {Text} from '../Text';
7
3
  import {ApplicationContext, MiniAppContext} from '../Application';
8
4
  import {Button} from '../Button';
5
+ import {Radius, Spacing, Styles} from '../Consts';
9
6
  import {Icon} from '../Icon';
7
+ import {Image} from '../Image';
8
+ import {Text} from '../Text';
9
+ import {PopupNotifyProps} from './types';
10
10
 
11
11
  const PopupNotify: React.FC<PopupNotifyProps> = ({
12
12
  id,
@@ -23,14 +23,12 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
23
23
  const {theme, navigator, translate} = useContext(ApplicationContext);
24
24
  const [scrollContent, setScrollContent] = useState(false);
25
25
  const closeAction = useRef('touch_outside');
26
-
27
26
  let descriptionStyle = {};
28
27
  let Description: any = View;
29
28
  if (scrollContent) {
30
29
  Description = ScrollView;
31
30
  descriptionStyle = {height: 80};
32
31
  }
33
-
34
32
  /**
35
33
  * tracking
36
34
  */
@@ -160,30 +158,32 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
160
158
  * build close action
161
159
  */
162
160
  const buildCloseIcon = () => {
163
- return (
164
- <TouchableOpacity
165
- accessibilityLabel={'ic_popup_close'}
166
- style={styles.iconCloseContainer}
167
- onPress={() => {
168
- closeAction.current = 'icon_close';
169
- onAction(onIconClose);
170
- }}>
171
- <View
172
- style={[
173
- styles.iconClose,
174
- {
175
- backgroundColor: theme.colors.text.default,
176
- borderColor: theme.colors.background.surface,
177
- },
178
- ]}>
179
- <Icon
180
- source="navigation_close"
181
- color={theme.colors.background.surface}
182
- size={14}
183
- />
184
- </View>
185
- </TouchableOpacity>
186
- );
161
+ if (typeof onIconClose === 'function') {
162
+ return (
163
+ <TouchableOpacity
164
+ accessibilityLabel={'ic_popup_close'}
165
+ style={styles.iconCloseContainer}
166
+ onPress={() => {
167
+ closeAction.current = 'icon_close';
168
+ onAction(onIconClose);
169
+ }}>
170
+ <View
171
+ style={[
172
+ styles.iconClose,
173
+ {
174
+ backgroundColor: theme.colors.text.default,
175
+ borderColor: theme.colors.background.surface,
176
+ },
177
+ ]}>
178
+ <Icon
179
+ source="navigation_close"
180
+ color={theme.colors.background.surface}
181
+ size={14}
182
+ />
183
+ </View>
184
+ </TouchableOpacity>
185
+ );
186
+ }
187
187
  };
188
188
 
189
189
  return (
package/Popup/types.ts CHANGED
@@ -1,4 +1,3 @@
1
- import {TextStyle} from 'react-native';
2
1
  import React from 'react';
3
2
 
4
3
  export type PopupNotifyProps = {
@@ -79,9 +78,4 @@ type PopupAction = {
79
78
  onPress: () => void;
80
79
  };
81
80
 
82
- export type HighlightText = {
83
- text: string;
84
- style: TextStyle;
85
- };
86
-
87
81
  type PopupActionDirection = 'row' | 'column' | 'auto';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.92.5",
3
+ "version": "0.92.6-beta.10",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {
@@ -16,12 +16,12 @@
16
16
  "react-native-gesture-handler": "1.10.3",
17
17
  "react-native-modalize": "2.1.1",
18
18
  "react-native-fast-image": "8.1.5",
19
- "@react-navigation/bottom-tabs": "git+https://gitlab.mservice.com.vn/momo-platform/react-navigation-bottom-tabs.git",
19
+ "@react-navigation/bottom-tabs": "https://oauth2:5WXQLHPMxxCyvGt_Py4D@gitlab.mservice.com.vn/momo-platform/react-native-bottom-tabs.git",
20
20
  "@react-navigation/core": "5.16.1",
21
21
  "@react-navigation/native": "5.9.8",
22
22
  "@react-navigation/routers": "5.7.4",
23
23
  "lottie-react-native": "3.3.2",
24
- "@react-navigation/stack": "git+https://gitlab.mservice.com.vn/momo-platform/react-navigation-stack.git"
24
+ "@react-navigation/stack": "https://oauth2:eX-jVhzQdLc343AjD3Sc@gitlab.mservice.com.vn/momo-platform/react-navigation-stack.git"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "react-native": "*"