@momo-kits/foundation 0.167.1-duo.5 → 0.167.1-duotest.1
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/DuoBarCompression.tsx +135 -0
- package/Application/BottomTab/DuoTabBar.tsx +107 -55
- package/Application/BottomTab/index.tsx +145 -139
- package/Application/Components/DuoGlassBackground.tsx +28 -26
- package/Application/Components/DuoQuickReturnHeader.tsx +42 -0
- package/Application/Components/HeaderExtendHeader.tsx +39 -5
- package/Application/Components/HeaderLeft.tsx +23 -2
- package/Application/Components/HeaderRight.tsx +86 -3
- package/Application/Components/HeaderTitle.tsx +17 -3
- package/Application/Components/OptionalScrollObserver.ts +12 -0
- package/Application/Components/SearchHeader.tsx +7 -1
- package/Application/Components/SideBarButton.tsx +15 -4
- package/Application/NavigationContainer.tsx +17 -16
- package/Application/StackScreen.tsx +61 -4
- package/Application/types.ts +22 -0
- package/Application/utils.tsx +245 -4
- package/Layout/ReservedRegions.tsx +47 -0
- package/Layout/Screen.tsx +58 -17
- package/Layout/styles.ts +5 -0
- package/Layout/utils.ts +194 -9
- package/package.json +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
ReactNode,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { Spacing } from '../../Consts';
|
|
9
|
+
import { useScreenLayout } from '../../Layout';
|
|
10
|
+
import { DuoMetrics } from '../../Layout/utils';
|
|
11
|
+
|
|
12
|
+
export type DuoBarCompressionBehavior = 'toolbar' | 'tabBar';
|
|
13
|
+
|
|
14
|
+
type DuoBarCompression = {
|
|
15
|
+
toolbarCompressed: boolean;
|
|
16
|
+
tabBarCompressed: boolean;
|
|
17
|
+
// the open rail needs the toolbar button slot too: only Back stays
|
|
18
|
+
toolbarHidden?: boolean;
|
|
19
|
+
// tapping the compressed bar expands it and compresses the other one
|
|
20
|
+
expandToolbar?: () => void;
|
|
21
|
+
expandTabBar?: () => void;
|
|
22
|
+
// a rail opened from the compressed state closes once a tab is picked
|
|
23
|
+
onTabSelected?: () => void;
|
|
24
|
+
// header buttons report their full height so the tab bar knows what is left
|
|
25
|
+
setToolbarHeight?: (height: number) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const RAIL_PADDING = 6;
|
|
29
|
+
const RAIL_GAP = Spacing.XXS;
|
|
30
|
+
// space kept between the top and the bottom groups (HIG Vertical controls)
|
|
31
|
+
const GROUP_GAP = Spacing.M;
|
|
32
|
+
// Back, then the right toolkit: help + home capsule
|
|
33
|
+
const DEFAULT_TOOLBAR_HEIGHT =
|
|
34
|
+
DuoMetrics.controlSize + Spacing.M + 2 * DuoMetrics.itemPitch;
|
|
35
|
+
// Back, then the single button the toolbar folds into
|
|
36
|
+
const COMPRESSED_TOOLBAR_HEIGHT = 2 * DuoMetrics.controlSize + Spacing.M;
|
|
37
|
+
// Back alone, while a rail opened from the compressed state takes the toolbar slot
|
|
38
|
+
const BACK_ONLY_HEIGHT = DuoMetrics.controlSize;
|
|
39
|
+
|
|
40
|
+
const NO_COMPRESSION: DuoBarCompression = {
|
|
41
|
+
toolbarCompressed: false,
|
|
42
|
+
tabBarCompressed: false,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const DuoBarCompressionContext =
|
|
46
|
+
createContext<DuoBarCompression>(NO_COMPRESSION);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* height of the vertical tab rail, plus the QR button under it
|
|
50
|
+
*/
|
|
51
|
+
const getRailHeight = (tabCount: number, hasQr: boolean) =>
|
|
52
|
+
2 * RAIL_PADDING +
|
|
53
|
+
tabCount * DuoMetrics.controlSize +
|
|
54
|
+
Math.max(tabCount - 1, 0) * RAIL_GAP +
|
|
55
|
+
(hasQr ? Spacing.M + DuoMetrics.controlSize : 0);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* keeps the side bar's toolbar and tab bar clear of each other (HIG, Designing for
|
|
59
|
+
* iPhone Duo: Vertical controls):
|
|
60
|
+
* - both fit: both stay whole
|
|
61
|
+
* - one fits whole beside the other compressed: one is compressed, tapping it swaps them
|
|
62
|
+
* - otherwise both rest compressed; tapping one opens it for a moment (the rail closes
|
|
63
|
+
* again once a tab is picked)
|
|
64
|
+
*/
|
|
65
|
+
const DuoBarCompressionProvider: React.FC<{
|
|
66
|
+
behavior?: DuoBarCompressionBehavior;
|
|
67
|
+
tabCount: number;
|
|
68
|
+
hasQr: boolean;
|
|
69
|
+
children: ReactNode;
|
|
70
|
+
}> = ({ behavior = 'toolbar', tabCount, hasQr, children }) => {
|
|
71
|
+
const { height, sideBar } = useScreenLayout();
|
|
72
|
+
const [toolbarHeight, setToolbarHeight] = useState(DEFAULT_TOOLBAR_HEIGHT);
|
|
73
|
+
// 'toolbar' compression keeps the tab bar whole until the toolbar is tapped
|
|
74
|
+
const [expanded, setExpanded] = useState<'toolbar' | 'tabBar'>(
|
|
75
|
+
behavior === 'toolbar' ? 'tabBar' : 'toolbar',
|
|
76
|
+
);
|
|
77
|
+
// bar opened for a moment while both rest compressed
|
|
78
|
+
const [opened, setOpened] = useState<'toolbar' | 'tabBar'>();
|
|
79
|
+
|
|
80
|
+
const value = useMemo(() => {
|
|
81
|
+
if (!sideBar) {
|
|
82
|
+
return NO_COMPRESSION;
|
|
83
|
+
}
|
|
84
|
+
const room = height - sideBar.bottom - sideBar.top - GROUP_GAP;
|
|
85
|
+
const tabBar = getRailHeight(tabCount, hasQr);
|
|
86
|
+
const compressedTabBar = getRailHeight(1, hasQr) - 2 * RAIL_PADDING;
|
|
87
|
+
const fits = (toolbar: number, tabs: number) => toolbar + tabs <= room;
|
|
88
|
+
|
|
89
|
+
if (fits(toolbarHeight, tabBar)) {
|
|
90
|
+
return { ...NO_COMPRESSION, setToolbarHeight };
|
|
91
|
+
}
|
|
92
|
+
const canSwap =
|
|
93
|
+
fits(COMPRESSED_TOOLBAR_HEIGHT, tabBar) &&
|
|
94
|
+
fits(toolbarHeight, compressedTabBar);
|
|
95
|
+
if (canSwap) {
|
|
96
|
+
return {
|
|
97
|
+
toolbarCompressed: expanded === 'tabBar',
|
|
98
|
+
tabBarCompressed: expanded === 'toolbar',
|
|
99
|
+
expandToolbar: () => setExpanded('toolbar'),
|
|
100
|
+
expandTabBar: () => setExpanded('tabBar'),
|
|
101
|
+
setToolbarHeight,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const open = {
|
|
106
|
+
expandToolbar: () => setOpened('toolbar'),
|
|
107
|
+
expandTabBar: () => setOpened('tabBar'),
|
|
108
|
+
setToolbarHeight,
|
|
109
|
+
};
|
|
110
|
+
if (opened === 'tabBar' && fits(BACK_ONLY_HEIGHT, tabBar)) {
|
|
111
|
+
return {
|
|
112
|
+
...open,
|
|
113
|
+
toolbarCompressed: true,
|
|
114
|
+
toolbarHidden: true,
|
|
115
|
+
tabBarCompressed: false,
|
|
116
|
+
onTabSelected: () => setOpened(undefined),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
...open,
|
|
121
|
+
toolbarCompressed: opened !== 'toolbar',
|
|
122
|
+
tabBarCompressed: true,
|
|
123
|
+
};
|
|
124
|
+
}, [expanded, opened, hasQr, height, sideBar, tabCount, toolbarHeight]);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<DuoBarCompressionContext.Provider value={value}>
|
|
128
|
+
{children}
|
|
129
|
+
</DuoBarCompressionContext.Provider>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const useDuoBarCompression = () => useContext(DuoBarCompressionContext);
|
|
134
|
+
|
|
135
|
+
export { DuoBarCompressionProvider, useDuoBarCompression };
|
|
@@ -18,6 +18,7 @@ import { Icon } from '../../Icon';
|
|
|
18
18
|
import { DuoMetrics, useScreenLayout } from '../../Layout/utils';
|
|
19
19
|
import { Text } from '../../Text';
|
|
20
20
|
import { GlassView } from '../Components/OptionalGlassView';
|
|
21
|
+
import { useDuoBarCompression } from './DuoBarCompression';
|
|
21
22
|
import { FloatingButtonProps } from '../types';
|
|
22
23
|
import { TabBarIcon } from './TabBarIcon';
|
|
23
24
|
|
|
@@ -146,76 +147,121 @@ const QrIcon: React.FC<{ icon: FloatingButtonProps['icon']; size: number }> = ({
|
|
|
146
147
|
return <>{icon}</>;
|
|
147
148
|
};
|
|
148
149
|
|
|
150
|
+
type TabItem = ReturnType<typeof getTabItems>[number];
|
|
151
|
+
|
|
149
152
|
/**
|
|
150
|
-
*
|
|
153
|
+
* glass column of tab items with the sliding selection bubble
|
|
151
154
|
*/
|
|
152
|
-
const
|
|
155
|
+
const Rail: React.FC<{
|
|
156
|
+
items: TabItem[];
|
|
157
|
+
index: number;
|
|
158
|
+
onItemPress?: () => void;
|
|
159
|
+
}> = ({ items, index, onItemPress }) => {
|
|
153
160
|
const { theme } = useContext(ApplicationContext);
|
|
154
|
-
const
|
|
155
|
-
const { state, floatingButton } = props;
|
|
156
|
-
const items = getTabItems(props);
|
|
157
|
-
const bubbleOffset = useSharedValue(state.index * (ITEM_SIZE + RAIL_GAP));
|
|
161
|
+
const bubbleOffset = useSharedValue(index * (ITEM_SIZE + RAIL_GAP));
|
|
158
162
|
|
|
159
163
|
useEffect(() => {
|
|
160
|
-
bubbleOffset.value = withSpring(
|
|
161
|
-
|
|
162
|
-
SPRING,
|
|
163
|
-
);
|
|
164
|
-
}, [state.index, bubbleOffset]);
|
|
164
|
+
bubbleOffset.value = withSpring(index * (ITEM_SIZE + RAIL_GAP), SPRING);
|
|
165
|
+
}, [index, bubbleOffset]);
|
|
165
166
|
|
|
166
167
|
const bubbleStyle = useAnimatedStyle(() => ({
|
|
167
168
|
transform: [{ translateY: bubbleOffset.value }],
|
|
168
169
|
}));
|
|
169
170
|
|
|
171
|
+
return (
|
|
172
|
+
<View style={styles.rail} accessibilityLabel={'bottom_tab_bar'}>
|
|
173
|
+
<BarMaterial radius={Radius.XL} />
|
|
174
|
+
<Animated.View
|
|
175
|
+
pointerEvents={'none'}
|
|
176
|
+
style={[
|
|
177
|
+
styles.railBubble,
|
|
178
|
+
{ backgroundColor: theme.colors.background.selected },
|
|
179
|
+
bubbleStyle,
|
|
180
|
+
]}
|
|
181
|
+
/>
|
|
182
|
+
{items.map(item => (
|
|
183
|
+
<TouchableOpacity
|
|
184
|
+
key={item.route.key}
|
|
185
|
+
accessibilityRole={'tab'}
|
|
186
|
+
accessibilityLabel={item.label}
|
|
187
|
+
accessibilityState={{ selected: item.focused }}
|
|
188
|
+
onPress={() => {
|
|
189
|
+
item.onPress();
|
|
190
|
+
onItemPress?.();
|
|
191
|
+
}}
|
|
192
|
+
onLongPress={item.onLongPress}
|
|
193
|
+
style={styles.railItem}
|
|
194
|
+
>
|
|
195
|
+
{item.icon}
|
|
196
|
+
</TouchableOpacity>
|
|
197
|
+
))}
|
|
198
|
+
</View>
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* vertical bar anchored to the bottom of the Duo side bar, QR button below it (Figma MT1-C/E).
|
|
204
|
+
* When the side bar is short, it can shrink to one button showing the current tab
|
|
205
|
+
* (HIG "Tab bar compressed"); tapping it expands the rail and compresses the toolbar
|
|
206
|
+
*/
|
|
207
|
+
const DuoRailTabBar: React.FC<Props> = props => {
|
|
208
|
+
const { theme } = useContext(ApplicationContext);
|
|
209
|
+
const { sideBar } = useScreenLayout();
|
|
210
|
+
const { tabBarCompressed, expandTabBar, onTabSelected } =
|
|
211
|
+
useDuoBarCompression();
|
|
212
|
+
const { state, floatingButton } = props;
|
|
213
|
+
const items = getTabItems(props);
|
|
214
|
+
const current = items[state.index];
|
|
215
|
+
|
|
216
|
+
const columnStyle = sideBar && [
|
|
217
|
+
styles.railColumn,
|
|
218
|
+
{
|
|
219
|
+
[sideBar.edge]: 0,
|
|
220
|
+
width: sideBar.width,
|
|
221
|
+
bottom: sideBar.bottom,
|
|
222
|
+
},
|
|
223
|
+
sideBar.edge === 'right'
|
|
224
|
+
? { alignItems: 'flex-start' as const, paddingLeft: Spacing.M }
|
|
225
|
+
: { alignItems: 'flex-end' as const, paddingRight: Spacing.M },
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
const qr = floatingButton && (
|
|
229
|
+
<TouchableOpacity
|
|
230
|
+
accessibilityRole={'button'}
|
|
231
|
+
accessibilityLabel={floatingButton.label}
|
|
232
|
+
onPress={floatingButton.onPress}
|
|
233
|
+
style={[styles.railQr, { backgroundColor: theme.colors.primary }]}
|
|
234
|
+
>
|
|
235
|
+
<QrIcon icon={floatingButton.icon} size={QR_ICON_SIZE} />
|
|
236
|
+
</TouchableOpacity>
|
|
237
|
+
);
|
|
238
|
+
|
|
170
239
|
if (!sideBar) {
|
|
171
240
|
return null;
|
|
172
241
|
}
|
|
173
242
|
|
|
243
|
+
if (!tabBarCompressed) {
|
|
244
|
+
return (
|
|
245
|
+
<View pointerEvents={'box-none'} style={columnStyle}>
|
|
246
|
+
<Rail items={items} index={state.index} onItemPress={onTabSelected} />
|
|
247
|
+
{qr}
|
|
248
|
+
</View>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
174
252
|
return (
|
|
175
|
-
<View
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
{
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
]}
|
|
184
|
-
>
|
|
185
|
-
<View style={styles.rail} accessibilityLabel={'bottom_tab_bar'}>
|
|
253
|
+
<View pointerEvents={'box-none'} style={columnStyle}>
|
|
254
|
+
<TouchableOpacity
|
|
255
|
+
accessibilityRole={'button'}
|
|
256
|
+
accessibilityLabel={current?.label}
|
|
257
|
+
accessibilityHint={'bottom_tab_bar'}
|
|
258
|
+
onPress={expandTabBar}
|
|
259
|
+
style={styles.railCompact}
|
|
260
|
+
>
|
|
186
261
|
<BarMaterial radius={Radius.XL} />
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
styles.railBubble,
|
|
191
|
-
{ backgroundColor: theme.colors.background.selected },
|
|
192
|
-
bubbleStyle,
|
|
193
|
-
]}
|
|
194
|
-
/>
|
|
195
|
-
{items.map(item => (
|
|
196
|
-
<TouchableOpacity
|
|
197
|
-
key={item.route.key}
|
|
198
|
-
accessibilityRole={'tab'}
|
|
199
|
-
accessibilityLabel={item.label}
|
|
200
|
-
accessibilityState={{ selected: item.focused }}
|
|
201
|
-
onPress={item.onPress}
|
|
202
|
-
onLongPress={item.onLongPress}
|
|
203
|
-
style={styles.railItem}
|
|
204
|
-
>
|
|
205
|
-
{item.icon}
|
|
206
|
-
</TouchableOpacity>
|
|
207
|
-
))}
|
|
208
|
-
</View>
|
|
209
|
-
{floatingButton && (
|
|
210
|
-
<TouchableOpacity
|
|
211
|
-
accessibilityRole={'button'}
|
|
212
|
-
accessibilityLabel={floatingButton.label}
|
|
213
|
-
onPress={floatingButton.onPress}
|
|
214
|
-
style={[styles.railQr, { backgroundColor: theme.colors.primary }]}
|
|
215
|
-
>
|
|
216
|
-
<QrIcon icon={floatingButton.icon} size={QR_ICON_SIZE} />
|
|
217
|
-
</TouchableOpacity>
|
|
218
|
-
)}
|
|
262
|
+
{current?.icon}
|
|
263
|
+
</TouchableOpacity>
|
|
264
|
+
{qr}
|
|
219
265
|
</View>
|
|
220
266
|
);
|
|
221
267
|
};
|
|
@@ -347,9 +393,15 @@ const styles = StyleSheet.create({
|
|
|
347
393
|
},
|
|
348
394
|
railColumn: {
|
|
349
395
|
position: 'absolute',
|
|
350
|
-
bottom: Spacing.XL,
|
|
351
396
|
gap: Spacing.M,
|
|
352
397
|
},
|
|
398
|
+
railCompact: {
|
|
399
|
+
width: ITEM_SIZE,
|
|
400
|
+
height: ITEM_SIZE,
|
|
401
|
+
borderRadius: ITEM_SIZE / 2,
|
|
402
|
+
alignItems: 'center',
|
|
403
|
+
justifyContent: 'center',
|
|
404
|
+
},
|
|
353
405
|
rail: {
|
|
354
406
|
width: ITEM_SIZE,
|
|
355
407
|
paddingVertical: RAIL_PADDING,
|
|
@@ -28,8 +28,10 @@ import { BottomTabProps } from '../types';
|
|
|
28
28
|
import { getOptions, getStackOptions, useAppState } from '../utils';
|
|
29
29
|
import BottomTabBar from './BottomTabBar';
|
|
30
30
|
import { DuoTabBar } from './DuoTabBar';
|
|
31
|
+
import { DuoBarCompressionProvider } from './DuoBarCompression';
|
|
31
32
|
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
32
33
|
import { useScreenLayout } from '../../Layout';
|
|
34
|
+
import { getHeaderRowOptions } from '../../Layout/utils';
|
|
33
35
|
|
|
34
36
|
const Tab = createBottomTabNavigator();
|
|
35
37
|
const Stack = createStackNavigator();
|
|
@@ -38,13 +40,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
38
40
|
const { navigator } = useContext(ApplicationContext);
|
|
39
41
|
const context = useContext<any>(MiniAppContext);
|
|
40
42
|
const { nested, options, screen, initialParams } = route?.params;
|
|
41
|
-
const
|
|
42
|
-
const headerStyleOptions = {
|
|
43
|
-
headerStyle: {
|
|
44
|
-
height: headerHeight,
|
|
45
|
-
},
|
|
46
|
-
headerStatusBarHeight,
|
|
47
|
-
};
|
|
43
|
+
const headerStyleOptions = getHeaderRowOptions(useScreenLayout());
|
|
48
44
|
|
|
49
45
|
const opacityValue = 0.3;
|
|
50
46
|
const scaleValue = 0.97;
|
|
@@ -205,6 +201,7 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
205
201
|
navigation,
|
|
206
202
|
initialRouteName,
|
|
207
203
|
floatingButton,
|
|
204
|
+
compressionBehavior,
|
|
208
205
|
}) => {
|
|
209
206
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
210
207
|
const { isIPhoneDuo } = useScreenLayout();
|
|
@@ -281,147 +278,156 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
281
278
|
};
|
|
282
279
|
|
|
283
280
|
return (
|
|
284
|
-
<
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
headerBackground: undefined,
|
|
289
|
-
tabBarLabelPosition: 'below-icon',
|
|
290
|
-
tabBarBackground: () => (
|
|
291
|
-
<View
|
|
292
|
-
style={[
|
|
293
|
-
styles.container,
|
|
294
|
-
{
|
|
295
|
-
backgroundColor: theme.colors.background.surface,
|
|
296
|
-
},
|
|
297
|
-
]}
|
|
298
|
-
>
|
|
299
|
-
<Animated.View style={[styles.indicatorContainer, indicatorStyle]}>
|
|
300
|
-
<View
|
|
301
|
-
style={[
|
|
302
|
-
styles.indicator,
|
|
303
|
-
{ backgroundColor: theme.colors.primary },
|
|
304
|
-
]}
|
|
305
|
-
/>
|
|
306
|
-
</Animated.View>
|
|
307
|
-
</View>
|
|
308
|
-
),
|
|
309
|
-
}}
|
|
310
|
-
tabBar={props =>
|
|
311
|
-
isIPhoneDuo ? (
|
|
312
|
-
<DuoTabBar
|
|
313
|
-
{...props}
|
|
314
|
-
floatingButton={floatingButton}
|
|
315
|
-
inactiveTintColor={theme.colors.text.default}
|
|
316
|
-
activeTintColor={theme.colors.primary}
|
|
317
|
-
/>
|
|
318
|
-
) : (
|
|
319
|
-
<BottomTabBar
|
|
320
|
-
{...props}
|
|
321
|
-
floatingButton={floatingButton}
|
|
322
|
-
inactiveTintColor={theme.colors.text.hint}
|
|
323
|
-
activeTintColor={theme.colors.primary}
|
|
324
|
-
style={[
|
|
325
|
-
{
|
|
326
|
-
height:
|
|
327
|
-
64 +
|
|
328
|
-
(Platform.OS === 'ios'
|
|
329
|
-
? Math.min(insets.bottom, 21)
|
|
330
|
-
: insets.bottom),
|
|
331
|
-
},
|
|
332
|
-
]}
|
|
333
|
-
/>
|
|
334
|
-
)
|
|
335
|
-
}
|
|
336
|
-
initialRouteName={initialRouteName}
|
|
281
|
+
<DuoBarCompressionProvider
|
|
282
|
+
behavior={compressionBehavior}
|
|
283
|
+
tabCount={tabs.length}
|
|
284
|
+
hasQr={!!floatingButton}
|
|
337
285
|
>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
286
|
+
<Tab.Navigator
|
|
287
|
+
screenOptions={{
|
|
288
|
+
headerShown: false,
|
|
289
|
+
headerTransparent: true,
|
|
290
|
+
headerBackground: undefined,
|
|
291
|
+
tabBarLabelPosition: 'below-icon',
|
|
292
|
+
tabBarBackground: () => (
|
|
293
|
+
<View
|
|
294
|
+
style={[
|
|
295
|
+
styles.container,
|
|
296
|
+
{
|
|
297
|
+
backgroundColor: theme.colors.background.surface,
|
|
298
|
+
},
|
|
299
|
+
]}
|
|
300
|
+
>
|
|
301
|
+
<Animated.View
|
|
302
|
+
style={[styles.indicatorContainer, indicatorStyle]}
|
|
303
|
+
>
|
|
304
|
+
<View
|
|
305
|
+
style={[
|
|
306
|
+
styles.indicator,
|
|
307
|
+
{ backgroundColor: theme.colors.primary },
|
|
308
|
+
]}
|
|
309
|
+
/>
|
|
310
|
+
</Animated.View>
|
|
311
|
+
</View>
|
|
312
|
+
),
|
|
313
|
+
}}
|
|
314
|
+
tabBar={props =>
|
|
315
|
+
isIPhoneDuo ? (
|
|
316
|
+
<DuoTabBar
|
|
317
|
+
{...props}
|
|
318
|
+
floatingButton={floatingButton}
|
|
319
|
+
inactiveTintColor={theme.colors.text.default}
|
|
320
|
+
activeTintColor={theme.colors.primary}
|
|
321
|
+
/>
|
|
322
|
+
) : (
|
|
323
|
+
<BottomTabBar
|
|
324
|
+
{...props}
|
|
325
|
+
floatingButton={floatingButton}
|
|
326
|
+
inactiveTintColor={theme.colors.text.hint}
|
|
327
|
+
activeTintColor={theme.colors.primary}
|
|
328
|
+
style={[
|
|
329
|
+
{
|
|
330
|
+
height:
|
|
331
|
+
64 +
|
|
332
|
+
(Platform.OS === 'ios'
|
|
333
|
+
? Math.min(insets.bottom, 21)
|
|
334
|
+
: insets.bottom),
|
|
335
|
+
},
|
|
336
|
+
]}
|
|
337
|
+
/>
|
|
338
|
+
)
|
|
346
339
|
}
|
|
340
|
+
initialRouteName={initialRouteName}
|
|
341
|
+
>
|
|
342
|
+
{tabs.map((item, index) => {
|
|
343
|
+
const isNum = !isNaN(parseInt(`${item?.badgeLabel}`, 10));
|
|
344
|
+
const isDot =
|
|
345
|
+
typeof item?.badgeLabel === 'string' && !item?.badgeLabel;
|
|
346
|
+
const isNumZero = isNum && Number(item.badgeLabel) === 0;
|
|
347
347
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
width?: number;
|
|
353
|
-
} = {
|
|
354
|
-
marginRight: -8,
|
|
355
|
-
top: -4,
|
|
356
|
-
backgroundColor: Colors.orange_03,
|
|
357
|
-
};
|
|
348
|
+
let badgeLabel = item?.badgeLabel;
|
|
349
|
+
if (isNumZero) {
|
|
350
|
+
badgeLabel = undefined;
|
|
351
|
+
}
|
|
358
352
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
353
|
+
let badgeStyle: {
|
|
354
|
+
marginRight: number;
|
|
355
|
+
top: number;
|
|
356
|
+
backgroundColor: string;
|
|
357
|
+
width?: number;
|
|
358
|
+
} = {
|
|
359
|
+
marginRight: -8,
|
|
362
360
|
top: -4,
|
|
363
|
-
backgroundColor: Colors.red_03,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
if (isDot) {
|
|
367
|
-
badgeStyle = {
|
|
368
|
-
marginRight: -2,
|
|
369
|
-
top: -2,
|
|
370
361
|
backgroundColor: Colors.orange_03,
|
|
371
|
-
width: 12,
|
|
372
362
|
};
|
|
373
|
-
}
|
|
374
363
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
alignSelf: 'center',
|
|
412
|
-
fontFamily: exportFontFamily('bold'),
|
|
364
|
+
if (isNum) {
|
|
365
|
+
badgeStyle = {
|
|
366
|
+
marginRight: -4,
|
|
367
|
+
top: -4,
|
|
368
|
+
backgroundColor: Colors.red_03,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (isDot) {
|
|
372
|
+
badgeStyle = {
|
|
373
|
+
marginRight: -2,
|
|
374
|
+
top: -2,
|
|
375
|
+
backgroundColor: Colors.orange_03,
|
|
376
|
+
width: 12,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<Tab.Screen
|
|
382
|
+
key={`BottomTab-${item.name}`}
|
|
383
|
+
name={item.name}
|
|
384
|
+
component={TabScreen}
|
|
385
|
+
initialParams={{ ...item, nested, index }}
|
|
386
|
+
listeners={handler}
|
|
387
|
+
options={{
|
|
388
|
+
tabBarLabel: ({ focused, color }) => {
|
|
389
|
+
return (
|
|
390
|
+
<Text
|
|
391
|
+
numberOfLines={1}
|
|
392
|
+
typography={
|
|
393
|
+
focused ? 'label_xs_medium' : 'description_xs_regular'
|
|
394
|
+
}
|
|
395
|
+
color={color}
|
|
396
|
+
>
|
|
397
|
+
{item.label}
|
|
398
|
+
</Text>
|
|
399
|
+
);
|
|
413
400
|
},
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
401
|
+
title: item.label,
|
|
402
|
+
tabBarStyle: [
|
|
403
|
+
{
|
|
404
|
+
height: 64,
|
|
405
|
+
},
|
|
406
|
+
styles.tabStyle,
|
|
407
|
+
],
|
|
408
|
+
tabBarBadge: badgeLabel,
|
|
409
|
+
tabBarBadgeStyle: [
|
|
410
|
+
{
|
|
411
|
+
borderColor: 'white',
|
|
412
|
+
borderWidth: 2,
|
|
413
|
+
fontSize: 10,
|
|
414
|
+
lineHeight: 14,
|
|
415
|
+
fontWeight: 'bold',
|
|
416
|
+
alignSelf: 'center',
|
|
417
|
+
fontFamily: exportFontFamily('bold'),
|
|
418
|
+
},
|
|
419
|
+
badgeStyle,
|
|
420
|
+
],
|
|
421
|
+
tabBarIcon: ({ color }) => (
|
|
422
|
+
<Icon source={item.icon} color={color} size={28} />
|
|
423
|
+
),
|
|
424
|
+
...item,
|
|
425
|
+
}}
|
|
426
|
+
/>
|
|
427
|
+
);
|
|
428
|
+
})}
|
|
429
|
+
</Tab.Navigator>
|
|
430
|
+
</DuoBarCompressionProvider>
|
|
425
431
|
);
|
|
426
432
|
};
|
|
427
433
|
|