@momo-kits/foundation 0.169.5-beta.1 → 0.169.5
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 +40 -37
- package/Application/BottomTab/ActiveIndicator.tsx +15 -14
- package/Application/BottomTab/BottomTabBar.tsx +43 -34
- package/Application/BottomTab/BottomTabInset.tsx +8 -4
- package/Application/BottomTab/CurvedContainer.tsx +14 -12
- package/Application/BottomTab/CustomBottomTabItem.tsx +10 -6
- package/Application/BottomTab/DuoBarCompression.tsx +13 -19
- package/Application/BottomTab/DuoRailTabBar.tsx +13 -13
- package/Application/BottomTab/DuoTabBar.tsx +8 -9
- package/Application/BottomTab/TabBarIcon.tsx +13 -8
- package/Application/BottomTab/TabScreen.tsx +1 -5
- package/Application/BottomTab/index.tsx +38 -30
- package/Application/BottomTab/metrics.ts +21 -20
- 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 +10 -10
- package/Application/Navigation.ts +14 -16
- package/Application/NavigationContainer.tsx +19 -18
- package/Application/Navigator.ts +20 -48
- package/Application/StackScreen.tracking.ts +11 -26
- package/Application/StackScreen.tsx +9 -39
- 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 +1 -0
- 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 +9 -0
- package/Title/index.tsx +41 -32
- package/ViewBoundary/index.tsx +4 -2
- package/index.ts +0 -6
- package/package.json +1 -1
- package/Application/KitAdoption/AdoptionBadge.tsx +0 -119
- package/Application/KitAdoption/AdoptionProbe.tsx +0 -145
- package/Application/KitAdoption/AdoptionStore.ts +0 -80
- package/Application/KitAdoption/Authorship/utils.ts +0 -137
- package/Application/KitAdoption/constants.ts +0 -118
- package/Application/KitAdoption/index.ts +0 -5
- package/Application/KitAdoption/styles.ts +0 -55
- package/Application/KitAdoption/types.ts +0 -48
- package/Application/KitAdoption/utils.ts +0 -375
- package/Application/__tests__/KitAdoption.integration.test.tsx +0 -235
- package/Application/__tests__/KitAdoption.test.ts +0 -283
- package/Application/__tests__/KitAdoptionAuthorship.test.ts +0 -132
- package/Application/__tests__/KitAdoptionStore.test.ts +0 -110
- 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.
|
|
@@ -18,7 +18,6 @@ import Animated, {
|
|
|
18
18
|
} from 'react-native-reanimated';
|
|
19
19
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
20
20
|
import { DUO_ORIENTATIONS, styles } from './BottomSheet.styles';
|
|
21
|
-
import { AdoptionProbe } from './KitAdoption';
|
|
22
21
|
import {
|
|
23
22
|
useBottomSheetLayout,
|
|
24
23
|
useDuoKeyboardLift,
|
|
@@ -31,6 +30,10 @@ import {
|
|
|
31
30
|
renderSheetHeader,
|
|
32
31
|
} from './BottomSheet.parts';
|
|
33
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
|
+
*/
|
|
34
37
|
const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
35
38
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
36
39
|
const context = useContext<any>(MiniAppContext);
|
|
@@ -99,8 +102,9 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
99
102
|
const panResponder = useRef(
|
|
100
103
|
PanResponder.create({
|
|
101
104
|
onStartShouldSetPanResponder: () => draggable,
|
|
102
|
-
onMoveShouldSetPanResponder: (_, gestureState) =>
|
|
103
|
-
draggable && gestureState.dy > 0
|
|
105
|
+
onMoveShouldSetPanResponder: (_, gestureState) => {
|
|
106
|
+
return draggable && gestureState.dy > 0;
|
|
107
|
+
},
|
|
104
108
|
onPanResponderMove: (_, gestureState) => {
|
|
105
109
|
if (gestureState.dy > 0) {
|
|
106
110
|
translateY.value = gestureState.dy;
|
|
@@ -132,10 +136,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
132
136
|
backgroundColor = theme.colors.background.surface;
|
|
133
137
|
}
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
* emit dismiss event, once: folding or rotating iPhone Duo changes the
|
|
137
|
-
* window height while the sheet stays open
|
|
138
|
-
*/
|
|
139
|
+
// Mount only, so folding or rotating iPhone Duo doesn't emit a dismiss.
|
|
139
140
|
useEffect(() => {
|
|
140
141
|
openAnimation();
|
|
141
142
|
return () => {
|
|
@@ -143,14 +144,14 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
143
144
|
};
|
|
144
145
|
}, [openAnimation]);
|
|
145
146
|
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
147
|
+
const handlers = useSheetDismiss({
|
|
148
|
+
barrierDismissible,
|
|
149
|
+
closeAnimation,
|
|
150
|
+
navigator,
|
|
151
|
+
leftOptions,
|
|
152
|
+
action,
|
|
153
|
+
});
|
|
154
|
+
const { onDismiss, onRequestClose, onPressBack, onPressClose } = handlers;
|
|
154
155
|
|
|
155
156
|
const showBack = !!leftOptions?.onPressIconLeft;
|
|
156
157
|
const iconBack = leftOptions?.iconLeft ?? 'ic_back';
|
|
@@ -174,18 +175,31 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
174
175
|
);
|
|
175
176
|
|
|
176
177
|
const screen = (
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
{...props}
|
|
183
|
-
{...props.route.params}
|
|
184
|
-
onRequestClose={onRequestClose}
|
|
185
|
-
/>
|
|
186
|
-
</AdoptionProbe>
|
|
178
|
+
<Screen
|
|
179
|
+
{...props}
|
|
180
|
+
{...props.route.params}
|
|
181
|
+
onRequestClose={onRequestClose}
|
|
182
|
+
/>
|
|
187
183
|
);
|
|
188
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
|
+
|
|
189
203
|
return (
|
|
190
204
|
<Container
|
|
191
205
|
transparent
|
|
@@ -231,18 +245,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
231
245
|
}),
|
|
232
246
|
]}
|
|
233
247
|
>
|
|
234
|
-
{
|
|
235
|
-
// content shrinks so a tall sheet keeps its header and footer
|
|
236
|
-
<View style={styles.shrink}>
|
|
237
|
-
{renderSheetHeader(headerParams)}
|
|
238
|
-
<View style={styles.shrink}>{screen}</View>
|
|
239
|
-
</View>
|
|
240
|
-
) : (
|
|
241
|
-
<>
|
|
242
|
-
{renderSheetHeader(headerParams)}
|
|
243
|
-
{screen}
|
|
244
|
-
</>
|
|
245
|
-
)}
|
|
248
|
+
{content}
|
|
246
249
|
{renderSheetFooter({ footerComponent, theme })}
|
|
247
250
|
{useBottomInset && (
|
|
248
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
|
|
@@ -27,24 +27,28 @@ type Props = BottomTabBarProps & {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* the center action needs a tab on each side
|
|
30
|
+
* Whether the bar shows the center action. It needs a tab on each side, so the
|
|
31
|
+
* Special type takes 2 or 4 tabs.
|
|
31
32
|
*/
|
|
32
|
-
const hasCenterAction = (floatingButton: unknown, tabCount: number) =>
|
|
33
|
-
!!floatingButton && tabCount % 2 === 0;
|
|
33
|
+
const hasCenterAction = (floatingButton: unknown, tabCount: number) => {
|
|
34
|
+
return !!floatingButton && tabCount % 2 === 0;
|
|
35
|
+
};
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
|
-
*
|
|
38
|
+
* Gap under the bar container for the home indicator / gesture bar area, at
|
|
39
|
+
* least `Spacing.S`; iOS caps it at the indicator height.
|
|
37
40
|
*/
|
|
38
|
-
const getSystemIndicatorHeight = (bottomInset: number) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
const getSystemIndicatorHeight = (bottomInset: number) => {
|
|
42
|
+
let height = bottomInset;
|
|
43
|
+
if (Platform.OS === 'ios') {
|
|
44
|
+
height = Math.min(bottomInset, TabBarMetrics.systemIndicatorHeight);
|
|
45
|
+
}
|
|
46
|
+
return Math.max(height, Spacing.S);
|
|
47
|
+
};
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
|
-
* center action
|
|
50
|
+
* Press feedback of the center action: the button shrinks while held, the label
|
|
51
|
+
* stays still.
|
|
48
52
|
*/
|
|
49
53
|
const useCenterActionPress = () => {
|
|
50
54
|
const scale = useSharedValue(1);
|
|
@@ -64,7 +68,7 @@ const useCenterActionPress = () => {
|
|
|
64
68
|
};
|
|
65
69
|
|
|
66
70
|
/**
|
|
67
|
-
*
|
|
71
|
+
* Center action (.Nav/CTA): a button rising out of the notch, its label below.
|
|
68
72
|
*/
|
|
69
73
|
const CenterAction: React.FC<{
|
|
70
74
|
floatingButton: FloatingButtonProps;
|
|
@@ -85,6 +89,22 @@ const CenterAction: React.FC<{
|
|
|
85
89
|
</Text>
|
|
86
90
|
);
|
|
87
91
|
|
|
92
|
+
let iconNode = icon;
|
|
93
|
+
if (typeof icon === 'string') {
|
|
94
|
+
iconNode = (
|
|
95
|
+
<Icon
|
|
96
|
+
source={icon}
|
|
97
|
+
size={TabBarMetrics.ctaIconSize}
|
|
98
|
+
color={Colors.black_01}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let labelNode = labelText;
|
|
104
|
+
if (fixedFontScale) {
|
|
105
|
+
labelNode = <FixedScaleText>{labelText}</FixedScaleText>;
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
return (
|
|
89
109
|
<View style={[styles.ctaSlot, showBaseLineDebug && styles.debugBaseLine]}>
|
|
90
110
|
<Pressable
|
|
@@ -102,22 +122,10 @@ const CenterAction: React.FC<{
|
|
|
102
122
|
buttonStyle,
|
|
103
123
|
]}
|
|
104
124
|
>
|
|
105
|
-
{
|
|
106
|
-
<Icon
|
|
107
|
-
source={icon}
|
|
108
|
-
size={TabBarMetrics.ctaIconSize}
|
|
109
|
-
color={Colors.black_01}
|
|
110
|
-
/>
|
|
111
|
-
) : (
|
|
112
|
-
icon
|
|
113
|
-
)}
|
|
125
|
+
{iconNode}
|
|
114
126
|
</Animated.View>
|
|
115
127
|
<View style={styles.ctaLabel}>
|
|
116
|
-
{
|
|
117
|
-
<FixedScaleText>{labelText}</FixedScaleText>
|
|
118
|
-
) : (
|
|
119
|
-
labelText
|
|
120
|
-
)}
|
|
128
|
+
{labelNode}
|
|
121
129
|
{labelContent && (
|
|
122
130
|
<View pointerEvents={'none'} style={styles.ctaLabelOverlay}>
|
|
123
131
|
{labelContent}
|
|
@@ -130,8 +138,8 @@ const CenterAction: React.FC<{
|
|
|
130
138
|
};
|
|
131
139
|
|
|
132
140
|
/**
|
|
133
|
-
*
|
|
134
|
-
* the center action in a notch in the middle
|
|
141
|
+
* Horizontal navigation bar: a floating container with evenly split items; the
|
|
142
|
+
* Special type puts the center action in a notch in the middle.
|
|
135
143
|
*/
|
|
136
144
|
const HorizontalTabBar: React.FC<{
|
|
137
145
|
items: TabItem[];
|
|
@@ -154,8 +162,8 @@ const HorizontalTabBar: React.FC<{
|
|
|
154
162
|
>({});
|
|
155
163
|
const selected = layouts[items.find(item => item.focused)?.route.key ?? ''];
|
|
156
164
|
|
|
157
|
-
const renderItems = (data: TabItem[]) =>
|
|
158
|
-
data.map(item => (
|
|
165
|
+
const renderItems = (data: TabItem[]) => {
|
|
166
|
+
return data.map(item => (
|
|
159
167
|
<CustomBottomTabItem
|
|
160
168
|
key={item.route.key}
|
|
161
169
|
onLayout={e => {
|
|
@@ -176,6 +184,7 @@ const HorizontalTabBar: React.FC<{
|
|
|
176
184
|
fixedFontScale={fixedFontScale}
|
|
177
185
|
/>
|
|
178
186
|
));
|
|
187
|
+
};
|
|
179
188
|
|
|
180
189
|
const renderContent = () => {
|
|
181
190
|
if (withCta && floatingButton) {
|
|
@@ -227,9 +236,9 @@ const HorizontalTabBar: React.FC<{
|
|
|
227
236
|
};
|
|
228
237
|
|
|
229
238
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
239
|
+
* Phone tab bar: the horizontal bar floats over the tab content above the home indicator,
|
|
240
|
+
* so content shows around it and through the notch. The transparent strip above it keeps
|
|
241
|
+
* the rising center action touchable.
|
|
233
242
|
*/
|
|
234
243
|
export default function BottomTabBar(props: Props) {
|
|
235
244
|
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,
|
|
@@ -37,8 +37,8 @@ type TabItemContentProps = {
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* icon in its indicator slot
|
|
41
|
-
* behind the slots;
|
|
40
|
+
* Tab item body: the icon in its indicator slot with the label under it. The bar slides one
|
|
41
|
+
* shared active indicator behind the slots; icon and label tints cross-fade on focus.
|
|
42
42
|
*/
|
|
43
43
|
const TabItemContent: React.FC<TabItemContentProps> = ({
|
|
44
44
|
label,
|
|
@@ -72,7 +72,7 @@ const TabItemContent: React.FC<TabItemContentProps> = ({
|
|
|
72
72
|
</Text>
|
|
73
73
|
);
|
|
74
74
|
|
|
75
|
-
//
|
|
75
|
+
// The active label fades in over the inactive one.
|
|
76
76
|
const text = (
|
|
77
77
|
<View style={!vertical && styles.labelBox}>
|
|
78
78
|
{renderText(inactiveTintColor)}
|
|
@@ -85,6 +85,11 @@ const TabItemContent: React.FC<TabItemContentProps> = ({
|
|
|
85
85
|
</View>
|
|
86
86
|
);
|
|
87
87
|
|
|
88
|
+
let labelNode = text;
|
|
89
|
+
if (fixedFontScale) {
|
|
90
|
+
labelNode = <FixedScaleText>{text}</FixedScaleText>;
|
|
91
|
+
}
|
|
92
|
+
|
|
88
93
|
return (
|
|
89
94
|
<>
|
|
90
95
|
<View style={vertical ? styles.railSlot : styles.slot}>
|
|
@@ -97,8 +102,7 @@ const TabItemContent: React.FC<TabItemContentProps> = ({
|
|
|
97
102
|
renderIcon={icon}
|
|
98
103
|
/>
|
|
99
104
|
</View>
|
|
100
|
-
{!hideLabel &&
|
|
101
|
-
(fixedFontScale ? <FixedScaleText>{text}</FixedScaleText> : text)}
|
|
105
|
+
{!hideLabel && labelNode}
|
|
102
106
|
</>
|
|
103
107
|
);
|
|
104
108
|
};
|
|
@@ -112,7 +116,7 @@ type CustomBottomTabItemProps = TabItemContentProps & {
|
|
|
112
116
|
};
|
|
113
117
|
|
|
114
118
|
/**
|
|
115
|
-
*
|
|
119
|
+
* Horizontal navigation item (.Nav/itemTab), filling an even share of the container.
|
|
116
120
|
*/
|
|
117
121
|
const CustomBottomTabItem: React.FC<CustomBottomTabItemProps> = ({
|
|
118
122
|
onPress,
|
|
@@ -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 };
|