@momo-kits/foundation 0.92.6-rc.1 → 0.92.6-rc.4
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/ModalScreen.tsx +3 -1
- package/Application/Navigation.ts +2 -2
- package/Application/StackScreen.tsx +1 -1
- package/Application/types.ts +1 -1
- package/Icon/types.ts +2 -1
- package/Input/Input.tsx +26 -6
- package/Input/InputDropDown.tsx +10 -5
- package/Input/InputMoney.tsx +13 -4
- package/Input/InputOTP.tsx +14 -6
- package/Input/InputSearch.tsx +19 -4
- package/Input/InputTextArea.tsx +26 -6
- package/Input/common.tsx +27 -13
- package/Input/index.tsx +19 -5
- package/Layout/FloatingButton.tsx +128 -0
- package/Layout/Screen.tsx +48 -20
- package/Layout/styles.ts +11 -7
- package/Layout/utils.ts +2 -2
- package/Popup/PopupNotify.tsx +30 -30
- package/Popup/types.ts +0 -6
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, {useContext, useEffect, useRef} from 'react';
|
|
2
2
|
import {
|
|
3
|
+
Dimensions,
|
|
3
4
|
KeyboardAvoidingView,
|
|
4
5
|
Platform,
|
|
5
6
|
Pressable,
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
import {Modalize} from 'react-native-modalize';
|
|
11
12
|
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
|
12
13
|
|
|
13
|
-
import {ApplicationContext} from '
|
|
14
|
+
import {ApplicationContext} from './index';
|
|
14
15
|
import {Radius, Spacing, Styles} from '../Consts';
|
|
15
16
|
import {Icon} from '../Icon';
|
|
16
17
|
import {Text} from '../Text';
|
|
@@ -207,6 +208,7 @@ const styles = StyleSheet.create({
|
|
|
207
208
|
backgroundColor: 'white',
|
|
208
209
|
borderTopRightRadius: 16,
|
|
209
210
|
borderTopLeftRadius: 16,
|
|
211
|
+
maxHeight: Dimensions.get('window').height * 0.75,
|
|
210
212
|
},
|
|
211
213
|
header: {
|
|
212
214
|
height: 50,
|
|
@@ -6,8 +6,8 @@ import {getOptions} from './utils';
|
|
|
6
6
|
import {HeaderToolkitAction} from './Components';
|
|
7
7
|
|
|
8
8
|
class Navigation {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
instance: NavigationProp<any>;
|
|
10
|
+
readonly theme: Theme;
|
|
11
11
|
|
|
12
12
|
constructor(instance: any, theme: Theme) {
|
|
13
13
|
this.instance = instance;
|
package/Application/types.ts
CHANGED
|
@@ -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
|
|
123
|
+
flatListProps?: FlatListProps<any>;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
export type NavigationButtonProps = {
|
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, {
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
236
|
+
<ErrorView
|
|
237
|
+
errorMessage={errorMessage}
|
|
238
|
+
errorSpacing={errorSpacing}
|
|
239
|
+
hintText={hintText}
|
|
240
|
+
/>
|
|
221
241
|
</View>
|
|
222
242
|
</ComponentContext.Provider>
|
|
223
243
|
);
|
package/Input/InputDropDown.tsx
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
122
|
+
<ErrorView
|
|
123
|
+
errorMessage={errorMessage}
|
|
124
|
+
errorSpacing={errorSpacing}
|
|
125
|
+
hintText={hintText}
|
|
126
|
+
/>
|
|
122
127
|
</TouchableOpacity>
|
|
123
128
|
</ComponentContext.Provider>
|
|
124
129
|
);
|
package/Input/InputMoney.tsx
CHANGED
|
@@ -13,10 +13,10 @@ import {
|
|
|
13
13
|
View,
|
|
14
14
|
} from 'react-native';
|
|
15
15
|
import {ApplicationContext, ComponentContext} from '../Application';
|
|
16
|
-
import
|
|
16
|
+
import {Icon} from '../Icon';
|
|
17
17
|
import {ErrorView, FloatingView, getBorderColor, getSizeStyle} from './common';
|
|
18
18
|
import {InputMoneyProps} from './index';
|
|
19
|
-
import
|
|
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 =
|
|
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
|
|
185
|
+
<ErrorView
|
|
186
|
+
errorMessage={errorMessage}
|
|
187
|
+
errorSpacing={errorSpacing}
|
|
188
|
+
hintText={hintText}
|
|
189
|
+
/>
|
|
181
190
|
</View>
|
|
182
191
|
</ComponentContext.Provider>
|
|
183
192
|
);
|
package/Input/InputOTP.tsx
CHANGED
|
@@ -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
|
|
246
|
+
<ErrorView
|
|
247
|
+
errorMessage={errorMessage}
|
|
248
|
+
errorSpacing={errorSpacing}
|
|
249
|
+
hintText={hintText}
|
|
250
|
+
/>
|
|
243
251
|
</TouchableOpacity>
|
|
244
252
|
</ComponentContext.Provider>
|
|
245
253
|
);
|
package/Input/InputSearch.tsx
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import 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 =
|
|
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
|
package/Input/InputTextArea.tsx
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import 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
|
|
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
|
|
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 =
|
|
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
|
|
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<{
|
|
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
|
-
|
|
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
|
|
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
|
|
3
|
-
import InputSearch from './InputSearch';
|
|
3
|
+
import InputDropDown from './InputDropDown';
|
|
4
4
|
import InputMoney from './InputMoney';
|
|
5
5
|
import InputOTP from './InputOTP';
|
|
6
|
-
import
|
|
7
|
-
import
|
|
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,
|
|
284
|
+
export {Input, InputDropDown, InputMoney, InputOTP, InputSearch, InputTextArea};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {default as React, useContext, useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
LayoutChangeEvent,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import {ApplicationContext} from '../Application';
|
|
9
|
+
import {Icon} from '../Icon';
|
|
10
|
+
import {Text} from '../Text';
|
|
11
|
+
|
|
12
|
+
export interface FloatingButtonProps {
|
|
13
|
+
label?: string;
|
|
14
|
+
onPress?: () => void;
|
|
15
|
+
position?: 'center' | 'right';
|
|
16
|
+
icon?: string;
|
|
17
|
+
iconColor?: string;
|
|
18
|
+
size?: 'small' | 'large';
|
|
19
|
+
animatedValue?: Animated.Value;
|
|
20
|
+
bottom?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const FloatingButton: React.FC<FloatingButtonProps> = ({
|
|
24
|
+
label = '',
|
|
25
|
+
onPress = () => {},
|
|
26
|
+
position = 'right',
|
|
27
|
+
icon = '',
|
|
28
|
+
iconColor = '',
|
|
29
|
+
size = 'small',
|
|
30
|
+
animatedValue,
|
|
31
|
+
bottom = 12,
|
|
32
|
+
}: FloatingButtonProps) => {
|
|
33
|
+
const {theme} = useContext(ApplicationContext);
|
|
34
|
+
const maxWidth = useRef(0);
|
|
35
|
+
const minWidth = size == 'small' ? 36 : 48;
|
|
36
|
+
const [widthAnimated, setWidthAnimated] = useState<Animated.Value>();
|
|
37
|
+
const lastOffset = useRef(0);
|
|
38
|
+
const debounce = useRef<any>();
|
|
39
|
+
const [showText, setShowText] = React.useState(true);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!label) return;
|
|
43
|
+
|
|
44
|
+
const listener = animatedValue?.addListener(({value}) => {
|
|
45
|
+
clearTimeout(debounce.current);
|
|
46
|
+
debounce.current = setTimeout(() => {
|
|
47
|
+
const down = value > lastOffset.current;
|
|
48
|
+
lastOffset.current = value;
|
|
49
|
+
if (down) {
|
|
50
|
+
Animated.timing(widthAnimated!, {
|
|
51
|
+
toValue: minWidth,
|
|
52
|
+
duration: 300,
|
|
53
|
+
useNativeDriver: false,
|
|
54
|
+
}).start(() => setShowText(false));
|
|
55
|
+
} else {
|
|
56
|
+
Animated.timing(widthAnimated!, {
|
|
57
|
+
toValue: maxWidth.current,
|
|
58
|
+
duration: 300,
|
|
59
|
+
useNativeDriver: false,
|
|
60
|
+
}).start(() => setShowText(true));
|
|
61
|
+
}
|
|
62
|
+
}, 500);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
if (listener != null) {
|
|
67
|
+
animatedValue?.removeListener(listener);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}, [label, widthAnimated]);
|
|
71
|
+
|
|
72
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
73
|
+
const layout = event.nativeEvent.layout;
|
|
74
|
+
if (widthAnimated) return;
|
|
75
|
+
maxWidth.current = layout.width;
|
|
76
|
+
setWidthAnimated(new Animated.Value(layout.width));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Animated.View
|
|
81
|
+
onLayout={handleLayout}
|
|
82
|
+
style={[
|
|
83
|
+
styles.container,
|
|
84
|
+
{
|
|
85
|
+
right: position === 'right' ? 12 : undefined,
|
|
86
|
+
alignSelf: position === 'center' ? 'center' : 'flex-end',
|
|
87
|
+
width: widthAnimated,
|
|
88
|
+
height: size == 'small' ? 36 : 48,
|
|
89
|
+
backgroundColor: theme.colors.primary,
|
|
90
|
+
bottom,
|
|
91
|
+
},
|
|
92
|
+
]}>
|
|
93
|
+
<TouchableOpacity
|
|
94
|
+
style={{flexDirection: 'row', alignItems: 'center'}}
|
|
95
|
+
onPress={onPress}>
|
|
96
|
+
<Icon
|
|
97
|
+
source={icon}
|
|
98
|
+
size={size === 'small' ? 12 : 24}
|
|
99
|
+
color={iconColor}
|
|
100
|
+
/>
|
|
101
|
+
{showText && !!label && (
|
|
102
|
+
<Text
|
|
103
|
+
style={styles.label}
|
|
104
|
+
color="white"
|
|
105
|
+
typography="action_default_bold">
|
|
106
|
+
{label}
|
|
107
|
+
</Text>
|
|
108
|
+
)}
|
|
109
|
+
</TouchableOpacity>
|
|
110
|
+
</Animated.View>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const styles = StyleSheet.create({
|
|
115
|
+
container: {
|
|
116
|
+
flexDirection: 'row',
|
|
117
|
+
alignContent: 'center',
|
|
118
|
+
alignItems: 'center',
|
|
119
|
+
position: 'absolute',
|
|
120
|
+
borderRadius: 100,
|
|
121
|
+
paddingHorizontal: 12,
|
|
122
|
+
flexWrap: 'wrap',
|
|
123
|
+
alignSelf: 'flex-end',
|
|
124
|
+
},
|
|
125
|
+
label: {
|
|
126
|
+
marginLeft: 8,
|
|
127
|
+
},
|
|
128
|
+
});
|
package/Layout/Screen.tsx
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {useHeaderHeight} from '@react-navigation/stack';
|
|
2
|
+
import React, {
|
|
3
|
+
forwardRef,
|
|
4
|
+
Fragment,
|
|
5
|
+
ReactNode,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
} from 'react';
|
|
1
10
|
import {
|
|
2
11
|
Animated,
|
|
3
12
|
KeyboardAvoidingView,
|
|
@@ -8,27 +17,19 @@ import {
|
|
|
8
17
|
View,
|
|
9
18
|
ViewProps,
|
|
10
19
|
} from 'react-native';
|
|
11
|
-
import {useHeaderHeight} from '@react-navigation/stack';
|
|
12
20
|
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
21
|
import {
|
|
24
22
|
ApplicationContext,
|
|
25
23
|
HeaderBackground,
|
|
26
24
|
HeaderTitle,
|
|
27
25
|
} from '../Application';
|
|
28
26
|
import Navigation from '../Application/Navigation';
|
|
29
|
-
import styles from './styles';
|
|
30
27
|
import {AnimatedHeader, NavigationOptions} from '../Application/types';
|
|
28
|
+
import {Colors, Spacing, Styles} from '../Consts';
|
|
29
|
+
import {FloatingButton, FloatingButtonProps} from './FloatingButton';
|
|
31
30
|
import {Image} from '../Image';
|
|
31
|
+
import {Card, Section, validateChildren} from './index';
|
|
32
|
+
import styles from './styles';
|
|
32
33
|
import {HeaderType} from './types';
|
|
33
34
|
|
|
34
35
|
export interface ScreenProps extends ViewProps {
|
|
@@ -82,6 +83,16 @@ export interface ScreenProps extends ViewProps {
|
|
|
82
83
|
* Optional. layout card offset overlap header banner.
|
|
83
84
|
*/
|
|
84
85
|
layoutOffset?: -8 | -24 | -56;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Optional. Props for the floating action button.
|
|
89
|
+
*/
|
|
90
|
+
floatingButtonProps?: Omit<FloatingButtonProps, 'animatedValue' | 'bottom'>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Optional. If `true`, the screen content is using grid layout by span.
|
|
94
|
+
*/
|
|
95
|
+
useGridLayout?: boolean;
|
|
85
96
|
}
|
|
86
97
|
|
|
87
98
|
const Screen = forwardRef(
|
|
@@ -98,6 +109,8 @@ const Screen = forwardRef(
|
|
|
98
109
|
layoutOffset = -56,
|
|
99
110
|
backgroundColor,
|
|
100
111
|
headerType = 'default',
|
|
112
|
+
floatingButtonProps,
|
|
113
|
+
useGridLayout = true,
|
|
101
114
|
}: ScreenProps,
|
|
102
115
|
ref,
|
|
103
116
|
) => {
|
|
@@ -106,12 +119,13 @@ const Screen = forwardRef(
|
|
|
106
119
|
const insets = useSafeAreaInsets();
|
|
107
120
|
const heightHeader = useHeaderHeight();
|
|
108
121
|
const currentTint = useRef(Colors.black_01);
|
|
122
|
+
const isTab = navigation?.instance?.getState?.()?.type == 'tab';
|
|
123
|
+
|
|
109
124
|
let styleAnimatedHeader: NavigationOptions;
|
|
110
125
|
let handleScroll;
|
|
111
126
|
let headerBackground: string | undefined = undefined;
|
|
112
127
|
let Component: any = View;
|
|
113
128
|
let keyboardVerticalOffset = heightHeader - 20;
|
|
114
|
-
animatedValue.current = undefined;
|
|
115
129
|
|
|
116
130
|
if (headerType == 'extended' || animatedHeader) {
|
|
117
131
|
keyboardVerticalOffset = -20;
|
|
@@ -155,7 +169,7 @@ const Screen = forwardRef(
|
|
|
155
169
|
|
|
156
170
|
useEffect(() => {
|
|
157
171
|
if (!animatedHeader) {
|
|
158
|
-
let headerOptions: NavigationOptions
|
|
172
|
+
let headerOptions: NavigationOptions;
|
|
159
173
|
switch (headerType) {
|
|
160
174
|
case 'none':
|
|
161
175
|
headerOptions = {
|
|
@@ -213,8 +227,9 @@ const Screen = forwardRef(
|
|
|
213
227
|
if (scrollable) {
|
|
214
228
|
Component = Animated.ScrollView;
|
|
215
229
|
handleScroll = scrollViewProps?.onScroll;
|
|
216
|
-
if (animatedHeader) {
|
|
230
|
+
if (animatedHeader || floatingButtonProps) {
|
|
217
231
|
animatedValue.current = new Animated.Value(0);
|
|
232
|
+
|
|
218
233
|
handleScroll = Animated.event(
|
|
219
234
|
[
|
|
220
235
|
{
|
|
@@ -224,9 +239,9 @@ const Screen = forwardRef(
|
|
|
224
239
|
},
|
|
225
240
|
],
|
|
226
241
|
{
|
|
227
|
-
useNativeDriver:
|
|
242
|
+
useNativeDriver: false,
|
|
228
243
|
listener: (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
229
|
-
if (animatedHeader
|
|
244
|
+
if (animatedHeader?.type == 'surface') {
|
|
230
245
|
const offsetY = e.nativeEvent.contentOffset.y;
|
|
231
246
|
let color = Colors.black_01;
|
|
232
247
|
if (offsetY > 50) {
|
|
@@ -239,7 +254,6 @@ const Screen = forwardRef(
|
|
|
239
254
|
});
|
|
240
255
|
}
|
|
241
256
|
}
|
|
242
|
-
scrollViewProps?.onScroll?.(e);
|
|
243
257
|
},
|
|
244
258
|
},
|
|
245
259
|
);
|
|
@@ -322,6 +336,7 @@ const Screen = forwardRef(
|
|
|
322
336
|
android: undefined,
|
|
323
337
|
})}>
|
|
324
338
|
{Header}
|
|
339
|
+
|
|
325
340
|
<Component
|
|
326
341
|
{...scrollViewProps}
|
|
327
342
|
ref={ref}
|
|
@@ -332,12 +347,25 @@ const Screen = forwardRef(
|
|
|
332
347
|
style={[styles.screenBanner, {maxHeight: 210 + layoutOffset}]}>
|
|
333
348
|
{renderHeader()}
|
|
334
349
|
</View>
|
|
335
|
-
{renderContent(children)}
|
|
350
|
+
{useGridLayout ? renderContent(children) : children}
|
|
336
351
|
</Component>
|
|
352
|
+
|
|
353
|
+
{floatingButtonProps && (
|
|
354
|
+
<View>
|
|
355
|
+
<FloatingButton
|
|
356
|
+
{...floatingButtonProps}
|
|
357
|
+
animatedValue={animatedValue.current}
|
|
358
|
+
bottom={
|
|
359
|
+
Footer || isTab ? 12 : Math.min(insets.bottom, 21) + Spacing.S
|
|
360
|
+
}
|
|
361
|
+
/>
|
|
362
|
+
</View>
|
|
363
|
+
)}
|
|
364
|
+
|
|
337
365
|
{Footer && (
|
|
338
366
|
<View
|
|
339
367
|
style={[
|
|
340
|
-
styles.
|
|
368
|
+
styles.shadow,
|
|
341
369
|
{
|
|
342
370
|
paddingBottom: Math.min(insets.bottom, 21) + Spacing.S,
|
|
343
371
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -52,4 +52,4 @@ const useGridSystem = () => {
|
|
|
52
52
|
return {numberOfColumns};
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
export {
|
|
55
|
+
export {useGridSystem, validateChildren};
|
package/Popup/PopupNotify.tsx
CHANGED
|
@@ -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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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';
|