@momo-kits/foundation 0.168.1-beta.24 → 0.168.1-beta.25
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/BottomTab/ActiveIndicator.tsx +90 -0
- package/Application/BottomTab/BottomTabBar.tsx +274 -253
- 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/DuoTabBar.tsx +216 -433
- package/Application/BottomTab/TabBarIcon.tsx +160 -72
- package/Application/BottomTab/index.tsx +106 -222
- package/Application/BottomTab/metrics.ts +165 -0
- package/Application/BottomTab/tabItems.ts +46 -0
- package/Layout/Screen.tsx +12 -2
- package/Popup/PopupNotify.tsx +27 -2
- package/Popup/types.ts +7 -0
- package/Popup/utils.ts +46 -1
- package/package.json +2 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React, { useContext, useEffect, useRef } from 'react';
|
|
2
|
+
import { StyleProp, StyleSheet, ViewStyle } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withTiming,
|
|
7
|
+
} from 'react-native-reanimated';
|
|
8
|
+
import { ApplicationContext } from '../../Context';
|
|
9
|
+
import { getIndicatorShadow, TabBarMotion } from './metrics';
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
axis: 'x' | 'y';
|
|
13
|
+
// position of the selected item along the axis, undefined until it is measured
|
|
14
|
+
offset?: number;
|
|
15
|
+
// horizontal bar: width of the selected item
|
|
16
|
+
extent?: number;
|
|
17
|
+
radius: number;
|
|
18
|
+
style: StyleProp<ViewStyle>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* one active indicator per bar (Background/tonal with its inner shadows) sliding to the
|
|
23
|
+
* selected item; it lands in place on the first measure and slides from then on
|
|
24
|
+
*/
|
|
25
|
+
const ActiveIndicator: React.FC<Props> = ({
|
|
26
|
+
axis,
|
|
27
|
+
offset,
|
|
28
|
+
extent,
|
|
29
|
+
radius,
|
|
30
|
+
style,
|
|
31
|
+
}) => {
|
|
32
|
+
const { theme } = useContext(ApplicationContext);
|
|
33
|
+
const position = useSharedValue(offset ?? 0);
|
|
34
|
+
const size = useSharedValue(extent ?? 0);
|
|
35
|
+
const visible = useSharedValue(offset === undefined ? 0 : 1);
|
|
36
|
+
const placed = useRef(offset !== undefined);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (offset === undefined) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
size.value = extent ?? 0;
|
|
43
|
+
if (!placed.current) {
|
|
44
|
+
placed.current = true;
|
|
45
|
+
position.value = offset;
|
|
46
|
+
visible.value = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
position.value = withTiming(offset, TabBarMotion.indicator);
|
|
50
|
+
}, [offset, extent, position, size, visible]);
|
|
51
|
+
|
|
52
|
+
const animatedStyle = useAnimatedStyle(() =>
|
|
53
|
+
axis === 'x'
|
|
54
|
+
? {
|
|
55
|
+
opacity: visible.value,
|
|
56
|
+
width: size.value,
|
|
57
|
+
transform: [{ translateX: position.value }],
|
|
58
|
+
}
|
|
59
|
+
: {
|
|
60
|
+
opacity: visible.value,
|
|
61
|
+
transform: [{ translateY: position.value }],
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Animated.View
|
|
67
|
+
pointerEvents={'none'}
|
|
68
|
+
style={[
|
|
69
|
+
styles.indicator,
|
|
70
|
+
{
|
|
71
|
+
borderRadius: radius,
|
|
72
|
+
backgroundColor: theme.colors.background.tonal,
|
|
73
|
+
boxShadow: getIndicatorShadow(theme.colors.primary),
|
|
74
|
+
},
|
|
75
|
+
style,
|
|
76
|
+
animatedStyle,
|
|
77
|
+
]}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const styles = StyleSheet.create({
|
|
83
|
+
indicator: {
|
|
84
|
+
position: 'absolute',
|
|
85
|
+
left: 0,
|
|
86
|
+
top: 0,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export { ActiveIndicator };
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
|
2
|
-
import {
|
|
3
|
-
import React, { useCallback, useContext } from 'react';
|
|
2
|
+
import React, { useContext, useState } from 'react';
|
|
4
3
|
import {
|
|
5
4
|
Platform,
|
|
5
|
+
Pressable,
|
|
6
6
|
StyleSheet,
|
|
7
|
-
TouchableOpacity,
|
|
8
7
|
useWindowDimensions,
|
|
9
8
|
View,
|
|
10
9
|
} from 'react-native';
|
|
@@ -13,311 +12,333 @@ import Animated, {
|
|
|
13
12
|
useSharedValue,
|
|
14
13
|
withTiming,
|
|
15
14
|
} from 'react-native-reanimated';
|
|
16
|
-
import {
|
|
17
|
-
import { Colors, Radius,
|
|
15
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
16
|
+
import { Colors, Radius, Spacing } from '../../Consts';
|
|
18
17
|
import { Icon } from '../../Icon';
|
|
19
18
|
import { Text } from '../../Text';
|
|
20
19
|
import { FloatingButtonProps } from '../types';
|
|
21
20
|
import CustomBottomTabItem from './CustomBottomTabItem';
|
|
22
21
|
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
22
|
+
import { CurvedContainer } from './CurvedContainer';
|
|
23
|
+
import { ActiveIndicator } from './ActiveIndicator';
|
|
24
|
+
import {
|
|
25
|
+
CTA_SHADOW,
|
|
26
|
+
getHorizontalBarWidth,
|
|
27
|
+
TabBarMetrics,
|
|
28
|
+
TabBarMotion,
|
|
29
|
+
} from './metrics';
|
|
30
|
+
import { getTabItems, TabItem } from './tabItems';
|
|
31
|
+
import { FixedScaleText } from './TabBarIcon';
|
|
23
32
|
|
|
24
33
|
type Props = BottomTabBarProps & {
|
|
25
|
-
activeTintColor
|
|
26
|
-
inactiveTintColor
|
|
34
|
+
activeTintColor: string;
|
|
35
|
+
inactiveTintColor: string;
|
|
27
36
|
floatingButton?: FloatingButtonProps;
|
|
28
|
-
activeBackgroundColor?: string;
|
|
29
|
-
allowFontScaling?: boolean;
|
|
30
|
-
inactiveBackgroundColor?: string;
|
|
31
|
-
labelStyle?: any;
|
|
32
|
-
iconStyle?: any;
|
|
33
|
-
showLabel?: boolean;
|
|
34
|
-
style?: any;
|
|
35
|
-
tabStyle?: any;
|
|
36
|
-
badgeStyle?: any;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
/**
|
|
40
|
+
* the center action needs a tab on each side: Special type takes 2 or 4 tabs
|
|
41
|
+
*/
|
|
42
|
+
const hasCenterAction = (floatingButton: unknown, tabCount: number) =>
|
|
43
|
+
!!floatingButton && tabCount % 2 === 0;
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
iconStyle,
|
|
54
|
-
showLabel,
|
|
55
|
-
style,
|
|
56
|
-
floatingButton,
|
|
57
|
-
}: Props) {
|
|
58
|
-
const { theme } = useContext(ApplicationContext);
|
|
59
|
-
const { routes } = state;
|
|
60
|
-
const widthDevice = useWindowDimensions()?.width;
|
|
61
|
-
const insets = useSafeAreaInsets();
|
|
62
|
-
const useFloating = floatingButton && state.routes.length % 2 === 0;
|
|
63
|
-
const itemWidth =
|
|
64
|
-
(widthDevice - insets.left - insets.right) /
|
|
65
|
-
(state.routes.length + (useFloating ? 1 : 0));
|
|
66
|
-
// const buildLink = useLinkBuilder();
|
|
67
|
-
const indicatorAnimated = useSharedValue(0);
|
|
45
|
+
/**
|
|
46
|
+
* gap under the container: the home indicator / gesture bar area
|
|
47
|
+
*/
|
|
48
|
+
const getSystemIndicatorHeight = (bottomInset: number) =>
|
|
49
|
+
Math.max(
|
|
50
|
+
Platform.OS === 'ios'
|
|
51
|
+
? Math.min(bottomInset, TabBarMetrics.systemIndicatorHeight)
|
|
52
|
+
: bottomInset,
|
|
53
|
+
Spacing.S,
|
|
54
|
+
);
|
|
68
55
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
56
|
+
/**
|
|
57
|
+
* center action press: the button shrinks while held and its label turns active
|
|
58
|
+
* (label in a Background/primary pill, white text)
|
|
59
|
+
*/
|
|
60
|
+
const useCenterActionPress = () => {
|
|
61
|
+
const [pressed, setPressed] = useState(false);
|
|
62
|
+
const scale = useSharedValue(1);
|
|
63
|
+
const buttonStyle = useAnimatedStyle(() => ({
|
|
64
|
+
transform: [{ scale: scale.value }],
|
|
72
65
|
}));
|
|
73
|
-
const
|
|
74
|
-
|
|
66
|
+
const onPressIn = () => {
|
|
67
|
+
setPressed(true);
|
|
68
|
+
scale.value = withTiming(
|
|
69
|
+
TabBarMotion.ctaPressedScale,
|
|
70
|
+
TabBarMotion.ctaPress,
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
const onPressOut = () => {
|
|
74
|
+
setPressed(false);
|
|
75
|
+
scale.value = withTiming(1, TabBarMotion.ctaPress);
|
|
76
|
+
};
|
|
77
|
+
return { pressed, buttonStyle, onPressIn, onPressOut };
|
|
78
|
+
};
|
|
75
79
|
|
|
80
|
+
/**
|
|
81
|
+
* center action (.Nav/CTA): button rising out of the notch, label under it
|
|
82
|
+
*/
|
|
83
|
+
const CenterAction: React.FC<{
|
|
84
|
+
floatingButton: FloatingButtonProps;
|
|
85
|
+
fixedFontScale?: boolean;
|
|
86
|
+
}> = ({ floatingButton, fixedFontScale }) => {
|
|
87
|
+
const { theme } = useContext(ApplicationContext);
|
|
76
88
|
const context = useContext<any>(MiniAppContext);
|
|
77
89
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const event = navigation.emit({
|
|
90
|
-
type: 'tabPress',
|
|
91
|
-
target: route.key,
|
|
92
|
-
canPreventDefault: true,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
if (!focused && !event.defaultPrevented) {
|
|
96
|
-
navigation.dispatch({
|
|
97
|
-
...CommonActions.navigate(route.name),
|
|
98
|
-
target: state.key,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const onLongPress = () => {
|
|
104
|
-
navigation.emit({
|
|
105
|
-
type: 'tabLongPress',
|
|
106
|
-
target: route.key,
|
|
107
|
-
});
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
return (
|
|
111
|
-
<CustomBottomTabItem
|
|
112
|
-
key={route.key}
|
|
113
|
-
route={route}
|
|
114
|
-
label={options.tabBarLabel || options.title || route.name}
|
|
115
|
-
focused={focused}
|
|
116
|
-
onPress={onPress}
|
|
117
|
-
onLongPress={onLongPress}
|
|
118
|
-
activeTintColor={activeTintColor ?? theme.colors.primary}
|
|
119
|
-
inactiveTintColor={inactiveTintColor ?? theme.colors.text.default}
|
|
120
|
-
activeBackgroundColor={activeBackgroundColor}
|
|
121
|
-
inactiveBackgroundColor={inactiveBackgroundColor}
|
|
122
|
-
icon={options.tabBarIcon}
|
|
123
|
-
badge={options.tabBarBadge}
|
|
124
|
-
showLabel={showLabel}
|
|
125
|
-
labelStyle={labelStyle}
|
|
126
|
-
iconStyle={iconStyle}
|
|
127
|
-
style={options.tabBarStyle}
|
|
128
|
-
badgeStyle={options.tabBarBadgeStyle}
|
|
129
|
-
allowFontScaling={allowFontScaling}
|
|
130
|
-
/>
|
|
131
|
-
);
|
|
132
|
-
});
|
|
133
|
-
},
|
|
134
|
-
[
|
|
135
|
-
focusedTab,
|
|
136
|
-
descriptors,
|
|
137
|
-
activeTintColor,
|
|
138
|
-
theme.colors.primary,
|
|
139
|
-
theme.colors.text.default,
|
|
140
|
-
inactiveTintColor,
|
|
141
|
-
activeBackgroundColor,
|
|
142
|
-
inactiveBackgroundColor,
|
|
143
|
-
showLabel,
|
|
144
|
-
labelStyle,
|
|
145
|
-
iconStyle,
|
|
146
|
-
allowFontScaling,
|
|
147
|
-
navigation,
|
|
148
|
-
state.key,
|
|
149
|
-
],
|
|
90
|
+
const { icon, label, onPress } = floatingButton;
|
|
91
|
+
const { pressed, buttonStyle, onPressIn, onPressOut } =
|
|
92
|
+
useCenterActionPress();
|
|
93
|
+
const labelText = (
|
|
94
|
+
<Text
|
|
95
|
+
numberOfLines={1}
|
|
96
|
+
typography={'label_s_medium'}
|
|
97
|
+
color={pressed ? Colors.black_01 : theme.colors.text.secondary}
|
|
98
|
+
>
|
|
99
|
+
{label}
|
|
100
|
+
</Text>
|
|
150
101
|
);
|
|
151
102
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
103
|
+
return (
|
|
104
|
+
<View style={[styles.ctaSlot, showBaseLineDebug && styles.debugBaseLine]}>
|
|
105
|
+
<Pressable
|
|
106
|
+
accessibilityRole={'button'}
|
|
107
|
+
accessibilityLabel={label}
|
|
108
|
+
onPress={onPress}
|
|
109
|
+
onPressIn={onPressIn}
|
|
110
|
+
onPressOut={onPressOut}
|
|
111
|
+
style={styles.cta}
|
|
112
|
+
>
|
|
113
|
+
<Animated.View
|
|
114
|
+
style={[
|
|
115
|
+
styles.ctaButton,
|
|
116
|
+
{ backgroundColor: theme.colors.primary },
|
|
117
|
+
buttonStyle,
|
|
118
|
+
]}
|
|
168
119
|
>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
);
|
|
196
|
-
}, [
|
|
197
|
-
activeTintColor,
|
|
198
|
-
floatingButton?.icon,
|
|
199
|
-
floatingButton?.label,
|
|
200
|
-
floatingButton?.onPress,
|
|
201
|
-
showBaseLineDebug,
|
|
202
|
-
theme.colors.primary,
|
|
203
|
-
]);
|
|
120
|
+
{typeof icon === 'string' ? (
|
|
121
|
+
<Icon
|
|
122
|
+
source={icon}
|
|
123
|
+
size={TabBarMetrics.ctaIconSize}
|
|
124
|
+
color={Colors.black_01}
|
|
125
|
+
/>
|
|
126
|
+
) : (
|
|
127
|
+
icon
|
|
128
|
+
)}
|
|
129
|
+
</Animated.View>
|
|
130
|
+
<View
|
|
131
|
+
style={[
|
|
132
|
+
styles.ctaLabel,
|
|
133
|
+
pressed && { backgroundColor: theme.colors.primary },
|
|
134
|
+
]}
|
|
135
|
+
>
|
|
136
|
+
{fixedFontScale ? (
|
|
137
|
+
<FixedScaleText>{labelText}</FixedScaleText>
|
|
138
|
+
) : (
|
|
139
|
+
labelText
|
|
140
|
+
)}
|
|
141
|
+
</View>
|
|
142
|
+
</Pressable>
|
|
143
|
+
</View>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
204
146
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
147
|
+
/**
|
|
148
|
+
* horizontal navigation bar: floating container, items split evenly, Special type puts
|
|
149
|
+
* the center action in a notch in the middle
|
|
150
|
+
*/
|
|
151
|
+
const HorizontalTabBar: React.FC<{
|
|
152
|
+
items: TabItem[];
|
|
153
|
+
width: number;
|
|
154
|
+
activeTintColor: string;
|
|
155
|
+
inactiveTintColor: string;
|
|
156
|
+
floatingButton?: FloatingButtonProps;
|
|
157
|
+
fixedFontScale?: boolean;
|
|
158
|
+
}> = ({
|
|
159
|
+
items,
|
|
160
|
+
width,
|
|
161
|
+
activeTintColor,
|
|
162
|
+
inactiveTintColor,
|
|
163
|
+
floatingButton,
|
|
164
|
+
fixedFontScale,
|
|
165
|
+
}) => {
|
|
166
|
+
const withCta = hasCenterAction(floatingButton, items.length);
|
|
167
|
+
const [layouts, setLayouts] = useState<
|
|
168
|
+
Record<string, { x: number; y: number; width: number }>
|
|
169
|
+
>({});
|
|
170
|
+
const selected = layouts[items.find(item => item.focused)?.route.key ?? ''];
|
|
221
171
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
172
|
+
const renderItems = (data: TabItem[]) =>
|
|
173
|
+
data.map(item => (
|
|
174
|
+
<CustomBottomTabItem
|
|
175
|
+
key={item.route.key}
|
|
176
|
+
onLayout={e => {
|
|
177
|
+
const { x, y, width: itemWidth } = e.nativeEvent.layout;
|
|
178
|
+
setLayouts(prev => ({
|
|
179
|
+
...prev,
|
|
180
|
+
[item.route.key]: { x, y, width: itemWidth },
|
|
181
|
+
}));
|
|
182
|
+
}}
|
|
183
|
+
label={item.label}
|
|
184
|
+
focused={item.focused}
|
|
185
|
+
icon={item.icon}
|
|
186
|
+
badge={item.badge}
|
|
187
|
+
onPress={item.onPress}
|
|
188
|
+
onLongPress={item.onLongPress}
|
|
189
|
+
activeTintColor={activeTintColor}
|
|
190
|
+
inactiveTintColor={inactiveTintColor}
|
|
191
|
+
fixedFontScale={fixedFontScale}
|
|
192
|
+
/>
|
|
193
|
+
));
|
|
194
|
+
|
|
195
|
+
const renderContent = () => {
|
|
196
|
+
if (withCta && floatingButton) {
|
|
197
|
+
const middle = items.length / 2;
|
|
230
198
|
return (
|
|
231
199
|
<>
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
|
|
200
|
+
{renderItems(items.slice(0, middle))}
|
|
201
|
+
<CenterAction
|
|
202
|
+
floatingButton={floatingButton}
|
|
203
|
+
fixedFontScale={fixedFontScale}
|
|
204
|
+
/>
|
|
205
|
+
{renderItems(items.slice(middle))}
|
|
235
206
|
</>
|
|
236
207
|
);
|
|
237
208
|
}
|
|
238
|
-
return
|
|
209
|
+
return renderItems(items);
|
|
239
210
|
};
|
|
240
211
|
|
|
212
|
+
const customContainer = withCta && floatingButton?.container;
|
|
213
|
+
|
|
241
214
|
return (
|
|
242
215
|
<View
|
|
216
|
+
style={[styles.container, { width }]}
|
|
217
|
+
accessibilityLabel={'bottom_tab_bar'}
|
|
218
|
+
>
|
|
219
|
+
{customContainer || (
|
|
220
|
+
<CurvedContainer
|
|
221
|
+
width={width}
|
|
222
|
+
height={TabBarMetrics.height}
|
|
223
|
+
radius={TabBarMetrics.height / 2}
|
|
224
|
+
notchCenterX={withCta ? width / 2 : undefined}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
<View style={styles.content}>
|
|
228
|
+
<ActiveIndicator
|
|
229
|
+
axis={'x'}
|
|
230
|
+
offset={selected?.x}
|
|
231
|
+
extent={selected?.width}
|
|
232
|
+
radius={TabBarMetrics.indicatorRadius}
|
|
233
|
+
style={[
|
|
234
|
+
styles.indicator,
|
|
235
|
+
{ top: selected?.y ?? TabBarMetrics.paddingVertical },
|
|
236
|
+
]}
|
|
237
|
+
/>
|
|
238
|
+
{renderContent()}
|
|
239
|
+
</View>
|
|
240
|
+
</View>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* phone tab bar: the horizontal navigation bar floating over the tab content above the home
|
|
246
|
+
* indicator, so the content shows around it and through the notch. The transparent strip
|
|
247
|
+
* above it keeps the rising center action touchable
|
|
248
|
+
*/
|
|
249
|
+
export default function BottomTabBar(props: Props) {
|
|
250
|
+
const { state, floatingButton, activeTintColor, inactiveTintColor } = props;
|
|
251
|
+
const window = useWindowDimensions();
|
|
252
|
+
const insets = useSafeAreaInsets();
|
|
253
|
+
const withCta = hasCenterAction(floatingButton, state.routes.length);
|
|
254
|
+
const width = getHorizontalBarWidth(
|
|
255
|
+
window.width,
|
|
256
|
+
window.height,
|
|
257
|
+
insets.left,
|
|
258
|
+
insets.right,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<View
|
|
263
|
+
pointerEvents={'box-none'}
|
|
243
264
|
style={[
|
|
244
|
-
|
|
265
|
+
styles.bar,
|
|
245
266
|
{
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
{
|
|
250
|
-
paddingBottom: paddingBottom,
|
|
267
|
+
paddingTop: withCta ? TabBarMetrics.ctaLift : 0,
|
|
268
|
+
paddingBottom: getSystemIndicatorHeight(insets.bottom),
|
|
269
|
+
// centered between the side safe areas
|
|
251
270
|
paddingLeft: insets.left,
|
|
252
271
|
paddingRight: insets.right,
|
|
253
272
|
},
|
|
254
|
-
style,
|
|
255
273
|
]}
|
|
256
|
-
pointerEvents={'auto'}
|
|
257
274
|
>
|
|
258
|
-
<
|
|
259
|
-
{
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
styles.indicator,
|
|
266
|
-
{ backgroundColor: activeTintColor ?? theme.colors.primary },
|
|
267
|
-
]}
|
|
268
|
-
/>
|
|
269
|
-
</Animated.View>
|
|
270
|
-
{renderTabBar()}
|
|
271
|
-
</View>
|
|
275
|
+
<HorizontalTabBar
|
|
276
|
+
items={getTabItems(props)}
|
|
277
|
+
width={width}
|
|
278
|
+
floatingButton={floatingButton}
|
|
279
|
+
activeTintColor={activeTintColor}
|
|
280
|
+
inactiveTintColor={inactiveTintColor}
|
|
281
|
+
/>
|
|
272
282
|
</View>
|
|
273
283
|
);
|
|
274
284
|
}
|
|
275
285
|
|
|
276
286
|
const styles = StyleSheet.create({
|
|
287
|
+
bar: {
|
|
288
|
+
position: 'absolute',
|
|
289
|
+
left: 0,
|
|
290
|
+
right: 0,
|
|
291
|
+
bottom: 0,
|
|
292
|
+
alignItems: 'center',
|
|
293
|
+
},
|
|
294
|
+
container: {
|
|
295
|
+
height: TabBarMetrics.height,
|
|
296
|
+
},
|
|
277
297
|
content: {
|
|
278
298
|
flex: 1,
|
|
279
299
|
flexDirection: 'row',
|
|
300
|
+
alignItems: 'center',
|
|
301
|
+
gap: TabBarMetrics.itemGap,
|
|
302
|
+
paddingVertical: TabBarMetrics.paddingVertical,
|
|
303
|
+
paddingHorizontal: TabBarMetrics.paddingHorizontal,
|
|
280
304
|
},
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
right: 0,
|
|
284
|
-
bottom: 0,
|
|
285
|
-
borderTopWidth: StyleSheet.hairlineWidth,
|
|
286
|
-
elevation: 8,
|
|
305
|
+
indicator: {
|
|
306
|
+
height: TabBarMetrics.indicatorHeight,
|
|
287
307
|
},
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
bottom: 0,
|
|
292
|
-
zIndex: 1,
|
|
293
|
-
shadowColor: Colors.black_20,
|
|
294
|
-
shadowOffset: { width: 0, height: -2 },
|
|
295
|
-
shadowOpacity: 0.05,
|
|
296
|
-
shadowRadius: 3,
|
|
297
|
-
elevation: 8,
|
|
308
|
+
ctaSlot: {
|
|
309
|
+
width: TabBarMetrics.ctaSlotWidth,
|
|
310
|
+
alignSelf: 'stretch',
|
|
298
311
|
},
|
|
299
|
-
|
|
312
|
+
// the button rises ctaLift above the container top, the label ends on the item baseline
|
|
313
|
+
cta: {
|
|
300
314
|
position: 'absolute',
|
|
301
|
-
alignItems: 'center',
|
|
302
315
|
left: 0,
|
|
303
316
|
right: 0,
|
|
304
|
-
top: -
|
|
305
|
-
|
|
317
|
+
top: -TabBarMetrics.ctaLift - TabBarMetrics.paddingVertical,
|
|
318
|
+
alignItems: 'center',
|
|
319
|
+
gap: TabBarMetrics.ctaLabelGap,
|
|
306
320
|
},
|
|
307
|
-
|
|
321
|
+
ctaButton: {
|
|
322
|
+
boxShadow: CTA_SHADOW,
|
|
323
|
+
width: TabBarMetrics.ctaSize,
|
|
324
|
+
height: TabBarMetrics.ctaSize,
|
|
325
|
+
borderRadius: TabBarMetrics.ctaSize / 2,
|
|
308
326
|
alignItems: 'center',
|
|
309
327
|
justifyContent: 'center',
|
|
310
|
-
height: 48,
|
|
311
|
-
width: 48,
|
|
312
|
-
borderRadius: 24,
|
|
313
|
-
top: -10,
|
|
314
328
|
},
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
329
|
+
ctaLabel: {
|
|
330
|
+
maxWidth: TabBarMetrics.ctaSlotWidth,
|
|
331
|
+
minHeight: TabBarMetrics.ctaLabelHeight,
|
|
332
|
+
paddingHorizontal: Spacing.XXS,
|
|
333
|
+
borderRadius: Radius.XL,
|
|
334
|
+
justifyContent: 'center',
|
|
321
335
|
},
|
|
322
336
|
debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
|
|
323
337
|
});
|
|
338
|
+
|
|
339
|
+
export {
|
|
340
|
+
HorizontalTabBar,
|
|
341
|
+
getSystemIndicatorHeight,
|
|
342
|
+
hasCenterAction,
|
|
343
|
+
useCenterActionPress,
|
|
344
|
+
};
|