@momo-kits/foundation 0.169.9-beta.1 → 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/ActiveIndicator.tsx +5 -1
- package/Application/BottomTab/BottomTabBar.styles.ts +18 -1
- package/Application/BottomTab/BottomTabBar.tsx +14 -16
- package/Application/BottomTab/CurvedContainer.tsx +8 -2
- package/Application/BottomTab/CustomBottomTabItem.tsx +2 -2
- package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
- package/Application/BottomTab/DuoTabBar.tsx +15 -3
- package/Application/BottomTab/TabScreen.tsx +11 -10
- package/Application/BottomTab/index.tsx +5 -5
- package/Application/BottomTab/metrics.ts +23 -3
- package/Application/BottomTab/usePressLift.ts +3 -3
- package/Application/Components/HeaderAnimated.tsx +13 -38
- package/Application/Components/HeaderBackground.tsx +3 -16
- package/Application/Components/HeaderExtendHeader.tsx +28 -41
- package/Application/Components/HeaderLeft.tsx +18 -4
- package/Application/Components/HeaderRight.styles.ts +13 -15
- package/Application/Components/HeaderTitle.shared.ts +11 -10
- package/Application/Components/HeaderToolkitAction.tsx +54 -21
- package/Application/Components/NavigationButton.tsx +6 -9
- package/Application/Components/SearchHeader.tsx +7 -20
- package/Application/Components/useToolkitActions.tsx +22 -21
- package/Application/ModalScreen.tsx +19 -15
- package/Application/StackScreen.tsx +13 -18
- package/Application/index.ts +1 -11
- package/Application/modalLayout.ts +1 -0
- package/Application/navigationOptions.tsx +15 -6
- package/Application/types.header.ts +2 -7
- package/Application/types.tabs.ts +0 -1
- package/Application/types.ts +0 -7
- 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 +26 -39
- package/Input/InputTextArea.tsx +29 -23
- package/Input/RenderTrailing.tsx +79 -0
- package/Input/constants.ts +8 -0
- package/Input/index.tsx +1 -7
- package/Input/utils.ts +126 -7
- package/Layout/FloatingButton.tsx +38 -24
- package/Layout/Screen.tsx +0 -3
- package/Layout/layoutMetrics.ts +43 -34
- package/Layout/screenHeader.tsx +52 -55
- package/Layout/screenTypes.ts +4 -13
- package/Pagination/PaginationScroll.tsx +5 -3
- package/Pagination/types.ts +3 -5
- package/Popup/PopupNotify.tsx +17 -14
- package/Popup/PopupPromotion.tsx +5 -4
- package/Popup/types.ts +2 -4
- package/Popup/utils.ts +7 -4
- package/Skeleton/index.tsx +15 -12
- package/package.json +1 -1
- package/Application/headerLayout.ts +0 -165
- package/Input/common.tsx +0 -257
- package/Input/moneyUtils.ts +0 -23
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Colors, Radius, Spacing, Styles } from '../../Consts';
|
|
2
|
-
import { InputRef, InputSearch
|
|
2
|
+
import { InputRef, InputSearch } from '../../Input';
|
|
3
3
|
import { StyleSheet, TouchableOpacity, View } from 'react-native';
|
|
4
4
|
import Animated, { useAnimatedStyle } from 'react-native-reanimated';
|
|
5
5
|
import React, { useContext } from 'react';
|
|
@@ -8,13 +8,7 @@ import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
|
8
8
|
import { Text } from '../../Text';
|
|
9
9
|
import { useNormalizedSharedValue } from '../hooks';
|
|
10
10
|
import { useScreenLayout } from '../../Layout';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
const { buttonSize, pillWidth, sidePadding, titleGap } = HeaderBarMetrics;
|
|
14
|
-
// help center | home pill and the bar padding right of it
|
|
15
|
-
const TOOLKIT_WIDTH = pillWidth + sidePadding;
|
|
16
|
-
// the iPhone Duo side bar keeps its right header width
|
|
17
|
-
const DUO_TOOLKIT_WIDTH = 73;
|
|
11
|
+
import { DuoMetrics } from '../../Layout/utils';
|
|
18
12
|
|
|
19
13
|
/**
|
|
20
14
|
* Header search bar whose fill turns from surface to default once scrolled, with
|
|
@@ -34,21 +28,15 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
|
|
|
34
28
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
35
29
|
const context = useContext<any>(MiniAppContext);
|
|
36
30
|
const layout = useScreenLayout();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (layout.sideBar) {
|
|
40
|
-
defaultRightWidth = DUO_TOOLKIT_WIDTH;
|
|
41
|
-
searchSize = 'default';
|
|
42
|
-
}
|
|
31
|
+
// the iPhone Duo status bar row keeps a side bar sized back button
|
|
32
|
+
const BACK_WIDTH = layout.statusBarClearance ? DuoMetrics.controlSize : 28;
|
|
43
33
|
|
|
44
34
|
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const headerRightWidth = props?.headerRightWidth ?? defaultRightWidth;
|
|
35
|
+
const leftPosition = props?.leftPosition ?? BACK_WIDTH + 20;
|
|
36
|
+
const headerRightWidth = props?.headerRightWidth ?? 73;
|
|
48
37
|
|
|
49
38
|
// iPhone Duo side bar holds back: the search shares the content width with the buttons only
|
|
50
|
-
|
|
51
|
-
let searchMaxWidth = rowWidth - headerRightWidth;
|
|
39
|
+
let searchMaxWidth = layout.width - leftPosition - 12 - headerRightWidth;
|
|
52
40
|
if (layout.sideBar) {
|
|
53
41
|
searchMaxWidth = layout.contentWidth - Spacing.M * 2 - headerRightWidth;
|
|
54
42
|
}
|
|
@@ -117,7 +105,6 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
|
|
|
117
105
|
<InputSearch
|
|
118
106
|
ref={ref}
|
|
119
107
|
{...props}
|
|
120
|
-
size={searchSize}
|
|
121
108
|
backgroundColor={'transparent'}
|
|
122
109
|
showButtonText={false}
|
|
123
110
|
/>
|
|
@@ -4,6 +4,7 @@ import { PopupNotify } from '../../Popup';
|
|
|
4
4
|
import { Tool, ToolGroup } from '../types';
|
|
5
5
|
|
|
6
6
|
const DID_SYNC_NEW_HOME = 'did_sync_new_home';
|
|
7
|
+
const LOADING_RESET_DELAY = 1000;
|
|
7
8
|
|
|
8
9
|
type ToolkitActionsParams = {
|
|
9
10
|
preventClose?: any;
|
|
@@ -49,8 +50,8 @@ const useToolkitActions = ({
|
|
|
49
50
|
const favoriteObserver = navigator?.maxApi?.observer?.(
|
|
50
51
|
DID_SYNC_NEW_HOME,
|
|
51
52
|
(updatedData: any) => {
|
|
52
|
-
const
|
|
53
|
-
setIsFavorite(
|
|
53
|
+
const isInFavorites = updatedData?.items?.includes(context?.code);
|
|
54
|
+
setIsFavorite(isInFavorites);
|
|
54
55
|
},
|
|
55
56
|
);
|
|
56
57
|
return () => {
|
|
@@ -59,7 +60,7 @@ const useToolkitActions = ({
|
|
|
59
60
|
}
|
|
60
61
|
}, [useShortcut, context, checkAppIsFavorite, navigator?.maxApi]);
|
|
61
62
|
|
|
62
|
-
const
|
|
63
|
+
const handleDismiss = () => {
|
|
63
64
|
if (useCloseIcon) {
|
|
64
65
|
navigator?.maxApi?.dispatchFunction?.(
|
|
65
66
|
'dismiss',
|
|
@@ -71,7 +72,7 @@ const useToolkitActions = ({
|
|
|
71
72
|
}
|
|
72
73
|
};
|
|
73
74
|
|
|
74
|
-
const
|
|
75
|
+
const handleClose = () => {
|
|
75
76
|
const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
|
|
76
77
|
context?.autoTracking?.({
|
|
77
78
|
...context,
|
|
@@ -89,24 +90,24 @@ const useToolkitActions = ({
|
|
|
89
90
|
title: preventClose.primary.title,
|
|
90
91
|
onPress: () => {
|
|
91
92
|
preventClose?.primary.onPress();
|
|
92
|
-
|
|
93
|
+
handleDismiss();
|
|
93
94
|
},
|
|
94
95
|
}}
|
|
95
96
|
/>
|
|
96
97
|
),
|
|
97
98
|
});
|
|
98
99
|
} else {
|
|
99
|
-
|
|
100
|
+
handleDismiss();
|
|
100
101
|
}
|
|
101
102
|
};
|
|
102
103
|
|
|
103
|
-
const
|
|
104
|
+
const handlePressShortcut = () => {
|
|
104
105
|
const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
|
|
105
|
-
const
|
|
106
|
+
const shouldBeFavorite = !isFavorite;
|
|
106
107
|
context?.autoTracking?.({
|
|
107
108
|
...context,
|
|
108
109
|
componentName: 'IconButton',
|
|
109
|
-
componentId: `${
|
|
110
|
+
componentId: `${shouldBeFavorite ? 'pin' : 'unpin'}_shortcut`,
|
|
110
111
|
screenName: currentRoute?.params?.screen?.name ?? currentRoute?.name,
|
|
111
112
|
});
|
|
112
113
|
setIsLoading(true);
|
|
@@ -120,16 +121,16 @@ const useToolkitActions = ({
|
|
|
120
121
|
},
|
|
121
122
|
({ success }: { success: boolean }) => {
|
|
122
123
|
if (success) {
|
|
123
|
-
setIsFavorite(
|
|
124
|
+
setIsFavorite(shouldBeFavorite);
|
|
124
125
|
}
|
|
125
126
|
},
|
|
126
127
|
);
|
|
127
128
|
setTimeout(() => {
|
|
128
129
|
setIsLoading(false);
|
|
129
|
-
},
|
|
130
|
+
}, LOADING_RESET_DELAY);
|
|
130
131
|
};
|
|
131
132
|
|
|
132
|
-
const
|
|
133
|
+
const handlePressHelpCenter = () => {
|
|
133
134
|
const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
|
|
134
135
|
context?.autoTracking?.({
|
|
135
136
|
...context,
|
|
@@ -141,7 +142,7 @@ const useToolkitActions = ({
|
|
|
141
142
|
navigator?.maxApi?.dispatchFunction?.('showHelpCenter', context);
|
|
142
143
|
};
|
|
143
144
|
|
|
144
|
-
const
|
|
145
|
+
const handlePressIconMore = () => {
|
|
145
146
|
const currentRoute = navigator?.ref?.current?.getCurrentRoute?.();
|
|
146
147
|
context?.autoTracking?.({
|
|
147
148
|
...context,
|
|
@@ -172,18 +173,18 @@ const useToolkitActions = ({
|
|
|
172
173
|
};
|
|
173
174
|
|
|
174
175
|
let iconShortcut = isFavorite ? 'pin_star_checked' : 'pin_star';
|
|
175
|
-
let shortcutOnPress =
|
|
176
|
+
let shortcutOnPress = handlePressShortcut;
|
|
176
177
|
const totalTools = tools.reduce(
|
|
177
178
|
(count: number, group: ToolGroup) => count + group?.items?.length,
|
|
178
179
|
0,
|
|
179
180
|
);
|
|
180
|
-
const
|
|
181
|
-
const
|
|
181
|
+
const hasManyTools = tools && totalTools > 1;
|
|
182
|
+
const hasOneTool = tools && totalTools === 1;
|
|
182
183
|
|
|
183
|
-
if (
|
|
184
|
+
if (hasManyTools || useMore) {
|
|
184
185
|
iconShortcut = 'navigation_more_icon';
|
|
185
|
-
shortcutOnPress =
|
|
186
|
-
} else if (
|
|
186
|
+
shortcutOnPress = handlePressIconMore;
|
|
187
|
+
} else if (hasOneTool) {
|
|
187
188
|
const singleToolGroup = tools.find(
|
|
188
189
|
(group: ToolGroup) => group?.items?.length === 1,
|
|
189
190
|
);
|
|
@@ -210,8 +211,8 @@ const useToolkitActions = ({
|
|
|
210
211
|
iconShortcut,
|
|
211
212
|
shortcutOnPress,
|
|
212
213
|
showBadge,
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
handlePressHelpCenter,
|
|
215
|
+
handleClose,
|
|
215
216
|
};
|
|
216
217
|
};
|
|
217
218
|
|
|
@@ -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,
|
|
@@ -8,7 +8,10 @@ import { SwipeBackHaptic } from './Components/SwipeBackHaptic';
|
|
|
8
8
|
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;
|
|
12
15
|
|
|
13
16
|
const runAfterInteractions = InteractionManager.runAfterInteractions;
|
|
14
17
|
|
|
@@ -52,7 +55,6 @@ const StackScreen: React.FC<any> = props => {
|
|
|
52
55
|
const navigation = useRef(new Navigation(props.navigation, context)).current;
|
|
53
56
|
const heightHeader = useHeaderHeight();
|
|
54
57
|
const { isBackgroundToForeground } = useAppState();
|
|
55
|
-
const headerFrameOptions = useHeaderFrameOptions(options?.contentLayout);
|
|
56
58
|
|
|
57
59
|
const data = {
|
|
58
60
|
...initialParams,
|
|
@@ -71,16 +73,9 @@ const StackScreen: React.FC<any> = props => {
|
|
|
71
73
|
}
|
|
72
74
|
}, [navigation, options]);
|
|
73
75
|
|
|
74
|
-
// re-applied on fold, unfold and rotation, apart from the other options
|
|
75
|
-
useLayoutEffect(() => {
|
|
76
|
-
if (headerFrameOptions) {
|
|
77
|
-
navigation.instance.setOptions(headerFrameOptions);
|
|
78
|
-
}
|
|
79
|
-
}, [navigation, headerFrameOptions]);
|
|
80
|
-
|
|
81
76
|
useEffect(() => {
|
|
82
77
|
let focusScreen: any;
|
|
83
|
-
let
|
|
78
|
+
let focusAppListener: { remove?: () => void } | undefined;
|
|
84
79
|
if (['Invalid', 'screen'].includes(screenName)) {
|
|
85
80
|
navigator?.maxApi?.showPopup?.('notice', {
|
|
86
81
|
title: 'Invalid screen name',
|
|
@@ -94,19 +89,19 @@ const StackScreen: React.FC<any> = props => {
|
|
|
94
89
|
if (!bottomTab) {
|
|
95
90
|
focusScreen = props.navigation.addListener('focus', () => {
|
|
96
91
|
navigator?.maxApi?.getDataObserver?.('current_screen', (item: any) => {
|
|
97
|
-
|
|
92
|
+
handleScreenNavigated(item?.screenName, screenName);
|
|
98
93
|
navigator?.maxApi?.setDataObserver?.('current_screen', {
|
|
99
94
|
screenName,
|
|
100
95
|
startTime,
|
|
101
96
|
});
|
|
102
97
|
});
|
|
103
98
|
});
|
|
104
|
-
|
|
99
|
+
focusAppListener = navigator?.maxApi?.listen?.('onFocusApp', () => {
|
|
105
100
|
if (props.navigation.isFocused()) {
|
|
106
101
|
navigator?.maxApi?.getDataObserver?.(
|
|
107
102
|
'current_screen',
|
|
108
103
|
(item: any) => {
|
|
109
|
-
|
|
104
|
+
handleScreenNavigated(item?.screenName, screenName);
|
|
110
105
|
navigator?.maxApi?.setDataObserver?.('current_screen', {
|
|
111
106
|
screenName,
|
|
112
107
|
startTime,
|
|
@@ -135,7 +130,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
135
130
|
tracking.current.timeoutTracking = setTimeout(() => {
|
|
136
131
|
onScreenLoad();
|
|
137
132
|
onScreenInteraction();
|
|
138
|
-
},
|
|
133
|
+
}, TRACKING_TIMEOUT);
|
|
139
134
|
|
|
140
135
|
return () => {
|
|
141
136
|
onScreenLoad();
|
|
@@ -146,13 +141,13 @@ const StackScreen: React.FC<any> = props => {
|
|
|
146
141
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
142
|
clearTimeout(tracking.current.timeoutLoading);
|
|
148
143
|
focusScreen?.();
|
|
149
|
-
|
|
144
|
+
focusAppListener?.remove?.();
|
|
150
145
|
};
|
|
151
146
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
152
147
|
}, []);
|
|
153
148
|
|
|
154
149
|
/** Reports a navigation to this screen: `back` if it was shown before. */
|
|
155
|
-
const
|
|
150
|
+
const handleScreenNavigated = (pre: string, current: string) => {
|
|
156
151
|
if (!isBackgroundToForeground) {
|
|
157
152
|
const item: any = {
|
|
158
153
|
preScreenName: pre,
|
|
@@ -243,7 +238,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
243
238
|
|
|
244
239
|
onScreenLoad();
|
|
245
240
|
onScreenInteraction();
|
|
246
|
-
},
|
|
241
|
+
}, ELEMENT_IDLE_TIMEOUT);
|
|
247
242
|
},
|
|
248
243
|
onLoading: (loading: boolean) => {
|
|
249
244
|
if (loading && tracking.current.timeStartLoading === 0) {
|
|
@@ -257,7 +252,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
257
252
|
clearTimeout(tracking.current.timeoutLoading);
|
|
258
253
|
tracking.current.timeoutLoading = setTimeout(() => {
|
|
259
254
|
onScreenLoading();
|
|
260
|
-
},
|
|
255
|
+
}, LOADING_IDLE_TIMEOUT);
|
|
261
256
|
},
|
|
262
257
|
onSetParams: (item: ScreenTrackingParams) => {
|
|
263
258
|
tracking.current.params = item;
|
package/Application/index.ts
CHANGED
|
@@ -26,13 +26,6 @@ import {
|
|
|
26
26
|
type ModalLayout,
|
|
27
27
|
type ModalRect,
|
|
28
28
|
} from './modalLayout';
|
|
29
|
-
import {
|
|
30
|
-
HeaderBarMetrics,
|
|
31
|
-
getHeaderLayout,
|
|
32
|
-
useHeaderLayout,
|
|
33
|
-
type HeaderContentLayout,
|
|
34
|
-
type HeaderLayout,
|
|
35
|
-
} from './headerLayout';
|
|
36
29
|
|
|
37
30
|
export {
|
|
38
31
|
NavigationContainer,
|
|
@@ -48,9 +41,6 @@ export {
|
|
|
48
41
|
useTooltipPortal,
|
|
49
42
|
getModalLayout,
|
|
50
43
|
useModalLayout,
|
|
51
|
-
HeaderBarMetrics,
|
|
52
|
-
getHeaderLayout,
|
|
53
|
-
useHeaderLayout,
|
|
54
44
|
};
|
|
55
45
|
|
|
56
|
-
export type { ModalLayout, ModalRect
|
|
46
|
+
export type { ModalLayout, ModalRect };
|
|
@@ -8,12 +8,11 @@ import {
|
|
|
8
8
|
TransitionPresets,
|
|
9
9
|
} from '@react-navigation/stack';
|
|
10
10
|
import type { SharedValue } from 'react-native-reanimated';
|
|
11
|
-
import { Colors } from '../Consts';
|
|
11
|
+
import { Colors, Spacing } from '../Consts';
|
|
12
12
|
import { HeaderBackground } from './Components/HeaderBackground';
|
|
13
13
|
import { HeaderLeft } from './Components/HeaderLeft';
|
|
14
14
|
import { HeaderRight } from './Components/HeaderRight';
|
|
15
15
|
import { HeaderTitle, TitleCustom } from './Components/HeaderTitle';
|
|
16
|
-
import { HeaderBarMetrics, TITLE_CONTAINER_RESET } from './headerLayout';
|
|
17
16
|
import type { HeaderTitleProps, NavigationOptions } from './types';
|
|
18
17
|
|
|
19
18
|
const CARD_SHADOW_OPACITY = 0.3;
|
|
@@ -56,8 +55,13 @@ const getStackOptions = (): StackNavigationOptions => {
|
|
|
56
55
|
headerTitleAlign: 'left',
|
|
57
56
|
headerTitle: props => <HeaderTitle {...props} />,
|
|
58
57
|
headerTitleContainerStyle: {
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
marginLeft: 0,
|
|
59
|
+
marginEnd: 0,
|
|
60
|
+
marginStart: 0,
|
|
61
|
+
marginHorizontal: 0,
|
|
62
|
+
marginVertical: 0,
|
|
63
|
+
margin: 0,
|
|
64
|
+
left: Spacing.S,
|
|
61
65
|
},
|
|
62
66
|
headerBackground: (props: any) => <HeaderBackground {...props} />,
|
|
63
67
|
headerLeft: (props: any) => <HeaderLeft {...props} floating />,
|
|
@@ -168,8 +172,13 @@ const getOptions = (
|
|
|
168
172
|
if (params.hiddenBack) {
|
|
169
173
|
headerLeft = null;
|
|
170
174
|
options.headerTitleContainerStyle = {
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
marginLeft: 0,
|
|
176
|
+
marginEnd: 0,
|
|
177
|
+
marginStart: 0,
|
|
178
|
+
marginHorizontal: 0,
|
|
179
|
+
marginVertical: 0,
|
|
180
|
+
margin: 0,
|
|
181
|
+
left: Spacing.M,
|
|
173
182
|
};
|
|
174
183
|
}
|
|
175
184
|
options = {
|
|
@@ -98,11 +98,6 @@ export interface HeaderAnimatedProps extends ViewProps {
|
|
|
98
98
|
image: string;
|
|
99
99
|
useScale?: boolean;
|
|
100
100
|
aspectRatio?: '16/9' | '3/2' | '1/1';
|
|
101
|
-
/**
|
|
102
|
-
* Optional. On wide windows the image keeps its phone width, centered; this
|
|
103
|
-
* color fills the sides and fades to transparent downward. No fill when unset.
|
|
104
|
-
*/
|
|
105
|
-
bannerBackgroundColor?: string;
|
|
106
101
|
}
|
|
107
102
|
|
|
108
103
|
export type AnimatedHeader = {
|
|
@@ -114,8 +109,8 @@ export type AnimatedHeader = {
|
|
|
114
109
|
export interface SearchHeaderProps extends InputSearchProps {
|
|
115
110
|
ref?: React.RefObject<InputRef>;
|
|
116
111
|
animatedValue?: Animated.Value | SharedValue<number>;
|
|
117
|
-
headerRightWidth?: 0 |
|
|
118
|
-
leftPosition?: 12 |
|
|
112
|
+
headerRightWidth?: 0 | 74 | 110 | number;
|
|
113
|
+
leftPosition?: 12 | 48 | number;
|
|
119
114
|
renderButtons?: () => ReactNode;
|
|
120
115
|
hiddenBack?: boolean;
|
|
121
116
|
}
|
package/Application/types.ts
CHANGED
|
@@ -9,7 +9,6 @@ import type {
|
|
|
9
9
|
OnBoarding,
|
|
10
10
|
} from './types.header';
|
|
11
11
|
import type { LocalizeProps, Theme } from './types.theme';
|
|
12
|
-
import type { HeaderContentLayout } from './headerLayout';
|
|
13
12
|
|
|
14
13
|
export * from './types.theme';
|
|
15
14
|
export * from './types.header';
|
|
@@ -129,10 +128,4 @@ export interface NavigationOptions
|
|
|
129
128
|
hiddenBack?: boolean;
|
|
130
129
|
headerTitle?: HeaderTitleProps | string;
|
|
131
130
|
headerRight?: OnBoarding | HeaderRightToolkit | any;
|
|
132
|
-
/**
|
|
133
|
-
* Optional. `stretch` centers back, title and the right cluster in a 600pt
|
|
134
|
-
* frame on wide windows (foldable inner display, tablet); the background stays
|
|
135
|
-
* full width. Defaults to `grid`, the header row at the window edges.
|
|
136
|
-
*/
|
|
137
|
-
contentLayout?: HeaderContentLayout;
|
|
138
131
|
}
|
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 };
|