@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.
Files changed (126) 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.tsx +48 -49
  6. package/Application/BottomTab/BottomTabInset.tsx +8 -4
  7. package/Application/BottomTab/CurvedContainer.tsx +14 -12
  8. package/Application/BottomTab/CustomBottomTabItem.tsx +12 -13
  9. package/Application/BottomTab/DuoBarCompression.tsx +13 -19
  10. package/Application/BottomTab/DuoRailTabBar.tsx +20 -20
  11. package/Application/BottomTab/DuoTabBar.tsx +8 -9
  12. package/Application/BottomTab/TabBarIcon.tsx +15 -29
  13. package/Application/BottomTab/TabScreen.tsx +1 -5
  14. package/Application/BottomTab/index.tsx +33 -30
  15. package/Application/BottomTab/metrics.ts +22 -25
  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 +9 -3
  36. package/Application/Navigation.ts +14 -16
  37. package/Application/NavigationContainer.tsx +19 -16
  38. package/Application/Navigator.ts +20 -48
  39. package/Application/ScaleSizeProvider.tsx +13 -6
  40. package/Application/StackScreen.tracking.ts +11 -26
  41. package/Application/StackScreen.tsx +8 -35
  42. package/Application/TooltipPortal.tsx +12 -5
  43. package/Application/WidgetContainer.tsx +10 -4
  44. package/Application/modalLayout.ts +13 -14
  45. package/Application/navigationOptions.tsx +21 -17
  46. package/Badge/Badge.tsx +5 -0
  47. package/Badge/BadgeDot.tsx +1 -0
  48. package/Badge/BadgeDotAnimation.tsx +15 -7
  49. package/Badge/BadgeRibbon.tsx +46 -38
  50. package/Badge/Shape.tsx +2 -0
  51. package/Badge/types.ts +2 -2
  52. package/Button/index.tsx +5 -16
  53. package/Button/utils.ts +9 -15
  54. package/CheckBox/index.tsx +4 -0
  55. package/Consts/theme.ts +10 -10
  56. package/Context/index.ts +5 -1
  57. package/Divider/DashDivider.tsx +1 -0
  58. package/Divider/index.tsx +4 -0
  59. package/FoundationList/index.tsx +4 -0
  60. package/Icon/index.tsx +6 -6
  61. package/Icon/types.ts +1 -1
  62. package/IconButton/index.tsx +4 -12
  63. package/Image/index.tsx +9 -7
  64. package/Input/Input.tsx +8 -9
  65. package/Input/InputDropDown.tsx +10 -12
  66. package/Input/InputMoney.tsx +12 -8
  67. package/Input/InputMoneyView.tsx +14 -7
  68. package/Input/InputOTP.tsx +35 -33
  69. package/Input/InputPhoneNumber.tsx +8 -9
  70. package/Input/InputSearch.tsx +4 -6
  71. package/Input/InputTextArea.tsx +4 -0
  72. package/Input/SearchTextTyping.tsx +4 -0
  73. package/Input/SystemTextInput.tsx +7 -2
  74. package/Input/TextTyping.tsx +4 -0
  75. package/Input/common.tsx +26 -12
  76. package/Input/index.tsx +17 -20
  77. package/Input/moneyUtils.ts +10 -4
  78. package/Input/styles.ts +0 -5
  79. package/Input/utils.ts +14 -12
  80. package/Layout/Card.tsx +7 -4
  81. package/Layout/FloatingButton.tsx +4 -0
  82. package/Layout/Item.tsx +6 -2
  83. package/Layout/ItemList.tsx +9 -7
  84. package/Layout/ItemSectionList.tsx +2 -1
  85. package/Layout/ReservedRegions.tsx +25 -25
  86. package/Layout/Screen.tsx +56 -58
  87. package/Layout/Section.tsx +8 -5
  88. package/Layout/TrackingScope.tsx +1 -0
  89. package/Layout/screenContent.tsx +4 -2
  90. package/Layout/screenFooter.tsx +28 -18
  91. package/Layout/screenHeader.tsx +34 -31
  92. package/Layout/screenQuickReturn.tsx +17 -20
  93. package/Layout/screenScroll.ts +4 -7
  94. package/Layout/screenTypes.ts +3 -2
  95. package/Layout/types.ts +2 -4
  96. package/Layout/utils.ts +31 -37
  97. package/Loader/DotLoader.tsx +4 -0
  98. package/Loader/Loader.tsx +4 -0
  99. package/Loader/ProgressBar.tsx +1 -0
  100. package/Loader/Spinner.tsx +4 -0
  101. package/Loader/utils.ts +11 -7
  102. package/Pagination/Dot.tsx +4 -0
  103. package/Pagination/PaginationDot.tsx +1 -0
  104. package/Pagination/PaginationNumber.tsx +5 -3
  105. package/Pagination/PaginationScroll.tsx +5 -0
  106. package/Pagination/PaginationWhiteDot.tsx +1 -0
  107. package/Pagination/index.tsx +4 -0
  108. package/Pagination/types.ts +2 -2
  109. package/Popup/PopupNotify.tsx +44 -48
  110. package/Popup/PopupPromotion.tsx +4 -7
  111. package/Popup/types.ts +4 -4
  112. package/Popup/utils.ts +4 -0
  113. package/Radio/index.tsx +4 -0
  114. package/Skeleton/index.tsx +8 -0
  115. package/Switch/index.tsx +1 -0
  116. package/Tag/index.tsx +5 -1
  117. package/Tag/types.ts +3 -3
  118. package/Text/index.tsx +14 -3
  119. package/Text/styles.ts +1 -1
  120. package/Text/types.ts +1 -6
  121. package/Text/utils.ts +20 -4
  122. package/Title/index.tsx +41 -32
  123. package/ViewBoundary/index.tsx +4 -2
  124. package/index.ts +0 -6
  125. package/package.json +1 -1
  126. 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
@@ -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) {
@@ -211,9 +210,9 @@ const HorizontalTabBar: React.FC<{
211
210
  };
212
211
 
213
212
  /**
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
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
- // 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,
@@ -35,8 +35,8 @@ type TabItemContentProps = {
35
35
  };
36
36
 
37
37
  /**
38
- * icon in its indicator slot, label under it. The bar slides one shared active indicator
39
- * behind the slots; the icon and label tint cross-fade on focus
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
- // the active label fades in over the inactive one
73
- const text = (
74
- <View style={!vertical && styles.labelBox}>
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 && text}
97
+ {!hideLabel && labelNode}
98
98
  </>
99
99
  );
100
100
  };
@@ -108,7 +108,7 @@ type CustomBottomTabItemProps = TabItemContentProps & {
108
108
  };
109
109
 
110
110
  /**
111
- * horizontal navigation item (.Nav/itemTab): fills an even share of the container
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
- labelBox: {
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
- justifyContent: 'flex-start',
169
- overflow: 'visible',
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
- * 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 };