@momo-kits/foundation 0.169.6-beta.1 → 0.169.7-beta.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/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.tsx +48 -49
- package/Application/BottomTab/BottomTabInset.tsx +8 -4
- package/Application/BottomTab/CurvedContainer.tsx +14 -12
- package/Application/BottomTab/CustomBottomTabItem.tsx +12 -13
- package/Application/BottomTab/DuoBarCompression.tsx +13 -19
- package/Application/BottomTab/DuoRailTabBar.tsx +20 -20
- package/Application/BottomTab/DuoTabBar.tsx +8 -9
- 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 +22 -25
- 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/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
|
|
@@ -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) {
|
|
@@ -211,9 +210,9 @@ const HorizontalTabBar: React.FC<{
|
|
|
211
210
|
};
|
|
212
211
|
|
|
213
212
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
213
|
+
* Phone tab bar: the horizontal bar floats over the tab content above the home indicator,
|
|
214
|
+
* so content shows around it and through the notch. The transparent strip above it keeps
|
|
215
|
+
* the rising center action touchable.
|
|
217
216
|
*/
|
|
218
217
|
export default function BottomTabBar(props: Props) {
|
|
219
218
|
const { state, floatingButton, activeTintColor, inactiveTintColor } = props;
|
|
@@ -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
|
|
|
@@ -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,
|
|
@@ -35,8 +35,8 @@ type TabItemContentProps = {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* icon in its indicator slot, label
|
|
39
|
-
*
|
|
38
|
+
* Tab item body: the icon in its indicator slot, the label in a fixed slot under it so a
|
|
39
|
+
* scaled label grows down. The bar slides one shared active indicator behind the icon slots.
|
|
40
40
|
*/
|
|
41
41
|
const TabItemContent: React.FC<TabItemContentProps> = ({
|
|
42
42
|
label,
|
|
@@ -69,9 +69,9 @@ const TabItemContent: React.FC<TabItemContentProps> = ({
|
|
|
69
69
|
</Text>
|
|
70
70
|
);
|
|
71
71
|
|
|
72
|
-
//
|
|
73
|
-
const
|
|
74
|
-
<View style={
|
|
72
|
+
// Active label fades in over the inactive one; a scaled label overflows down.
|
|
73
|
+
const labelNode = (
|
|
74
|
+
<View style={vertical ? styles.railLabelSlot : styles.labelSlot}>
|
|
75
75
|
{renderText(inactiveTintColor)}
|
|
76
76
|
<Animated.View
|
|
77
77
|
pointerEvents={'none'}
|
|
@@ -94,7 +94,7 @@ const TabItemContent: React.FC<TabItemContentProps> = ({
|
|
|
94
94
|
renderIcon={icon}
|
|
95
95
|
/>
|
|
96
96
|
</View>
|
|
97
|
-
{!hideLabel &&
|
|
97
|
+
{!hideLabel && labelNode}
|
|
98
98
|
</>
|
|
99
99
|
);
|
|
100
100
|
};
|
|
@@ -108,7 +108,7 @@ type CustomBottomTabItemProps = TabItemContentProps & {
|
|
|
108
108
|
};
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
*
|
|
111
|
+
* Horizontal navigation item (.Nav/itemTab), filling an even share of the container.
|
|
112
112
|
*/
|
|
113
113
|
const CustomBottomTabItem: React.FC<CustomBottomTabItemProps> = ({
|
|
114
114
|
onPress,
|
|
@@ -160,13 +160,12 @@ const styles = StyleSheet.create({
|
|
|
160
160
|
paddingVertical: TabBarMetrics.railIndicatorPaddingVertical,
|
|
161
161
|
paddingHorizontal: TabBarMetrics.indicatorPaddingHorizontal,
|
|
162
162
|
},
|
|
163
|
-
|
|
164
|
-
alignSelf: 'stretch',
|
|
165
|
-
// fixed to the design line height so a capped-scale label overflows downward and the
|
|
166
|
-
// icon above it stays put
|
|
163
|
+
labelSlot: {
|
|
167
164
|
height: TabBarMetrics.labelHeight,
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
alignSelf: 'stretch',
|
|
166
|
+
},
|
|
167
|
+
railLabelSlot: {
|
|
168
|
+
height: TabBarMetrics.railLabelHeight,
|
|
170
169
|
},
|
|
171
170
|
label: {
|
|
172
171
|
textAlign: 'center',
|
|
@@ -28,9 +28,9 @@ type DuoBarCompression = {
|
|
|
28
28
|
|
|
29
29
|
// space kept between the top and the bottom groups (HIG Vertical controls)
|
|
30
30
|
const GROUP_GAP = Spacing.M;
|
|
31
|
+
const BACK_SLOT_HEIGHT = DuoMetrics.controlSize + Spacing.M;
|
|
31
32
|
// Back, then the right toolkit: help + home capsule
|
|
32
|
-
const DEFAULT_TOOLBAR_HEIGHT =
|
|
33
|
-
DuoMetrics.controlSize + Spacing.M + 2 * DuoMetrics.itemPitch;
|
|
33
|
+
const DEFAULT_TOOLBAR_HEIGHT = BACK_SLOT_HEIGHT + 2 * DuoMetrics.itemPitch;
|
|
34
34
|
// Back, then the single button the toolbar folds into
|
|
35
35
|
const COMPRESSED_TOOLBAR_HEIGHT = 2 * DuoMetrics.controlSize + Spacing.M;
|
|
36
36
|
|
|
@@ -39,16 +39,12 @@ const NO_COMPRESSION: DuoBarCompression = {
|
|
|
39
39
|
tabBarCompressed: false,
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
const DuoBarCompressionContext =
|
|
43
|
-
createContext<DuoBarCompression>(NO_COMPRESSION);
|
|
42
|
+
const DuoBarCompressionContext = createContext(NO_COMPRESSION);
|
|
44
43
|
|
|
45
44
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* - one fits whole beside the other compressed: one is compressed, tapping it swaps them
|
|
50
|
-
* - otherwise both rest compressed; tapping one opens it for a moment (the rail closes
|
|
51
|
-
* again once a tab is picked)
|
|
45
|
+
* Keeps the Duo side bar's toolbar and tab bar clear of each other (HIG Vertical controls):
|
|
46
|
+
* both stay whole if they fit, else one is compressed and tapping it swaps them, else both
|
|
47
|
+
* rest compressed and tapping one opens it until a tab is picked.
|
|
52
48
|
*/
|
|
53
49
|
const DuoBarCompressionProvider: React.FC<{
|
|
54
50
|
behavior?: DuoBarCompressionBehavior;
|
|
@@ -71,18 +67,16 @@ const DuoBarCompressionProvider: React.FC<{
|
|
|
71
67
|
}
|
|
72
68
|
const room = height - sideBar.bottom - sideBar.top - GROUP_GAP;
|
|
73
69
|
const tabBar = getRailHeight(tabCount, hasQr);
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
(hasQr ? TabBarMetrics.railCtaGap + TabBarMetrics.railCtaHeight : 0);
|
|
70
|
+
const qrHeight = TabBarMetrics.railCtaGap + TabBarMetrics.railCtaHeight;
|
|
71
|
+
const compressedTabBar = DuoMetrics.controlSize + (hasQr ? qrHeight : 0);
|
|
77
72
|
const fits = (toolbar: number, tabs: number) => toolbar + tabs <= room;
|
|
78
73
|
|
|
79
74
|
if (fits(toolbarHeight, tabBar)) {
|
|
80
75
|
return { ...NO_COMPRESSION, setToolbarHeight };
|
|
81
76
|
}
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (canSwap) {
|
|
77
|
+
const canCompressToolbar = fits(COMPRESSED_TOOLBAR_HEIGHT, tabBar);
|
|
78
|
+
const canCompressTabBar = fits(toolbarHeight, compressedTabBar);
|
|
79
|
+
if (canCompressToolbar && canCompressTabBar) {
|
|
86
80
|
return {
|
|
87
81
|
toolbarCompressed: expanded === 'tabBar',
|
|
88
82
|
tabBarCompressed: expanded === 'toolbar',
|
|
@@ -97,8 +91,7 @@ const DuoBarCompressionProvider: React.FC<{
|
|
|
97
91
|
expandTabBar: () => setOpened('tabBar'),
|
|
98
92
|
setToolbarHeight,
|
|
99
93
|
};
|
|
100
|
-
//
|
|
101
|
-
// it closes again once a tab is picked
|
|
94
|
+
// An opened rail shows even if only the group gap is short; picking a tab closes it.
|
|
102
95
|
if (opened === 'tabBar') {
|
|
103
96
|
return {
|
|
104
97
|
...open,
|
|
@@ -122,6 +115,7 @@ const DuoBarCompressionProvider: React.FC<{
|
|
|
122
115
|
);
|
|
123
116
|
};
|
|
124
117
|
|
|
118
|
+
/** Compression state of the Duo side bars; no compression outside the provider. */
|
|
125
119
|
const useDuoBarCompression = () => useContext(DuoBarCompressionContext);
|
|
126
120
|
|
|
127
121
|
export { DuoBarCompressionProvider, useDuoBarCompression };
|