@momo-kits/foundation 0.169.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 +17 -37
- package/Application/BottomSheet.parts.tsx +81 -165
- package/Application/BottomSheet.styles.ts +7 -23
- package/Application/BottomSheet.tsx +15 -67
- package/Application/BottomTab/ActiveIndicator.tsx +90 -0
- package/Application/BottomTab/BottomTabBar.styles.ts +48 -43
- package/Application/BottomTab/BottomTabBar.tsx +229 -223
- 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 +174 -96
- package/Application/BottomTab/DuoTabBar.styles.ts +42 -84
- package/Application/BottomTab/DuoTabBar.tsx +35 -115
- package/Application/BottomTab/TabBarIcon.tsx +160 -72
- package/Application/BottomTab/TabScreen.tsx +48 -61
- package/Application/BottomTab/index.tsx +85 -125
- package/Application/BottomTab/metrics.ts +164 -0
- package/Application/BottomTab/tabItems.ts +46 -0
- package/Application/BottomTab/usePressLift.ts +32 -0
- package/Application/ModalIconButton.tsx +66 -0
- package/Application/ModalScreen.tsx +32 -15
- package/Application/__tests__/modalLayout.test.ts +128 -0
- package/Application/index.ts +10 -0
- package/Application/modalLayout.ts +177 -0
- package/Application/types.tabs.ts +5 -0
- package/Application/utils.ts +0 -36
- package/Layout/ReservedRegions.tsx +21 -4
- package/Layout/Screen.tsx +11 -2
- package/Layout/index.ts +2 -0
- package/Popup/PopupNotify.tsx +38 -68
- package/Popup/PopupPromotion.tsx +5 -4
- package/Popup/utils.ts +1 -26
- package/package.json +2 -1
- package/Application/BottomTab/DuoTabBar.parts.tsx +0 -202
- package/Application/BottomTab/badge.ts +0 -41
- package/Application/BottomTab/hooks.ts +0 -358
- package/Application/BottomTab/index.styles.ts +0 -30
|
@@ -4,39 +4,40 @@ import {
|
|
|
4
4
|
useFocusEffect,
|
|
5
5
|
} from '@react-navigation/native';
|
|
6
6
|
import { createStackNavigator } from '@react-navigation/stack';
|
|
7
|
-
import React, { useContext, useEffect } from 'react';
|
|
8
|
-
import
|
|
9
|
-
useAnimatedStyle,
|
|
10
|
-
useSharedValue,
|
|
11
|
-
withTiming,
|
|
12
|
-
} from 'react-native-reanimated';
|
|
7
|
+
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
|
8
|
+
import { View } from 'react-native';
|
|
13
9
|
import StackScreen from '../StackScreen';
|
|
14
10
|
import { getOptions, getStackOptions } from '../navigationOptions';
|
|
15
11
|
import { useAppState } from '../hooks';
|
|
16
12
|
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
13
|
+
import { Styles } from '../../Consts';
|
|
17
14
|
import { useScreenLayout } from '../../Layout';
|
|
18
15
|
import { getHeaderRowOptions } from '../../Layout/utils';
|
|
19
|
-
import {
|
|
16
|
+
import { BottomTabInsetContext } from './BottomTabInset';
|
|
20
17
|
|
|
21
18
|
const Stack = createStackNavigator();
|
|
22
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Content of one tab: its own stack, padded clear of the floating tab bar
|
|
22
|
+
* until a Screen inside claims that inset, plus screen_navigated tracking.
|
|
23
|
+
*/
|
|
23
24
|
const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
24
25
|
const { navigator } = useContext(ApplicationContext);
|
|
25
26
|
const context = useContext<any>(MiniAppContext);
|
|
26
27
|
const { nested, options, screen, initialParams } = route?.params;
|
|
27
28
|
const headerStyleOptions = getHeaderRowOptions(useScreenLayout());
|
|
28
29
|
|
|
29
|
-
const opacityValue = 0.3;
|
|
30
|
-
const scaleValue = 0.97;
|
|
31
|
-
const opacity = useSharedValue(opacityValue);
|
|
32
|
-
const scale = useSharedValue(scaleValue);
|
|
33
30
|
const screenName = screen?.name || screen?.type?.name || 'Invalid';
|
|
34
31
|
const { isBackgroundToForeground } = useAppState();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
// the floating tab bar covers the bottom of the tab: pad the content unless a Screen in it
|
|
33
|
+
// keeps clear of the bar itself (scroll content running under the bar)
|
|
34
|
+
const { inset: tabBarInset } = useContext(BottomTabInsetContext);
|
|
35
|
+
const [insetClaimed, setInsetClaimed] = useState(false);
|
|
36
|
+
const tabInsetValue = useMemo(
|
|
37
|
+
() => ({ inset: tabBarInset, claim: () => setInsetClaimed(true) }),
|
|
38
|
+
[tabBarInset],
|
|
39
|
+
);
|
|
40
|
+
const tabInsetStyle = { paddingBottom: insetClaimed ? 0 : tabBarInset };
|
|
40
41
|
|
|
41
42
|
const onScreenNavigated = (pre: string, current: string) => {
|
|
42
43
|
if (!isBackgroundToForeground) {
|
|
@@ -67,9 +68,6 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
67
68
|
useFocusEffect(
|
|
68
69
|
React.useCallback(
|
|
69
70
|
() => {
|
|
70
|
-
opacity.value = withTiming(1, { duration: 200 });
|
|
71
|
-
scale.value = withTiming(1, { duration: 200 });
|
|
72
|
-
|
|
73
71
|
setTimeout(() => {
|
|
74
72
|
navigator?.maxApi?.getDataObserver?.(
|
|
75
73
|
'current_screen',
|
|
@@ -81,13 +79,6 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
81
79
|
},
|
|
82
80
|
);
|
|
83
81
|
}, 100);
|
|
84
|
-
|
|
85
|
-
return () => {
|
|
86
|
-
if (navigation.getState().index !== route?.params.index) {
|
|
87
|
-
opacity.value = withTiming(opacityValue, { duration: 200 });
|
|
88
|
-
scale.value = withTiming(scaleValue, { duration: 200 });
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
82
|
},
|
|
92
83
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
93
84
|
[],
|
|
@@ -125,36 +116,8 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
125
116
|
|
|
126
117
|
if (nested) {
|
|
127
118
|
return (
|
|
128
|
-
<
|
|
129
|
-
<
|
|
130
|
-
<Stack.Screen
|
|
131
|
-
name="Stack"
|
|
132
|
-
component={StackScreen}
|
|
133
|
-
initialParams={{
|
|
134
|
-
screen,
|
|
135
|
-
options: stackOptions,
|
|
136
|
-
initialParams,
|
|
137
|
-
bottomTab: 'nested',
|
|
138
|
-
}}
|
|
139
|
-
options={{ ...getStackOptions() }}
|
|
140
|
-
/>
|
|
141
|
-
</Stack.Navigator>
|
|
142
|
-
</Animated.View>
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return (
|
|
147
|
-
<Animated.View
|
|
148
|
-
style={[
|
|
149
|
-
styles.container,
|
|
150
|
-
{
|
|
151
|
-
opacity,
|
|
152
|
-
transform: [{ scale }],
|
|
153
|
-
},
|
|
154
|
-
]}
|
|
155
|
-
>
|
|
156
|
-
<NavigationIndependentTree>
|
|
157
|
-
<NavigationContainer>
|
|
119
|
+
<BottomTabInsetContext.Provider value={tabInsetValue}>
|
|
120
|
+
<View style={[Styles.flex, tabInsetStyle]}>
|
|
158
121
|
<Stack.Navigator screenOptions={headerStyleOptions}>
|
|
159
122
|
<Stack.Screen
|
|
160
123
|
name="Stack"
|
|
@@ -163,14 +126,38 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
163
126
|
screen,
|
|
164
127
|
options: stackOptions,
|
|
165
128
|
initialParams,
|
|
166
|
-
bottomTab: '
|
|
129
|
+
bottomTab: 'nested',
|
|
167
130
|
}}
|
|
168
|
-
options={{ ...getStackOptions()
|
|
131
|
+
options={{ ...getStackOptions() }}
|
|
169
132
|
/>
|
|
170
133
|
</Stack.Navigator>
|
|
171
|
-
</
|
|
172
|
-
</
|
|
173
|
-
|
|
134
|
+
</View>
|
|
135
|
+
</BottomTabInsetContext.Provider>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<BottomTabInsetContext.Provider value={tabInsetValue}>
|
|
141
|
+
<View style={[Styles.flex, tabInsetStyle]}>
|
|
142
|
+
<NavigationIndependentTree>
|
|
143
|
+
<NavigationContainer>
|
|
144
|
+
<Stack.Navigator screenOptions={headerStyleOptions}>
|
|
145
|
+
<Stack.Screen
|
|
146
|
+
name="Stack"
|
|
147
|
+
component={StackScreen}
|
|
148
|
+
initialParams={{
|
|
149
|
+
screen,
|
|
150
|
+
options: stackOptions,
|
|
151
|
+
initialParams,
|
|
152
|
+
bottomTab: 'default',
|
|
153
|
+
}}
|
|
154
|
+
options={{ ...getStackOptions(), ...options }}
|
|
155
|
+
/>
|
|
156
|
+
</Stack.Navigator>
|
|
157
|
+
</NavigationContainer>
|
|
158
|
+
</NavigationIndependentTree>
|
|
159
|
+
</View>
|
|
160
|
+
</BottomTabInsetContext.Provider>
|
|
174
161
|
);
|
|
175
162
|
};
|
|
176
163
|
|
|
@@ -1,33 +1,59 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BottomTabNavigationOptions,
|
|
3
|
+
createBottomTabNavigator,
|
|
4
|
+
} from '@react-navigation/bottom-tabs';
|
|
2
5
|
import { useFocusEffect } from '@react-navigation/native';
|
|
3
|
-
import React, { useContext,
|
|
4
|
-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
6
|
+
import React, { useContext, useLayoutEffect } from 'react';
|
|
5
7
|
import {
|
|
8
|
+
Easing,
|
|
6
9
|
NativeModules,
|
|
7
10
|
Platform,
|
|
8
11
|
StatusBar,
|
|
9
12
|
useWindowDimensions,
|
|
10
|
-
View,
|
|
11
13
|
} from 'react-native';
|
|
12
|
-
import Animated, {
|
|
13
|
-
useAnimatedStyle,
|
|
14
|
-
useSharedValue,
|
|
15
|
-
withTiming,
|
|
16
|
-
} from 'react-native-reanimated';
|
|
17
14
|
import { Icon } from '../../Icon';
|
|
18
|
-
import { exportFontFamily, Text } from '../../Text';
|
|
19
15
|
import { BottomTabProps } from '../types';
|
|
20
|
-
import BottomTabBar from './BottomTabBar';
|
|
21
|
-
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';
|
|
22
21
|
import { DuoBarCompressionProvider } from './DuoBarCompression';
|
|
22
|
+
import TabScreen from './TabScreen';
|
|
23
23
|
import { ApplicationContext } from '../../Context';
|
|
24
24
|
import { useScreenLayout } from '../../Layout';
|
|
25
|
-
import TabScreen from './TabScreen';
|
|
26
|
-
import { getBadgeProps } from './badge';
|
|
27
|
-
import { styles } from './index.styles';
|
|
28
25
|
|
|
29
26
|
const Tab = createBottomTabNavigator();
|
|
30
27
|
|
|
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
|
+
};
|
|
33
|
+
|
|
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: [
|
|
43
|
+
{
|
|
44
|
+
translateX: current.progress.interpolate({
|
|
45
|
+
inputRange: [-1, 0, 1],
|
|
46
|
+
outputRange: [-width, 0, width],
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
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
|
+
*/
|
|
31
57
|
const BottomTab: React.FC<BottomTabProps> = ({
|
|
32
58
|
nested,
|
|
33
59
|
tabs,
|
|
@@ -39,17 +65,15 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
39
65
|
}) => {
|
|
40
66
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
41
67
|
const { isIPhoneDuo } = useScreenLayout();
|
|
42
|
-
const
|
|
43
|
-
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;
|
|
44
75
|
const initialIndex = tabs.findIndex(i => i.name === initialRouteName);
|
|
45
|
-
const indicatorAnimated = useSharedValue(0);
|
|
46
76
|
const activeIndex = React.useRef(initialIndex > 0 ? initialIndex : 0);
|
|
47
|
-
const itemWidth = dimensions.width / tabs.length;
|
|
48
|
-
|
|
49
|
-
const indicatorStyle = useAnimatedStyle(() => ({
|
|
50
|
-
width: itemWidth,
|
|
51
|
-
transform: [{ translateX: indicatorAnimated.value }],
|
|
52
|
-
}));
|
|
53
77
|
|
|
54
78
|
useLayoutEffect(() => {
|
|
55
79
|
navigation?.setOptions({
|
|
@@ -59,12 +83,6 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
59
83
|
});
|
|
60
84
|
}, [navigation]);
|
|
61
85
|
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
indicatorAnimated.value = withTiming(itemWidth * activeIndex.current, {
|
|
64
|
-
duration: 200,
|
|
65
|
-
});
|
|
66
|
-
}, [itemWidth, indicatorAnimated]);
|
|
67
|
-
|
|
68
86
|
// Focus, not mount: pushing a screen from a tab leaves BottomTab mounted but blurred, and
|
|
69
87
|
// onFocus only fires on tab changes.
|
|
70
88
|
useFocusEffect(
|
|
@@ -79,9 +97,6 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
79
97
|
|
|
80
98
|
const onFocus = (e: any) => {
|
|
81
99
|
activeIndex.current = tabs.findIndex(i => e.target.includes(i.name));
|
|
82
|
-
indicatorAnimated.value = withTiming(itemWidth * activeIndex.current, {
|
|
83
|
-
duration: 200,
|
|
84
|
-
});
|
|
85
100
|
|
|
86
101
|
const isFirstTab = activeIndex.current === 0;
|
|
87
102
|
const gestureEnabled = Platform.OS === 'ios' && isFirstTab;
|
|
@@ -113,67 +128,43 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
113
128
|
},
|
|
114
129
|
};
|
|
115
130
|
|
|
116
|
-
const isIOS = Platform.OS === 'ios';
|
|
117
|
-
const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
|
|
118
|
-
|
|
119
131
|
return (
|
|
120
132
|
<DuoBarCompressionProvider
|
|
121
133
|
behavior={compressionBehavior}
|
|
122
134
|
tabCount={tabs.length}
|
|
123
135
|
hasQr={!!floatingButton}
|
|
124
136
|
>
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
{...props}
|
|
157
|
-
floatingButton={floatingButton}
|
|
158
|
-
inactiveTintColor={theme.colors.text.hint}
|
|
159
|
-
activeTintColor={theme.colors.primary}
|
|
160
|
-
/>
|
|
161
|
-
) : (
|
|
162
|
-
<BottomTabBar
|
|
163
|
-
{...props}
|
|
164
|
-
floatingButton={floatingButton}
|
|
165
|
-
inactiveTintColor={theme.colors.text.hint}
|
|
166
|
-
activeTintColor={theme.colors.primary}
|
|
167
|
-
style={[{ height: 64 + bottomInset }]}
|
|
168
|
-
/>
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
|
-
initialRouteName={initialRouteName}
|
|
172
|
-
>
|
|
173
|
-
{tabs.map((item, index) => {
|
|
174
|
-
const { badgeLabel, badgeStyle } = getBadgeProps(item?.badgeLabel);
|
|
175
|
-
|
|
176
|
-
return (
|
|
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
|
+
)
|
|
164
|
+
}
|
|
165
|
+
initialRouteName={initialRouteName}
|
|
166
|
+
>
|
|
167
|
+
{tabs.map((item, index) => (
|
|
177
168
|
<Tab.Screen
|
|
178
169
|
key={`BottomTab-${item.name}`}
|
|
179
170
|
name={item.name}
|
|
@@ -181,48 +172,17 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
181
172
|
initialParams={{ ...item, nested, index }}
|
|
182
173
|
listeners={handler}
|
|
183
174
|
options={{
|
|
184
|
-
tabBarLabel: ({ focused, color }) => {
|
|
185
|
-
return (
|
|
186
|
-
<Text
|
|
187
|
-
numberOfLines={1}
|
|
188
|
-
typography={
|
|
189
|
-
focused ? 'label_xs_medium' : 'description_xs_regular'
|
|
190
|
-
}
|
|
191
|
-
color={color}
|
|
192
|
-
>
|
|
193
|
-
{item.label}
|
|
194
|
-
</Text>
|
|
195
|
-
);
|
|
196
|
-
},
|
|
197
175
|
title: item.label,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
},
|
|
202
|
-
styles.tabStyle,
|
|
203
|
-
],
|
|
204
|
-
tabBarBadge: badgeLabel,
|
|
205
|
-
tabBarBadgeStyle: [
|
|
206
|
-
{
|
|
207
|
-
borderColor: 'white',
|
|
208
|
-
borderWidth: 2,
|
|
209
|
-
fontSize: 10,
|
|
210
|
-
lineHeight: 14,
|
|
211
|
-
fontWeight: 'bold',
|
|
212
|
-
alignSelf: 'center',
|
|
213
|
-
fontFamily: exportFontFamily('bold'),
|
|
214
|
-
},
|
|
215
|
-
badgeStyle,
|
|
216
|
-
],
|
|
217
|
-
tabBarIcon: ({ color }) => (
|
|
218
|
-
<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} />
|
|
219
179
|
),
|
|
220
180
|
...item,
|
|
221
181
|
}}
|
|
222
182
|
/>
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
</
|
|
183
|
+
))}
|
|
184
|
+
</Tab.Navigator>
|
|
185
|
+
</BottomTabInsetContext.Provider>
|
|
226
186
|
</DuoBarCompressionProvider>
|
|
227
187
|
);
|
|
228
188
|
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
|
2
|
+
import { CommonActions, Route } from '@react-navigation/native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* navigation items of the tab bar, with the press handlers of the system tab bar
|
|
6
|
+
*/
|
|
7
|
+
const getTabItems = ({ state, descriptors, navigation }: BottomTabBarProps) => {
|
|
8
|
+
return state.routes.map((route: Route<string>, index: number) => {
|
|
9
|
+
const { options } = descriptors[route.key];
|
|
10
|
+
const focused = index === state.index;
|
|
11
|
+
const label = `${options.title ?? route.name}`;
|
|
12
|
+
|
|
13
|
+
const onPress = () => {
|
|
14
|
+
const event = navigation.emit({
|
|
15
|
+
type: 'tabPress',
|
|
16
|
+
target: route.key,
|
|
17
|
+
canPreventDefault: true,
|
|
18
|
+
});
|
|
19
|
+
if (!focused && !event.defaultPrevented) {
|
|
20
|
+
navigation.dispatch({
|
|
21
|
+
...CommonActions.navigate(route.name),
|
|
22
|
+
target: state.key,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const onLongPress = () => {
|
|
28
|
+
navigation.emit({ type: 'tabLongPress', target: route.key });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
route,
|
|
33
|
+
focused,
|
|
34
|
+
label,
|
|
35
|
+
icon: options.tabBarIcon as any,
|
|
36
|
+
badge: options.tabBarBadge,
|
|
37
|
+
onPress,
|
|
38
|
+
onLongPress,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type TabItem = ReturnType<typeof getTabItems>[number];
|
|
44
|
+
|
|
45
|
+
export { getTabItems };
|
|
46
|
+
export type { TabItem };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useAnimatedStyle,
|
|
3
|
+
useReducedMotion,
|
|
4
|
+
useSharedValue,
|
|
5
|
+
withSpring,
|
|
6
|
+
} from 'react-native-reanimated';
|
|
7
|
+
|
|
8
|
+
const SPRING = { damping: 18, stiffness: 220, mass: 0.8 };
|
|
9
|
+
// the button grows by a tenth while held
|
|
10
|
+
const PRESSED_SCALE = 1.1;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* lift for a single button, e.g. the compressed rail
|
|
14
|
+
*/
|
|
15
|
+
const usePressLift = () => {
|
|
16
|
+
const reduceMotion = useReducedMotion();
|
|
17
|
+
const lift = useSharedValue(0);
|
|
18
|
+
const style = useAnimatedStyle(() => ({
|
|
19
|
+
transform: [{ scale: 1 + lift.value * (PRESSED_SCALE - 1) }],
|
|
20
|
+
}));
|
|
21
|
+
const onPressIn = () => {
|
|
22
|
+
if (!reduceMotion) {
|
|
23
|
+
lift.value = withSpring(1, SPRING);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const onPressOut = () => {
|
|
27
|
+
lift.value = withSpring(0, SPRING);
|
|
28
|
+
};
|
|
29
|
+
return { style, onPressIn, onPressOut };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export { usePressLift };
|