@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.
Files changed (128) hide show
  1. package/Application/BottomSheet.hooks.ts +8 -8
  2. package/Application/BottomSheet.parts.tsx +44 -34
  3. package/Application/BottomSheet.tsx +35 -26
  4. package/Application/BottomTab/ActiveIndicator.tsx +15 -14
  5. package/Application/BottomTab/BottomTabBar.styles.ts +1 -18
  6. package/Application/BottomTab/BottomTabBar.tsx +58 -57
  7. package/Application/BottomTab/BottomTabInset.tsx +8 -4
  8. package/Application/BottomTab/CurvedContainer.tsx +16 -20
  9. package/Application/BottomTab/CustomBottomTabItem.tsx +14 -15
  10. package/Application/BottomTab/DuoBarCompression.tsx +13 -19
  11. package/Application/BottomTab/DuoRailTabBar.tsx +20 -20
  12. package/Application/BottomTab/DuoTabBar.tsx +11 -24
  13. package/Application/BottomTab/TabBarIcon.tsx +15 -29
  14. package/Application/BottomTab/TabScreen.tsx +1 -5
  15. package/Application/BottomTab/index.tsx +33 -30
  16. package/Application/BottomTab/metrics.ts +25 -48
  17. package/Application/BottomTab/tabItems.ts +1 -1
  18. package/Application/BottomTab/usePressLift.ts +2 -2
  19. package/Application/Components/BackgroundImageView.tsx +4 -0
  20. package/Application/Components/DuoGlassBackground.tsx +5 -4
  21. package/Application/Components/HeaderAnimated.tsx +4 -0
  22. package/Application/Components/HeaderBackground.tsx +4 -0
  23. package/Application/Components/HeaderExtendHeader.tsx +11 -10
  24. package/Application/Components/HeaderLeft.tsx +2 -1
  25. package/Application/Components/HeaderRight.tsx +2 -1
  26. package/Application/Components/HeaderTitle.shared.ts +11 -9
  27. package/Application/Components/HeaderTitle.tsx +4 -3
  28. package/Application/Components/HeaderToolkitAction.tsx +17 -20
  29. package/Application/Components/NavigationButton.tsx +2 -1
  30. package/Application/Components/SearchHeader.tsx +17 -10
  31. package/Application/Components/TitleJourney.tsx +2 -2
  32. package/Application/Components/TitleLocation.tsx +2 -2
  33. package/Application/Components/TitleUser.tsx +2 -2
  34. package/Application/Components/useToolkitActions.tsx +17 -32
  35. package/Application/Localize.ts +7 -0
  36. package/Application/ModalScreen.tsx +9 -3
  37. package/Application/Navigation.ts +14 -16
  38. package/Application/NavigationContainer.tsx +19 -16
  39. package/Application/Navigator.ts +20 -48
  40. package/Application/ScaleSizeProvider.tsx +13 -6
  41. package/Application/StackScreen.tracking.ts +11 -26
  42. package/Application/StackScreen.tsx +8 -35
  43. package/Application/TooltipPortal.tsx +12 -5
  44. package/Application/WidgetContainer.tsx +10 -4
  45. package/Application/modalLayout.ts +13 -14
  46. package/Application/navigationOptions.tsx +21 -17
  47. package/Application/types.tabs.ts +1 -0
  48. package/Badge/Badge.tsx +5 -0
  49. package/Badge/BadgeDot.tsx +1 -0
  50. package/Badge/BadgeDotAnimation.tsx +15 -7
  51. package/Badge/BadgeRibbon.tsx +46 -38
  52. package/Badge/Shape.tsx +2 -0
  53. package/Badge/types.ts +2 -2
  54. package/Button/index.tsx +5 -16
  55. package/Button/utils.ts +9 -15
  56. package/CheckBox/index.tsx +4 -0
  57. package/Consts/theme.ts +10 -10
  58. package/Context/index.ts +5 -1
  59. package/Divider/DashDivider.tsx +1 -0
  60. package/Divider/index.tsx +4 -0
  61. package/FoundationList/index.tsx +4 -0
  62. package/Icon/index.tsx +6 -6
  63. package/Icon/types.ts +1 -1
  64. package/IconButton/index.tsx +4 -12
  65. package/Image/index.tsx +9 -7
  66. package/Input/Input.tsx +8 -9
  67. package/Input/InputDropDown.tsx +10 -12
  68. package/Input/InputMoney.tsx +12 -8
  69. package/Input/InputMoneyView.tsx +14 -7
  70. package/Input/InputOTP.tsx +35 -33
  71. package/Input/InputPhoneNumber.tsx +8 -9
  72. package/Input/InputSearch.tsx +4 -6
  73. package/Input/InputTextArea.tsx +4 -0
  74. package/Input/SearchTextTyping.tsx +4 -0
  75. package/Input/SystemTextInput.tsx +7 -2
  76. package/Input/TextTyping.tsx +4 -0
  77. package/Input/common.tsx +26 -12
  78. package/Input/index.tsx +17 -20
  79. package/Input/moneyUtils.ts +10 -4
  80. package/Input/styles.ts +0 -5
  81. package/Input/utils.ts +14 -12
  82. package/Layout/Card.tsx +7 -4
  83. package/Layout/FloatingButton.tsx +4 -0
  84. package/Layout/Item.tsx +6 -2
  85. package/Layout/ItemList.tsx +9 -7
  86. package/Layout/ItemSectionList.tsx +2 -1
  87. package/Layout/ReservedRegions.tsx +25 -25
  88. package/Layout/Screen.tsx +56 -58
  89. package/Layout/Section.tsx +8 -5
  90. package/Layout/TrackingScope.tsx +1 -0
  91. package/Layout/screenContent.tsx +4 -2
  92. package/Layout/screenFooter.tsx +28 -18
  93. package/Layout/screenHeader.tsx +34 -31
  94. package/Layout/screenQuickReturn.tsx +17 -20
  95. package/Layout/screenScroll.ts +4 -7
  96. package/Layout/screenTypes.ts +3 -2
  97. package/Layout/types.ts +2 -4
  98. package/Layout/utils.ts +31 -37
  99. package/Loader/DotLoader.tsx +4 -0
  100. package/Loader/Loader.tsx +4 -0
  101. package/Loader/ProgressBar.tsx +1 -0
  102. package/Loader/Spinner.tsx +4 -0
  103. package/Loader/utils.ts +11 -7
  104. package/Pagination/Dot.tsx +4 -0
  105. package/Pagination/PaginationDot.tsx +1 -0
  106. package/Pagination/PaginationNumber.tsx +5 -3
  107. package/Pagination/PaginationScroll.tsx +5 -0
  108. package/Pagination/PaginationWhiteDot.tsx +1 -0
  109. package/Pagination/index.tsx +4 -0
  110. package/Pagination/types.ts +2 -2
  111. package/Popup/PopupNotify.tsx +44 -48
  112. package/Popup/PopupPromotion.tsx +4 -7
  113. package/Popup/types.ts +4 -4
  114. package/Popup/utils.ts +4 -0
  115. package/Radio/index.tsx +4 -0
  116. package/Skeleton/index.tsx +8 -0
  117. package/Switch/index.tsx +1 -0
  118. package/Tag/index.tsx +5 -1
  119. package/Tag/types.ts +3 -3
  120. package/Text/index.tsx +14 -3
  121. package/Text/styles.ts +1 -1
  122. package/Text/types.ts +1 -6
  123. package/Text/utils.ts +20 -4
  124. package/Title/index.tsx +41 -32
  125. package/ViewBoundary/index.tsx +4 -2
  126. package/index.ts +0 -6
  127. package/package.json +1 -1
  128. package/Application/__tests__/modalLayout.test.ts +0 -128
@@ -51,8 +51,10 @@ export const useBottomSheetLayout = () => {
51
51
  };
52
52
  };
53
53
 
54
- // KeyboardAvoidingView lifts the whole sheet past the top on iPhone Duo,
55
- // so lift it here and shrink maxHeight by the same amount instead
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
- <View {...panHandlers}>
39
- <View style={{ height: INDICATOR_HEIGHT }}>
40
- <View style={styles.indicator} />
41
- </View>
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
- {options.header ? (
56
- <View style={Styles.flex}>{options.header}</View>
57
- ) : (
58
- <>
59
- {!leftOptions ? <View style={styles.iconButtonSpace} /> : null}
60
- <Text
61
- style={[Styles.flex, styles.title]}
62
- numberOfLines={1}
63
- typography="header_default_bold"
64
- >
65
- {options.title ?? ''}
66
- </Text>
67
- </>
68
- )}
69
- <ModalIconButton icon="navigation_close" onPress={onPressClose} />
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
- </View>
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 { onDismiss, onRequestClose, onPressBack, onPressClose } =
146
- useSheetDismiss({
147
- barrierDismissible,
148
- closeAnimation,
149
- navigator,
150
- leftOptions,
151
- action,
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
- {isFoldable ? (
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
- * one active indicator per bar (Background/tonal with its inner shadows) sliding to the
23
- * selected item; it lands in place on the first measure and slides from then on
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
- opacity: visible.value,
56
- width: size.value,
57
- transform: [{ translateX: position.value }],
58
- }
59
- : {
60
- opacity: visible.value,
61
- transform: [{ translateY: position.value }],
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: CTA_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: Special type takes 2 or 4 tabs
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
- * gap under the container: the home indicator / gesture bar area
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
- Math.max(
39
- Platform.OS === 'ios'
40
- ? Math.min(bottomInset, TabBarMetrics.systemIndicatorHeight)
41
- : bottomInset,
42
- Spacing.S,
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 press: the button shrinks while held; the label stays plain text
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
- * center action (.Nav/CTA): button rising out of the notch, label under it
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: FloatingButtonProps;
70
- }> = ({ floatingButton }) => {
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
- const labelText = (
77
- <Text
78
- numberOfLines={1}
79
- typography={'label_s_medium'}
80
- color={theme.colors.text.secondary}
81
- >
82
- {label}
83
- </Text>
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
- {typeof icon === 'string' ? (
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
- {labelText}
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
- * horizontal navigation bar: floating container, items split evenly, Special type puts
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
- <CurvedContainer
191
- width={width}
192
- height={TabBarMetrics.height}
193
- radius={TabBarMetrics.height / 2}
194
- notchCenterX={withCta ? width / 2 : undefined}
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
- * phone tab bar: the horizontal navigation bar floating over the tab content above the home
215
- * indicator, so the content shows around it and through the notch. The transparent strip
216
- * above it keeps the rising center action touchable
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
- // space the floating tab bar takes over the bottom of a tab's content (bar + home
5
- // indicator area, plus the rising center action when shown); 0 on the iPhone Duo rail
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
- // a Screen in the tab keeps its content clear of the bar itself (inside its scroll view,
8
- // or above the bar when static), so the tab stops padding its content
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, TabBarMetrics } from './metrics';
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
- // Figma Shadow/floating: 12% blur 24, plus 4% blur 32 spread -16 offset 12; svg blur takes
19
- // half the CSS blur radius, the spread shrinks the shape the key shadow is cast from
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 path placed at (`x`, `y`) on the canvas, with the notch dipped into the
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
- notch =
47
- ` L ${cx - NOTCH_HALF_WIDTH} ${top}` +
48
- NOTCH_CURVES.map(
49
- ([c1, c2, end]) => ` C ${point(c1)} ${point(c2)} ${point(end)}`,
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
- * navigation bar container: surface fill with the floating shadow, drawn as a shape so
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
- {width > 2 * KEY_SPREAD && height > 2 * KEY_SPREAD && (
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
  };