@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.
Files changed (138) hide show
  1. package/Application/BottomSheet.hooks.ts +8 -8
  2. package/Application/BottomSheet.parts.tsx +44 -34
  3. package/Application/BottomSheet.tsx +40 -37
  4. package/Application/BottomTab/ActiveIndicator.tsx +15 -14
  5. package/Application/BottomTab/BottomTabBar.tsx +43 -34
  6. package/Application/BottomTab/BottomTabInset.tsx +8 -4
  7. package/Application/BottomTab/CurvedContainer.tsx +14 -12
  8. package/Application/BottomTab/CustomBottomTabItem.tsx +10 -6
  9. package/Application/BottomTab/DuoBarCompression.tsx +13 -19
  10. package/Application/BottomTab/DuoRailTabBar.tsx +13 -13
  11. package/Application/BottomTab/DuoTabBar.tsx +8 -9
  12. package/Application/BottomTab/TabBarIcon.tsx +13 -8
  13. package/Application/BottomTab/TabScreen.tsx +1 -5
  14. package/Application/BottomTab/index.tsx +38 -30
  15. package/Application/BottomTab/metrics.ts +21 -20
  16. package/Application/BottomTab/tabItems.ts +1 -1
  17. package/Application/BottomTab/usePressLift.ts +2 -2
  18. package/Application/Components/BackgroundImageView.tsx +4 -0
  19. package/Application/Components/DuoGlassBackground.tsx +5 -4
  20. package/Application/Components/HeaderAnimated.tsx +4 -0
  21. package/Application/Components/HeaderBackground.tsx +4 -0
  22. package/Application/Components/HeaderExtendHeader.tsx +11 -10
  23. package/Application/Components/HeaderLeft.tsx +2 -1
  24. package/Application/Components/HeaderRight.tsx +2 -1
  25. package/Application/Components/HeaderTitle.shared.ts +11 -9
  26. package/Application/Components/HeaderTitle.tsx +4 -3
  27. package/Application/Components/HeaderToolkitAction.tsx +17 -20
  28. package/Application/Components/NavigationButton.tsx +2 -1
  29. package/Application/Components/SearchHeader.tsx +17 -10
  30. package/Application/Components/TitleJourney.tsx +2 -2
  31. package/Application/Components/TitleLocation.tsx +2 -2
  32. package/Application/Components/TitleUser.tsx +2 -2
  33. package/Application/Components/useToolkitActions.tsx +17 -32
  34. package/Application/Localize.ts +7 -0
  35. package/Application/ModalScreen.tsx +10 -10
  36. package/Application/Navigation.ts +14 -16
  37. package/Application/NavigationContainer.tsx +19 -18
  38. package/Application/Navigator.ts +20 -48
  39. package/Application/StackScreen.tracking.ts +11 -26
  40. package/Application/StackScreen.tsx +9 -39
  41. package/Application/TooltipPortal.tsx +12 -5
  42. package/Application/WidgetContainer.tsx +10 -4
  43. package/Application/modalLayout.ts +13 -14
  44. package/Application/navigationOptions.tsx +21 -17
  45. package/Badge/Badge.tsx +5 -0
  46. package/Badge/BadgeDot.tsx +1 -0
  47. package/Badge/BadgeDotAnimation.tsx +15 -7
  48. package/Badge/BadgeRibbon.tsx +46 -38
  49. package/Badge/Shape.tsx +2 -0
  50. package/Badge/types.ts +2 -2
  51. package/Button/index.tsx +5 -16
  52. package/Button/utils.ts +9 -15
  53. package/CheckBox/index.tsx +4 -0
  54. package/Consts/theme.ts +10 -10
  55. package/Context/index.ts +1 -0
  56. package/Divider/DashDivider.tsx +1 -0
  57. package/Divider/index.tsx +4 -0
  58. package/FoundationList/index.tsx +4 -0
  59. package/Icon/index.tsx +6 -6
  60. package/Icon/types.ts +1 -1
  61. package/IconButton/index.tsx +4 -12
  62. package/Image/index.tsx +9 -7
  63. package/Input/Input.tsx +8 -9
  64. package/Input/InputDropDown.tsx +10 -12
  65. package/Input/InputMoney.tsx +12 -8
  66. package/Input/InputMoneyView.tsx +14 -7
  67. package/Input/InputOTP.tsx +35 -33
  68. package/Input/InputPhoneNumber.tsx +8 -9
  69. package/Input/InputSearch.tsx +4 -6
  70. package/Input/InputTextArea.tsx +4 -0
  71. package/Input/SearchTextTyping.tsx +4 -0
  72. package/Input/SystemTextInput.tsx +7 -2
  73. package/Input/TextTyping.tsx +4 -0
  74. package/Input/common.tsx +26 -12
  75. package/Input/index.tsx +17 -20
  76. package/Input/moneyUtils.ts +10 -4
  77. package/Input/styles.ts +0 -5
  78. package/Input/utils.ts +14 -12
  79. package/Layout/Card.tsx +7 -4
  80. package/Layout/FloatingButton.tsx +4 -0
  81. package/Layout/Item.tsx +6 -2
  82. package/Layout/ItemList.tsx +9 -7
  83. package/Layout/ItemSectionList.tsx +2 -1
  84. package/Layout/ReservedRegions.tsx +25 -25
  85. package/Layout/Screen.tsx +56 -58
  86. package/Layout/Section.tsx +8 -5
  87. package/Layout/TrackingScope.tsx +1 -0
  88. package/Layout/screenContent.tsx +4 -2
  89. package/Layout/screenFooter.tsx +28 -18
  90. package/Layout/screenHeader.tsx +34 -31
  91. package/Layout/screenQuickReturn.tsx +17 -20
  92. package/Layout/screenScroll.ts +4 -7
  93. package/Layout/screenTypes.ts +3 -2
  94. package/Layout/types.ts +2 -4
  95. package/Layout/utils.ts +31 -37
  96. package/Loader/DotLoader.tsx +4 -0
  97. package/Loader/Loader.tsx +4 -0
  98. package/Loader/ProgressBar.tsx +1 -0
  99. package/Loader/Spinner.tsx +4 -0
  100. package/Loader/utils.ts +11 -7
  101. package/Pagination/Dot.tsx +4 -0
  102. package/Pagination/PaginationDot.tsx +1 -0
  103. package/Pagination/PaginationNumber.tsx +5 -3
  104. package/Pagination/PaginationScroll.tsx +5 -0
  105. package/Pagination/PaginationWhiteDot.tsx +1 -0
  106. package/Pagination/index.tsx +4 -0
  107. package/Pagination/types.ts +2 -2
  108. package/Popup/PopupNotify.tsx +44 -48
  109. package/Popup/PopupPromotion.tsx +4 -7
  110. package/Popup/types.ts +4 -4
  111. package/Popup/utils.ts +4 -0
  112. package/Radio/index.tsx +4 -0
  113. package/Skeleton/index.tsx +8 -0
  114. package/Switch/index.tsx +1 -0
  115. package/Tag/index.tsx +5 -1
  116. package/Tag/types.ts +3 -3
  117. package/Text/index.tsx +14 -3
  118. package/Text/styles.ts +1 -1
  119. package/Text/types.ts +1 -6
  120. package/Text/utils.ts +9 -0
  121. package/Title/index.tsx +41 -32
  122. package/ViewBoundary/index.tsx +4 -2
  123. package/index.ts +0 -6
  124. package/package.json +1 -1
  125. package/Application/KitAdoption/AdoptionBadge.tsx +0 -119
  126. package/Application/KitAdoption/AdoptionProbe.tsx +0 -145
  127. package/Application/KitAdoption/AdoptionStore.ts +0 -80
  128. package/Application/KitAdoption/Authorship/utils.ts +0 -137
  129. package/Application/KitAdoption/constants.ts +0 -118
  130. package/Application/KitAdoption/index.ts +0 -5
  131. package/Application/KitAdoption/styles.ts +0 -55
  132. package/Application/KitAdoption/types.ts +0 -48
  133. package/Application/KitAdoption/utils.ts +0 -375
  134. package/Application/__tests__/KitAdoption.integration.test.tsx +0 -235
  135. package/Application/__tests__/KitAdoption.test.ts +0 -283
  136. package/Application/__tests__/KitAdoptionAuthorship.test.ts +0 -132
  137. package/Application/__tests__/KitAdoptionStore.test.ts +0 -110
  138. 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.
@@ -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 { onDismiss, onRequestClose, onPressBack, onPressClose } =
147
- useSheetDismiss({
148
- barrierDismissible,
149
- closeAnimation,
150
- navigator,
151
- leftOptions,
152
- action,
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
- <AdoptionProbe
178
- screenName={Screen?.name || 'Invalid'}
179
- navigation={props.navigation}
180
- >
181
- <Screen
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
- {isFoldable ? (
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
- * 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
@@ -27,24 +27,28 @@ type Props = BottomTabBarProps & {
27
27
  };
28
28
 
29
29
  /**
30
- * the center action needs a tab on each side: Special type takes 2 or 4 tabs
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
- * gap under the container: the home indicator / gesture bar area
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
- Math.max(
40
- Platform.OS === 'ios'
41
- ? Math.min(bottomInset, TabBarMetrics.systemIndicatorHeight)
42
- : bottomInset,
43
- Spacing.S,
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 press: the button shrinks while held; the label stays plain text
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
- * center action (.Nav/CTA): button rising out of the notch, label under it
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
- {typeof icon === 'string' ? (
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
- {fixedFontScale ? (
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
- * horizontal navigation bar: floating container, items split evenly, Special type puts
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
- * phone tab bar: the horizontal navigation bar floating over the tab content above the home
231
- * indicator, so the content shows around it and through the notch. The transparent strip
232
- * above it keeps the rising center action touchable
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
- // 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
 
@@ -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,
@@ -37,8 +37,8 @@ type TabItemContentProps = {
37
37
  };
38
38
 
39
39
  /**
40
- * icon in its indicator slot, label under it. The bar slides one shared active indicator
41
- * behind the slots; the icon and label tint cross-fade on focus
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
- // the active label fades in over the inactive one
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
- * horizontal navigation item (.Nav/itemTab): fills an even share of the container
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
- * keeps the side bar's toolbar and tab bar clear of each other (HIG, Designing for
47
- * iPhone Duo: Vertical controls):
48
- * - both fit: both stay whole
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 compressedTabBar =
75
- DuoMetrics.controlSize +
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 canSwap =
83
- fits(COMPRESSED_TOOLBAR_HEIGHT, tabBar) &&
84
- fits(toolbarHeight, compressedTabBar);
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
- // an opened rail always shows, even when only the gap between the groups is short:
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 };