@momo-kits/foundation 0.169.9-beta.1 → 0.169.9
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 +27 -17
- package/Application/BottomSheet.tsx +35 -27
- package/Application/BottomTab/ActiveIndicator.tsx +5 -1
- package/Application/BottomTab/BottomTabBar.styles.ts +18 -1
- package/Application/BottomTab/BottomTabBar.tsx +14 -16
- package/Application/BottomTab/CurvedContainer.tsx +8 -2
- package/Application/BottomTab/CustomBottomTabItem.tsx +2 -2
- package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
- package/Application/BottomTab/DuoTabBar.tsx +15 -3
- package/Application/BottomTab/TabScreen.tsx +11 -10
- package/Application/BottomTab/index.tsx +5 -5
- package/Application/BottomTab/metrics.ts +23 -3
- package/Application/BottomTab/usePressLift.ts +3 -3
- package/Application/Components/HeaderAnimated.tsx +13 -38
- package/Application/Components/HeaderBackground.tsx +3 -16
- package/Application/Components/HeaderExtendHeader.tsx +28 -41
- package/Application/Components/HeaderLeft.tsx +18 -4
- package/Application/Components/HeaderRight.styles.ts +13 -15
- package/Application/Components/HeaderTitle.shared.ts +11 -10
- package/Application/Components/HeaderToolkitAction.tsx +54 -21
- package/Application/Components/NavigationButton.tsx +6 -9
- package/Application/Components/SearchHeader.tsx +7 -20
- package/Application/Components/useToolkitActions.tsx +22 -21
- package/Application/ModalScreen.tsx +19 -15
- package/Application/StackScreen.tsx +13 -18
- package/Application/index.ts +1 -11
- package/Application/modalLayout.ts +1 -0
- package/Application/navigationOptions.tsx +15 -6
- package/Application/types.header.ts +2 -7
- package/Application/types.tabs.ts +0 -1
- package/Application/types.ts +0 -7
- package/Context/index.ts +3 -1
- package/Image/index.tsx +6 -6
- package/Input/ErrorView.tsx +59 -0
- package/Input/FloatingView.tsx +101 -0
- package/Input/Input.tsx +40 -37
- package/Input/InputDropDown.tsx +13 -10
- package/Input/InputMoney.tsx +17 -17
- package/Input/InputMoneyView.tsx +12 -12
- package/Input/InputOTP.tsx +46 -34
- package/Input/InputPhoneNumber.tsx +48 -43
- package/Input/InputSearch.tsx +26 -39
- package/Input/InputTextArea.tsx +29 -23
- package/Input/RenderTrailing.tsx +79 -0
- package/Input/constants.ts +8 -0
- package/Input/index.tsx +1 -7
- package/Input/utils.ts +126 -7
- package/Layout/FloatingButton.tsx +38 -24
- package/Layout/Screen.tsx +0 -3
- package/Layout/layoutMetrics.ts +43 -34
- package/Layout/screenHeader.tsx +52 -55
- package/Layout/screenTypes.ts +4 -13
- package/Pagination/PaginationScroll.tsx +5 -3
- package/Pagination/types.ts +3 -5
- package/Popup/PopupNotify.tsx +17 -14
- package/Popup/PopupPromotion.tsx +5 -4
- package/Popup/types.ts +2 -4
- package/Popup/utils.ts +7 -4
- package/Skeleton/index.tsx +15 -12
- package/package.json +1 -1
- package/Application/headerLayout.ts +0 -165
- package/Input/common.tsx +0 -257
- package/Input/moneyUtils.ts +0 -23
|
@@ -21,6 +21,8 @@ import { BottomSheetParams, NavigatorProps } from './types';
|
|
|
21
21
|
import { isIPhoneDuo } from './utils';
|
|
22
22
|
import { useModalLayout } from './modalLayout';
|
|
23
23
|
|
|
24
|
+
const MAX_IOS_BOTTOM_INSET = 21;
|
|
25
|
+
|
|
24
26
|
/**
|
|
25
27
|
* Sheet geometry for the current window: foldable placement and size from
|
|
26
28
|
* `useModalLayout`, plus the insets and offsets the keyboard handling needs.
|
|
@@ -35,7 +37,10 @@ export const useBottomSheetLayout = () => {
|
|
|
35
37
|
const insets = useSafeAreaInsets();
|
|
36
38
|
const heightHeader = useHeaderHeight();
|
|
37
39
|
const isIOS = Platform.OS === 'ios';
|
|
38
|
-
|
|
40
|
+
let bottomInset = insets.bottom;
|
|
41
|
+
if (isIOS) {
|
|
42
|
+
bottomInset = Math.min(insets.bottom, MAX_IOS_BOTTOM_INSET);
|
|
43
|
+
}
|
|
39
44
|
const keyboardOffset = heightHeader - bottomInset;
|
|
40
45
|
|
|
41
46
|
return {
|
|
@@ -68,7 +73,7 @@ export const useDuoKeyboardLift = (
|
|
|
68
73
|
if (!isDuo || !useKeyboardAvoidingView) {
|
|
69
74
|
return;
|
|
70
75
|
}
|
|
71
|
-
const
|
|
76
|
+
const handleKeyboardChange = (e: KeyboardEvent) => {
|
|
72
77
|
const overlap = windowHeight - e.endCoordinates.screenY;
|
|
73
78
|
const lift = Math.max(
|
|
74
79
|
overlap - bottomInset - (keyboardVerticalOffset ?? 0),
|
|
@@ -86,8 +91,8 @@ export const useDuoKeyboardLift = (
|
|
|
86
91
|
setKeyboardLift(lift);
|
|
87
92
|
};
|
|
88
93
|
const subscriptions = [
|
|
89
|
-
Keyboard.addListener('keyboardWillChangeFrame',
|
|
90
|
-
Keyboard.addListener('keyboardWillHide',
|
|
94
|
+
Keyboard.addListener('keyboardWillChangeFrame', handleKeyboardChange),
|
|
95
|
+
Keyboard.addListener('keyboardWillHide', handleKeyboardChange),
|
|
91
96
|
];
|
|
92
97
|
return () => subscriptions.forEach(sub => sub.remove());
|
|
93
98
|
}, [
|
|
@@ -149,7 +154,7 @@ export const useSheetDismiss = ({
|
|
|
149
154
|
leftOptions,
|
|
150
155
|
action,
|
|
151
156
|
}: SheetDismissParams) => {
|
|
152
|
-
const
|
|
157
|
+
const handleDismiss = useCallback(
|
|
153
158
|
(force = false, callback?: () => void) => {
|
|
154
159
|
if (barrierDismissible && !force) {
|
|
155
160
|
return;
|
|
@@ -164,32 +169,37 @@ export const useSheetDismiss = ({
|
|
|
164
169
|
[barrierDismissible, closeAnimation, navigator],
|
|
165
170
|
);
|
|
166
171
|
|
|
167
|
-
const
|
|
172
|
+
const handleRequestClose = useCallback(
|
|
168
173
|
(callback?: () => void) => {
|
|
169
|
-
|
|
174
|
+
handleDismiss(true, callback);
|
|
170
175
|
},
|
|
171
|
-
[
|
|
176
|
+
[handleDismiss],
|
|
172
177
|
);
|
|
173
178
|
|
|
174
179
|
useEffect(() => {
|
|
175
180
|
const backHandler = BackHandler.addEventListener(
|
|
176
181
|
'hardwareBackPress',
|
|
177
182
|
() => {
|
|
178
|
-
|
|
183
|
+
handleDismiss();
|
|
179
184
|
return true;
|
|
180
185
|
},
|
|
181
186
|
);
|
|
182
187
|
return () => backHandler.remove();
|
|
183
|
-
}, [barrierDismissible,
|
|
188
|
+
}, [barrierDismissible, handleDismiss]);
|
|
184
189
|
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
}, [leftOptions,
|
|
190
|
+
const handlePressBack = useCallback(() => {
|
|
191
|
+
handleDismiss(true, leftOptions?.onPressIconLeft);
|
|
192
|
+
}, [leftOptions, handleDismiss]);
|
|
188
193
|
|
|
189
|
-
const
|
|
194
|
+
const handlePressClose = useCallback(() => {
|
|
190
195
|
action.current = 'icon_close';
|
|
191
|
-
|
|
192
|
-
}, [action,
|
|
196
|
+
handleDismiss(true);
|
|
197
|
+
}, [action, handleDismiss]);
|
|
193
198
|
|
|
194
|
-
return {
|
|
199
|
+
return {
|
|
200
|
+
handleDismiss,
|
|
201
|
+
handleRequestClose,
|
|
202
|
+
handlePressBack,
|
|
203
|
+
handlePressClose,
|
|
204
|
+
};
|
|
195
205
|
};
|
|
@@ -30,6 +30,23 @@ import {
|
|
|
30
30
|
renderSheetHeader,
|
|
31
31
|
} from './BottomSheet.parts';
|
|
32
32
|
|
|
33
|
+
const OPEN_TIMING = {
|
|
34
|
+
duration: 350,
|
|
35
|
+
easing: Easing.bezier(0.05, 0.7, 0.1, 1),
|
|
36
|
+
};
|
|
37
|
+
const CLOSE_TIMING = {
|
|
38
|
+
duration: 200,
|
|
39
|
+
easing: Easing.bezier(0.3, 0.0, 0.8, 0.15),
|
|
40
|
+
};
|
|
41
|
+
const SETTLE_SPRING = {
|
|
42
|
+
damping: 20,
|
|
43
|
+
mass: 1,
|
|
44
|
+
stiffness: 100,
|
|
45
|
+
overshootClamping: false,
|
|
46
|
+
energyThreshold: 1e-10,
|
|
47
|
+
};
|
|
48
|
+
const DISMISS_DRAG_DISTANCE = 100;
|
|
49
|
+
|
|
33
50
|
/**
|
|
34
51
|
* Bottom sheet route of the modal stack: slides the given screen up under a
|
|
35
52
|
* drag handle and header, and reports how it was closed through `onDismiss`.
|
|
@@ -79,22 +96,14 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
79
96
|
);
|
|
80
97
|
|
|
81
98
|
const openAnimation = useCallback(() => {
|
|
82
|
-
translateY.value = withTiming(0,
|
|
83
|
-
duration: 350,
|
|
84
|
-
easing: Easing.bezier(0.05, 0.7, 0.1, 1),
|
|
85
|
-
});
|
|
99
|
+
translateY.value = withTiming(0, OPEN_TIMING);
|
|
86
100
|
}, [translateY]);
|
|
87
101
|
|
|
88
102
|
const closeAnimation = useCallback(
|
|
89
103
|
(callback = () => {}) => {
|
|
90
|
-
translateY.value = withTiming(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
duration: 200,
|
|
94
|
-
easing: Easing.bezier(0.3, 0.0, 0.8, 0.15),
|
|
95
|
-
},
|
|
96
|
-
() => scheduleOnRN(callback),
|
|
97
|
-
);
|
|
104
|
+
translateY.value = withTiming(heightDevice, CLOSE_TIMING, () => {
|
|
105
|
+
return scheduleOnRN(callback);
|
|
106
|
+
});
|
|
98
107
|
},
|
|
99
108
|
[heightDevice, translateY],
|
|
100
109
|
);
|
|
@@ -111,17 +120,11 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
111
120
|
}
|
|
112
121
|
},
|
|
113
122
|
onPanResponderRelease: (_, gestureState) => {
|
|
114
|
-
if (gestureState.dy >
|
|
123
|
+
if (gestureState.dy > DISMISS_DRAG_DISTANCE) {
|
|
115
124
|
action.current = 'gesture';
|
|
116
|
-
|
|
125
|
+
handleDismiss(true);
|
|
117
126
|
} else {
|
|
118
|
-
translateY.value = withSpring(0,
|
|
119
|
-
damping: 20,
|
|
120
|
-
mass: 1,
|
|
121
|
-
stiffness: 100,
|
|
122
|
-
overshootClamping: false,
|
|
123
|
-
energyThreshold: 1e-10,
|
|
124
|
-
});
|
|
127
|
+
translateY.value = withSpring(0, SETTLE_SPRING);
|
|
125
128
|
}
|
|
126
129
|
},
|
|
127
130
|
}),
|
|
@@ -151,7 +154,12 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
151
154
|
leftOptions,
|
|
152
155
|
action,
|
|
153
156
|
});
|
|
154
|
-
const {
|
|
157
|
+
const {
|
|
158
|
+
handleDismiss,
|
|
159
|
+
handleRequestClose,
|
|
160
|
+
handlePressBack,
|
|
161
|
+
handlePressClose,
|
|
162
|
+
} = handlers;
|
|
155
163
|
|
|
156
164
|
const showBack = !!leftOptions?.onPressIconLeft;
|
|
157
165
|
const iconBack = leftOptions?.iconLeft ?? 'ic_back';
|
|
@@ -161,8 +169,8 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
161
169
|
useDivider,
|
|
162
170
|
theme,
|
|
163
171
|
showBack,
|
|
164
|
-
onPressBack,
|
|
165
|
-
onPressClose,
|
|
172
|
+
onPressBack: handlePressBack,
|
|
173
|
+
onPressClose: handlePressClose,
|
|
166
174
|
iconBack,
|
|
167
175
|
options,
|
|
168
176
|
leftOptions,
|
|
@@ -178,7 +186,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
178
186
|
<Screen
|
|
179
187
|
{...props}
|
|
180
188
|
{...props.route.params}
|
|
181
|
-
onRequestClose={
|
|
189
|
+
onRequestClose={handleRequestClose}
|
|
182
190
|
/>
|
|
183
191
|
);
|
|
184
192
|
|
|
@@ -205,7 +213,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
205
213
|
transparent
|
|
206
214
|
visible={true}
|
|
207
215
|
onRequestClose={() => {
|
|
208
|
-
|
|
216
|
+
handleDismiss();
|
|
209
217
|
}}
|
|
210
218
|
style={StyleSheet.absoluteFillObject}
|
|
211
219
|
isModalKit={true}
|
|
@@ -225,7 +233,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
225
233
|
style={StyleSheet.absoluteFillObject}
|
|
226
234
|
onPress={() => {
|
|
227
235
|
action.current = 'touch';
|
|
228
|
-
|
|
236
|
+
handleDismiss();
|
|
229
237
|
}}
|
|
230
238
|
>
|
|
231
239
|
<Animated.View style={[styles.overlay, animatedOverlayStyle]} />
|
|
@@ -5,6 +5,7 @@ import Animated, {
|
|
|
5
5
|
useSharedValue,
|
|
6
6
|
withTiming,
|
|
7
7
|
} from 'react-native-reanimated';
|
|
8
|
+
import { defaultTheme } from '../../Consts';
|
|
8
9
|
import { ApplicationContext } from '../../Context';
|
|
9
10
|
import { getIndicatorShadow, TabBarMotion } from './metrics';
|
|
10
11
|
|
|
@@ -70,7 +71,10 @@ const ActiveIndicator: React.FC<Props> = ({
|
|
|
70
71
|
styles.indicator,
|
|
71
72
|
{
|
|
72
73
|
borderRadius: radius,
|
|
73
|
-
|
|
74
|
+
// a mini app's own theme may lack `tonal`; without the fallback the indicator is transparent
|
|
75
|
+
backgroundColor:
|
|
76
|
+
theme.colors.background.tonal ??
|
|
77
|
+
defaultTheme.colors.background.tonal,
|
|
74
78
|
boxShadow: getIndicatorShadow(theme.colors.primary),
|
|
75
79
|
},
|
|
76
80
|
style,
|
|
@@ -2,6 +2,20 @@ import { StyleSheet } from 'react-native';
|
|
|
2
2
|
import { Colors, Spacing } from '../../Consts';
|
|
3
3
|
import { CTA_SHADOW, TabBarMetrics } from './metrics';
|
|
4
4
|
|
|
5
|
+
const ITEM_HEIGHT =
|
|
6
|
+
TabBarMetrics.indicatorHeight +
|
|
7
|
+
TabBarMetrics.itemIconLabelGap +
|
|
8
|
+
TabBarMetrics.labelHeight;
|
|
9
|
+
const CONTENT_HEIGHT = TabBarMetrics.height - 2 * TabBarMetrics.paddingVertical;
|
|
10
|
+
// the items are centered in the content, so their label starts this far below the content top
|
|
11
|
+
const ITEM_LABEL_TOP =
|
|
12
|
+
(CONTENT_HEIGHT - ITEM_HEIGHT) / 2 +
|
|
13
|
+
TabBarMetrics.indicatorHeight +
|
|
14
|
+
TabBarMetrics.itemIconLabelGap;
|
|
15
|
+
// the center action column is placed so its label starts on the same line as the tab labels
|
|
16
|
+
const CTA_TOP =
|
|
17
|
+
ITEM_LABEL_TOP - TabBarMetrics.ctaLabelGap - TabBarMetrics.ctaSize;
|
|
18
|
+
|
|
5
19
|
export const styles = StyleSheet.create({
|
|
6
20
|
bar: {
|
|
7
21
|
position: 'absolute',
|
|
@@ -37,7 +51,7 @@ export const styles = StyleSheet.create({
|
|
|
37
51
|
position: 'absolute',
|
|
38
52
|
left: 0,
|
|
39
53
|
right: 0,
|
|
40
|
-
top:
|
|
54
|
+
top: CTA_TOP,
|
|
41
55
|
alignItems: 'center',
|
|
42
56
|
gap: TabBarMetrics.ctaLabelGap,
|
|
43
57
|
},
|
|
@@ -46,6 +60,9 @@ export const styles = StyleSheet.create({
|
|
|
46
60
|
width: TabBarMetrics.ctaSize,
|
|
47
61
|
height: TabBarMetrics.ctaSize,
|
|
48
62
|
borderRadius: TabBarMetrics.ctaSize / 2,
|
|
63
|
+
// Figma Icon Button border: white 60% hairline
|
|
64
|
+
borderWidth: 1,
|
|
65
|
+
borderColor: `rgba(255, 255, 255, ${TabBarMetrics.ctaBorderOpacity})`,
|
|
49
66
|
alignItems: 'center',
|
|
50
67
|
justifyContent: 'center',
|
|
51
68
|
},
|
|
@@ -54,16 +54,16 @@ const useCenterActionPress = () => {
|
|
|
54
54
|
const buttonStyle = useAnimatedStyle(() => ({
|
|
55
55
|
transform: [{ scale: scale.value }],
|
|
56
56
|
}));
|
|
57
|
-
const
|
|
57
|
+
const handlePressIn = () => {
|
|
58
58
|
scale.value = withTiming(
|
|
59
59
|
TabBarMotion.ctaPressedScale,
|
|
60
60
|
TabBarMotion.ctaPress,
|
|
61
61
|
);
|
|
62
62
|
};
|
|
63
|
-
const
|
|
63
|
+
const handlePressOut = () => {
|
|
64
64
|
scale.value = withTiming(1, TabBarMotion.ctaPress);
|
|
65
65
|
};
|
|
66
|
-
return { buttonStyle,
|
|
66
|
+
return { buttonStyle, handlePressIn, handlePressOut };
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
/**
|
|
@@ -77,7 +77,7 @@ const CenterAction: React.FC<{ floatingButton: FloatingButtonProps }> = ({
|
|
|
77
77
|
const context = useContext<any>(MiniAppContext);
|
|
78
78
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
79
79
|
const { icon, label, onPress, labelContent } = floatingButton;
|
|
80
|
-
const { buttonStyle,
|
|
80
|
+
const { buttonStyle, handlePressIn, handlePressOut } = useCenterActionPress();
|
|
81
81
|
let iconNode = icon;
|
|
82
82
|
if (typeof icon === 'string') {
|
|
83
83
|
iconNode = (
|
|
@@ -95,8 +95,8 @@ const CenterAction: React.FC<{ floatingButton: FloatingButtonProps }> = ({
|
|
|
95
95
|
accessibilityRole={'button'}
|
|
96
96
|
accessibilityLabel={label}
|
|
97
97
|
onPress={onPress}
|
|
98
|
-
onPressIn={
|
|
99
|
-
onPressOut={
|
|
98
|
+
onPressIn={handlePressIn}
|
|
99
|
+
onPressOut={handlePressOut}
|
|
100
100
|
style={styles.cta}
|
|
101
101
|
>
|
|
102
102
|
<Animated.View
|
|
@@ -181,21 +181,17 @@ const HorizontalTabBar: React.FC<{
|
|
|
181
181
|
return renderItems(items);
|
|
182
182
|
};
|
|
183
183
|
|
|
184
|
-
const customContainer = withCta && floatingButton?.container;
|
|
185
|
-
|
|
186
184
|
return (
|
|
187
185
|
<View
|
|
188
186
|
style={[styles.container, { width }]}
|
|
189
187
|
accessibilityLabel={'bottom_tab_bar'}
|
|
190
188
|
>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
/>
|
|
198
|
-
)}
|
|
189
|
+
<CurvedContainer
|
|
190
|
+
width={width}
|
|
191
|
+
height={TabBarMetrics.height}
|
|
192
|
+
radius={TabBarMetrics.height / 2}
|
|
193
|
+
notchCenterX={withCta ? width / 2 : undefined}
|
|
194
|
+
/>
|
|
199
195
|
<View style={styles.content}>
|
|
200
196
|
<ActiveIndicator
|
|
201
197
|
axis={'x'}
|
|
@@ -228,6 +224,8 @@ export default function BottomTabBar(props: Props) {
|
|
|
228
224
|
window.height,
|
|
229
225
|
insets.left,
|
|
230
226
|
insets.right,
|
|
227
|
+
state.routes.length,
|
|
228
|
+
withCta,
|
|
231
229
|
);
|
|
232
230
|
|
|
233
231
|
return (
|
|
@@ -3,7 +3,7 @@ import { StyleSheet } from 'react-native';
|
|
|
3
3
|
import Svg, { Defs, FeGaussianBlur, Filter, Path } from 'react-native-svg';
|
|
4
4
|
import { Colors } from '../../Consts';
|
|
5
5
|
import { ApplicationContext } from '../../Context';
|
|
6
|
-
import { NOTCH_CURVES, NOTCH_HALF_WIDTH } from './metrics';
|
|
6
|
+
import { NOTCH_CURVES, NOTCH_HALF_WIDTH, TabBarMetrics } from './metrics';
|
|
7
7
|
|
|
8
8
|
type Props = {
|
|
9
9
|
width: number;
|
|
@@ -139,7 +139,13 @@ const CurvedContainer: React.FC<Props> = ({
|
|
|
139
139
|
filter={'url(#keyShadow)'}
|
|
140
140
|
/>
|
|
141
141
|
)}
|
|
142
|
-
<Path
|
|
142
|
+
<Path
|
|
143
|
+
d={path}
|
|
144
|
+
fill={theme.colors.background.surface}
|
|
145
|
+
fillOpacity={TabBarMetrics.backgroundOpacity}
|
|
146
|
+
stroke={theme.colors.border.default}
|
|
147
|
+
strokeWidth={1}
|
|
148
|
+
/>
|
|
143
149
|
</Svg>
|
|
144
150
|
);
|
|
145
151
|
};
|
|
@@ -13,7 +13,7 @@ import Animated, {
|
|
|
13
13
|
import { TabBarIcon } from './TabBarIcon';
|
|
14
14
|
import { Text } from '../../Text';
|
|
15
15
|
import { MiniAppContext } from '../../Context';
|
|
16
|
-
import { Colors
|
|
16
|
+
import { Colors } from '../../Consts';
|
|
17
17
|
import { TabBarMetrics, TabBarMotion } from './metrics';
|
|
18
18
|
|
|
19
19
|
type IconRenderer = (props: {
|
|
@@ -143,7 +143,7 @@ const styles = StyleSheet.create({
|
|
|
143
143
|
minWidth: 1,
|
|
144
144
|
alignItems: 'center',
|
|
145
145
|
justifyContent: 'center',
|
|
146
|
-
gap:
|
|
146
|
+
gap: TabBarMetrics.itemIconLabelGap,
|
|
147
147
|
},
|
|
148
148
|
slot: {
|
|
149
149
|
height: TabBarMetrics.indicatorHeight,
|
|
@@ -94,14 +94,14 @@ const RailCenterAction: React.FC<{ floatingButton: FloatingButtonProps }> = ({
|
|
|
94
94
|
floatingButton,
|
|
95
95
|
}) => {
|
|
96
96
|
const { theme } = useContext(ApplicationContext);
|
|
97
|
-
const { buttonStyle,
|
|
97
|
+
const { buttonStyle, handlePressIn, handlePressOut } = useCenterActionPress();
|
|
98
98
|
return (
|
|
99
99
|
<Pressable
|
|
100
100
|
accessibilityRole={'button'}
|
|
101
101
|
accessibilityLabel={floatingButton.label}
|
|
102
102
|
onPress={floatingButton.onPress}
|
|
103
|
-
onPressIn={
|
|
104
|
-
onPressOut={
|
|
103
|
+
onPressIn={handlePressIn}
|
|
104
|
+
onPressOut={handlePressOut}
|
|
105
105
|
style={styles.railCta}
|
|
106
106
|
>
|
|
107
107
|
<Animated.View
|
|
@@ -190,8 +190,8 @@ const DuoRailTabBar: React.FC<DuoTabBarProps> = props => {
|
|
|
190
190
|
accessibilityLabel={current?.label}
|
|
191
191
|
accessibilityHint={'bottom_tab_bar'}
|
|
192
192
|
activeOpacity={1}
|
|
193
|
-
onPressIn={compactLift.
|
|
194
|
-
onPressOut={compactLift.
|
|
193
|
+
onPressIn={compactLift.handlePressIn}
|
|
194
|
+
onPressOut={compactLift.handlePressOut}
|
|
195
195
|
onPress={expandTabBar}
|
|
196
196
|
style={styles.railCompact}
|
|
197
197
|
>
|
|
@@ -3,7 +3,11 @@ import React from 'react';
|
|
|
3
3
|
import { useWindowDimensions, View } from 'react-native';
|
|
4
4
|
import { useScreenLayout } from '../../Layout/utils';
|
|
5
5
|
import { FloatingButtonProps } from '../types';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getSystemIndicatorHeight,
|
|
8
|
+
hasCenterAction,
|
|
9
|
+
HorizontalTabBar,
|
|
10
|
+
} from './BottomTabBar';
|
|
7
11
|
import { getHorizontalBarWidth, TabBarMetrics } from './metrics';
|
|
8
12
|
import { getTabItems } from './tabItems';
|
|
9
13
|
import { DuoRailTabBar } from './DuoRailTabBar';
|
|
@@ -34,6 +38,7 @@ const DuoFloatingTabBar: React.FC<DuoTabBarProps> = props => {
|
|
|
34
38
|
const { insets } = useScreenLayout();
|
|
35
39
|
const { width, height } = useWindowDimensions();
|
|
36
40
|
const { floatingButton, activeTintColor, inactiveTintColor } = props;
|
|
41
|
+
const tabItems = getTabItems(props);
|
|
37
42
|
|
|
38
43
|
return (
|
|
39
44
|
<View
|
|
@@ -44,8 +49,15 @@ const DuoFloatingTabBar: React.FC<DuoTabBarProps> = props => {
|
|
|
44
49
|
]}
|
|
45
50
|
>
|
|
46
51
|
<HorizontalTabBar
|
|
47
|
-
items={
|
|
48
|
-
width={getHorizontalBarWidth(
|
|
52
|
+
items={tabItems}
|
|
53
|
+
width={getHorizontalBarWidth(
|
|
54
|
+
width,
|
|
55
|
+
height,
|
|
56
|
+
insets.left,
|
|
57
|
+
insets.right,
|
|
58
|
+
tabItems.length,
|
|
59
|
+
hasCenterAction(floatingButton, tabItems.length),
|
|
60
|
+
)}
|
|
49
61
|
floatingButton={floatingButton}
|
|
50
62
|
activeTintColor={activeTintColor}
|
|
51
63
|
inactiveTintColor={inactiveTintColor}
|
|
@@ -15,6 +15,8 @@ import { useScreenLayout } from '../../Layout';
|
|
|
15
15
|
import { getHeaderRowOptions } from '../../Layout/utils';
|
|
16
16
|
import { BottomTabInsetContext } from './BottomTabInset';
|
|
17
17
|
|
|
18
|
+
const NAVIGATED_REPORT_DELAY = 100;
|
|
19
|
+
|
|
18
20
|
const Stack = createStackNavigator();
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -29,16 +31,15 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
29
31
|
|
|
30
32
|
const screenName = screen?.name || screen?.type?.name || 'Invalid';
|
|
31
33
|
const { isBackgroundToForeground } = useAppState();
|
|
32
|
-
// Pad clear of the floating bar unless a Screen inside claims the inset to scroll under it.
|
|
33
34
|
const { inset: tabBarInset } = useContext(BottomTabInsetContext);
|
|
34
|
-
const [
|
|
35
|
+
const [isInsetClaimed, setIsInsetClaimed] = useState(false);
|
|
35
36
|
const tabInsetValue = useMemo(
|
|
36
|
-
() => ({ inset: tabBarInset, claim: () =>
|
|
37
|
+
() => ({ inset: tabBarInset, claim: () => setIsInsetClaimed(true) }),
|
|
37
38
|
[tabBarInset],
|
|
38
39
|
);
|
|
39
|
-
const tabInsetStyle = { paddingBottom:
|
|
40
|
+
const tabInsetStyle = { paddingBottom: isInsetClaimed ? 0 : tabBarInset };
|
|
40
41
|
|
|
41
|
-
const
|
|
42
|
+
const handleScreenNavigated = (pre: string, current: string) => {
|
|
42
43
|
if (!isBackgroundToForeground) {
|
|
43
44
|
const data: any = {
|
|
44
45
|
preScreenName: pre,
|
|
@@ -68,13 +69,13 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
68
69
|
navigator?.maxApi?.getDataObserver?.(
|
|
69
70
|
'current_screen',
|
|
70
71
|
(data: any) => {
|
|
71
|
-
|
|
72
|
+
handleScreenNavigated(data?.screenName, screenName);
|
|
72
73
|
navigator?.maxApi?.setObserver?.('current_screen', {
|
|
73
74
|
screenName,
|
|
74
75
|
});
|
|
75
76
|
},
|
|
76
77
|
);
|
|
77
|
-
},
|
|
78
|
+
}, NAVIGATED_REPORT_DELAY);
|
|
78
79
|
},
|
|
79
80
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
81
|
[],
|
|
@@ -83,12 +84,12 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
83
84
|
|
|
84
85
|
useEffect(
|
|
85
86
|
() => {
|
|
86
|
-
const
|
|
87
|
+
const focusAppListener = navigator?.maxApi?.listen?.('onFocusApp', () => {
|
|
87
88
|
if (navigation.isFocused()) {
|
|
88
89
|
navigator?.maxApi?.getDataObserver?.(
|
|
89
90
|
'current_screen',
|
|
90
91
|
(data: any) => {
|
|
91
|
-
|
|
92
|
+
handleScreenNavigated(data?.screenName, screenName);
|
|
92
93
|
navigator?.maxApi?.setObserver?.('current_screen', {
|
|
93
94
|
screenName,
|
|
94
95
|
});
|
|
@@ -98,7 +99,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
98
99
|
});
|
|
99
100
|
|
|
100
101
|
return () => {
|
|
101
|
-
|
|
102
|
+
focusAppListener?.remove?.();
|
|
102
103
|
};
|
|
103
104
|
},
|
|
104
105
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -99,16 +99,16 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
99
99
|
}, []),
|
|
100
100
|
);
|
|
101
101
|
|
|
102
|
-
const
|
|
102
|
+
const handleFocus = (e: any) => {
|
|
103
103
|
activeIndex.current = tabs.findIndex(i => e.target.includes(i.name));
|
|
104
104
|
|
|
105
105
|
const isFirstTab = activeIndex.current === 0;
|
|
106
|
-
const
|
|
106
|
+
const isGestureEnabled = Platform.OS === 'ios' && isFirstTab;
|
|
107
107
|
navigation?.setOptions({
|
|
108
|
-
gestureEnabled:
|
|
108
|
+
gestureEnabled: isGestureEnabled,
|
|
109
109
|
});
|
|
110
110
|
NativeModules?.MiniAppModule?.setShouldUseFloatingArrowBack?.(
|
|
111
|
-
!
|
|
111
|
+
!isGestureEnabled,
|
|
112
112
|
);
|
|
113
113
|
};
|
|
114
114
|
|
|
@@ -123,7 +123,7 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
123
123
|
listeners?.tabPress?.(e);
|
|
124
124
|
},
|
|
125
125
|
focus: e => {
|
|
126
|
-
|
|
126
|
+
handleFocus(e);
|
|
127
127
|
StatusBar.setBarStyle('dark-content', true);
|
|
128
128
|
listeners?.focus?.(e);
|
|
129
129
|
},
|
|
@@ -8,11 +8,18 @@ import { Colors, Radius, Spacing } from '../../Consts';
|
|
|
8
8
|
* iPhone Duo rail (center action under the container).
|
|
9
9
|
*/
|
|
10
10
|
const TabBarMetrics = {
|
|
11
|
-
height:
|
|
11
|
+
height: 60,
|
|
12
12
|
margin: Spacing.S,
|
|
13
|
-
paddingVertical: Spacing.
|
|
13
|
+
paddingVertical: Spacing.XS,
|
|
14
14
|
paddingHorizontal: Spacing.M,
|
|
15
|
-
itemGap:
|
|
15
|
+
itemGap: 1,
|
|
16
|
+
// gap between the icon slot and the label inside a tab (Figma: none)
|
|
17
|
+
itemIconLabelGap: 0,
|
|
18
|
+
// .Nav/Background: no backdrop blur, so use the Figma Blur=Off spec (opacity 95%, no blur) plus the 1px
|
|
19
|
+
// border — nearly solid, matching the frosted sample; surface keeps dark-mode support
|
|
20
|
+
backgroundOpacity: 0.95,
|
|
21
|
+
// white 60% hairline around the center action button (Figma Icon Button border)
|
|
22
|
+
ctaBorderOpacity: 0.6,
|
|
16
23
|
iconSize: 24,
|
|
17
24
|
// active indicator behind the icon: 24 icon + XS·M padding
|
|
18
25
|
indicatorPaddingVertical: Spacing.XS,
|
|
@@ -25,6 +32,9 @@ const TabBarMetrics = {
|
|
|
25
32
|
systemIndicatorHeight: 24,
|
|
26
33
|
foldableMinShortSide: 600,
|
|
27
34
|
foldableWidthRatio: 0.65,
|
|
35
|
+
// a 2-tab bar doesn't stretch to the full width: each item is this fixed width and the bar is
|
|
36
|
+
// sized to its content, centered (Figma Navigation · 2 Tab: item 100, bar ~225)
|
|
37
|
+
twoTabItemWidth: 100,
|
|
28
38
|
ctaSlotWidth: 92,
|
|
29
39
|
ctaSize: 48,
|
|
30
40
|
ctaIconSize: 28,
|
|
@@ -132,7 +142,17 @@ const getHorizontalBarWidth = (
|
|
|
132
142
|
height: number,
|
|
133
143
|
insetLeft: number,
|
|
134
144
|
insetRight: number,
|
|
145
|
+
itemCount: number,
|
|
146
|
+
hasCta: boolean,
|
|
135
147
|
) => {
|
|
148
|
+
// 2 tabs (no center action) stay content-sized and centered instead of filling the window
|
|
149
|
+
if (itemCount === 2 && !hasCta) {
|
|
150
|
+
return (
|
|
151
|
+
TabBarMetrics.twoTabItemWidth * 2 +
|
|
152
|
+
TabBarMetrics.paddingHorizontal * 2 +
|
|
153
|
+
TabBarMetrics.itemGap
|
|
154
|
+
);
|
|
155
|
+
}
|
|
136
156
|
const shortSide = Math.min(width, height);
|
|
137
157
|
if (shortSide >= TabBarMetrics.foldableMinShortSide) {
|
|
138
158
|
return shortSide * TabBarMetrics.foldableWidthRatio;
|
|
@@ -18,15 +18,15 @@ const usePressLift = () => {
|
|
|
18
18
|
const style = useAnimatedStyle(() => ({
|
|
19
19
|
transform: [{ scale: 1 + lift.value * (PRESSED_SCALE - 1) }],
|
|
20
20
|
}));
|
|
21
|
-
const
|
|
21
|
+
const handlePressIn = () => {
|
|
22
22
|
if (!reduceMotion) {
|
|
23
23
|
lift.value = withSpring(1, SPRING);
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
|
-
const
|
|
26
|
+
const handlePressOut = () => {
|
|
27
27
|
lift.value = withSpring(0, SPRING);
|
|
28
28
|
};
|
|
29
|
-
return { style,
|
|
29
|
+
return { style, handlePressIn, handlePressOut };
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
export { usePressLift };
|