@momo-kits/foundation 0.168.1-beta.24 → 0.168.1-beta.26
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 +218 -431
- package/Application/BottomTab/TabBarIcon.tsx +160 -72
- package/Application/BottomTab/index.tsx +129 -240
- package/Application/BottomTab/metrics.ts +164 -0
- package/Application/BottomTab/tabItems.ts +46 -0
- package/Application/BottomTab/usePressLift.ts +32 -0
- package/Application/types.ts +5 -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
- package/Application/BottomTab/useLiquidSelection.ts +0 -374
|
@@ -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,
|
|
15
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
16
|
+
import { Colors, 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; the label stays plain text
|
|
58
|
+
*/
|
|
59
|
+
const useCenterActionPress = () => {
|
|
60
|
+
const scale = useSharedValue(1);
|
|
61
|
+
const buttonStyle = useAnimatedStyle(() => ({
|
|
62
|
+
transform: [{ scale: scale.value }],
|
|
72
63
|
}));
|
|
73
|
-
const
|
|
74
|
-
|
|
64
|
+
const onPressIn = () => {
|
|
65
|
+
scale.value = withTiming(
|
|
66
|
+
TabBarMotion.ctaPressedScale,
|
|
67
|
+
TabBarMotion.ctaPress,
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
const onPressOut = () => {
|
|
71
|
+
scale.value = withTiming(1, TabBarMotion.ctaPress);
|
|
72
|
+
};
|
|
73
|
+
return { buttonStyle, onPressIn, onPressOut };
|
|
74
|
+
};
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* center action (.Nav/CTA): button rising out of the notch, label under it
|
|
78
|
+
*/
|
|
79
|
+
const CenterAction: React.FC<{
|
|
80
|
+
floatingButton: FloatingButtonProps;
|
|
81
|
+
fixedFontScale?: boolean;
|
|
82
|
+
}> = ({ floatingButton, fixedFontScale }) => {
|
|
83
|
+
const { theme } = useContext(ApplicationContext);
|
|
76
84
|
const context = useContext<any>(MiniAppContext);
|
|
77
85
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const onPress = () => {
|
|
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
|
-
],
|
|
86
|
+
const { icon, label, onPress, labelContent } = floatingButton;
|
|
87
|
+
const { buttonStyle, onPressIn, onPressOut } = useCenterActionPress();
|
|
88
|
+
const labelText = (
|
|
89
|
+
<Text
|
|
90
|
+
numberOfLines={1}
|
|
91
|
+
typography={'label_s_medium'}
|
|
92
|
+
color={theme.colors.text.secondary}
|
|
93
|
+
>
|
|
94
|
+
{label}
|
|
95
|
+
</Text>
|
|
150
96
|
);
|
|
151
97
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
98
|
+
return (
|
|
99
|
+
<View style={[styles.ctaSlot, showBaseLineDebug && styles.debugBaseLine]}>
|
|
100
|
+
<Pressable
|
|
101
|
+
accessibilityRole={'button'}
|
|
102
|
+
accessibilityLabel={label}
|
|
103
|
+
onPress={onPress}
|
|
104
|
+
onPressIn={onPressIn}
|
|
105
|
+
onPressOut={onPressOut}
|
|
106
|
+
style={styles.cta}
|
|
107
|
+
>
|
|
108
|
+
<Animated.View
|
|
109
|
+
style={[
|
|
110
|
+
styles.ctaButton,
|
|
111
|
+
{ backgroundColor: theme.colors.primary },
|
|
112
|
+
buttonStyle,
|
|
113
|
+
]}
|
|
168
114
|
>
|
|
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
|
-
]);
|
|
115
|
+
{typeof icon === 'string' ? (
|
|
116
|
+
<Icon
|
|
117
|
+
source={icon}
|
|
118
|
+
size={TabBarMetrics.ctaIconSize}
|
|
119
|
+
color={Colors.black_01}
|
|
120
|
+
/>
|
|
121
|
+
) : (
|
|
122
|
+
icon
|
|
123
|
+
)}
|
|
124
|
+
</Animated.View>
|
|
125
|
+
<View style={styles.ctaLabel}>
|
|
126
|
+
{fixedFontScale ? (
|
|
127
|
+
<FixedScaleText>{labelText}</FixedScaleText>
|
|
128
|
+
) : (
|
|
129
|
+
labelText
|
|
130
|
+
)}
|
|
131
|
+
{labelContent && (
|
|
132
|
+
<View pointerEvents={'none'} style={styles.ctaLabelOverlay}>
|
|
133
|
+
{labelContent}
|
|
134
|
+
</View>
|
|
135
|
+
)}
|
|
136
|
+
</View>
|
|
137
|
+
</Pressable>
|
|
138
|
+
</View>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
204
141
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
142
|
+
/**
|
|
143
|
+
* horizontal navigation bar: floating container, items split evenly, Special type puts
|
|
144
|
+
* the center action in a notch in the middle
|
|
145
|
+
*/
|
|
146
|
+
const HorizontalTabBar: React.FC<{
|
|
147
|
+
items: TabItem[];
|
|
148
|
+
width: number;
|
|
149
|
+
activeTintColor: string;
|
|
150
|
+
inactiveTintColor: string;
|
|
151
|
+
floatingButton?: FloatingButtonProps;
|
|
152
|
+
fixedFontScale?: boolean;
|
|
153
|
+
}> = ({
|
|
154
|
+
items,
|
|
155
|
+
width,
|
|
156
|
+
activeTintColor,
|
|
157
|
+
inactiveTintColor,
|
|
158
|
+
floatingButton,
|
|
159
|
+
fixedFontScale,
|
|
160
|
+
}) => {
|
|
161
|
+
const withCta = hasCenterAction(floatingButton, items.length);
|
|
162
|
+
const [layouts, setLayouts] = useState<
|
|
163
|
+
Record<string, { x: number; y: number; width: number }>
|
|
164
|
+
>({});
|
|
165
|
+
const selected = layouts[items.find(item => item.focused)?.route.key ?? ''];
|
|
166
|
+
|
|
167
|
+
const renderItems = (data: TabItem[]) =>
|
|
168
|
+
data.map(item => (
|
|
169
|
+
<CustomBottomTabItem
|
|
170
|
+
key={item.route.key}
|
|
171
|
+
onLayout={e => {
|
|
172
|
+
const { x, y, width: itemWidth } = e.nativeEvent.layout;
|
|
173
|
+
setLayouts(prev => ({
|
|
174
|
+
...prev,
|
|
175
|
+
[item.route.key]: { x, y, width: itemWidth },
|
|
176
|
+
}));
|
|
177
|
+
}}
|
|
178
|
+
label={item.label}
|
|
179
|
+
focused={item.focused}
|
|
180
|
+
icon={item.icon}
|
|
181
|
+
badge={item.badge}
|
|
182
|
+
onPress={item.onPress}
|
|
183
|
+
onLongPress={item.onLongPress}
|
|
184
|
+
activeTintColor={activeTintColor}
|
|
185
|
+
inactiveTintColor={inactiveTintColor}
|
|
186
|
+
fixedFontScale={fixedFontScale}
|
|
187
|
+
/>
|
|
188
|
+
));
|
|
221
189
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const renderTabBar = () => {
|
|
226
|
-
if (useFloating) {
|
|
227
|
-
const middleIndex = routes.length / 2;
|
|
228
|
-
const leftArr = routes.slice(0, middleIndex);
|
|
229
|
-
const rightArr = routes.slice(middleIndex);
|
|
190
|
+
const renderContent = () => {
|
|
191
|
+
if (withCta && floatingButton) {
|
|
192
|
+
const middle = items.length / 2;
|
|
230
193
|
return (
|
|
231
194
|
<>
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
|
|
195
|
+
{renderItems(items.slice(0, middle))}
|
|
196
|
+
<CenterAction
|
|
197
|
+
floatingButton={floatingButton}
|
|
198
|
+
fixedFontScale={fixedFontScale}
|
|
199
|
+
/>
|
|
200
|
+
{renderItems(items.slice(middle))}
|
|
235
201
|
</>
|
|
236
202
|
);
|
|
237
203
|
}
|
|
238
|
-
return
|
|
204
|
+
return renderItems(items);
|
|
239
205
|
};
|
|
240
206
|
|
|
207
|
+
const customContainer = withCta && floatingButton?.container;
|
|
208
|
+
|
|
241
209
|
return (
|
|
242
210
|
<View
|
|
211
|
+
style={[styles.container, { width }]}
|
|
212
|
+
accessibilityLabel={'bottom_tab_bar'}
|
|
213
|
+
>
|
|
214
|
+
{customContainer || (
|
|
215
|
+
<CurvedContainer
|
|
216
|
+
width={width}
|
|
217
|
+
height={TabBarMetrics.height}
|
|
218
|
+
radius={TabBarMetrics.height / 2}
|
|
219
|
+
notchCenterX={withCta ? width / 2 : undefined}
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
<View style={styles.content}>
|
|
223
|
+
<ActiveIndicator
|
|
224
|
+
axis={'x'}
|
|
225
|
+
offset={selected?.x}
|
|
226
|
+
extent={selected?.width}
|
|
227
|
+
radius={TabBarMetrics.indicatorRadius}
|
|
228
|
+
style={[
|
|
229
|
+
styles.indicator,
|
|
230
|
+
{ top: selected?.y ?? TabBarMetrics.paddingVertical },
|
|
231
|
+
]}
|
|
232
|
+
/>
|
|
233
|
+
{renderContent()}
|
|
234
|
+
</View>
|
|
235
|
+
</View>
|
|
236
|
+
);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* phone tab bar: the horizontal navigation bar floating over the tab content above the home
|
|
241
|
+
* indicator, so the content shows around it and through the notch. The transparent strip
|
|
242
|
+
* above it keeps the rising center action touchable
|
|
243
|
+
*/
|
|
244
|
+
export default function BottomTabBar(props: Props) {
|
|
245
|
+
const { state, floatingButton, activeTintColor, inactiveTintColor } = props;
|
|
246
|
+
const window = useWindowDimensions();
|
|
247
|
+
const insets = useSafeAreaInsets();
|
|
248
|
+
const withCta = hasCenterAction(floatingButton, state.routes.length);
|
|
249
|
+
const width = getHorizontalBarWidth(
|
|
250
|
+
window.width,
|
|
251
|
+
window.height,
|
|
252
|
+
insets.left,
|
|
253
|
+
insets.right,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<View
|
|
258
|
+
pointerEvents={'box-none'}
|
|
243
259
|
style={[
|
|
244
|
-
|
|
245
|
-
{
|
|
246
|
-
backgroundColor: theme.colors.background.surface,
|
|
247
|
-
borderTopColor: theme.colors.border.default,
|
|
248
|
-
},
|
|
260
|
+
styles.bar,
|
|
249
261
|
{
|
|
250
|
-
|
|
262
|
+
paddingTop: withCta ? TabBarMetrics.ctaLift : 0,
|
|
263
|
+
paddingBottom: getSystemIndicatorHeight(insets.bottom),
|
|
264
|
+
// centered between the side safe areas
|
|
251
265
|
paddingLeft: insets.left,
|
|
252
266
|
paddingRight: insets.right,
|
|
253
267
|
},
|
|
254
|
-
style,
|
|
255
268
|
]}
|
|
256
|
-
pointerEvents={'auto'}
|
|
257
269
|
>
|
|
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>
|
|
270
|
+
<HorizontalTabBar
|
|
271
|
+
items={getTabItems(props)}
|
|
272
|
+
width={width}
|
|
273
|
+
floatingButton={floatingButton}
|
|
274
|
+
activeTintColor={activeTintColor}
|
|
275
|
+
inactiveTintColor={inactiveTintColor}
|
|
276
|
+
/>
|
|
272
277
|
</View>
|
|
273
278
|
);
|
|
274
279
|
}
|
|
275
280
|
|
|
276
281
|
const styles = StyleSheet.create({
|
|
282
|
+
bar: {
|
|
283
|
+
position: 'absolute',
|
|
284
|
+
left: 0,
|
|
285
|
+
right: 0,
|
|
286
|
+
bottom: 0,
|
|
287
|
+
alignItems: 'center',
|
|
288
|
+
},
|
|
289
|
+
container: {
|
|
290
|
+
height: TabBarMetrics.height,
|
|
291
|
+
},
|
|
277
292
|
content: {
|
|
278
293
|
flex: 1,
|
|
279
294
|
flexDirection: 'row',
|
|
295
|
+
alignItems: 'center',
|
|
296
|
+
gap: TabBarMetrics.itemGap,
|
|
297
|
+
paddingVertical: TabBarMetrics.paddingVertical,
|
|
298
|
+
paddingHorizontal: TabBarMetrics.paddingHorizontal,
|
|
280
299
|
},
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
right: 0,
|
|
284
|
-
bottom: 0,
|
|
285
|
-
borderTopWidth: StyleSheet.hairlineWidth,
|
|
286
|
-
elevation: 8,
|
|
300
|
+
indicator: {
|
|
301
|
+
height: TabBarMetrics.indicatorHeight,
|
|
287
302
|
},
|
|
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,
|
|
303
|
+
ctaSlot: {
|
|
304
|
+
width: TabBarMetrics.ctaSlotWidth,
|
|
305
|
+
alignSelf: 'stretch',
|
|
298
306
|
},
|
|
299
|
-
|
|
307
|
+
// the button rises ctaLift above the container top, the label ends on the item baseline
|
|
308
|
+
cta: {
|
|
300
309
|
position: 'absolute',
|
|
301
|
-
alignItems: 'center',
|
|
302
310
|
left: 0,
|
|
303
311
|
right: 0,
|
|
304
|
-
top: -
|
|
305
|
-
|
|
312
|
+
top: -TabBarMetrics.ctaLift - TabBarMetrics.paddingVertical,
|
|
313
|
+
alignItems: 'center',
|
|
314
|
+
gap: TabBarMetrics.ctaLabelGap,
|
|
306
315
|
},
|
|
307
|
-
|
|
316
|
+
ctaButton: {
|
|
317
|
+
boxShadow: CTA_SHADOW,
|
|
318
|
+
width: TabBarMetrics.ctaSize,
|
|
319
|
+
height: TabBarMetrics.ctaSize,
|
|
320
|
+
borderRadius: TabBarMetrics.ctaSize / 2,
|
|
308
321
|
alignItems: 'center',
|
|
309
322
|
justifyContent: 'center',
|
|
310
|
-
height: 48,
|
|
311
|
-
width: 48,
|
|
312
|
-
borderRadius: 24,
|
|
313
|
-
top: -10,
|
|
314
323
|
},
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
324
|
+
// custom label content over the label text, centered and unclipped
|
|
325
|
+
ctaLabelOverlay: {
|
|
326
|
+
...StyleSheet.absoluteFillObject,
|
|
327
|
+
alignItems: 'center',
|
|
328
|
+
justifyContent: 'center',
|
|
329
|
+
},
|
|
330
|
+
ctaLabel: {
|
|
331
|
+
maxWidth: TabBarMetrics.ctaSlotWidth,
|
|
332
|
+
minHeight: TabBarMetrics.ctaLabelHeight,
|
|
333
|
+
paddingHorizontal: Spacing.XXS,
|
|
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
|
+
};
|