@momo-kits/foundation 0.167.2-beta.1 → 0.168.1-beta.2

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 (53) hide show
  1. package/Application/BottomSheet.tsx +113 -14
  2. package/Application/BottomTab/BottomTabBar.tsx +6 -3
  3. package/Application/BottomTab/CustomBottomTabItem.tsx +3 -5
  4. package/Application/BottomTab/DuoBarCompression.tsx +135 -0
  5. package/Application/BottomTab/DuoTabBar.tsx +537 -0
  6. package/Application/BottomTab/index.tsx +145 -134
  7. package/Application/Components/DuoGlassBackground.tsx +158 -0
  8. package/Application/Components/DuoQuickReturnHeader.tsx +42 -0
  9. package/Application/Components/HeaderAnimated.tsx +9 -3
  10. package/Application/Components/HeaderExtendHeader.tsx +70 -28
  11. package/Application/Components/HeaderLeft.tsx +38 -0
  12. package/Application/Components/HeaderRight.tsx +144 -9
  13. package/Application/Components/HeaderTitle.tsx +43 -7
  14. package/Application/Components/SearchHeader.tsx +20 -15
  15. package/Application/Components/SideBarButton.tsx +134 -0
  16. package/Application/ModalScreen.tsx +16 -2
  17. package/Application/NavigationContainer.tsx +22 -38
  18. package/Application/Navigator.ts +1 -3
  19. package/Application/StackScreen.tsx +2 -5
  20. package/Application/device.ts +29 -0
  21. package/Application/types.ts +10 -1
  22. package/Application/utils.tsx +10 -7
  23. package/Badge/Badge.tsx +2 -3
  24. package/Badge/BadgeDotAnimation.tsx +3 -3
  25. package/Button/index.tsx +11 -11
  26. package/Consts/theme.ts +3 -4
  27. package/Input/Input.tsx +3 -4
  28. package/Input/InputMoney.tsx +22 -45
  29. package/Input/InputOTP.tsx +9 -12
  30. package/Input/InputPhoneNumber.tsx +3 -4
  31. package/Input/InputSearch.tsx +17 -14
  32. package/Input/InputTextArea.tsx +3 -4
  33. package/Input/SystemTextInput.tsx +2 -3
  34. package/Input/TextTyping.tsx +11 -6
  35. package/Input/utils.ts +2 -3
  36. package/Layout/Card.tsx +8 -26
  37. package/Layout/FloatingButton.tsx +3 -3
  38. package/Layout/Item.tsx +1 -10
  39. package/Layout/ReservedRegions.tsx +78 -0
  40. package/Layout/Screen.tsx +242 -48
  41. package/Layout/Section.tsx +5 -18
  42. package/Layout/index.ts +4 -4
  43. package/Layout/styles.ts +5 -0
  44. package/Layout/utils.ts +477 -5
  45. package/Pagination/Dot.tsx +3 -6
  46. package/Popup/PopupNotify.tsx +33 -22
  47. package/Popup/PopupPromotion.tsx +21 -8
  48. package/Popup/utils.ts +32 -0
  49. package/Radio/index.tsx +2 -3
  50. package/Text/utils.ts +1 -2
  51. package/Title/index.tsx +8 -11
  52. package/package.json +5 -2
  53. package/Layout/GridSystem.tsx +0 -137
@@ -1,14 +1,24 @@
1
- import React, { useCallback, useContext, useEffect, useRef } from 'react';
1
+ import React, {
2
+ useCallback,
3
+ useContext,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
2
8
  import {
3
9
  BackHandler,
4
10
  Dimensions,
11
+ Keyboard,
5
12
  KeyboardAvoidingView,
13
+ KeyboardEvent,
14
+ LayoutAnimation,
6
15
  Modal,
7
16
  PanResponder,
8
17
  Platform,
9
18
  Pressable,
10
19
  StyleSheet,
11
20
  TouchableOpacity,
21
+ useWindowDimensions,
12
22
  View,
13
23
  } from 'react-native';
14
24
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -22,22 +32,37 @@ import Animated, {
22
32
  Easing,
23
33
  Extrapolation,
24
34
  interpolate,
25
- runOnJS,
26
35
  useAnimatedStyle,
27
36
  useSharedValue,
28
37
  withSpring,
29
38
  withTiming,
30
39
  } from 'react-native-reanimated';
40
+ import { scheduleOnRN } from 'react-native-worklets';
31
41
  import layoutStyles from '../Layout/styles';
42
+ import { getDuoTopInset, isIPhoneDuo } from './device';
43
+
44
+ // iPhone Duo inner display: center the sheet instead of spanning the width
45
+ const DUO_REGULAR_MIN_WIDTH = 600;
32
46
 
33
47
  const BottomSheet: React.FC<BottomSheetParams> = props => {
34
48
  const { theme, navigator } = useContext(ApplicationContext);
35
49
  const context = useContext<any>(MiniAppContext);
36
- const heightDevice = Dimensions.get('screen').height;
50
+ const windowSize = useWindowDimensions();
51
+ const isDuo = isIPhoneDuo();
52
+ const isDuoCentered = isDuo && windowSize.width >= DUO_REGULAR_MIN_WIDTH;
53
+ const isPortrait = windowSize.height > windowSize.width;
54
+ // 'screen' is the outer display on iPhone Duo, even when unfolded
55
+ const screenHeight = Dimensions.get('screen').height;
56
+ const heightDevice = isDuo ? windowSize.height : screenHeight;
37
57
  const action = useRef<undefined | string>(undefined);
38
58
  const insets = useSafeAreaInsets();
39
59
  const heightHeader = useHeaderHeight();
40
- const bottomInset = Platform.OS === 'ios' ? Math.min(insets.bottom, 21) : insets.bottom;
60
+ const topGap = Math.max(
61
+ getDuoTopInset(insets, windowSize.width, windowSize.height),
62
+ Spacing.XL,
63
+ );
64
+ const isIOS = Platform.OS === 'ios';
65
+ const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
41
66
  const keyboardOffset = heightHeader - bottomInset;
42
67
 
43
68
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
@@ -58,6 +83,43 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
58
83
  }: BottomSheetParams = props.route.params;
59
84
 
60
85
  const translateY = useSharedValue(heightDevice);
86
+ const [keyboardLift, setKeyboardLift] = useState(0);
87
+
88
+ // KeyboardAvoidingView lifts the whole sheet past the top on iPhone Duo,
89
+ // so lift it here and shrink maxHeight by the same amount instead
90
+ useEffect(() => {
91
+ if (!isDuo || !useKeyboardAvoidingView) {
92
+ return;
93
+ }
94
+ const onKeyboardChange = (e: KeyboardEvent) => {
95
+ const overlap = windowSize.height - e.endCoordinates.screenY;
96
+ const lift = Math.max(
97
+ overlap - bottomInset - (keyboardVerticalOffset ?? 0),
98
+ 0,
99
+ );
100
+ if (e.duration) {
101
+ LayoutAnimation.configureNext({
102
+ duration: e.duration,
103
+ update: {
104
+ duration: e.duration,
105
+ type: LayoutAnimation.Types[e.easing] || 'keyboard',
106
+ },
107
+ });
108
+ }
109
+ setKeyboardLift(lift);
110
+ };
111
+ const subscriptions = [
112
+ Keyboard.addListener('keyboardWillChangeFrame', onKeyboardChange),
113
+ Keyboard.addListener('keyboardWillHide', onKeyboardChange),
114
+ ];
115
+ return () => subscriptions.forEach(sub => sub.remove());
116
+ }, [
117
+ bottomInset,
118
+ isDuo,
119
+ keyboardVerticalOffset,
120
+ useKeyboardAvoidingView,
121
+ windowSize.height,
122
+ ]);
61
123
 
62
124
  const openAnimation = useCallback(() => {
63
125
  translateY.value = withTiming(0, {
@@ -74,7 +136,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
74
136
  duration: 200,
75
137
  easing: Easing.bezier(0.3, 0.0, 0.8, 0.15),
76
138
  },
77
- () => runOnJS(callback)(),
139
+ () => scheduleOnRN(callback),
78
140
  );
79
141
  },
80
142
  [heightDevice, translateY],
@@ -137,9 +199,9 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
137
199
  }
138
200
  closeAnimation(() => {
139
201
  navigator?.pop();
140
- runOnJS(() => {
202
+ scheduleOnRN(() => {
141
203
  callback?.();
142
- })();
204
+ });
143
205
  });
144
206
  },
145
207
  [barrierDismissible, closeAnimation, navigator],
@@ -248,6 +310,22 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
248
310
  };
249
311
  });
250
312
 
313
+ // landscape side buttons span the full height, so inset the content; in
314
+ // portrait the side status bar only covers the top corner
315
+ const needsSideInset = isDuo && !isDuoCentered && !isPortrait;
316
+ const sideInsetStyle = needsSideInset && {
317
+ paddingLeft: insets.left,
318
+ paddingRight: insets.right,
319
+ };
320
+
321
+ const screen = (
322
+ <Screen
323
+ {...props}
324
+ {...props.route.params}
325
+ onRequestClose={onRequestClose}
326
+ />
327
+ );
328
+
251
329
  return (
252
330
  <Container
253
331
  transparent
@@ -264,7 +342,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
264
342
  android: undefined,
265
343
  })}
266
344
  keyboardVerticalOffset={keyboardVerticalOffset ?? keyboardOffset}
267
- enabled={useKeyboardAvoidingView}
345
+ enabled={useKeyboardAvoidingView && !isDuo}
268
346
  contentContainerStyle={styles.container}
269
347
  style={styles.container}
270
348
  >
@@ -287,15 +365,34 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
287
365
  borderTopRightRadius: Radius.M,
288
366
  overflow: 'hidden',
289
367
  },
368
+ isDuo && {
369
+ maxHeight: windowSize.height - topGap - keyboardLift,
370
+ marginBottom: keyboardLift,
371
+ },
372
+ isDuoCentered && {
373
+ // keeps the portrait width in landscape, like system sheets
374
+ width: Math.min(
375
+ Math.min(windowSize.width, windowSize.height) - Spacing.L * 2,
376
+ windowSize.width -
377
+ Math.max(insets.left, insets.right, Spacing.L) * 2,
378
+ ),
379
+ alignSelf: 'center',
380
+ },
290
381
  showBaseLineDebug && styles.debugBaseLine,
291
382
  ]}
292
383
  >
293
- {renderHeader()}
294
- <Screen
295
- {...props}
296
- {...props.route.params}
297
- onRequestClose={onRequestClose}
298
- />
384
+ {isDuo ? (
385
+ // content shrinks so a tall sheet keeps its header and footer
386
+ <View style={[styles.shrink, sideInsetStyle]}>
387
+ {renderHeader()}
388
+ <View style={styles.shrink}>{screen}</View>
389
+ </View>
390
+ ) : (
391
+ <>
392
+ {renderHeader()}
393
+ {screen}
394
+ </>
395
+ )}
299
396
  {footerComponent && (
300
397
  <View
301
398
  style={[
@@ -304,6 +401,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
304
401
  paddingBottom: Spacing.S,
305
402
  backgroundColor: theme.colors.background.surface,
306
403
  },
404
+ sideInsetStyle,
307
405
  ]}
308
406
  >
309
407
  {footerComponent}
@@ -353,6 +451,7 @@ const styles = StyleSheet.create({
353
451
  justifyContent: 'center',
354
452
  marginHorizontal: Spacing.M,
355
453
  },
454
+ shrink: { flexShrink: 1 },
356
455
  debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
357
456
  });
358
457
 
@@ -58,11 +58,13 @@ export default function BottomTabBar({
58
58
  const { theme } = useContext(ApplicationContext);
59
59
  const { routes } = state;
60
60
  const widthDevice = useWindowDimensions()?.width;
61
+ const insets = useSafeAreaInsets();
61
62
  const useFloating = floatingButton && state.routes.length % 2 === 0;
62
- const itemWidth = widthDevice / (state.routes.length + (useFloating ? 1 : 0));
63
+ const itemWidth =
64
+ (widthDevice - insets.left - insets.right) /
65
+ (state.routes.length + (useFloating ? 1 : 0));
63
66
  // const buildLink = useLinkBuilder();
64
67
  const indicatorAnimated = useSharedValue(0);
65
- const insets = useSafeAreaInsets();
66
68
 
67
69
  const indicatorStyle = useAnimatedStyle(() => ({
68
70
  width: itemWidth,
@@ -246,7 +248,8 @@ export default function BottomTabBar({
246
248
  },
247
249
  {
248
250
  paddingBottom: paddingBottom,
249
- paddingHorizontal: Math.max(insets.left, insets.right),
251
+ paddingLeft: insets.left,
252
+ paddingRight: insets.right,
250
253
  },
251
254
  style,
252
255
  ]}
@@ -116,17 +116,15 @@ const CustomBottomTabItem: React.FC<CustomBottomTabItemProps> = ({
116
116
  });
117
117
  };
118
118
 
119
+ const background = focused ? activeBackgroundColor : inactiveBackgroundColor;
120
+
119
121
  return (
120
122
  <TouchableOpacity
121
123
  onPress={onPress}
122
124
  onLongPress={onLongPress}
123
125
  style={[
124
126
  styles.container,
125
- {
126
- backgroundColor: focused
127
- ? activeBackgroundColor
128
- : inactiveBackgroundColor,
129
- },
127
+ { backgroundColor: background },
130
128
  style,
131
129
  showBaseLineDebug && styles.debugBaseLine,
132
130
  ]}
@@ -0,0 +1,135 @@
1
+ import React, {
2
+ createContext,
3
+ ReactNode,
4
+ useContext,
5
+ useMemo,
6
+ useState,
7
+ } from 'react';
8
+ import { Spacing } from '../../Consts';
9
+ import { useScreenLayout } from '../../Layout';
10
+ import { DuoMetrics } from '../../Layout/utils';
11
+
12
+ export type DuoBarCompressionBehavior = 'toolbar' | 'tabBar';
13
+
14
+ type DuoBarCompression = {
15
+ toolbarCompressed: boolean;
16
+ tabBarCompressed: boolean;
17
+ // the open rail needs the toolbar button slot too: only Back stays
18
+ toolbarHidden?: boolean;
19
+ // tapping the compressed bar expands it and compresses the other one
20
+ expandToolbar?: () => void;
21
+ expandTabBar?: () => void;
22
+ // a rail opened from the compressed state closes once a tab is picked
23
+ onTabSelected?: () => void;
24
+ // header buttons report their full height so the tab bar knows what is left
25
+ setToolbarHeight?: (height: number) => void;
26
+ };
27
+
28
+ const RAIL_PADDING = 6;
29
+ const RAIL_GAP = Spacing.XXS;
30
+ // space kept between the top and the bottom groups (HIG Vertical controls)
31
+ const GROUP_GAP = Spacing.M;
32
+ // Back, then the right toolkit: help + home capsule
33
+ const DEFAULT_TOOLBAR_HEIGHT =
34
+ DuoMetrics.controlSize + Spacing.M + 2 * DuoMetrics.itemPitch;
35
+ // Back, then the single button the toolbar folds into
36
+ const COMPRESSED_TOOLBAR_HEIGHT = 2 * DuoMetrics.controlSize + Spacing.M;
37
+
38
+ const NO_COMPRESSION: DuoBarCompression = {
39
+ toolbarCompressed: false,
40
+ tabBarCompressed: false,
41
+ };
42
+
43
+ const DuoBarCompressionContext =
44
+ createContext<DuoBarCompression>(NO_COMPRESSION);
45
+
46
+ /**
47
+ * height of the vertical tab rail, plus the QR button under it
48
+ */
49
+ const getRailHeight = (tabCount: number, hasQr: boolean) =>
50
+ 2 * RAIL_PADDING +
51
+ tabCount * DuoMetrics.controlSize +
52
+ Math.max(tabCount - 1, 0) * RAIL_GAP +
53
+ (hasQr ? Spacing.M + DuoMetrics.controlSize : 0);
54
+
55
+ /**
56
+ * keeps the side bar's toolbar and tab bar clear of each other (HIG, Designing for
57
+ * iPhone Duo: Vertical controls):
58
+ * - both fit: both stay whole
59
+ * - one fits whole beside the other compressed: one is compressed, tapping it swaps them
60
+ * - otherwise both rest compressed; tapping one opens it for a moment (the rail closes
61
+ * again once a tab is picked)
62
+ */
63
+ const DuoBarCompressionProvider: React.FC<{
64
+ behavior?: DuoBarCompressionBehavior;
65
+ tabCount: number;
66
+ hasQr: boolean;
67
+ children: ReactNode;
68
+ }> = ({ behavior = 'toolbar', tabCount, hasQr, children }) => {
69
+ const { height, sideBar } = useScreenLayout();
70
+ const [toolbarHeight, setToolbarHeight] = useState(DEFAULT_TOOLBAR_HEIGHT);
71
+ // 'toolbar' compression keeps the tab bar whole until the toolbar is tapped
72
+ const [expanded, setExpanded] = useState<'toolbar' | 'tabBar'>(
73
+ behavior === 'toolbar' ? 'tabBar' : 'toolbar',
74
+ );
75
+ // bar opened for a moment while both rest compressed
76
+ const [opened, setOpened] = useState<'toolbar' | 'tabBar'>();
77
+
78
+ const value = useMemo(() => {
79
+ if (!sideBar) {
80
+ return NO_COMPRESSION;
81
+ }
82
+ const room = height - sideBar.bottom - sideBar.top - GROUP_GAP;
83
+ const tabBar = getRailHeight(tabCount, hasQr);
84
+ const compressedTabBar = getRailHeight(1, hasQr) - 2 * RAIL_PADDING;
85
+ const fits = (toolbar: number, tabs: number) => toolbar + tabs <= room;
86
+
87
+ if (fits(toolbarHeight, tabBar)) {
88
+ return { ...NO_COMPRESSION, setToolbarHeight };
89
+ }
90
+ const canSwap =
91
+ fits(COMPRESSED_TOOLBAR_HEIGHT, tabBar) &&
92
+ fits(toolbarHeight, compressedTabBar);
93
+ if (canSwap) {
94
+ return {
95
+ toolbarCompressed: expanded === 'tabBar',
96
+ tabBarCompressed: expanded === 'toolbar',
97
+ expandToolbar: () => setExpanded('toolbar'),
98
+ expandTabBar: () => setExpanded('tabBar'),
99
+ setToolbarHeight,
100
+ };
101
+ }
102
+
103
+ const open = {
104
+ expandToolbar: () => setOpened('toolbar'),
105
+ expandTabBar: () => setOpened('tabBar'),
106
+ setToolbarHeight,
107
+ };
108
+ // an opened rail always shows, even when only the gap between the groups is short:
109
+ // it closes again once a tab is picked
110
+ if (opened === 'tabBar') {
111
+ return {
112
+ ...open,
113
+ toolbarCompressed: true,
114
+ toolbarHidden: true,
115
+ tabBarCompressed: false,
116
+ onTabSelected: () => setOpened(undefined),
117
+ };
118
+ }
119
+ return {
120
+ ...open,
121
+ toolbarCompressed: opened !== 'toolbar',
122
+ tabBarCompressed: true,
123
+ };
124
+ }, [expanded, opened, hasQr, height, sideBar, tabCount, toolbarHeight]);
125
+
126
+ return (
127
+ <DuoBarCompressionContext.Provider value={value}>
128
+ {children}
129
+ </DuoBarCompressionContext.Provider>
130
+ );
131
+ };
132
+
133
+ const useDuoBarCompression = () => useContext(DuoBarCompressionContext);
134
+
135
+ export { DuoBarCompressionProvider, useDuoBarCompression };