@momo-kits/foundation 0.169.8 → 0.169.9
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 +27 -17
- package/Application/BottomSheet.tsx +35 -27
- package/Application/BottomTab/BottomTabBar.tsx +6 -6
- package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
- package/Application/BottomTab/TabScreen.tsx +11 -10
- package/Application/BottomTab/index.tsx +5 -5
- package/Application/BottomTab/usePressLift.ts +3 -3
- package/Application/Components/HeaderToolkitAction.tsx +26 -18
- package/Application/Components/useToolkitActions.tsx +22 -21
- package/Application/ModalScreen.tsx +19 -15
- package/Application/StackScreen.tsx +13 -9
- package/Application/modalLayout.ts +1 -3
- package/Context/index.ts +3 -1
- package/Image/index.tsx +6 -6
- package/Input/ErrorView.tsx +59 -0
- package/Input/FloatingView.tsx +101 -0
- package/Input/Input.tsx +40 -37
- package/Input/InputDropDown.tsx +13 -10
- package/Input/InputMoney.tsx +17 -17
- package/Input/InputMoneyView.tsx +12 -12
- package/Input/InputOTP.tsx +46 -34
- package/Input/InputPhoneNumber.tsx +48 -43
- package/Input/InputSearch.tsx +27 -24
- package/Input/InputTextArea.tsx +29 -23
- package/Input/RenderTrailing.tsx +79 -0
- package/Input/constants.ts +8 -0
- package/Input/utils.ts +126 -7
- package/Layout/FloatingButton.tsx +38 -24
- package/Layout/layoutMetrics.ts +43 -34
- package/Layout/screenTypes.ts +2 -3
- package/Pagination/PaginationScroll.tsx +5 -3
- package/Pagination/types.ts +3 -5
- package/Popup/PopupNotify.tsx +16 -13
- package/Popup/PopupPromotion.tsx +5 -4
- package/Popup/types.ts +2 -4
- package/Skeleton/index.tsx +15 -12
- package/package.json +1 -1
- package/Input/common.tsx +0 -257
- package/Input/moneyUtils.ts +0 -23
|
@@ -27,6 +27,12 @@ import { isIPhoneDuo } from './utils';
|
|
|
27
27
|
import { useModalLayout } from './modalLayout';
|
|
28
28
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
29
29
|
|
|
30
|
+
const DUO_ORIENTATIONS = ['portrait', 'landscape'];
|
|
31
|
+
const ENTER_DURATION = 250;
|
|
32
|
+
const EXIT_DURATION = 200;
|
|
33
|
+
const INITIAL_SCALE = 0.8;
|
|
34
|
+
const ENTER_EASING = Easing.bezier(0.2, 0.0, 0, 1.0);
|
|
35
|
+
|
|
30
36
|
/**
|
|
31
37
|
* Screen of the `Modal` route: a bottom sheet when pushed by
|
|
32
38
|
* `showBottomSheet`, otherwise a centered popup.
|
|
@@ -72,7 +78,7 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
72
78
|
}, [context, description, navigator?.maxApi, title]);
|
|
73
79
|
|
|
74
80
|
const opacity = useRef(new Animated.Value(0)).current;
|
|
75
|
-
const scale = useRef(new Animated.Value(
|
|
81
|
+
const scale = useRef(new Animated.Value(INITIAL_SCALE)).current;
|
|
76
82
|
const scrimStyle = useMemo(
|
|
77
83
|
() => ({
|
|
78
84
|
opacity: opacity.interpolate({
|
|
@@ -98,13 +104,13 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
98
104
|
Animated.parallel([
|
|
99
105
|
Animated.timing(opacity, {
|
|
100
106
|
toValue: 1,
|
|
101
|
-
duration:
|
|
107
|
+
duration: ENTER_DURATION,
|
|
102
108
|
useNativeDriver: true,
|
|
103
109
|
}),
|
|
104
110
|
Animated.timing(scale, {
|
|
105
111
|
toValue: 1,
|
|
106
|
-
duration:
|
|
107
|
-
easing:
|
|
112
|
+
duration: ENTER_DURATION,
|
|
113
|
+
easing: ENTER_EASING,
|
|
108
114
|
useNativeDriver: true,
|
|
109
115
|
}),
|
|
110
116
|
]).start();
|
|
@@ -114,7 +120,7 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
114
120
|
};
|
|
115
121
|
}, [opacity, props.route.params, scale]);
|
|
116
122
|
|
|
117
|
-
const
|
|
123
|
+
const handleDismiss = useCallback(
|
|
118
124
|
(callback = () => {}, preventClose = false) => {
|
|
119
125
|
if (preventClose) {
|
|
120
126
|
return;
|
|
@@ -122,12 +128,12 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
122
128
|
Animated.parallel([
|
|
123
129
|
Animated.timing(opacity, {
|
|
124
130
|
toValue: 0,
|
|
125
|
-
duration:
|
|
131
|
+
duration: EXIT_DURATION,
|
|
126
132
|
useNativeDriver: true,
|
|
127
133
|
}),
|
|
128
134
|
Animated.timing(scale, {
|
|
129
|
-
toValue:
|
|
130
|
-
duration:
|
|
135
|
+
toValue: INITIAL_SCALE,
|
|
136
|
+
duration: EXIT_DURATION,
|
|
131
137
|
easing: Easing.linear,
|
|
132
138
|
useNativeDriver: true,
|
|
133
139
|
}),
|
|
@@ -143,18 +149,18 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
143
149
|
const backHandler = BackHandler.addEventListener(
|
|
144
150
|
'hardwareBackPress',
|
|
145
151
|
() => {
|
|
146
|
-
|
|
152
|
+
handleDismiss(undefined, barrierDismissible);
|
|
147
153
|
return true;
|
|
148
154
|
},
|
|
149
155
|
);
|
|
150
156
|
return () => backHandler.remove();
|
|
151
|
-
}, [barrierDismissible,
|
|
157
|
+
}, [barrierDismissible, handleDismiss]);
|
|
152
158
|
|
|
153
159
|
return (
|
|
154
160
|
<Container
|
|
155
161
|
transparent
|
|
156
162
|
visible={true}
|
|
157
|
-
onRequestClose={
|
|
163
|
+
onRequestClose={handleDismiss}
|
|
158
164
|
style={StyleSheet.absoluteFillObject}
|
|
159
165
|
isModalKit={true}
|
|
160
166
|
supportedOrientations={isDuo ? DUO_ORIENTATIONS : undefined}
|
|
@@ -165,7 +171,7 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
165
171
|
>
|
|
166
172
|
<Pressable
|
|
167
173
|
style={StyleSheet.absoluteFillObject}
|
|
168
|
-
onPress={() =>
|
|
174
|
+
onPress={() => handleDismiss(undefined, barrierDismissible)}
|
|
169
175
|
>
|
|
170
176
|
<Animated.View style={[styles.overlayContent, scrimStyle]} />
|
|
171
177
|
</Pressable>
|
|
@@ -188,15 +194,13 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
188
194
|
modalStyle,
|
|
189
195
|
]}
|
|
190
196
|
>
|
|
191
|
-
<Component {...params} onRequestClose={
|
|
197
|
+
<Component {...params} onRequestClose={handleDismiss} />
|
|
192
198
|
</Animated.View>
|
|
193
199
|
</KeyboardAvoidingView>
|
|
194
200
|
</Container>
|
|
195
201
|
);
|
|
196
202
|
};
|
|
197
203
|
|
|
198
|
-
const DUO_ORIENTATIONS = ['portrait', 'landscape'];
|
|
199
|
-
|
|
200
204
|
const styles = StyleSheet.create({
|
|
201
205
|
overlayContent: {
|
|
202
206
|
flex: 1,
|
|
@@ -9,6 +9,10 @@ import { useAppState } from './hooks';
|
|
|
9
9
|
import { createScreenTracking } from './StackScreen.tracking';
|
|
10
10
|
import { TooltipPortalHost, TooltipPortalProvider } from './TooltipPortal';
|
|
11
11
|
|
|
12
|
+
const TRACKING_TIMEOUT = 5000;
|
|
13
|
+
const ELEMENT_IDLE_TIMEOUT = 2000;
|
|
14
|
+
const LOADING_IDLE_TIMEOUT = 2000;
|
|
15
|
+
|
|
12
16
|
const runAfterInteractions = InteractionManager.runAfterInteractions;
|
|
13
17
|
|
|
14
18
|
/**
|
|
@@ -71,7 +75,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
71
75
|
|
|
72
76
|
useEffect(() => {
|
|
73
77
|
let focusScreen: any;
|
|
74
|
-
let
|
|
78
|
+
let focusAppListener: { remove?: () => void } | undefined;
|
|
75
79
|
if (['Invalid', 'screen'].includes(screenName)) {
|
|
76
80
|
navigator?.maxApi?.showPopup?.('notice', {
|
|
77
81
|
title: 'Invalid screen name',
|
|
@@ -85,19 +89,19 @@ const StackScreen: React.FC<any> = props => {
|
|
|
85
89
|
if (!bottomTab) {
|
|
86
90
|
focusScreen = props.navigation.addListener('focus', () => {
|
|
87
91
|
navigator?.maxApi?.getDataObserver?.('current_screen', (item: any) => {
|
|
88
|
-
|
|
92
|
+
handleScreenNavigated(item?.screenName, screenName);
|
|
89
93
|
navigator?.maxApi?.setDataObserver?.('current_screen', {
|
|
90
94
|
screenName,
|
|
91
95
|
startTime,
|
|
92
96
|
});
|
|
93
97
|
});
|
|
94
98
|
});
|
|
95
|
-
|
|
99
|
+
focusAppListener = navigator?.maxApi?.listen?.('onFocusApp', () => {
|
|
96
100
|
if (props.navigation.isFocused()) {
|
|
97
101
|
navigator?.maxApi?.getDataObserver?.(
|
|
98
102
|
'current_screen',
|
|
99
103
|
(item: any) => {
|
|
100
|
-
|
|
104
|
+
handleScreenNavigated(item?.screenName, screenName);
|
|
101
105
|
navigator?.maxApi?.setDataObserver?.('current_screen', {
|
|
102
106
|
screenName,
|
|
103
107
|
startTime,
|
|
@@ -126,7 +130,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
126
130
|
tracking.current.timeoutTracking = setTimeout(() => {
|
|
127
131
|
onScreenLoad();
|
|
128
132
|
onScreenInteraction();
|
|
129
|
-
},
|
|
133
|
+
}, TRACKING_TIMEOUT);
|
|
130
134
|
|
|
131
135
|
return () => {
|
|
132
136
|
onScreenLoad();
|
|
@@ -137,13 +141,13 @@ const StackScreen: React.FC<any> = props => {
|
|
|
137
141
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
138
142
|
clearTimeout(tracking.current.timeoutLoading);
|
|
139
143
|
focusScreen?.();
|
|
140
|
-
|
|
144
|
+
focusAppListener?.remove?.();
|
|
141
145
|
};
|
|
142
146
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
143
147
|
}, []);
|
|
144
148
|
|
|
145
149
|
/** Reports a navigation to this screen: `back` if it was shown before. */
|
|
146
|
-
const
|
|
150
|
+
const handleScreenNavigated = (pre: string, current: string) => {
|
|
147
151
|
if (!isBackgroundToForeground) {
|
|
148
152
|
const item: any = {
|
|
149
153
|
preScreenName: pre,
|
|
@@ -234,7 +238,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
234
238
|
|
|
235
239
|
onScreenLoad();
|
|
236
240
|
onScreenInteraction();
|
|
237
|
-
},
|
|
241
|
+
}, ELEMENT_IDLE_TIMEOUT);
|
|
238
242
|
},
|
|
239
243
|
onLoading: (loading: boolean) => {
|
|
240
244
|
if (loading && tracking.current.timeStartLoading === 0) {
|
|
@@ -248,7 +252,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
248
252
|
clearTimeout(tracking.current.timeoutLoading);
|
|
249
253
|
tracking.current.timeoutLoading = setTimeout(() => {
|
|
250
254
|
onScreenLoading();
|
|
251
|
-
},
|
|
255
|
+
}, LOADING_IDLE_TIMEOUT);
|
|
252
256
|
},
|
|
253
257
|
onSetParams: (item: ScreenTrackingParams) => {
|
|
254
258
|
tracking.current.params = item;
|
|
@@ -73,9 +73,7 @@ type ModalLayoutInput = {
|
|
|
73
73
|
hinge?: ReservedRegion;
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
/**
|
|
77
|
-
* `value` limited to the `[min, max]` range.
|
|
78
|
-
*/
|
|
76
|
+
/** `value` limited to the `[min, max]` range. */
|
|
79
77
|
const clamp = (value: number, min: number, max: number) => {
|
|
80
78
|
return Math.min(Math.max(value, min), max);
|
|
81
79
|
};
|
package/Context/index.ts
CHANGED
|
@@ -5,9 +5,11 @@ import { defaultContext } from '../Consts';
|
|
|
5
5
|
const Context = createContext({});
|
|
6
6
|
const ApplicationContext = createContext(defaultContext);
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/** Mini app context shared by the host via `Platform`; local if standalone. */
|
|
9
9
|
const MiniAppContext = (Platform as any).MiniAppContext ?? Context;
|
|
10
|
+
/** Screen context shared by the host via `Platform`; local if standalone. */
|
|
10
11
|
const ScreenContext = (Platform as any).ScreenContext ?? Context;
|
|
12
|
+
/** Component context shared by the host via `Platform`; local if standalone. */
|
|
11
13
|
const ComponentContext = (Platform as any).ComponentContext ?? Context;
|
|
12
14
|
const SkeletonContext = createContext({ loading: false });
|
|
13
15
|
/**
|
package/Image/index.tsx
CHANGED
|
@@ -44,7 +44,7 @@ const Image: React.FC<ImageProps> = ({
|
|
|
44
44
|
const component: any = useContext(ComponentContext);
|
|
45
45
|
const context = useContext<any>(MiniAppContext);
|
|
46
46
|
const skeleton = useContext(SkeletonContext);
|
|
47
|
-
const
|
|
47
|
+
const hasErrorRef = useRef(false);
|
|
48
48
|
const [status, setStatus] = useState<Status>('success');
|
|
49
49
|
|
|
50
50
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
@@ -92,30 +92,30 @@ const Image: React.FC<ImageProps> = ({
|
|
|
92
92
|
showBaseLineDebug && styles.debugBaseLine,
|
|
93
93
|
]}
|
|
94
94
|
onProgress={event => {
|
|
95
|
-
|
|
95
|
+
hasErrorRef.current = false;
|
|
96
96
|
if (status !== 'loading' && loading) {
|
|
97
97
|
setStatus('loading');
|
|
98
98
|
}
|
|
99
99
|
onProgress?.(event);
|
|
100
100
|
}}
|
|
101
101
|
onLoadStart={() => {
|
|
102
|
-
|
|
102
|
+
hasErrorRef.current = false;
|
|
103
103
|
onLoadStart?.();
|
|
104
104
|
}}
|
|
105
105
|
onLoad={event => {
|
|
106
|
-
|
|
106
|
+
hasErrorRef.current = false;
|
|
107
107
|
onLoad?.(event);
|
|
108
108
|
}}
|
|
109
109
|
onLoadEnd={() => {
|
|
110
110
|
let current: Status = 'success';
|
|
111
|
-
if (
|
|
111
|
+
if (hasErrorRef.current) {
|
|
112
112
|
current = 'error';
|
|
113
113
|
}
|
|
114
114
|
setStatus(prevState => (prevState !== current ? current : prevState));
|
|
115
115
|
onLoadEnd?.();
|
|
116
116
|
}}
|
|
117
117
|
onError={() => {
|
|
118
|
-
|
|
118
|
+
hasErrorRef.current = true;
|
|
119
119
|
onError?.();
|
|
120
120
|
}}
|
|
121
121
|
>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React, { type FC, useContext } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { Styles } from '../Consts';
|
|
4
|
+
import { ApplicationContext } from '../Context';
|
|
5
|
+
import { Icon } from '../Icon';
|
|
6
|
+
import { Text, useScaleSize } from '../Text';
|
|
7
|
+
import styles from './styles';
|
|
8
|
+
|
|
9
|
+
const ICON_SIZE = 16;
|
|
10
|
+
|
|
11
|
+
type ErrorViewProps = {
|
|
12
|
+
errorMessage?: string;
|
|
13
|
+
errorSpacing?: boolean;
|
|
14
|
+
hintText?: string;
|
|
15
|
+
componentId?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Error or hint row under an input (error wins); with neither, `errorSpacing`
|
|
20
|
+
* keeps an empty row so the layout doesn't jump when an error appears.
|
|
21
|
+
*/
|
|
22
|
+
const ErrorView: FC<ErrorViewProps> = ({
|
|
23
|
+
errorMessage,
|
|
24
|
+
errorSpacing,
|
|
25
|
+
hintText,
|
|
26
|
+
componentId,
|
|
27
|
+
}) => {
|
|
28
|
+
const { theme } = useContext(ApplicationContext);
|
|
29
|
+
const errorColor = theme.colors.error.primary;
|
|
30
|
+
const hintColor = theme.colors.text.hint;
|
|
31
|
+
const hintTextDefault = hintText ?? 'Không thể chỉnh sửa';
|
|
32
|
+
const scaledIconSize = useScaleSize(ICON_SIZE);
|
|
33
|
+
|
|
34
|
+
if (errorMessage || hintText) {
|
|
35
|
+
const messageColor = errorMessage ? errorColor : hintColor;
|
|
36
|
+
const messageType = errorMessage ? 'error' : 'hint';
|
|
37
|
+
return (
|
|
38
|
+
<View style={styles.errorView}>
|
|
39
|
+
<View style={styles.errorIcon}>
|
|
40
|
+
<Icon color={messageColor} source="ic_error" size={scaledIconSize} />
|
|
41
|
+
</View>
|
|
42
|
+
<Text
|
|
43
|
+
accessibilityLabel={`${componentId}|${messageType}-text`}
|
|
44
|
+
style={Styles.flex}
|
|
45
|
+
color={messageColor}
|
|
46
|
+
typography={'description_default_regular'}
|
|
47
|
+
>
|
|
48
|
+
{errorMessage ?? hintTextDefault}
|
|
49
|
+
</Text>
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (errorSpacing) {
|
|
54
|
+
return <View style={[styles.errorView, styles.errorSpacing]} />;
|
|
55
|
+
}
|
|
56
|
+
return <View />;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export { ErrorView };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React, { type FC, useContext } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type GestureResponderEvent,
|
|
4
|
+
TouchableOpacity,
|
|
5
|
+
View,
|
|
6
|
+
type ViewStyle,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import { ApplicationContext } from '../Context';
|
|
9
|
+
import { Icon } from '../Icon';
|
|
10
|
+
import { Text, useScaleSize } from '../Text';
|
|
11
|
+
import { PRESSED_OPACITY } from './constants';
|
|
12
|
+
import styles from './styles';
|
|
13
|
+
|
|
14
|
+
const LABEL_OFFSET = 10;
|
|
15
|
+
const ICON_SIZE = 16;
|
|
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
|
+
/**
|
|
29
|
+
* Floating label on the input's top border with a required `*` and an
|
|
30
|
+
* optional pressable icon; renders nothing without `floatingValue`.
|
|
31
|
+
*/
|
|
32
|
+
const FloatingView: FC<FloatingViewProps> = ({
|
|
33
|
+
floatingValue,
|
|
34
|
+
floatingIconColor,
|
|
35
|
+
disabled,
|
|
36
|
+
floatingIcon,
|
|
37
|
+
required,
|
|
38
|
+
style,
|
|
39
|
+
onPress,
|
|
40
|
+
componentId,
|
|
41
|
+
}) => {
|
|
42
|
+
const { theme } = useContext(ApplicationContext);
|
|
43
|
+
const scaledTop = useScaleSize(LABEL_OFFSET);
|
|
44
|
+
if (!floatingValue) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let floatingTextColor = theme.colors.text.hint;
|
|
49
|
+
let floatingIconTintColor = floatingIconColor;
|
|
50
|
+
if (disabled) {
|
|
51
|
+
floatingTextColor = theme.colors.text.disable;
|
|
52
|
+
floatingIconTintColor = theme.colors.text.disable;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<View
|
|
57
|
+
style={[
|
|
58
|
+
styles.floatingView,
|
|
59
|
+
styles.floatingViewLayer,
|
|
60
|
+
{
|
|
61
|
+
backgroundColor: theme.colors.background.surface,
|
|
62
|
+
top: -scaledTop,
|
|
63
|
+
},
|
|
64
|
+
style,
|
|
65
|
+
]}
|
|
66
|
+
>
|
|
67
|
+
<Text
|
|
68
|
+
color={floatingTextColor}
|
|
69
|
+
typography={'label_s_medium'}
|
|
70
|
+
accessibilityLabel={`${componentId}|floating-text`}
|
|
71
|
+
>
|
|
72
|
+
{floatingValue}
|
|
73
|
+
{required && (
|
|
74
|
+
<Text
|
|
75
|
+
typography={'label_s_medium'}
|
|
76
|
+
color={theme.colors.error.primary}
|
|
77
|
+
>
|
|
78
|
+
*
|
|
79
|
+
</Text>
|
|
80
|
+
)}
|
|
81
|
+
</Text>
|
|
82
|
+
{!!floatingIcon && (
|
|
83
|
+
<TouchableOpacity
|
|
84
|
+
activeOpacity={onPress ? PRESSED_OPACITY : 1}
|
|
85
|
+
onPress={onPress}
|
|
86
|
+
accessibilityLabel={`${componentId}|floating-icon-touch`}
|
|
87
|
+
>
|
|
88
|
+
<Icon
|
|
89
|
+
color={floatingIconTintColor}
|
|
90
|
+
source={floatingIcon}
|
|
91
|
+
size={ICON_SIZE}
|
|
92
|
+
style={styles.floatingIcon}
|
|
93
|
+
accessibilityLabel={`${componentId}|floating-icon`}
|
|
94
|
+
/>
|
|
95
|
+
</TouchableOpacity>
|
|
96
|
+
)}
|
|
97
|
+
</View>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export { FloatingView };
|
package/Input/Input.tsx
CHANGED
|
@@ -13,22 +13,25 @@ import {
|
|
|
13
13
|
} from 'react-native';
|
|
14
14
|
import { useComponentId } from '../Application';
|
|
15
15
|
import { Icon } from '../Icon';
|
|
16
|
-
import { exportFontFamily, useScaleSize } from '../Text';
|
|
17
|
-
import {
|
|
18
|
-
ErrorView,
|
|
19
|
-
FloatingView,
|
|
20
|
-
getBorderColor,
|
|
21
|
-
getSizeStyle,
|
|
22
|
-
RenderTrailing,
|
|
23
|
-
} from './common';
|
|
24
|
-
import { InputProps } from './index';
|
|
25
|
-
import styles from './styles';
|
|
26
|
-
import SystemTextInput from './SystemTextInput';
|
|
27
16
|
import {
|
|
28
17
|
ApplicationContext,
|
|
29
18
|
ComponentContext,
|
|
30
19
|
MiniAppContext,
|
|
31
20
|
} from '../Context';
|
|
21
|
+
import { exportFontFamily, useScaleSize } from '../Text';
|
|
22
|
+
import { CLEAR_ICON_SIZE, INPUT_FONT_SIZE } from './constants';
|
|
23
|
+
import { ErrorView } from './ErrorView';
|
|
24
|
+
import { FloatingView } from './FloatingView';
|
|
25
|
+
import { InputProps } from './index';
|
|
26
|
+
import { RenderTrailing } from './RenderTrailing';
|
|
27
|
+
import styles from './styles';
|
|
28
|
+
import SystemTextInput from './SystemTextInput';
|
|
29
|
+
import {
|
|
30
|
+
getBorderColor,
|
|
31
|
+
getInputHeight,
|
|
32
|
+
getLeadingIconSize,
|
|
33
|
+
getSizeStyle,
|
|
34
|
+
} from './utils';
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* Text input with floating label, leading/trailing/clear icons and an error or
|
|
@@ -73,11 +76,11 @@ const Input = forwardRef(
|
|
|
73
76
|
) => {
|
|
74
77
|
const { theme } = useContext(ApplicationContext);
|
|
75
78
|
const context = useContext<any>(MiniAppContext);
|
|
76
|
-
const scaledFontSize = useScaleSize(
|
|
77
|
-
const scaleHeight = useScaleSize(size
|
|
78
|
-
const [
|
|
79
|
+
const scaledFontSize = useScaleSize(INPUT_FONT_SIZE);
|
|
80
|
+
const scaleHeight = useScaleSize(getInputHeight(size));
|
|
81
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
79
82
|
const [internalText, setInternalText] = useState(defaultValue || '');
|
|
80
|
-
const
|
|
83
|
+
const hasValue = value !== undefined ? !!value : !!internalText;
|
|
81
84
|
const inputRef = useRef<TextInput | null>(null);
|
|
82
85
|
const componentName = 'Input';
|
|
83
86
|
|
|
@@ -88,30 +91,30 @@ const Input = forwardRef(
|
|
|
88
91
|
props.accessibilityLabel,
|
|
89
92
|
);
|
|
90
93
|
|
|
91
|
-
const
|
|
94
|
+
const handleClearText = () => {
|
|
92
95
|
inputRef?.current?.clear();
|
|
93
96
|
inputRef.current?.setNativeProps({ text: '' });
|
|
94
|
-
|
|
97
|
+
handleChangeText('');
|
|
95
98
|
};
|
|
96
99
|
|
|
97
|
-
const
|
|
100
|
+
const handleChangeText = (text: string) => {
|
|
98
101
|
if (value === undefined) {
|
|
99
102
|
setInternalText(text);
|
|
100
103
|
}
|
|
101
104
|
onChangeText?.(text);
|
|
102
105
|
};
|
|
103
106
|
|
|
104
|
-
const
|
|
105
|
-
|
|
107
|
+
const handleFocus = (e: TextInputFocusEvent) => {
|
|
108
|
+
setIsFocused(true);
|
|
106
109
|
onFocus?.(e);
|
|
107
110
|
};
|
|
108
111
|
|
|
109
|
-
const
|
|
110
|
-
|
|
112
|
+
const handleBlur = (e: TextInputFocusEvent) => {
|
|
113
|
+
setIsFocused(false);
|
|
111
114
|
onBlur?.(e);
|
|
112
115
|
};
|
|
113
116
|
|
|
114
|
-
const
|
|
117
|
+
const handleSetText = (text: string) => {
|
|
115
118
|
if (__DEV__ && value !== undefined) {
|
|
116
119
|
console.error(
|
|
117
120
|
`${componentName}: calling ref.setText() on a controlled input (\`value\` is provided) is an anti-pattern. ` +
|
|
@@ -120,21 +123,21 @@ const Input = forwardRef(
|
|
|
120
123
|
);
|
|
121
124
|
}
|
|
122
125
|
inputRef?.current?.setNativeProps({ text });
|
|
123
|
-
|
|
126
|
+
handleChangeText(text);
|
|
124
127
|
};
|
|
125
128
|
|
|
126
129
|
useImperativeHandle(ref, () => {
|
|
127
130
|
return {
|
|
128
|
-
clear:
|
|
131
|
+
clear: handleClearText,
|
|
129
132
|
focus: () => inputRef.current?.focus(),
|
|
130
133
|
blur: () => inputRef.current?.blur(),
|
|
131
|
-
setText:
|
|
134
|
+
setText: handleSetText,
|
|
132
135
|
};
|
|
133
136
|
});
|
|
134
137
|
|
|
135
138
|
const renderInputView = () => {
|
|
136
139
|
const disabledColor = theme.colors.text.disable;
|
|
137
|
-
const
|
|
140
|
+
const isSecure = secureTextEntry;
|
|
138
141
|
let textColor = theme.colors.text.default;
|
|
139
142
|
let placeholderColor = theme.colors.text.hint;
|
|
140
143
|
let iconTintColor = trailingColor;
|
|
@@ -156,7 +159,7 @@ const Input = forwardRef(
|
|
|
156
159
|
styles.inputWrapper,
|
|
157
160
|
{ backgroundColor: theme.colors.background.surface },
|
|
158
161
|
getSizeStyle(size, undefined, scaleHeight),
|
|
159
|
-
getBorderColor(theme,
|
|
162
|
+
getBorderColor(theme, isFocused, errorMessage, disabled),
|
|
160
163
|
]}
|
|
161
164
|
>
|
|
162
165
|
<FloatingView
|
|
@@ -178,7 +181,7 @@ const Input = forwardRef(
|
|
|
178
181
|
<Icon
|
|
179
182
|
color={leadingIconColor}
|
|
180
183
|
source={leadingIcon}
|
|
181
|
-
size={size
|
|
184
|
+
size={getLeadingIconSize(size)}
|
|
182
185
|
accessibilityLabel={`${componentId}|leading-icon`}
|
|
183
186
|
/>
|
|
184
187
|
</TouchableOpacity>
|
|
@@ -188,7 +191,7 @@ const Input = forwardRef(
|
|
|
188
191
|
{...props}
|
|
189
192
|
accessibilityLabel={`${componentId}`}
|
|
190
193
|
editable={editable && !disabled}
|
|
191
|
-
secureTextEntry={
|
|
194
|
+
secureTextEntry={isSecure}
|
|
192
195
|
textAlignVertical="center"
|
|
193
196
|
ref={inputRef}
|
|
194
197
|
accessibilityState={{
|
|
@@ -202,15 +205,15 @@ const Input = forwardRef(
|
|
|
202
205
|
color: textColor,
|
|
203
206
|
fontFamily: exportFontFamily(fontWeight ?? 'regular'),
|
|
204
207
|
},
|
|
205
|
-
|
|
208
|
+
isSecure && hasValue && secureTextStyle,
|
|
206
209
|
]}
|
|
207
210
|
defaultValue={defaultValue}
|
|
208
211
|
allowFontScaling={false}
|
|
209
212
|
textBreakStrategy="highQuality"
|
|
210
213
|
value={value}
|
|
211
|
-
onChangeText={
|
|
212
|
-
onFocus={
|
|
213
|
-
onBlur={
|
|
214
|
+
onChangeText={handleChangeText}
|
|
215
|
+
onFocus={handleFocus}
|
|
216
|
+
onBlur={handleBlur}
|
|
214
217
|
placeholder={placeholder}
|
|
215
218
|
selectionColor={theme.colors.primary}
|
|
216
219
|
placeholderTextColor={placeholderColor}
|
|
@@ -218,16 +221,16 @@ const Input = forwardRef(
|
|
|
218
221
|
/>
|
|
219
222
|
</View>
|
|
220
223
|
<View style={styles.iconView}>
|
|
221
|
-
{showClearIcon &&
|
|
224
|
+
{showClearIcon && isFocused && hasValue && (
|
|
222
225
|
<TouchableOpacity
|
|
223
226
|
accessibilityLabel={`${componentId}|clear-icon-touch`}
|
|
224
227
|
accessible={false}
|
|
225
228
|
style={styles.iconWrapper}
|
|
226
|
-
onPress={
|
|
229
|
+
onPress={handleClearText}
|
|
227
230
|
>
|
|
228
231
|
<Icon
|
|
229
232
|
source="24_navigation_close_circle_full"
|
|
230
|
-
size={
|
|
233
|
+
size={CLEAR_ICON_SIZE}
|
|
231
234
|
color={theme.colors.text.hint}
|
|
232
235
|
accessibilityLabel={`${componentId}|clear-icon`}
|
|
233
236
|
/>
|
package/Input/InputDropDown.tsx
CHANGED
|
@@ -6,17 +6,20 @@ import {
|
|
|
6
6
|
ComponentContext,
|
|
7
7
|
MiniAppContext,
|
|
8
8
|
} from '../Context';
|
|
9
|
+
import { Spacing } from '../Consts';
|
|
9
10
|
import { Icon } from '../Icon';
|
|
11
|
+
import { Text, useScaleSize } from '../Text';
|
|
12
|
+
import { PRESSED_OPACITY } from './constants';
|
|
13
|
+
import { ErrorView } from './ErrorView';
|
|
14
|
+
import { FloatingView } from './FloatingView';
|
|
15
|
+
import { InputDropDownProps } from './index';
|
|
16
|
+
import styles from './styles';
|
|
10
17
|
import {
|
|
11
|
-
ErrorView,
|
|
12
|
-
FloatingView,
|
|
13
18
|
getBorderColor,
|
|
19
|
+
getInputHeight,
|
|
20
|
+
getLeadingIconSize,
|
|
14
21
|
getSizeStyle,
|
|
15
|
-
} from './
|
|
16
|
-
import { InputDropDownProps } from './index';
|
|
17
|
-
import { Text, useScaleSize } from '../Text';
|
|
18
|
-
import styles from './styles';
|
|
19
|
-
import { Spacing } from '../Consts';
|
|
22
|
+
} from './utils';
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Pressable field styled like an input with a chevron; shows `value` or
|
|
@@ -44,7 +47,7 @@ const InputDropDown = ({
|
|
|
44
47
|
}: InputDropDownProps) => {
|
|
45
48
|
const { theme } = useContext(ApplicationContext);
|
|
46
49
|
const context = useContext<any>(MiniAppContext);
|
|
47
|
-
const scaleHeight = useScaleSize(size
|
|
50
|
+
const scaleHeight = useScaleSize(getInputHeight(size));
|
|
48
51
|
const componentName = 'InputDropDown';
|
|
49
52
|
|
|
50
53
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
@@ -99,7 +102,7 @@ const InputDropDown = ({
|
|
|
99
102
|
<Icon
|
|
100
103
|
color={leadingIconColor}
|
|
101
104
|
source={leadingIcon}
|
|
102
|
-
size={size
|
|
105
|
+
size={getLeadingIconSize(size)}
|
|
103
106
|
accessibilityLabel={`${componentId}|leading-icon`}
|
|
104
107
|
/>
|
|
105
108
|
</View>
|
|
@@ -136,7 +139,7 @@ const InputDropDown = ({
|
|
|
136
139
|
{...props}
|
|
137
140
|
accessibilityLabel={`${componentId}|touch`}
|
|
138
141
|
accessibilityState={{ disabled: disabled as boolean }}
|
|
139
|
-
activeOpacity={
|
|
142
|
+
activeOpacity={PRESSED_OPACITY}
|
|
140
143
|
style={[
|
|
141
144
|
style,
|
|
142
145
|
styles.wrapper,
|