@momo-kits/foundation 0.169.6-beta.1 → 0.169.6
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 +8 -8
- package/Application/BottomSheet.parts.tsx +44 -34
- package/Application/BottomSheet.tsx +35 -26
- package/Application/BottomTab/ActiveIndicator.tsx +15 -14
- package/Application/BottomTab/BottomTabBar.styles.ts +1 -18
- package/Application/BottomTab/BottomTabBar.tsx +58 -57
- package/Application/BottomTab/BottomTabInset.tsx +8 -4
- package/Application/BottomTab/CurvedContainer.tsx +16 -20
- package/Application/BottomTab/CustomBottomTabItem.tsx +14 -15
- package/Application/BottomTab/DuoBarCompression.tsx +13 -19
- package/Application/BottomTab/DuoRailTabBar.tsx +20 -20
- package/Application/BottomTab/DuoTabBar.tsx +11 -24
- package/Application/BottomTab/TabBarIcon.tsx +15 -29
- package/Application/BottomTab/TabScreen.tsx +1 -5
- package/Application/BottomTab/index.tsx +33 -30
- package/Application/BottomTab/metrics.ts +25 -48
- package/Application/BottomTab/tabItems.ts +1 -1
- package/Application/BottomTab/usePressLift.ts +2 -2
- package/Application/Components/BackgroundImageView.tsx +4 -0
- package/Application/Components/DuoGlassBackground.tsx +5 -4
- package/Application/Components/HeaderAnimated.tsx +4 -0
- package/Application/Components/HeaderBackground.tsx +4 -0
- package/Application/Components/HeaderExtendHeader.tsx +11 -10
- package/Application/Components/HeaderLeft.tsx +2 -1
- package/Application/Components/HeaderRight.tsx +2 -1
- package/Application/Components/HeaderTitle.shared.ts +11 -9
- package/Application/Components/HeaderTitle.tsx +4 -3
- package/Application/Components/HeaderToolkitAction.tsx +17 -20
- package/Application/Components/NavigationButton.tsx +2 -1
- package/Application/Components/SearchHeader.tsx +17 -10
- package/Application/Components/TitleJourney.tsx +2 -2
- package/Application/Components/TitleLocation.tsx +2 -2
- package/Application/Components/TitleUser.tsx +2 -2
- package/Application/Components/useToolkitActions.tsx +17 -32
- package/Application/Localize.ts +7 -0
- package/Application/ModalScreen.tsx +9 -3
- package/Application/Navigation.ts +14 -16
- package/Application/NavigationContainer.tsx +19 -16
- package/Application/Navigator.ts +20 -48
- package/Application/ScaleSizeProvider.tsx +13 -6
- package/Application/StackScreen.tracking.ts +11 -26
- package/Application/StackScreen.tsx +8 -35
- package/Application/TooltipPortal.tsx +12 -5
- package/Application/WidgetContainer.tsx +10 -4
- package/Application/modalLayout.ts +13 -14
- package/Application/navigationOptions.tsx +21 -17
- package/Application/types.tabs.ts +1 -0
- package/Badge/Badge.tsx +5 -0
- package/Badge/BadgeDot.tsx +1 -0
- package/Badge/BadgeDotAnimation.tsx +15 -7
- package/Badge/BadgeRibbon.tsx +46 -38
- package/Badge/Shape.tsx +2 -0
- package/Badge/types.ts +2 -2
- package/Button/index.tsx +5 -16
- package/Button/utils.ts +9 -15
- package/CheckBox/index.tsx +4 -0
- package/Consts/theme.ts +10 -10
- package/Context/index.ts +5 -1
- package/Divider/DashDivider.tsx +1 -0
- package/Divider/index.tsx +4 -0
- package/FoundationList/index.tsx +4 -0
- package/Icon/index.tsx +6 -6
- package/Icon/types.ts +1 -1
- package/IconButton/index.tsx +4 -12
- package/Image/index.tsx +9 -7
- package/Input/Input.tsx +8 -9
- package/Input/InputDropDown.tsx +10 -12
- package/Input/InputMoney.tsx +12 -8
- package/Input/InputMoneyView.tsx +14 -7
- package/Input/InputOTP.tsx +35 -33
- package/Input/InputPhoneNumber.tsx +8 -9
- package/Input/InputSearch.tsx +4 -6
- package/Input/InputTextArea.tsx +4 -0
- package/Input/SearchTextTyping.tsx +4 -0
- package/Input/SystemTextInput.tsx +7 -2
- package/Input/TextTyping.tsx +4 -0
- package/Input/common.tsx +26 -12
- package/Input/index.tsx +17 -20
- package/Input/moneyUtils.ts +10 -4
- package/Input/styles.ts +0 -5
- package/Input/utils.ts +14 -12
- package/Layout/Card.tsx +7 -4
- package/Layout/FloatingButton.tsx +4 -0
- package/Layout/Item.tsx +6 -2
- package/Layout/ItemList.tsx +9 -7
- package/Layout/ItemSectionList.tsx +2 -1
- package/Layout/ReservedRegions.tsx +25 -25
- package/Layout/Screen.tsx +56 -58
- package/Layout/Section.tsx +8 -5
- package/Layout/TrackingScope.tsx +1 -0
- package/Layout/screenContent.tsx +4 -2
- package/Layout/screenFooter.tsx +28 -18
- package/Layout/screenHeader.tsx +34 -31
- package/Layout/screenQuickReturn.tsx +17 -20
- package/Layout/screenScroll.ts +4 -7
- package/Layout/screenTypes.ts +3 -2
- package/Layout/types.ts +2 -4
- package/Layout/utils.ts +31 -37
- package/Loader/DotLoader.tsx +4 -0
- package/Loader/Loader.tsx +4 -0
- package/Loader/ProgressBar.tsx +1 -0
- package/Loader/Spinner.tsx +4 -0
- package/Loader/utils.ts +11 -7
- package/Pagination/Dot.tsx +4 -0
- package/Pagination/PaginationDot.tsx +1 -0
- package/Pagination/PaginationNumber.tsx +5 -3
- package/Pagination/PaginationScroll.tsx +5 -0
- package/Pagination/PaginationWhiteDot.tsx +1 -0
- package/Pagination/index.tsx +4 -0
- package/Pagination/types.ts +2 -2
- package/Popup/PopupNotify.tsx +44 -48
- package/Popup/PopupPromotion.tsx +4 -7
- package/Popup/types.ts +4 -4
- package/Popup/utils.ts +4 -0
- package/Radio/index.tsx +4 -0
- package/Skeleton/index.tsx +8 -0
- package/Switch/index.tsx +1 -0
- package/Tag/index.tsx +5 -1
- package/Tag/types.ts +3 -3
- package/Text/index.tsx +14 -3
- package/Text/styles.ts +1 -1
- package/Text/types.ts +1 -6
- package/Text/utils.ts +20 -4
- package/Title/index.tsx +41 -32
- package/ViewBoundary/index.tsx +4 -2
- package/index.ts +0 -6
- package/package.json +1 -1
- package/Application/__tests__/modalLayout.test.ts +0 -128
|
@@ -51,8 +51,10 @@ export const useBottomSheetLayout = () => {
|
|
|
51
51
|
};
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Keyboard lift of the sheet on iPhone Duo, where KeyboardAvoidingView pushes
|
|
56
|
+
* the whole sheet past the top; the sheet lifts by it and shrinks its maxHeight.
|
|
57
|
+
*/
|
|
56
58
|
export const useDuoKeyboardLift = (
|
|
57
59
|
isDuo: boolean,
|
|
58
60
|
useKeyboardAvoidingView: boolean,
|
|
@@ -136,6 +138,10 @@ type SheetDismissParams = {
|
|
|
136
138
|
action: MutableRefObject<undefined | string>;
|
|
137
139
|
};
|
|
138
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Close handlers of a bottom sheet. A non-forced dismiss (scrim tap, hardware
|
|
143
|
+
* back) is ignored while `barrierDismissible` is set.
|
|
144
|
+
*/
|
|
139
145
|
export const useSheetDismiss = ({
|
|
140
146
|
barrierDismissible,
|
|
141
147
|
closeAnimation,
|
|
@@ -143,9 +149,6 @@ export const useSheetDismiss = ({
|
|
|
143
149
|
leftOptions,
|
|
144
150
|
action,
|
|
145
151
|
}: SheetDismissParams) => {
|
|
146
|
-
/**
|
|
147
|
-
* handler dismiss
|
|
148
|
-
*/
|
|
149
152
|
const onDismiss = useCallback(
|
|
150
153
|
(force = false, callback?: () => void) => {
|
|
151
154
|
if (barrierDismissible && !force) {
|
|
@@ -161,9 +164,6 @@ export const useSheetDismiss = ({
|
|
|
161
164
|
[barrierDismissible, closeAnimation, navigator],
|
|
162
165
|
);
|
|
163
166
|
|
|
164
|
-
/**
|
|
165
|
-
* on request close
|
|
166
|
-
*/
|
|
167
167
|
const onRequestClose = useCallback(
|
|
168
168
|
(callback?: () => void) => {
|
|
169
169
|
onDismiss(true, callback);
|
|
@@ -34,42 +34,52 @@ export const renderSheetHeader = ({
|
|
|
34
34
|
iconBack,
|
|
35
35
|
options,
|
|
36
36
|
leftOptions,
|
|
37
|
-
}: SheetHeaderParams) =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<View
|
|
43
|
-
style={[
|
|
44
|
-
styles.header,
|
|
45
|
-
useDivider && styles.headerDivider,
|
|
46
|
-
useDivider && {
|
|
47
|
-
borderBottomColor: theme.colors.border.default,
|
|
48
|
-
},
|
|
49
|
-
]}
|
|
50
|
-
>
|
|
51
|
-
{showBack ? (
|
|
52
|
-
<ModalIconButton icon={iconBack} onPress={onPressBack} />
|
|
53
|
-
) : null}
|
|
37
|
+
}: SheetHeaderParams) => {
|
|
38
|
+
let backButton: React.ReactNode = null;
|
|
39
|
+
if (showBack) {
|
|
40
|
+
backButton = <ModalIconButton icon={iconBack} onPress={onPressBack} />;
|
|
41
|
+
}
|
|
54
42
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
43
|
+
let headerContent: React.ReactNode;
|
|
44
|
+
if (options.header) {
|
|
45
|
+
headerContent = <View style={Styles.flex}>{options.header}</View>;
|
|
46
|
+
} else {
|
|
47
|
+
headerContent = (
|
|
48
|
+
<>
|
|
49
|
+
{!leftOptions ? <View style={styles.iconButtonSpace} /> : null}
|
|
50
|
+
<Text
|
|
51
|
+
style={[Styles.flex, styles.title]}
|
|
52
|
+
numberOfLines={1}
|
|
53
|
+
typography="header_default_bold"
|
|
54
|
+
>
|
|
55
|
+
{options.title ?? ''}
|
|
56
|
+
</Text>
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<View {...panHandlers}>
|
|
63
|
+
<View style={{ height: INDICATOR_HEIGHT }}>
|
|
64
|
+
<View style={styles.indicator} />
|
|
65
|
+
</View>
|
|
66
|
+
<View
|
|
67
|
+
style={[
|
|
68
|
+
styles.header,
|
|
69
|
+
useDivider && styles.headerDivider,
|
|
70
|
+
useDivider && {
|
|
71
|
+
borderBottomColor: theme.colors.border.default,
|
|
72
|
+
},
|
|
73
|
+
]}
|
|
74
|
+
>
|
|
75
|
+
{backButton}
|
|
76
|
+
|
|
77
|
+
{headerContent}
|
|
78
|
+
<ModalIconButton icon="navigation_close" onPress={onPressClose} />
|
|
79
|
+
</View>
|
|
70
80
|
</View>
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
);
|
|
82
|
+
};
|
|
73
83
|
|
|
74
84
|
/**
|
|
75
85
|
* Footer pinned under the sheet content, `null` when the sheet has none.
|
|
@@ -30,6 +30,10 @@ import {
|
|
|
30
30
|
renderSheetHeader,
|
|
31
31
|
} from './BottomSheet.parts';
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Bottom sheet route of the modal stack: slides the given screen up under a
|
|
35
|
+
* drag handle and header, and reports how it was closed through `onDismiss`.
|
|
36
|
+
*/
|
|
33
37
|
const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
34
38
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
35
39
|
const context = useContext<any>(MiniAppContext);
|
|
@@ -98,8 +102,9 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
98
102
|
const panResponder = useRef(
|
|
99
103
|
PanResponder.create({
|
|
100
104
|
onStartShouldSetPanResponder: () => draggable,
|
|
101
|
-
onMoveShouldSetPanResponder: (_, gestureState) =>
|
|
102
|
-
draggable && gestureState.dy > 0
|
|
105
|
+
onMoveShouldSetPanResponder: (_, gestureState) => {
|
|
106
|
+
return draggable && gestureState.dy > 0;
|
|
107
|
+
},
|
|
103
108
|
onPanResponderMove: (_, gestureState) => {
|
|
104
109
|
if (gestureState.dy > 0) {
|
|
105
110
|
translateY.value = gestureState.dy;
|
|
@@ -131,10 +136,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
131
136
|
backgroundColor = theme.colors.background.surface;
|
|
132
137
|
}
|
|
133
138
|
|
|
134
|
-
|
|
135
|
-
* emit dismiss event, once: folding or rotating iPhone Duo changes the
|
|
136
|
-
* window height while the sheet stays open
|
|
137
|
-
*/
|
|
139
|
+
// Mount only, so folding or rotating iPhone Duo doesn't emit a dismiss.
|
|
138
140
|
useEffect(() => {
|
|
139
141
|
openAnimation();
|
|
140
142
|
return () => {
|
|
@@ -142,14 +144,14 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
142
144
|
};
|
|
143
145
|
}, [openAnimation]);
|
|
144
146
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
const handlers = useSheetDismiss({
|
|
148
|
+
barrierDismissible,
|
|
149
|
+
closeAnimation,
|
|
150
|
+
navigator,
|
|
151
|
+
leftOptions,
|
|
152
|
+
action,
|
|
153
|
+
});
|
|
154
|
+
const { onDismiss, onRequestClose, onPressBack, onPressClose } = handlers;
|
|
153
155
|
|
|
154
156
|
const showBack = !!leftOptions?.onPressIconLeft;
|
|
155
157
|
const iconBack = leftOptions?.iconLeft ?? 'ic_back';
|
|
@@ -180,6 +182,24 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
180
182
|
/>
|
|
181
183
|
);
|
|
182
184
|
|
|
185
|
+
let content: React.ReactNode;
|
|
186
|
+
if (isFoldable) {
|
|
187
|
+
// Content shrinks so a tall sheet keeps its header and footer.
|
|
188
|
+
content = (
|
|
189
|
+
<View style={styles.shrink}>
|
|
190
|
+
{renderSheetHeader(headerParams)}
|
|
191
|
+
<View style={styles.shrink}>{screen}</View>
|
|
192
|
+
</View>
|
|
193
|
+
);
|
|
194
|
+
} else {
|
|
195
|
+
content = (
|
|
196
|
+
<>
|
|
197
|
+
{renderSheetHeader(headerParams)}
|
|
198
|
+
{screen}
|
|
199
|
+
</>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
183
203
|
return (
|
|
184
204
|
<Container
|
|
185
205
|
transparent
|
|
@@ -225,18 +245,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
225
245
|
}),
|
|
226
246
|
]}
|
|
227
247
|
>
|
|
228
|
-
{
|
|
229
|
-
// content shrinks so a tall sheet keeps its header and footer
|
|
230
|
-
<View style={styles.shrink}>
|
|
231
|
-
{renderSheetHeader(headerParams)}
|
|
232
|
-
<View style={styles.shrink}>{screen}</View>
|
|
233
|
-
</View>
|
|
234
|
-
) : (
|
|
235
|
-
<>
|
|
236
|
-
{renderSheetHeader(headerParams)}
|
|
237
|
-
{screen}
|
|
238
|
-
</>
|
|
239
|
-
)}
|
|
248
|
+
{content}
|
|
240
249
|
{renderSheetFooter({ footerComponent, theme })}
|
|
241
250
|
{useBottomInset && (
|
|
242
251
|
<View
|
|
@@ -19,8 +19,8 @@ type Props = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
* selected item
|
|
22
|
+
* The single active indicator of a bar (Background/tonal with inner shadows) that slides
|
|
23
|
+
* to the selected item. It lands in place on the first measure and slides from then on.
|
|
24
24
|
*/
|
|
25
25
|
const ActiveIndicator: React.FC<Props> = ({
|
|
26
26
|
axis,
|
|
@@ -49,18 +49,19 @@ const ActiveIndicator: React.FC<Props> = ({
|
|
|
49
49
|
position.value = withTiming(offset, TabBarMotion.indicator);
|
|
50
50
|
}, [offset, extent, position, size, visible]);
|
|
51
51
|
|
|
52
|
-
const animatedStyle = useAnimatedStyle(() =>
|
|
53
|
-
axis === 'x'
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
53
|
+
if (axis === 'x') {
|
|
54
|
+
return {
|
|
55
|
+
opacity: visible.value,
|
|
56
|
+
width: size.value,
|
|
57
|
+
transform: [{ translateX: position.value }],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
opacity: visible.value,
|
|
62
|
+
transform: [{ translateY: position.value }],
|
|
63
|
+
};
|
|
64
|
+
});
|
|
64
65
|
|
|
65
66
|
return (
|
|
66
67
|
<Animated.View
|
|
@@ -2,20 +2,6 @@ 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
|
-
|
|
19
5
|
export const styles = StyleSheet.create({
|
|
20
6
|
bar: {
|
|
21
7
|
position: 'absolute',
|
|
@@ -51,7 +37,7 @@ export const styles = StyleSheet.create({
|
|
|
51
37
|
position: 'absolute',
|
|
52
38
|
left: 0,
|
|
53
39
|
right: 0,
|
|
54
|
-
top:
|
|
40
|
+
top: -TabBarMetrics.ctaLift - TabBarMetrics.paddingVertical,
|
|
55
41
|
alignItems: 'center',
|
|
56
42
|
gap: TabBarMetrics.ctaLabelGap,
|
|
57
43
|
},
|
|
@@ -60,9 +46,6 @@ export const styles = StyleSheet.create({
|
|
|
60
46
|
width: TabBarMetrics.ctaSize,
|
|
61
47
|
height: TabBarMetrics.ctaSize,
|
|
62
48
|
borderRadius: TabBarMetrics.ctaSize / 2,
|
|
63
|
-
// Figma Icon Button border: white 60% hairline
|
|
64
|
-
borderWidth: 1,
|
|
65
|
-
borderColor: `rgba(255, 255, 255, ${TabBarMetrics.ctaBorderOpacity})`,
|
|
66
49
|
alignItems: 'center',
|
|
67
50
|
justifyContent: 'center',
|
|
68
51
|
},
|
|
@@ -26,24 +26,28 @@ type Props = BottomTabBarProps & {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* the center action needs a tab on each side
|
|
29
|
+
* Whether the bar shows the center action. It needs a tab on each side, so the
|
|
30
|
+
* Special type takes 2 or 4 tabs.
|
|
30
31
|
*/
|
|
31
|
-
const hasCenterAction = (floatingButton: unknown, tabCount: number) =>
|
|
32
|
-
!!floatingButton && tabCount % 2 === 0;
|
|
32
|
+
const hasCenterAction = (floatingButton: unknown, tabCount: number) => {
|
|
33
|
+
return !!floatingButton && tabCount % 2 === 0;
|
|
34
|
+
};
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
37
|
+
* Gap under the bar container for the home indicator / gesture bar area, at
|
|
38
|
+
* least `Spacing.S`; iOS caps it at the indicator height.
|
|
36
39
|
*/
|
|
37
|
-
const getSystemIndicatorHeight = (bottomInset: number) =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const getSystemIndicatorHeight = (bottomInset: number) => {
|
|
41
|
+
let height = bottomInset;
|
|
42
|
+
if (Platform.OS === 'ios') {
|
|
43
|
+
height = Math.min(bottomInset, TabBarMetrics.systemIndicatorHeight);
|
|
44
|
+
}
|
|
45
|
+
return Math.max(height, Spacing.S);
|
|
46
|
+
};
|
|
44
47
|
|
|
45
48
|
/**
|
|
46
|
-
* center action
|
|
49
|
+
* Press feedback of the center action: the button shrinks while held, the label
|
|
50
|
+
* stays still.
|
|
47
51
|
*/
|
|
48
52
|
const useCenterActionPress = () => {
|
|
49
53
|
const scale = useSharedValue(1);
|
|
@@ -63,25 +67,27 @@ const useCenterActionPress = () => {
|
|
|
63
67
|
};
|
|
64
68
|
|
|
65
69
|
/**
|
|
66
|
-
*
|
|
70
|
+
* Center action (.Nav/CTA): a button rising out of the notch, its label below
|
|
71
|
+
* (growing downward when scaled).
|
|
67
72
|
*/
|
|
68
|
-
const CenterAction: React.FC<{
|
|
69
|
-
floatingButton
|
|
70
|
-
}
|
|
73
|
+
const CenterAction: React.FC<{ floatingButton: FloatingButtonProps }> = ({
|
|
74
|
+
floatingButton,
|
|
75
|
+
}) => {
|
|
71
76
|
const { theme } = useContext(ApplicationContext);
|
|
72
77
|
const context = useContext<any>(MiniAppContext);
|
|
73
78
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
74
79
|
const { icon, label, onPress, labelContent } = floatingButton;
|
|
75
80
|
const { buttonStyle, onPressIn, onPressOut } = useCenterActionPress();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
let iconNode = icon;
|
|
82
|
+
if (typeof icon === 'string') {
|
|
83
|
+
iconNode = (
|
|
84
|
+
<Icon
|
|
85
|
+
source={icon}
|
|
86
|
+
size={TabBarMetrics.ctaIconSize}
|
|
87
|
+
color={Colors.black_01}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
85
91
|
|
|
86
92
|
return (
|
|
87
93
|
<View style={[styles.ctaSlot, showBaseLineDebug && styles.debugBaseLine]}>
|
|
@@ -100,18 +106,16 @@ const CenterAction: React.FC<{
|
|
|
100
106
|
buttonStyle,
|
|
101
107
|
]}
|
|
102
108
|
>
|
|
103
|
-
{
|
|
104
|
-
<Icon
|
|
105
|
-
source={icon}
|
|
106
|
-
size={TabBarMetrics.ctaIconSize}
|
|
107
|
-
color={Colors.black_01}
|
|
108
|
-
/>
|
|
109
|
-
) : (
|
|
110
|
-
icon
|
|
111
|
-
)}
|
|
109
|
+
{iconNode}
|
|
112
110
|
</Animated.View>
|
|
113
111
|
<View style={styles.ctaLabel}>
|
|
114
|
-
|
|
112
|
+
<Text
|
|
113
|
+
numberOfLines={1}
|
|
114
|
+
typography={'label_s_medium'}
|
|
115
|
+
color={theme.colors.text.secondary}
|
|
116
|
+
>
|
|
117
|
+
{label}
|
|
118
|
+
</Text>
|
|
115
119
|
{labelContent && (
|
|
116
120
|
<View pointerEvents={'none'} style={styles.ctaLabelOverlay}>
|
|
117
121
|
{labelContent}
|
|
@@ -124,8 +128,8 @@ const CenterAction: React.FC<{
|
|
|
124
128
|
};
|
|
125
129
|
|
|
126
130
|
/**
|
|
127
|
-
*
|
|
128
|
-
* the center action in a notch in the middle
|
|
131
|
+
* Horizontal navigation bar: a floating container with evenly split items; the
|
|
132
|
+
* Special type puts the center action in a notch in the middle.
|
|
129
133
|
*/
|
|
130
134
|
const HorizontalTabBar: React.FC<{
|
|
131
135
|
items: TabItem[];
|
|
@@ -133,21 +137,15 @@ const HorizontalTabBar: React.FC<{
|
|
|
133
137
|
activeTintColor: string;
|
|
134
138
|
inactiveTintColor: string;
|
|
135
139
|
floatingButton?: FloatingButtonProps;
|
|
136
|
-
}> = ({
|
|
137
|
-
items,
|
|
138
|
-
width,
|
|
139
|
-
activeTintColor,
|
|
140
|
-
inactiveTintColor,
|
|
141
|
-
floatingButton,
|
|
142
|
-
}) => {
|
|
140
|
+
}> = ({ items, width, activeTintColor, inactiveTintColor, floatingButton }) => {
|
|
143
141
|
const withCta = hasCenterAction(floatingButton, items.length);
|
|
144
142
|
const [layouts, setLayouts] = useState<
|
|
145
143
|
Record<string, { x: number; y: number; width: number }>
|
|
146
144
|
>({});
|
|
147
145
|
const selected = layouts[items.find(item => item.focused)?.route.key ?? ''];
|
|
148
146
|
|
|
149
|
-
const renderItems = (data: TabItem[]) =>
|
|
150
|
-
data.map(item => (
|
|
147
|
+
const renderItems = (data: TabItem[]) => {
|
|
148
|
+
return data.map(item => (
|
|
151
149
|
<CustomBottomTabItem
|
|
152
150
|
key={item.route.key}
|
|
153
151
|
onLayout={e => {
|
|
@@ -167,6 +165,7 @@ const HorizontalTabBar: React.FC<{
|
|
|
167
165
|
inactiveTintColor={inactiveTintColor}
|
|
168
166
|
/>
|
|
169
167
|
));
|
|
168
|
+
};
|
|
170
169
|
|
|
171
170
|
const renderContent = () => {
|
|
172
171
|
if (withCta && floatingButton) {
|
|
@@ -182,17 +181,21 @@ const HorizontalTabBar: React.FC<{
|
|
|
182
181
|
return renderItems(items);
|
|
183
182
|
};
|
|
184
183
|
|
|
184
|
+
const customContainer = withCta && floatingButton?.container;
|
|
185
|
+
|
|
185
186
|
return (
|
|
186
187
|
<View
|
|
187
188
|
style={[styles.container, { width }]}
|
|
188
189
|
accessibilityLabel={'bottom_tab_bar'}
|
|
189
190
|
>
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
191
|
+
{customContainer || (
|
|
192
|
+
<CurvedContainer
|
|
193
|
+
width={width}
|
|
194
|
+
height={TabBarMetrics.height}
|
|
195
|
+
radius={TabBarMetrics.height / 2}
|
|
196
|
+
notchCenterX={withCta ? width / 2 : undefined}
|
|
197
|
+
/>
|
|
198
|
+
)}
|
|
196
199
|
<View style={styles.content}>
|
|
197
200
|
<ActiveIndicator
|
|
198
201
|
axis={'x'}
|
|
@@ -211,9 +214,9 @@ const HorizontalTabBar: React.FC<{
|
|
|
211
214
|
};
|
|
212
215
|
|
|
213
216
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
+
* Phone tab bar: the horizontal bar floats over the tab content above the home indicator,
|
|
218
|
+
* so content shows around it and through the notch. The transparent strip above it keeps
|
|
219
|
+
* the rising center action touchable.
|
|
217
220
|
*/
|
|
218
221
|
export default function BottomTabBar(props: Props) {
|
|
219
222
|
const { state, floatingButton, activeTintColor, inactiveTintColor } = props;
|
|
@@ -225,8 +228,6 @@ export default function BottomTabBar(props: Props) {
|
|
|
225
228
|
window.height,
|
|
226
229
|
insets.left,
|
|
227
230
|
insets.right,
|
|
228
|
-
state.routes.length,
|
|
229
|
-
withCta,
|
|
230
231
|
);
|
|
231
232
|
|
|
232
233
|
return (
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { createContext } from 'react';
|
|
2
2
|
|
|
3
3
|
type BottomTabInset = {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Space the floating tab bar takes over the bottom of a tab's content (bar + home
|
|
6
|
+
* indicator area, plus the rising center action when shown); 0 on the iPhone Duo rail.
|
|
7
|
+
*/
|
|
6
8
|
inset: number;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Called by a Screen in the tab that keeps its content clear of the bar itself (inside
|
|
11
|
+
* its scroll view, or above the bar when static), so the tab stops padding its content.
|
|
12
|
+
*/
|
|
9
13
|
claim?: () => void;
|
|
10
14
|
};
|
|
11
15
|
|
|
@@ -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
|
|
6
|
+
import { NOTCH_CURVES, NOTCH_HALF_WIDTH } from './metrics';
|
|
7
7
|
|
|
8
8
|
type Props = {
|
|
9
9
|
width: number;
|
|
@@ -15,16 +15,18 @@ type Props = {
|
|
|
15
15
|
|
|
16
16
|
// room for the floating shadow around the shape: the key shadow reaches 12 + 3 × 16 below it
|
|
17
17
|
const SHADOW_PADDING = 64;
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Figma Shadow/floating: 12% blur 24, plus 4% blur 32 spread -16 offset 12. SVG blur takes
|
|
20
|
+
* half the CSS blur radius; the spread shrinks the shape the key shadow is cast from.
|
|
21
|
+
*/
|
|
20
22
|
const AMBIENT_BLUR = 12;
|
|
21
23
|
const KEY_BLUR = 16;
|
|
22
24
|
const KEY_SPREAD = 16;
|
|
23
25
|
const KEY_OFFSET_Y = 12;
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
|
-
* rounded container
|
|
27
|
-
* top edge around `notchCenterX
|
|
28
|
+
* SVG path of the rounded container placed at (`x`, `y`) on the canvas, with the notch
|
|
29
|
+
* dipped into the top edge around `notchCenterX`.
|
|
28
30
|
*/
|
|
29
31
|
const getContainerPath = (
|
|
30
32
|
width: number,
|
|
@@ -43,11 +45,10 @@ const getContainerPath = (
|
|
|
43
45
|
if (notchCenterX !== undefined) {
|
|
44
46
|
const cx = x + notchCenterX;
|
|
45
47
|
const point = ([dx, dy]: [number, number]) => `${cx + dx} ${top + dy}`;
|
|
46
|
-
|
|
47
|
-
`
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
).join('');
|
|
48
|
+
const curves = NOTCH_CURVES.map(([c1, c2, end]) => {
|
|
49
|
+
return ` C ${point(c1)} ${point(c2)} ${point(end)}`;
|
|
50
|
+
});
|
|
51
|
+
notch = ` L ${cx - NOTCH_HALF_WIDTH} ${top}${curves.join('')}`;
|
|
51
52
|
}
|
|
52
53
|
return (
|
|
53
54
|
`M ${left + r} ${top}` +
|
|
@@ -64,8 +65,8 @@ const getContainerPath = (
|
|
|
64
65
|
};
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
|
-
*
|
|
68
|
-
* the shadow follows the notch
|
|
68
|
+
* Navigation bar container: surface fill with the floating shadow, drawn as an SVG shape
|
|
69
|
+
* so the shadow follows the notch.
|
|
69
70
|
*/
|
|
70
71
|
const CurvedContainer: React.FC<Props> = ({
|
|
71
72
|
width,
|
|
@@ -87,6 +88,7 @@ const CurvedContainer: React.FC<Props> = ({
|
|
|
87
88
|
SHADOW_PADDING,
|
|
88
89
|
notchCenterX,
|
|
89
90
|
);
|
|
91
|
+
const hasKeyShadow = width > 2 * KEY_SPREAD && height > 2 * KEY_SPREAD;
|
|
90
92
|
|
|
91
93
|
return (
|
|
92
94
|
<Svg
|
|
@@ -123,7 +125,7 @@ const CurvedContainer: React.FC<Props> = ({
|
|
|
123
125
|
fillOpacity={0.12}
|
|
124
126
|
filter={'url(#ambientShadow)'}
|
|
125
127
|
/>
|
|
126
|
-
{
|
|
128
|
+
{hasKeyShadow && (
|
|
127
129
|
<Path
|
|
128
130
|
d={getContainerPath(
|
|
129
131
|
width - 2 * KEY_SPREAD,
|
|
@@ -137,13 +139,7 @@ const CurvedContainer: React.FC<Props> = ({
|
|
|
137
139
|
filter={'url(#keyShadow)'}
|
|
138
140
|
/>
|
|
139
141
|
)}
|
|
140
|
-
<Path
|
|
141
|
-
d={path}
|
|
142
|
-
fill={theme.colors.background.surface}
|
|
143
|
-
fillOpacity={TabBarMetrics.backgroundOpacity}
|
|
144
|
-
stroke={theme.colors.border.default}
|
|
145
|
-
strokeWidth={1}
|
|
146
|
-
/>
|
|
142
|
+
<Path d={path} fill={theme.colors.background.surface} />
|
|
147
143
|
</Svg>
|
|
148
144
|
);
|
|
149
145
|
};
|