@momo-kits/foundation 0.169.9 → 0.169.10-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.
- package/Application/BottomSheet.hooks.ts +17 -27
- package/Application/BottomSheet.tsx +27 -35
- package/Application/BottomTab/BottomTabBar.tsx +6 -6
- package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
- package/Application/BottomTab/TabScreen.tsx +10 -11
- package/Application/BottomTab/index.tsx +5 -5
- package/Application/BottomTab/usePressLift.ts +3 -3
- package/Application/Components/HeaderAnimated.tsx +38 -13
- package/Application/Components/HeaderBackground.tsx +16 -3
- package/Application/Components/HeaderExtendHeader.tsx +41 -28
- package/Application/Components/HeaderLeft.tsx +4 -18
- package/Application/Components/HeaderRight.styles.ts +15 -13
- package/Application/Components/HeaderTitle.shared.ts +10 -11
- package/Application/Components/HeaderToolkitAction.tsx +21 -54
- package/Application/Components/NavigationButton.tsx +9 -6
- package/Application/Components/SearchHeader.tsx +20 -7
- package/Application/Components/useToolkitActions.tsx +21 -22
- package/Application/ModalScreen.tsx +15 -19
- package/Application/StackScreen.tsx +18 -13
- package/Application/headerLayout.ts +165 -0
- package/Application/index.ts +11 -1
- package/Application/modalLayout.ts +0 -1
- package/Application/navigationOptions.tsx +6 -15
- package/Application/types.header.ts +7 -2
- package/Application/types.ts +7 -0
- package/Context/index.ts +1 -3
- package/Image/index.tsx +6 -6
- package/Input/Input.tsx +37 -40
- package/Input/InputDropDown.tsx +10 -13
- package/Input/InputMoney.tsx +17 -17
- package/Input/InputMoneyView.tsx +12 -12
- package/Input/InputOTP.tsx +34 -46
- package/Input/InputPhoneNumber.tsx +43 -48
- package/Input/InputSearch.tsx +39 -26
- package/Input/InputTextArea.tsx +23 -29
- package/Input/common.tsx +257 -0
- package/Input/index.tsx +7 -1
- package/Input/moneyUtils.ts +23 -0
- package/Input/utils.ts +7 -126
- package/Layout/FloatingButton.tsx +24 -38
- package/Layout/Screen.tsx +3 -0
- package/Layout/layoutMetrics.ts +34 -43
- package/Layout/screenHeader.tsx +55 -52
- package/Layout/screenTypes.ts +13 -4
- package/Pagination/PaginationScroll.tsx +3 -5
- package/Pagination/types.ts +5 -3
- package/Popup/PopupNotify.tsx +14 -17
- package/Popup/PopupPromotion.tsx +4 -5
- package/Popup/types.ts +4 -2
- package/Popup/utils.ts +4 -7
- package/Skeleton/index.tsx +12 -15
- package/package.json +1 -1
- package/Input/ErrorView.tsx +0 -59
- package/Input/FloatingView.tsx +0 -101
- package/Input/RenderTrailing.tsx +0 -79
- package/Input/constants.ts +0 -8
package/Input/common.tsx
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import React, { FC, ReactNode, useContext } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
GestureResponderEvent,
|
|
4
|
+
TouchableOpacity,
|
|
5
|
+
View,
|
|
6
|
+
ViewStyle,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import { ApplicationContext } from '../Context';
|
|
9
|
+
import { Theme } from '../Application/types';
|
|
10
|
+
import { Styles } from '../Consts';
|
|
11
|
+
import { Icon } from '../Icon';
|
|
12
|
+
import { useScaleSize, Text } from '../Text';
|
|
13
|
+
import styles from './styles';
|
|
14
|
+
import { Loader } from '../Loader';
|
|
15
|
+
import IconSources from '../Assets/icon.json';
|
|
16
|
+
|
|
17
|
+
type FloatingViewProps = {
|
|
18
|
+
floatingValue?: string;
|
|
19
|
+
floatingIconColor?: string;
|
|
20
|
+
componentId?: string;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
floatingIcon?: string;
|
|
23
|
+
required?: boolean;
|
|
24
|
+
style?: ViewStyle;
|
|
25
|
+
onPress?: (e: GestureResponderEvent) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type TrailingProps = {
|
|
29
|
+
loading?: boolean;
|
|
30
|
+
color?: string;
|
|
31
|
+
icon?: string;
|
|
32
|
+
componentId?: string;
|
|
33
|
+
onPressIcon?: () => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const MAX_LENGTH = 300;
|
|
37
|
+
|
|
38
|
+
/** Border color by state; priority is disabled > error > focused > default. */
|
|
39
|
+
export const getBorderColor = (
|
|
40
|
+
theme: Theme,
|
|
41
|
+
focused: boolean,
|
|
42
|
+
errorMessage?: string,
|
|
43
|
+
disabled?: boolean,
|
|
44
|
+
) => {
|
|
45
|
+
let borderColor = theme.colors.border.default;
|
|
46
|
+
|
|
47
|
+
if (focused) {
|
|
48
|
+
borderColor = theme.colors.primary;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (errorMessage) {
|
|
52
|
+
borderColor = theme.colors.error.primary;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (disabled) {
|
|
56
|
+
borderColor = theme.colors.border.disable;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { borderColor };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Container style for `size`; multiline uses `height` as minHeight so the
|
|
64
|
+
* field can grow with its content.
|
|
65
|
+
*/
|
|
66
|
+
export const getSizeStyle = (
|
|
67
|
+
size?: 'small' | 'large',
|
|
68
|
+
multiline: boolean = false,
|
|
69
|
+
height?: number,
|
|
70
|
+
) => {
|
|
71
|
+
let scaleStyle: ViewStyle = {};
|
|
72
|
+
if (height) {
|
|
73
|
+
scaleStyle = { height };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (multiline) {
|
|
77
|
+
return [
|
|
78
|
+
styles.multilineContainer,
|
|
79
|
+
{
|
|
80
|
+
minHeight: height,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (size === 'small') {
|
|
86
|
+
return [styles.smallContainer, scaleStyle];
|
|
87
|
+
}
|
|
88
|
+
return [styles.container, scaleStyle];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Error or hint row under an input (error wins); with neither, `errorSpacing`
|
|
93
|
+
* keeps an empty row so the layout doesn't jump when an error appears.
|
|
94
|
+
*/
|
|
95
|
+
export const ErrorView: FC<{
|
|
96
|
+
errorMessage?: string;
|
|
97
|
+
errorSpacing?: boolean;
|
|
98
|
+
hintText?: string;
|
|
99
|
+
componentId?: string;
|
|
100
|
+
}> = ({ errorMessage, errorSpacing, hintText, componentId }) => {
|
|
101
|
+
const { theme } = useContext(ApplicationContext);
|
|
102
|
+
const errorColor = theme.colors.error.primary;
|
|
103
|
+
const hintColor = theme.colors.text.hint;
|
|
104
|
+
const hintTextDefault = hintText ?? 'Không thể chỉnh sửa';
|
|
105
|
+
const scaledIconSize = useScaleSize(16);
|
|
106
|
+
|
|
107
|
+
if (errorMessage || hintText) {
|
|
108
|
+
const messageColor = errorMessage ? errorColor : hintColor;
|
|
109
|
+
const messageType = errorMessage ? 'error' : 'hint';
|
|
110
|
+
return (
|
|
111
|
+
<View style={styles.errorView}>
|
|
112
|
+
<View style={styles.errorIcon}>
|
|
113
|
+
<Icon color={messageColor} source="ic_error" size={scaledIconSize} />
|
|
114
|
+
</View>
|
|
115
|
+
<Text
|
|
116
|
+
accessibilityLabel={`${componentId}|${messageType}-text`}
|
|
117
|
+
style={Styles.flex}
|
|
118
|
+
color={messageColor}
|
|
119
|
+
typography={'description_default_regular'}
|
|
120
|
+
>
|
|
121
|
+
{errorMessage ?? hintTextDefault}
|
|
122
|
+
</Text>
|
|
123
|
+
</View>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (errorSpacing) {
|
|
127
|
+
return <View style={[styles.errorView, styles.errorSpacing]} />;
|
|
128
|
+
}
|
|
129
|
+
return <View />;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Floating label on the input's top border with a required `*` and an
|
|
134
|
+
* optional pressable icon; renders nothing without `floatingValue`.
|
|
135
|
+
*/
|
|
136
|
+
export const FloatingView: FC<FloatingViewProps> = ({
|
|
137
|
+
floatingValue,
|
|
138
|
+
floatingIconColor,
|
|
139
|
+
disabled,
|
|
140
|
+
floatingIcon,
|
|
141
|
+
required,
|
|
142
|
+
style,
|
|
143
|
+
onPress,
|
|
144
|
+
componentId,
|
|
145
|
+
}) => {
|
|
146
|
+
const { theme } = useContext(ApplicationContext);
|
|
147
|
+
const scaledTop = useScaleSize(10);
|
|
148
|
+
if (floatingValue) {
|
|
149
|
+
let floatingTextColor = theme.colors.text.hint;
|
|
150
|
+
let floatingIconTintColor = floatingIconColor;
|
|
151
|
+
if (disabled) {
|
|
152
|
+
floatingTextColor = theme.colors.text.disable;
|
|
153
|
+
floatingIconTintColor = theme.colors.text.disable;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<View
|
|
158
|
+
style={[
|
|
159
|
+
styles.floatingView,
|
|
160
|
+
styles.floatingViewLayer,
|
|
161
|
+
{
|
|
162
|
+
backgroundColor: theme.colors.background.surface,
|
|
163
|
+
top: -scaledTop,
|
|
164
|
+
},
|
|
165
|
+
style,
|
|
166
|
+
]}
|
|
167
|
+
>
|
|
168
|
+
<Text
|
|
169
|
+
color={floatingTextColor}
|
|
170
|
+
typography={'label_s_medium'}
|
|
171
|
+
accessibilityLabel={`${componentId}|floating-text`}
|
|
172
|
+
>
|
|
173
|
+
{floatingValue}
|
|
174
|
+
{required && (
|
|
175
|
+
<Text
|
|
176
|
+
typography={'label_s_medium'}
|
|
177
|
+
color={theme.colors.error.primary}
|
|
178
|
+
>
|
|
179
|
+
*
|
|
180
|
+
</Text>
|
|
181
|
+
)}
|
|
182
|
+
</Text>
|
|
183
|
+
{!!floatingIcon && (
|
|
184
|
+
<TouchableOpacity
|
|
185
|
+
activeOpacity={onPress ? 0.6 : 1}
|
|
186
|
+
onPress={onPress}
|
|
187
|
+
accessibilityLabel={`${componentId}|floating-icon-touch`}
|
|
188
|
+
>
|
|
189
|
+
<Icon
|
|
190
|
+
color={floatingIconTintColor}
|
|
191
|
+
source={floatingIcon}
|
|
192
|
+
size={16}
|
|
193
|
+
style={styles.floatingIcon}
|
|
194
|
+
accessibilityLabel={`${componentId}|floating-icon`}
|
|
195
|
+
/>
|
|
196
|
+
</TouchableOpacity>
|
|
197
|
+
)}
|
|
198
|
+
</View>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Trailing slot: spinner while `loading`, an icon when `icon` is a known icon
|
|
206
|
+
* name or URL, otherwise `icon` as a text action (first 15 chars).
|
|
207
|
+
*/
|
|
208
|
+
export const RenderTrailing: FC<TrailingProps> = ({
|
|
209
|
+
loading,
|
|
210
|
+
color,
|
|
211
|
+
icon,
|
|
212
|
+
onPressIcon,
|
|
213
|
+
componentId,
|
|
214
|
+
}) => {
|
|
215
|
+
const { theme } = useContext(ApplicationContext);
|
|
216
|
+
if (loading) {
|
|
217
|
+
return <Loader type={'spinner'} color={color} style={styles.icon} />;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (icon) {
|
|
221
|
+
const isIconName = !!(IconSources as any)?.[icon]?.uri;
|
|
222
|
+
const isIcon = isIconName || icon.includes('http');
|
|
223
|
+
const renderIconTouchable = (content: ReactNode) => {
|
|
224
|
+
return (
|
|
225
|
+
<TouchableOpacity
|
|
226
|
+
accessibilityLabel={`${componentId}|trailing-touch`}
|
|
227
|
+
onPress={onPressIcon}
|
|
228
|
+
style={styles.icon}
|
|
229
|
+
>
|
|
230
|
+
{content}
|
|
231
|
+
</TouchableOpacity>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
if (isIcon) {
|
|
236
|
+
return renderIconTouchable(
|
|
237
|
+
<Icon
|
|
238
|
+
color={color}
|
|
239
|
+
source={icon as string}
|
|
240
|
+
size={24}
|
|
241
|
+
accessibilityLabel={`${componentId}|trailing-icon`}
|
|
242
|
+
/>,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return renderIconTouchable(
|
|
246
|
+
<Text
|
|
247
|
+
typography="action_xs_bold"
|
|
248
|
+
color={color ?? theme.colors.primary}
|
|
249
|
+
numberOfLines={1}
|
|
250
|
+
accessibilityLabel={`${componentId}|trailing-text`}
|
|
251
|
+
>
|
|
252
|
+
{icon!.substring(0, 15)}
|
|
253
|
+
</Text>,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
};
|
package/Input/index.tsx
CHANGED
|
@@ -194,6 +194,12 @@ export interface InputSearchProps extends TextInputProps {
|
|
|
194
194
|
*/
|
|
195
195
|
typingData?: string[];
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Optional. `header` is the 32pt field with a 20pt search icon that sits in the
|
|
199
|
+
* navigation header. Defaults to `default`, the 36pt field for content.
|
|
200
|
+
*/
|
|
201
|
+
size?: 'default' | 'header';
|
|
202
|
+
|
|
197
203
|
/**
|
|
198
204
|
* Optional. Called when the field is pressed while showing `typingData`.
|
|
199
205
|
*/
|
|
@@ -367,4 +373,4 @@ export {
|
|
|
367
373
|
InputPhoneNumber,
|
|
368
374
|
};
|
|
369
375
|
|
|
370
|
-
export {checkValidPhoneNumber};
|
|
376
|
+
export { checkValidPhoneNumber };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const moneyFormatter = new Intl.NumberFormat('vi-VN');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Locates the single edited span between `old` and `text`: common prefix
|
|
5
|
+
* length (`start`), common suffix length (`suffix`) and removed + inserted
|
|
6
|
+
* char count (`size`).
|
|
7
|
+
*/
|
|
8
|
+
export const findEdit = (old: string, text: string) => {
|
|
9
|
+
let start = 0;
|
|
10
|
+
while (start < text.length && old[start] === text[start]) {
|
|
11
|
+
start += 1;
|
|
12
|
+
}
|
|
13
|
+
const maxSuffix = Math.min(text.length, old.length) - start;
|
|
14
|
+
let suffix = 0;
|
|
15
|
+
while (suffix < maxSuffix) {
|
|
16
|
+
if (old[old.length - 1 - suffix] !== text[text.length - 1 - suffix]) {
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
suffix += 1;
|
|
20
|
+
}
|
|
21
|
+
const size = old.length + text.length - 2 * (start + suffix);
|
|
22
|
+
return { old, start, suffix, size };
|
|
23
|
+
};
|
package/Input/utils.ts
CHANGED
|
@@ -1,110 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import Localize from '../Application/Localize';
|
|
3
|
-
import { type Theme } from '../Application/types';
|
|
4
|
-
import styles from './styles';
|
|
5
|
-
|
|
6
|
-
const SMALL_HEIGHT = 48;
|
|
7
|
-
const LARGE_HEIGHT = 56;
|
|
8
|
-
const SMALL_LEADING_ICON_SIZE = 24;
|
|
9
|
-
const LARGE_LEADING_ICON_SIZE = 32;
|
|
10
|
-
const THOUSANDS_GROUP_SIZE = 3;
|
|
11
|
-
const PHONE_DIGITS_WITHOUT_ZERO = 9;
|
|
12
|
-
const MIN_PHONE_DIGITS = 10;
|
|
13
|
-
const MAX_PHONE_DIGITS = 11;
|
|
14
|
-
|
|
15
|
-
/** vi-VN number format (`.` thousands) for the InputMoney display value. */
|
|
16
|
-
const moneyFormatter = new Intl.NumberFormat('vi-VN');
|
|
17
|
-
|
|
18
|
-
/** Field height for `size` before font scaling; unset means large. */
|
|
19
|
-
const getInputHeight = (size?: 'small' | 'large') => {
|
|
20
|
-
if (size === 'small') {
|
|
21
|
-
return SMALL_HEIGHT;
|
|
22
|
-
}
|
|
23
|
-
return LARGE_HEIGHT;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/** Leading icon size for `size`; unset means large. */
|
|
27
|
-
const getLeadingIconSize = (size?: 'small' | 'large') => {
|
|
28
|
-
if (size === 'small') {
|
|
29
|
-
return SMALL_LEADING_ICON_SIZE;
|
|
30
|
-
}
|
|
31
|
-
return LARGE_LEADING_ICON_SIZE;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/** Border color by state; priority is disabled > error > focused > default. */
|
|
35
|
-
const getBorderColor = (
|
|
36
|
-
theme: Theme,
|
|
37
|
-
isFocused: boolean,
|
|
38
|
-
errorMessage?: string,
|
|
39
|
-
isDisabled?: boolean,
|
|
40
|
-
) => {
|
|
41
|
-
let borderColor = theme.colors.border.default;
|
|
42
|
-
|
|
43
|
-
if (isFocused) {
|
|
44
|
-
borderColor = theme.colors.primary;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (errorMessage) {
|
|
48
|
-
borderColor = theme.colors.error.primary;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (isDisabled) {
|
|
52
|
-
borderColor = theme.colors.border.disable;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return { borderColor };
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Container style for `size`; multiline uses `height` as minHeight so the
|
|
60
|
-
* field can grow with its content.
|
|
61
|
-
*/
|
|
62
|
-
const getSizeStyle = (
|
|
63
|
-
size?: 'small' | 'large',
|
|
64
|
-
isMultiline: boolean = false,
|
|
65
|
-
height?: number,
|
|
66
|
-
) => {
|
|
67
|
-
let scaleStyle: ViewStyle = {};
|
|
68
|
-
if (height) {
|
|
69
|
-
scaleStyle = { height };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (isMultiline) {
|
|
73
|
-
return [
|
|
74
|
-
styles.multilineContainer,
|
|
75
|
-
{
|
|
76
|
-
minHeight: height,
|
|
77
|
-
},
|
|
78
|
-
];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (size === 'small') {
|
|
82
|
-
return [styles.smallContainer, scaleStyle];
|
|
83
|
-
}
|
|
84
|
-
return [styles.container, scaleStyle];
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Locates the single edited span between `old` and `text`: common prefix
|
|
89
|
-
* length (`start`), common suffix length (`suffix`) and removed + inserted
|
|
90
|
-
* char count (`size`).
|
|
91
|
-
*/
|
|
92
|
-
const findEdit = (old: string, text: string) => {
|
|
93
|
-
let start = 0;
|
|
94
|
-
while (start < text.length && old[start] === text[start]) {
|
|
95
|
-
start += 1;
|
|
96
|
-
}
|
|
97
|
-
const maxSuffix = Math.min(text.length, old.length) - start;
|
|
98
|
-
let suffix = 0;
|
|
99
|
-
while (suffix < maxSuffix) {
|
|
100
|
-
if (old[old.length - 1 - suffix] !== text[text.length - 1 - suffix]) {
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
suffix += 1;
|
|
104
|
-
}
|
|
105
|
-
const size = old.length + text.length - 2 * (start + suffix);
|
|
106
|
-
return { old, start, suffix, size };
|
|
107
|
-
};
|
|
1
|
+
import { Localize } from '../Application';
|
|
108
2
|
|
|
109
3
|
/**
|
|
110
4
|
* Formats a number Vietnamese-style (`.` thousands, `,` decimals) with
|
|
@@ -124,7 +18,7 @@ const formatNumberToMoney = (number: number, currency = '') => {
|
|
|
124
18
|
}
|
|
125
19
|
|
|
126
20
|
const numberString = Math.abs(number).toString();
|
|
127
|
-
if (numberString.length <
|
|
21
|
+
if (numberString.length < 3) {
|
|
128
22
|
const sign = isNegative ? '-' : '';
|
|
129
23
|
return `${sign}${numberString}${currency}`;
|
|
130
24
|
}
|
|
@@ -138,7 +32,7 @@ const formatNumberToMoney = (number: number, currency = '') => {
|
|
|
138
32
|
} else {
|
|
139
33
|
array.push(numberString[i] as string);
|
|
140
34
|
}
|
|
141
|
-
if (count ===
|
|
35
|
+
if (count === 3 && i >= 1) {
|
|
142
36
|
array.push('.');
|
|
143
37
|
count = 0;
|
|
144
38
|
}
|
|
@@ -173,7 +67,6 @@ const formatMoneyToNumber = (money: string, currencyUnit: string) => {
|
|
|
173
67
|
return Number(money);
|
|
174
68
|
};
|
|
175
69
|
|
|
176
|
-
/** Digits of `phone`, every other char stripped. */
|
|
177
70
|
function formatPhoneNumber(phone: string): string {
|
|
178
71
|
return phone.replace(/\D/g, '');
|
|
179
72
|
}
|
|
@@ -186,32 +79,20 @@ function checkValidPhoneNumber(phone: string): {
|
|
|
186
79
|
phoneFormatted: string;
|
|
187
80
|
error?: string;
|
|
188
81
|
} {
|
|
189
|
-
const
|
|
190
|
-
const isMissingLeadingZero = hasLengthWithoutZero && /^[1-9]/.test(phone);
|
|
82
|
+
const isMissingLeadingZero = phone.length === 9 && /^[1-9]/.test(phone);
|
|
191
83
|
const phoneNumber = isMissingLeadingZero ? `0${phone}` : phone;
|
|
192
84
|
|
|
193
85
|
const phoneFormatted = formatPhoneNumber(phoneNumber);
|
|
194
|
-
const digitCount = phoneFormatted.length;
|
|
195
86
|
const localize = new Localize({ vi: {}, en: {} });
|
|
196
87
|
let error: string | undefined;
|
|
197
88
|
|
|
198
|
-
if (
|
|
89
|
+
if (phoneFormatted.length === 0) {
|
|
199
90
|
error = localize.translate('enterPhoneNumber');
|
|
200
|
-
} else if (
|
|
91
|
+
} else if (phoneFormatted.length < 10 || phoneFormatted.length > 11) {
|
|
201
92
|
error = localize.translate('invalidPhoneNumber');
|
|
202
93
|
}
|
|
203
94
|
|
|
204
95
|
return { phoneFormatted: phoneFormatted, error };
|
|
205
96
|
}
|
|
206
97
|
|
|
207
|
-
export {
|
|
208
|
-
moneyFormatter,
|
|
209
|
-
getInputHeight,
|
|
210
|
-
getLeadingIconSize,
|
|
211
|
-
getBorderColor,
|
|
212
|
-
getSizeStyle,
|
|
213
|
-
findEdit,
|
|
214
|
-
formatMoneyToNumber,
|
|
215
|
-
formatNumberToMoney,
|
|
216
|
-
checkValidPhoneNumber,
|
|
217
|
-
};
|
|
98
|
+
export { formatMoneyToNumber, formatNumberToMoney, checkValidPhoneNumber };
|
|
@@ -13,22 +13,12 @@ import Animated, {
|
|
|
13
13
|
} from 'react-native-reanimated';
|
|
14
14
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
15
15
|
import { ApplicationContext } from '../Context';
|
|
16
|
-
import { Colors
|
|
16
|
+
import { Colors } from '../Consts';
|
|
17
17
|
import { Icon } from '../Icon';
|
|
18
18
|
import { useScaleSize, exportFontFamily } from '../Text';
|
|
19
19
|
import { type AnimatedCompatValue } from '../Application/utils';
|
|
20
20
|
import { useNormalizedSharedValue } from '../Application/hooks';
|
|
21
21
|
|
|
22
|
-
const DEFAULT_BOTTOM = Spacing.M;
|
|
23
|
-
const PILL_RADIUS = 100;
|
|
24
|
-
const LABEL_FONT_SIZE = 16;
|
|
25
|
-
const LABEL_LINE_HEIGHT = 22;
|
|
26
|
-
const SMALL_SIZE = 36;
|
|
27
|
-
const LARGE_SIZE = 48;
|
|
28
|
-
const SMALL_ICON_SIZE = 12;
|
|
29
|
-
const LARGE_ICON_SIZE = 24;
|
|
30
|
-
const ANIMATION_DURATION = 100;
|
|
31
|
-
|
|
32
22
|
export interface FloatingButtonProps {
|
|
33
23
|
label?: string;
|
|
34
24
|
onPress?: () => void;
|
|
@@ -54,64 +44,60 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
|
|
|
54
44
|
iconColor = '',
|
|
55
45
|
size = 'small',
|
|
56
46
|
animatedValue,
|
|
57
|
-
bottom =
|
|
47
|
+
bottom = 12,
|
|
58
48
|
}: FloatingButtonProps) => {
|
|
59
49
|
const { theme } = useContext(ApplicationContext);
|
|
60
|
-
const
|
|
50
|
+
const enabled = animatedValue != null;
|
|
61
51
|
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
62
|
-
const scaledFontSize = useScaleSize(
|
|
63
|
-
const scaledLineHeight = useScaleSize(
|
|
52
|
+
const scaledFontSize = useScaleSize(16);
|
|
53
|
+
const scaledLineHeight = useScaleSize(22);
|
|
64
54
|
const labelFontFamily = exportFontFamily('bold');
|
|
65
55
|
const maxWidth = useSharedValue(0);
|
|
66
|
-
const minWidth = size === 'small' ?
|
|
56
|
+
const minWidth = size === 'small' ? 36 : 48;
|
|
67
57
|
const opacityAnimated = useSharedValue(0);
|
|
68
58
|
const widthAnimated = useSharedValue<number | null>(null);
|
|
69
59
|
const lastOffset = useSharedValue(0);
|
|
70
60
|
const lastDirection = useSharedValue<string | null>(null);
|
|
71
|
-
const [
|
|
61
|
+
const [showText, setShowText] = useState(true);
|
|
72
62
|
|
|
73
63
|
useEffect(() => {
|
|
74
64
|
if (!label) return;
|
|
75
|
-
opacityAnimated.value = withTiming(1, { duration:
|
|
65
|
+
opacityAnimated.value = withTiming(1, { duration: 100 });
|
|
76
66
|
}, [label, opacityAnimated]);
|
|
77
67
|
|
|
78
68
|
useAnimatedReaction(
|
|
79
69
|
() => sharedValue.value,
|
|
80
70
|
value => {
|
|
81
71
|
'worklet';
|
|
82
|
-
if (!label || !
|
|
72
|
+
if (!label || !enabled) return;
|
|
83
73
|
if (value !== lastOffset.value && value > 0) {
|
|
84
74
|
const direction = value > lastOffset.value ? 'down' : 'up';
|
|
85
75
|
lastOffset.value = value;
|
|
86
76
|
if (lastDirection.value !== direction) {
|
|
87
77
|
lastDirection.value = direction;
|
|
88
78
|
if (direction === 'down') {
|
|
89
|
-
opacityAnimated.value = withTiming(0, {
|
|
90
|
-
duration: ANIMATION_DURATION,
|
|
91
|
-
});
|
|
79
|
+
opacityAnimated.value = withTiming(0, { duration: 100 });
|
|
92
80
|
widthAnimated.value = withTiming(
|
|
93
81
|
minWidth,
|
|
94
|
-
{ duration:
|
|
82
|
+
{ duration: 100 },
|
|
95
83
|
finished => {
|
|
96
|
-
if (finished) scheduleOnRN(
|
|
84
|
+
if (finished) scheduleOnRN(setShowText, false);
|
|
97
85
|
},
|
|
98
86
|
);
|
|
99
87
|
} else {
|
|
100
|
-
opacityAnimated.value = withTiming(1, {
|
|
101
|
-
duration: ANIMATION_DURATION,
|
|
102
|
-
});
|
|
88
|
+
opacityAnimated.value = withTiming(1, { duration: 100 });
|
|
103
89
|
widthAnimated.value = withTiming(
|
|
104
90
|
maxWidth.value,
|
|
105
|
-
{ duration:
|
|
91
|
+
{ duration: 100 },
|
|
106
92
|
finished => {
|
|
107
|
-
if (finished) scheduleOnRN(
|
|
93
|
+
if (finished) scheduleOnRN(setShowText, true);
|
|
108
94
|
},
|
|
109
95
|
);
|
|
110
96
|
}
|
|
111
97
|
}
|
|
112
98
|
}
|
|
113
99
|
},
|
|
114
|
-
[label,
|
|
100
|
+
[label, enabled, minWidth],
|
|
115
101
|
);
|
|
116
102
|
|
|
117
103
|
const containerStyle = useAnimatedStyle(() => ({
|
|
@@ -160,10 +146,10 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
|
|
|
160
146
|
<TouchableOpacity style={styles.content} onPress={onPress}>
|
|
161
147
|
<Icon
|
|
162
148
|
source={icon}
|
|
163
|
-
size={size === 'small' ?
|
|
149
|
+
size={size === 'small' ? 12 : 24}
|
|
164
150
|
color={iconColor}
|
|
165
151
|
/>
|
|
166
|
-
{
|
|
152
|
+
{showText && !!label && (
|
|
167
153
|
<Animated.Text
|
|
168
154
|
allowFontScaling={false}
|
|
169
155
|
style={[
|
|
@@ -192,19 +178,19 @@ const styles = StyleSheet.create({
|
|
|
192
178
|
alignContent: 'center',
|
|
193
179
|
alignItems: 'center',
|
|
194
180
|
position: 'absolute',
|
|
195
|
-
borderRadius:
|
|
196
|
-
paddingHorizontal:
|
|
181
|
+
borderRadius: 100,
|
|
182
|
+
paddingHorizontal: 12,
|
|
197
183
|
flexWrap: 'wrap',
|
|
198
184
|
alignSelf: 'flex-end',
|
|
199
185
|
},
|
|
200
186
|
label: {
|
|
201
|
-
marginLeft:
|
|
187
|
+
marginLeft: 8,
|
|
202
188
|
},
|
|
203
|
-
alignRight: { right:
|
|
189
|
+
alignRight: { right: 12 },
|
|
204
190
|
alignCenter: { alignSelf: 'center' },
|
|
205
191
|
alignEnd: { alignSelf: 'flex-end' },
|
|
206
|
-
small: { height:
|
|
207
|
-
large: { height:
|
|
192
|
+
small: { height: 36 },
|
|
193
|
+
large: { height: 48 },
|
|
208
194
|
content: {
|
|
209
195
|
flexDirection: 'row',
|
|
210
196
|
alignItems: 'center',
|
package/Layout/Screen.tsx
CHANGED
|
@@ -67,6 +67,7 @@ const Screen = forwardRef(
|
|
|
67
67
|
headerBackground,
|
|
68
68
|
gradientColor,
|
|
69
69
|
trackingParams,
|
|
70
|
+
contentLayout,
|
|
70
71
|
}: ScreenProps,
|
|
71
72
|
ref: any,
|
|
72
73
|
) => {
|
|
@@ -144,6 +145,7 @@ const Screen = forwardRef(
|
|
|
144
145
|
gradientColor,
|
|
145
146
|
headerBackground,
|
|
146
147
|
layoutOffset,
|
|
148
|
+
contentLayout,
|
|
147
149
|
});
|
|
148
150
|
|
|
149
151
|
// mutates the content node so callers keep its methods plus the header setters
|
|
@@ -223,6 +225,7 @@ const Screen = forwardRef(
|
|
|
223
225
|
useShadowHeader={useShadowHeader}
|
|
224
226
|
gradientColor={gradientColor}
|
|
225
227
|
headerBackground={headerBackground}
|
|
228
|
+
contentLayout={contentLayout}
|
|
226
229
|
/>
|
|
227
230
|
);
|
|
228
231
|
};
|