@momo-kits/foundation 0.168.1-rn862.1 → 0.169.2
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 +195 -0
- package/Application/BottomSheet.parts.tsx +147 -0
- package/Application/BottomSheet.styles.ts +43 -0
- package/Application/BottomSheet.tsx +76 -279
- package/Application/BottomTab/ActiveIndicator.tsx +90 -0
- package/Application/BottomTab/BottomTabBar.styles.ts +65 -0
- package/Application/BottomTab/BottomTabBar.tsx +229 -276
- package/Application/BottomTab/BottomTabInset.tsx +14 -0
- package/Application/BottomTab/CurvedContainer.tsx +153 -0
- package/Application/BottomTab/CustomBottomTabItem.tsx +132 -108
- package/Application/BottomTab/DuoBarCompression.tsx +4 -12
- package/Application/BottomTab/DuoRailTabBar.tsx +227 -0
- package/Application/BottomTab/DuoTabBar.styles.ts +81 -0
- package/Application/BottomTab/DuoTabBar.tsx +29 -495
- package/Application/BottomTab/TabBarIcon.tsx +160 -72
- package/Application/BottomTab/TabScreen.tsx +164 -0
- package/Application/BottomTab/index.tsx +84 -351
- package/Application/BottomTab/metrics.ts +164 -0
- package/Application/BottomTab/tabItems.ts +46 -0
- package/Application/BottomTab/usePressLift.ts +32 -0
- package/Application/Components/DuoGlassBackground.tsx +7 -9
- package/Application/Components/HeaderAnimated.tsx +1 -1
- package/Application/Components/HeaderBackground.tsx +1 -1
- package/Application/Components/HeaderExtendHeader.tsx +17 -11
- package/Application/Components/HeaderRight.styles.ts +144 -0
- package/Application/Components/HeaderRight.tsx +6 -528
- package/Application/Components/HeaderTitle.shared.ts +43 -0
- package/Application/Components/HeaderTitle.styles.ts +125 -0
- package/Application/Components/HeaderTitle.tsx +12 -576
- package/Application/Components/HeaderToolkitAction.tsx +208 -0
- package/Application/Components/NavigationButton.tsx +9 -6
- package/Application/Components/SearchHeader.tsx +1 -1
- package/Application/Components/SwipeBackHaptic.tsx +95 -0
- package/Application/Components/TitleJourney.tsx +110 -0
- package/Application/Components/TitleLocation.tsx +73 -0
- package/Application/Components/TitleUser.tsx +304 -0
- package/Application/Components/useToolkitActions.tsx +233 -0
- package/Application/ModalIconButton.tsx +66 -0
- package/Application/ModalScreen.tsx +39 -18
- package/Application/Navigation.ts +1 -1
- package/Application/NavigationContainer.tsx +7 -8
- package/Application/StackScreen.tracking.ts +158 -0
- package/Application/StackScreen.tsx +10 -137
- package/Application/WidgetContainer.tsx +10 -3
- package/Application/__tests__/modalLayout.test.ts +128 -0
- package/Application/hooks.ts +137 -0
- package/Application/index.ts +13 -1
- package/Application/modalLayout.ts +177 -0
- package/Application/navigationOptions.tsx +217 -0
- package/Application/types.header.ts +116 -0
- package/Application/types.tabs.ts +48 -0
- package/Application/types.theme.ts +87 -0
- package/Application/types.ts +12 -244
- package/Application/utils.ts +107 -0
- package/Badge/BadgeRibbon.tsx +3 -3
- package/Badge/styles.ts +2 -0
- package/Button/index.tsx +16 -136
- package/Button/styles.ts +1 -1
- package/Button/utils.ts +139 -0
- package/CheckBox/index.tsx +1 -6
- package/Divider/DashDivider.tsx +10 -11
- package/Divider/index.tsx +18 -8
- package/Image/index.tsx +1 -46
- package/Input/Input.tsx +7 -8
- package/Input/InputDropDown.tsx +3 -4
- package/Input/InputMoney.tsx +38 -164
- package/Input/InputMoneyView.tsx +187 -0
- package/Input/InputOTP.tsx +1 -1
- package/Input/InputPhoneNumber.tsx +5 -1
- package/Input/InputSearch.tsx +2 -108
- package/Input/InputTextArea.tsx +1 -5
- package/Input/SearchTextTyping.tsx +104 -0
- package/Input/common.tsx +4 -4
- package/Input/moneyUtils.ts +17 -0
- package/Input/styles.ts +26 -2
- package/Layout/FloatingButton.tsx +22 -21
- package/Layout/ReservedRegions.tsx +21 -4
- package/Layout/Screen.tsx +74 -670
- package/Layout/Section.tsx +1 -2
- package/Layout/index.ts +10 -4
- package/Layout/layoutMetrics.ts +58 -0
- package/Layout/layoutTypes.ts +86 -0
- package/Layout/screenContent.tsx +90 -0
- package/Layout/screenFooter.tsx +53 -0
- package/Layout/screenHeader.tsx +315 -0
- package/Layout/screenQuickReturn.tsx +67 -0
- package/Layout/screenScroll.ts +110 -0
- package/Layout/screenTypes.ts +134 -0
- package/Layout/styles.ts +2 -0
- package/Layout/utils.ts +10 -144
- package/Loader/DotLoader.tsx +7 -3
- package/Loader/ProgressBar.tsx +2 -6
- package/Loader/Spinner.tsx +7 -3
- package/Loader/styles.ts +4 -0
- package/Popup/PopupNotify.tsx +70 -64
- package/Popup/PopupPromotion.tsx +5 -4
- package/Popup/types.ts +7 -0
- package/Popup/utils.ts +46 -24
- package/Radio/index.tsx +2 -10
- package/Radio/styles.ts +6 -0
- package/Skeleton/index.tsx +1 -9
- package/Skeleton/styles.ts +1 -0
- package/Tag/index.tsx +3 -3
- package/Text/index.tsx +11 -2
- package/Title/index.tsx +2 -9
- package/Title/styles.ts +6 -0
- package/package.json +21 -22
- package/Application/device.ts +0 -29
- package/Application/utils.tsx +0 -497
|
@@ -1,199 +1,59 @@
|
|
|
1
|
-
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '@react-navigation/native';
|
|
7
|
-
import {
|
|
8
|
-
import React, { useContext, useEffect, useLayoutEffect } from 'react';
|
|
9
|
-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
2
|
+
BottomTabNavigationOptions,
|
|
3
|
+
createBottomTabNavigator,
|
|
4
|
+
} from '@react-navigation/bottom-tabs';
|
|
5
|
+
import { useFocusEffect } from '@react-navigation/native';
|
|
6
|
+
import React, { useContext, useLayoutEffect } from 'react';
|
|
10
7
|
import {
|
|
8
|
+
Easing,
|
|
11
9
|
NativeModules,
|
|
12
10
|
Platform,
|
|
13
11
|
StatusBar,
|
|
14
|
-
StyleSheet,
|
|
15
12
|
useWindowDimensions,
|
|
16
|
-
View,
|
|
17
13
|
} from 'react-native';
|
|
18
|
-
import Animated, {
|
|
19
|
-
useAnimatedStyle,
|
|
20
|
-
useSharedValue,
|
|
21
|
-
withTiming,
|
|
22
|
-
} from 'react-native-reanimated';
|
|
23
|
-
import { Colors } from '../../Consts';
|
|
24
14
|
import { Icon } from '../../Icon';
|
|
25
|
-
import { exportFontFamily, Text } from '../../Text';
|
|
26
|
-
import StackScreen from '../StackScreen';
|
|
27
15
|
import { BottomTabProps } from '../types';
|
|
28
|
-
import
|
|
29
|
-
import
|
|
30
|
-
import {
|
|
16
|
+
import BottomTabBar, { hasCenterAction } from './BottomTabBar';
|
|
17
|
+
import { TabBarMetrics } from './metrics';
|
|
18
|
+
import { getTabBadge } from './TabBarIcon';
|
|
19
|
+
import { DuoTabBar, useDuoTabBarInset } from './DuoTabBar';
|
|
20
|
+
import { BottomTabInsetContext } from './BottomTabInset';
|
|
31
21
|
import { DuoBarCompressionProvider } from './DuoBarCompression';
|
|
32
|
-
import
|
|
22
|
+
import TabScreen from './TabScreen';
|
|
23
|
+
import { ApplicationContext } from '../../Context';
|
|
33
24
|
import { useScreenLayout } from '../../Layout';
|
|
34
|
-
import { getHeaderRowOptions } from '../../Layout/utils';
|
|
35
25
|
|
|
36
26
|
const Tab = createBottomTabNavigator();
|
|
37
|
-
const Stack = createStackNavigator();
|
|
38
27
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const opacityValue = 0.3;
|
|
46
|
-
const scaleValue = 0.97;
|
|
47
|
-
const opacity = useSharedValue(opacityValue);
|
|
48
|
-
const scale = useSharedValue(scaleValue);
|
|
49
|
-
const screenName = screen?.name || screen?.type?.name || 'Invalid';
|
|
50
|
-
const { isBackgroundToForeground } = useAppState();
|
|
51
|
-
|
|
52
|
-
const tabAnimatedStyle = useAnimatedStyle(() => ({
|
|
53
|
-
opacity: opacity.value,
|
|
54
|
-
transform: [{ scale: scale.value }],
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
const onScreenNavigated = (pre: string, current: string) => {
|
|
58
|
-
if (!isBackgroundToForeground) {
|
|
59
|
-
const data: any = {
|
|
60
|
-
preScreenName: pre,
|
|
61
|
-
screenName: current,
|
|
62
|
-
componentName: 'Screen',
|
|
63
|
-
state: 'navigated',
|
|
64
|
-
action: 'push',
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
context?.autoTracking?.({
|
|
68
|
-
...context,
|
|
69
|
-
...data,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* debug toast
|
|
74
|
-
*/
|
|
75
|
-
navigator?.maxApi?.showToastDebug?.({
|
|
76
|
-
appId: context.appId,
|
|
77
|
-
message: `${screenName} screen_navigated`,
|
|
78
|
-
type: 'ERROR',
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
useFocusEffect(
|
|
84
|
-
React.useCallback(
|
|
85
|
-
() => {
|
|
86
|
-
opacity.value = withTiming(1, { duration: 200 });
|
|
87
|
-
scale.value = withTiming(1, { duration: 200 });
|
|
88
|
-
|
|
89
|
-
setTimeout(() => {
|
|
90
|
-
navigator?.maxApi?.getDataObserver?.(
|
|
91
|
-
'current_screen',
|
|
92
|
-
(data: any) => {
|
|
93
|
-
onScreenNavigated(data?.screenName, screenName);
|
|
94
|
-
navigator?.maxApi?.setObserver?.('current_screen', {
|
|
95
|
-
screenName,
|
|
96
|
-
});
|
|
97
|
-
},
|
|
98
|
-
);
|
|
99
|
-
}, 100);
|
|
100
|
-
|
|
101
|
-
return () => {
|
|
102
|
-
if (navigation.getState().index !== route?.params.index) {
|
|
103
|
-
opacity.value = withTiming(opacityValue, { duration: 200 });
|
|
104
|
-
scale.value = withTiming(scaleValue, { duration: 200 });
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
},
|
|
108
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
109
|
-
[],
|
|
110
|
-
),
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
useEffect(
|
|
114
|
-
() => {
|
|
115
|
-
const onFocusApp = navigator?.maxApi?.listen?.('onFocusApp', () => {
|
|
116
|
-
if (navigation.isFocused()) {
|
|
117
|
-
navigator?.maxApi?.getDataObserver?.(
|
|
118
|
-
'current_screen',
|
|
119
|
-
(data: any) => {
|
|
120
|
-
onScreenNavigated(data?.screenName, screenName);
|
|
121
|
-
navigator?.maxApi?.setObserver?.('current_screen', {
|
|
122
|
-
screenName,
|
|
123
|
-
});
|
|
124
|
-
},
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
return () => {
|
|
130
|
-
onFocusApp?.remove?.();
|
|
131
|
-
};
|
|
132
|
-
},
|
|
133
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
134
|
-
[],
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
let stackOptions = {};
|
|
138
|
-
if (options) {
|
|
139
|
-
stackOptions = getOptions(options);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (nested) {
|
|
143
|
-
return (
|
|
144
|
-
<Animated.View style={[styles.container, tabAnimatedStyle]}>
|
|
145
|
-
<Stack.Navigator
|
|
146
|
-
screenOptions={headerStyleOptions}
|
|
147
|
-
>
|
|
148
|
-
<Stack.Screen
|
|
149
|
-
name="Stack"
|
|
150
|
-
component={StackScreen}
|
|
151
|
-
initialParams={{
|
|
152
|
-
screen,
|
|
153
|
-
options: stackOptions,
|
|
154
|
-
initialParams,
|
|
155
|
-
bottomTab: 'nested',
|
|
156
|
-
}}
|
|
157
|
-
options={{ ...getStackOptions() }}
|
|
158
|
-
/>
|
|
159
|
-
</Stack.Navigator>
|
|
160
|
-
</Animated.View>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
28
|
+
// the tab screens slide in index order over 280ms, CSS `ease`
|
|
29
|
+
const TAB_SWITCH_SPEC = {
|
|
30
|
+
animation: 'timing' as const,
|
|
31
|
+
config: { duration: 280, easing: Easing.bezier(0.25, 0.1, 0.25, 1) },
|
|
32
|
+
};
|
|
163
33
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
34
|
+
/**
|
|
35
|
+
* scene progress is -1 for tabs before the active one, 1 for tabs after it: they wait a full
|
|
36
|
+
* width off screen on their side
|
|
37
|
+
*/
|
|
38
|
+
const getTabSlide =
|
|
39
|
+
(width: number): BottomTabNavigationOptions['sceneStyleInterpolator'] =>
|
|
40
|
+
({ current }) => ({
|
|
41
|
+
sceneStyle: {
|
|
42
|
+
transform: [
|
|
168
43
|
{
|
|
169
|
-
|
|
170
|
-
|
|
44
|
+
translateX: current.progress.interpolate({
|
|
45
|
+
inputRange: [-1, 0, 1],
|
|
46
|
+
outputRange: [-width, 0, width],
|
|
47
|
+
}),
|
|
171
48
|
},
|
|
172
|
-
]
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<NavigationContainer>
|
|
176
|
-
<Stack.Navigator
|
|
177
|
-
screenOptions={headerStyleOptions}
|
|
178
|
-
>
|
|
179
|
-
<Stack.Screen
|
|
180
|
-
name="Stack"
|
|
181
|
-
component={StackScreen}
|
|
182
|
-
initialParams={{
|
|
183
|
-
screen,
|
|
184
|
-
options: stackOptions,
|
|
185
|
-
initialParams,
|
|
186
|
-
bottomTab: 'default',
|
|
187
|
-
}}
|
|
188
|
-
options={{ ...getStackOptions(), ...options }}
|
|
189
|
-
/>
|
|
190
|
-
</Stack.Navigator>
|
|
191
|
-
</NavigationContainer>
|
|
192
|
-
</NavigationIndependentTree>
|
|
193
|
-
</Animated.View>
|
|
194
|
-
);
|
|
195
|
-
};
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
196
52
|
|
|
53
|
+
/**
|
|
54
|
+
* App tab navigator: the floating bar on phones, the rail or floating bar on
|
|
55
|
+
* iPhone Duo, tabs sliding in index order. Shares the bar inset with its tabs.
|
|
56
|
+
*/
|
|
197
57
|
const BottomTab: React.FC<BottomTabProps> = ({
|
|
198
58
|
nested,
|
|
199
59
|
tabs,
|
|
@@ -205,17 +65,15 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
205
65
|
}) => {
|
|
206
66
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
207
67
|
const { isIPhoneDuo } = useScreenLayout();
|
|
208
|
-
const
|
|
209
|
-
const
|
|
68
|
+
const { width: windowWidth } = useWindowDimensions();
|
|
69
|
+
const barInset = useDuoTabBarInset();
|
|
70
|
+
// the center action's button rises above the bar, tab content leaves room for it too
|
|
71
|
+
const tabBarInset =
|
|
72
|
+
barInset && hasCenterAction(floatingButton, tabs.length)
|
|
73
|
+
? barInset + TabBarMetrics.ctaLift
|
|
74
|
+
: barInset;
|
|
210
75
|
const initialIndex = tabs.findIndex(i => i.name === initialRouteName);
|
|
211
|
-
const indicatorAnimated = useSharedValue(0);
|
|
212
76
|
const activeIndex = React.useRef(initialIndex > 0 ? initialIndex : 0);
|
|
213
|
-
const itemWidth = dimensions.width / tabs.length;
|
|
214
|
-
|
|
215
|
-
const indicatorStyle = useAnimatedStyle(() => ({
|
|
216
|
-
width: itemWidth,
|
|
217
|
-
transform: [{ translateX: indicatorAnimated.value }],
|
|
218
|
-
}));
|
|
219
77
|
|
|
220
78
|
useLayoutEffect(() => {
|
|
221
79
|
navigation?.setOptions({
|
|
@@ -225,12 +83,6 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
225
83
|
});
|
|
226
84
|
}, [navigation]);
|
|
227
85
|
|
|
228
|
-
useEffect(() => {
|
|
229
|
-
indicatorAnimated.value = withTiming(itemWidth * activeIndex.current, {
|
|
230
|
-
duration: 200,
|
|
231
|
-
});
|
|
232
|
-
}, [itemWidth, indicatorAnimated]);
|
|
233
|
-
|
|
234
86
|
// Focus, not mount: pushing a screen from a tab leaves BottomTab mounted but blurred, and
|
|
235
87
|
// onFocus only fires on tab changes.
|
|
236
88
|
useFocusEffect(
|
|
@@ -245,16 +97,15 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
245
97
|
|
|
246
98
|
const onFocus = (e: any) => {
|
|
247
99
|
activeIndex.current = tabs.findIndex(i => e.target.includes(i.name));
|
|
248
|
-
indicatorAnimated.value = withTiming(itemWidth * activeIndex.current, {
|
|
249
|
-
duration: 200,
|
|
250
|
-
});
|
|
251
100
|
|
|
252
101
|
const isFirstTab = activeIndex.current === 0;
|
|
253
102
|
const gestureEnabled = Platform.OS === 'ios' && isFirstTab;
|
|
254
103
|
navigation?.setOptions({
|
|
255
104
|
gestureEnabled: gestureEnabled,
|
|
256
105
|
});
|
|
257
|
-
NativeModules?.MiniAppModule?.setShouldUseFloatingArrowBack?.(
|
|
106
|
+
NativeModules?.MiniAppModule?.setShouldUseFloatingArrowBack?.(
|
|
107
|
+
!gestureEnabled,
|
|
108
|
+
);
|
|
258
109
|
};
|
|
259
110
|
|
|
260
111
|
const handler: {
|
|
@@ -277,102 +128,43 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
277
128
|
},
|
|
278
129
|
};
|
|
279
130
|
|
|
280
|
-
const isIOS = Platform.OS === 'ios';
|
|
281
|
-
const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
|
|
282
|
-
|
|
283
131
|
return (
|
|
284
132
|
<DuoBarCompressionProvider
|
|
285
133
|
behavior={compressionBehavior}
|
|
286
134
|
tabCount={tabs.length}
|
|
287
135
|
hasQr={!!floatingButton}
|
|
288
136
|
>
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}}
|
|
317
|
-
tabBar={props =>
|
|
318
|
-
isIPhoneDuo ? (
|
|
319
|
-
<DuoTabBar
|
|
320
|
-
{...props}
|
|
321
|
-
floatingButton={floatingButton}
|
|
322
|
-
inactiveTintColor={theme.colors.text.hint}
|
|
323
|
-
activeTintColor={theme.colors.primary}
|
|
324
|
-
/>
|
|
325
|
-
) : (
|
|
326
|
-
<BottomTabBar
|
|
327
|
-
{...props}
|
|
328
|
-
floatingButton={floatingButton}
|
|
329
|
-
inactiveTintColor={theme.colors.text.hint}
|
|
330
|
-
activeTintColor={theme.colors.primary}
|
|
331
|
-
style={[{ height: 64 + bottomInset }]}
|
|
332
|
-
/>
|
|
333
|
-
)
|
|
334
|
-
}
|
|
335
|
-
initialRouteName={initialRouteName}
|
|
336
|
-
>
|
|
337
|
-
{tabs.map((item, index) => {
|
|
338
|
-
const isNum = !isNaN(parseInt(`${item?.badgeLabel}`, 10));
|
|
339
|
-
const isDot =
|
|
340
|
-
typeof item?.badgeLabel === 'string' && !item?.badgeLabel;
|
|
341
|
-
const isNumZero = isNum && Number(item.badgeLabel) === 0;
|
|
342
|
-
|
|
343
|
-
let badgeLabel = item?.badgeLabel;
|
|
344
|
-
if (isNumZero) {
|
|
345
|
-
badgeLabel = undefined;
|
|
137
|
+
<BottomTabInsetContext.Provider value={{ inset: tabBarInset }}>
|
|
138
|
+
<Tab.Navigator
|
|
139
|
+
screenOptions={{
|
|
140
|
+
headerShown: false,
|
|
141
|
+
headerTransparent: true,
|
|
142
|
+
headerBackground: undefined,
|
|
143
|
+
tabBarLabelPosition: 'below-icon',
|
|
144
|
+
animation: 'shift',
|
|
145
|
+
transitionSpec: TAB_SWITCH_SPEC,
|
|
146
|
+
sceneStyleInterpolator: getTabSlide(windowWidth),
|
|
147
|
+
}}
|
|
148
|
+
tabBar={props =>
|
|
149
|
+
isIPhoneDuo ? (
|
|
150
|
+
<DuoTabBar
|
|
151
|
+
{...props}
|
|
152
|
+
floatingButton={floatingButton}
|
|
153
|
+
inactiveTintColor={theme.colors.text.secondary}
|
|
154
|
+
activeTintColor={theme.colors.primary}
|
|
155
|
+
/>
|
|
156
|
+
) : (
|
|
157
|
+
<BottomTabBar
|
|
158
|
+
{...props}
|
|
159
|
+
floatingButton={floatingButton}
|
|
160
|
+
inactiveTintColor={theme.colors.text.secondary}
|
|
161
|
+
activeTintColor={theme.colors.primary}
|
|
162
|
+
/>
|
|
163
|
+
)
|
|
346
164
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
top: number;
|
|
351
|
-
backgroundColor: string;
|
|
352
|
-
width?: number;
|
|
353
|
-
} = {
|
|
354
|
-
marginRight: -8,
|
|
355
|
-
top: -4,
|
|
356
|
-
backgroundColor: Colors.orange_03,
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
if (isNum) {
|
|
360
|
-
badgeStyle = {
|
|
361
|
-
marginRight: -4,
|
|
362
|
-
top: -4,
|
|
363
|
-
backgroundColor: Colors.red_03,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
if (isDot) {
|
|
367
|
-
badgeStyle = {
|
|
368
|
-
marginRight: -2,
|
|
369
|
-
top: -2,
|
|
370
|
-
backgroundColor: Colors.orange_03,
|
|
371
|
-
width: 12,
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return (
|
|
165
|
+
initialRouteName={initialRouteName}
|
|
166
|
+
>
|
|
167
|
+
{tabs.map((item, index) => (
|
|
376
168
|
<Tab.Screen
|
|
377
169
|
key={`BottomTab-${item.name}`}
|
|
378
170
|
name={item.name}
|
|
@@ -380,78 +172,19 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
380
172
|
initialParams={{ ...item, nested, index }}
|
|
381
173
|
listeners={handler}
|
|
382
174
|
options={{
|
|
383
|
-
tabBarLabel: ({ focused, color }) => {
|
|
384
|
-
return (
|
|
385
|
-
<Text
|
|
386
|
-
numberOfLines={1}
|
|
387
|
-
typography={
|
|
388
|
-
focused ? 'label_xs_medium' : 'description_xs_regular'
|
|
389
|
-
}
|
|
390
|
-
color={color}
|
|
391
|
-
>
|
|
392
|
-
{item.label}
|
|
393
|
-
</Text>
|
|
394
|
-
);
|
|
395
|
-
},
|
|
396
175
|
title: item.label,
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
},
|
|
401
|
-
styles.tabStyle,
|
|
402
|
-
],
|
|
403
|
-
tabBarBadge: badgeLabel,
|
|
404
|
-
tabBarBadgeStyle: [
|
|
405
|
-
{
|
|
406
|
-
borderColor: 'white',
|
|
407
|
-
borderWidth: 2,
|
|
408
|
-
fontSize: 10,
|
|
409
|
-
lineHeight: 14,
|
|
410
|
-
fontWeight: 'bold',
|
|
411
|
-
alignSelf: 'center',
|
|
412
|
-
fontFamily: exportFontFamily('bold'),
|
|
413
|
-
},
|
|
414
|
-
badgeStyle,
|
|
415
|
-
],
|
|
416
|
-
tabBarIcon: ({ color }) => (
|
|
417
|
-
<Icon source={item.icon} color={color} size={28} />
|
|
176
|
+
tabBarBadge: getTabBadge(item.badgeLabel, item.showDot),
|
|
177
|
+
tabBarIcon: ({ color, size }) => (
|
|
178
|
+
<Icon source={item.icon} color={color} size={size} />
|
|
418
179
|
),
|
|
419
180
|
...item,
|
|
420
181
|
}}
|
|
421
182
|
/>
|
|
422
|
-
)
|
|
423
|
-
|
|
424
|
-
</
|
|
183
|
+
))}
|
|
184
|
+
</Tab.Navigator>
|
|
185
|
+
</BottomTabInsetContext.Provider>
|
|
425
186
|
</DuoBarCompressionProvider>
|
|
426
187
|
);
|
|
427
188
|
};
|
|
428
189
|
|
|
429
|
-
const styles = StyleSheet.create({
|
|
430
|
-
container: { flex: 1 },
|
|
431
|
-
indicatorContainer: { position: 'absolute', top: 0 },
|
|
432
|
-
indicator: {
|
|
433
|
-
height: 2,
|
|
434
|
-
width: 48,
|
|
435
|
-
alignSelf: 'center',
|
|
436
|
-
borderBottomLeftRadius: 2,
|
|
437
|
-
borderBottomRightRadius: 2,
|
|
438
|
-
},
|
|
439
|
-
tabBarStyle: {
|
|
440
|
-
...Platform.select({
|
|
441
|
-
ios: {
|
|
442
|
-
shadowColor: Colors.black_10,
|
|
443
|
-
shadowOffset: { width: 0, height: 4 },
|
|
444
|
-
shadowOpacity: 1,
|
|
445
|
-
shadowRadius: 4,
|
|
446
|
-
},
|
|
447
|
-
android: {
|
|
448
|
-
elevation: 4,
|
|
449
|
-
},
|
|
450
|
-
}),
|
|
451
|
-
},
|
|
452
|
-
tabStyle: {
|
|
453
|
-
paddingVertical: 4,
|
|
454
|
-
},
|
|
455
|
-
});
|
|
456
|
-
|
|
457
190
|
export default BottomTab;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { BoxShadowValue } from 'react-native';
|
|
2
|
+
import { Easing } from 'react-native-reanimated';
|
|
3
|
+
import { Colors, Radius, Spacing } from '../../Consts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Navigation bar measurements (Figma "Specs Navigation", DS-984), kept in sync with
|
|
7
|
+
* momo-native-kits `TabBarMetrics`
|
|
8
|
+
* - horizontal: floating container with items split evenly, center action in a notch
|
|
9
|
+
* - vertical: iPhone Duo rail, center action under the container
|
|
10
|
+
*/
|
|
11
|
+
const TabBarMetrics = {
|
|
12
|
+
height: 68,
|
|
13
|
+
margin: Spacing.S,
|
|
14
|
+
paddingVertical: Spacing.S,
|
|
15
|
+
paddingHorizontal: Spacing.M,
|
|
16
|
+
itemGap: Spacing.XS,
|
|
17
|
+
iconSize: 24,
|
|
18
|
+
// active indicator behind the icon: 24 icon + XS·M padding
|
|
19
|
+
indicatorPaddingVertical: Spacing.XS,
|
|
20
|
+
indicatorPaddingHorizontal: Spacing.M,
|
|
21
|
+
indicatorRadius: Radius.XL,
|
|
22
|
+
indicatorHeight: 32,
|
|
23
|
+
// home indicator / gesture bar area under the container
|
|
24
|
+
systemIndicatorHeight: 24,
|
|
25
|
+
// unfolded foldables (short side from 600, the smallest in the spec) use 65% of the short
|
|
26
|
+
// side, centered; phones and folded outer displays fill the window
|
|
27
|
+
foldableMinShortSide: 600,
|
|
28
|
+
foldableWidthRatio: 0.65,
|
|
29
|
+
ctaSlotWidth: 92,
|
|
30
|
+
ctaSize: 48,
|
|
31
|
+
// the icon sits 10 inside the 48 button (prototype .cta: 28 icon), 12 inside the 52 rail one
|
|
32
|
+
ctaIconSize: 28,
|
|
33
|
+
// the button rises 22 above the container top edge
|
|
34
|
+
ctaLift: 22,
|
|
35
|
+
ctaLabelGap: Spacing.M,
|
|
36
|
+
ctaLabelHeight: 18,
|
|
37
|
+
railContainerWidth: 56,
|
|
38
|
+
railPaddingHorizontal: Spacing.XS,
|
|
39
|
+
railPaddingBottom: Spacing.S,
|
|
40
|
+
railItemWidth: 48,
|
|
41
|
+
railItemHeight: 62,
|
|
42
|
+
railIndicatorHeight: 40,
|
|
43
|
+
railIndicatorPaddingVertical: Spacing.S,
|
|
44
|
+
railIndicatorRadius: 20,
|
|
45
|
+
railCtaGap: Spacing.L,
|
|
46
|
+
railCtaSize: 52,
|
|
47
|
+
railCtaLabelGap: Spacing.XS,
|
|
48
|
+
railCtaWidth: 68,
|
|
49
|
+
railCtaHeight: 70,
|
|
50
|
+
badgeMaxWidth: 58,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* motion of the navigation bar (MoMo Navigation Prototype):
|
|
55
|
+
* - one active indicator slides to the selected item, overshooting a little
|
|
56
|
+
* - icons cross-fade and the label color follows
|
|
57
|
+
* - the center action shrinks while pressed
|
|
58
|
+
*/
|
|
59
|
+
const TabBarMotion = {
|
|
60
|
+
indicator: { duration: 550, easing: Easing.bezier(0.34, 1.4, 0.5, 1) },
|
|
61
|
+
tint: { duration: 250 },
|
|
62
|
+
ctaPress: { duration: 150 },
|
|
63
|
+
ctaPressedScale: 0.92,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Figma Effect/Popover on the center action button: 8% y8 blur 24 spread -12, 4% y12 blur 32
|
|
68
|
+
* spread -16
|
|
69
|
+
*/
|
|
70
|
+
const CTA_SHADOW: BoxShadowValue[] = [
|
|
71
|
+
{
|
|
72
|
+
offsetX: 0,
|
|
73
|
+
offsetY: 8,
|
|
74
|
+
blurRadius: 24,
|
|
75
|
+
spreadDistance: -12,
|
|
76
|
+
color: `${Colors.black_20}14`,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
offsetX: 0,
|
|
80
|
+
offsetY: 12,
|
|
81
|
+
blurRadius: 32,
|
|
82
|
+
spreadDistance: -16,
|
|
83
|
+
color: `${Colors.black_20}0A`,
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* inner light and tint of the active indicator: white 12% from below, primary 12% from above
|
|
89
|
+
*/
|
|
90
|
+
const getIndicatorShadow = (primary: string): BoxShadowValue[] => [
|
|
91
|
+
{
|
|
92
|
+
offsetX: 0,
|
|
93
|
+
offsetY: -2,
|
|
94
|
+
blurRadius: 2,
|
|
95
|
+
color: `${Colors.black_01}1F`,
|
|
96
|
+
inset: true,
|
|
97
|
+
},
|
|
98
|
+
{ offsetX: 0, offsetY: 1, blurRadius: 2, color: `${primary}1F`, inset: true },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* notch dipped into the container top edge under the center action (Figma "Backgroud" path):
|
|
103
|
+
* starts NOTCH_HALF_WIDTH left of the center, then cubic segments as [control1, control2, end],
|
|
104
|
+
* each point [dx from the center, dy below the top edge]
|
|
105
|
+
*/
|
|
106
|
+
const NOTCH_HALF_WIDTH = 50.758;
|
|
107
|
+
const NOTCH_CURVES: [number, number][][] = [
|
|
108
|
+
[
|
|
109
|
+
[-39.749, 0],
|
|
110
|
+
[-31.231, 9.765],
|
|
111
|
+
[-24.563, 18.526],
|
|
112
|
+
],
|
|
113
|
+
[
|
|
114
|
+
[-18.928, 25.93],
|
|
115
|
+
[-10.022, 30.709],
|
|
116
|
+
[0, 30.709],
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
[10.022, 30.709],
|
|
120
|
+
[18.928, 25.93],
|
|
121
|
+
[24.563, 18.526],
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
[31.231, 9.765],
|
|
125
|
+
[39.749, 0],
|
|
126
|
+
[NOTCH_HALF_WIDTH, 0],
|
|
127
|
+
],
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* width of the horizontal bar: 65% of the short side on an unfolded foldable, otherwise the
|
|
132
|
+
* window minus the side safe areas and margins
|
|
133
|
+
*/
|
|
134
|
+
const getHorizontalBarWidth = (
|
|
135
|
+
width: number,
|
|
136
|
+
height: number,
|
|
137
|
+
insetLeft: number,
|
|
138
|
+
insetRight: number,
|
|
139
|
+
) => {
|
|
140
|
+
const shortSide = Math.min(width, height);
|
|
141
|
+
if (shortSide >= TabBarMetrics.foldableMinShortSide) {
|
|
142
|
+
return shortSide * TabBarMetrics.foldableWidthRatio;
|
|
143
|
+
}
|
|
144
|
+
return width - insetLeft - insetRight - 2 * TabBarMetrics.margin;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* height of the vertical rail container, plus the center action under it
|
|
149
|
+
*/
|
|
150
|
+
const getRailHeight = (tabCount: number, hasCta: boolean) =>
|
|
151
|
+
tabCount * TabBarMetrics.railItemHeight +
|
|
152
|
+
TabBarMetrics.railPaddingBottom +
|
|
153
|
+
(hasCta ? TabBarMetrics.railCtaGap + TabBarMetrics.railCtaHeight : 0);
|
|
154
|
+
|
|
155
|
+
export {
|
|
156
|
+
TabBarMetrics,
|
|
157
|
+
TabBarMotion,
|
|
158
|
+
getHorizontalBarWidth,
|
|
159
|
+
getRailHeight,
|
|
160
|
+
getIndicatorShadow,
|
|
161
|
+
CTA_SHADOW,
|
|
162
|
+
NOTCH_CURVES,
|
|
163
|
+
NOTCH_HALF_WIDTH,
|
|
164
|
+
};
|