@momo-kits/foundation 0.167.1-beta.5 → 0.167.1-duo-test.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.
- package/Application/BottomSheet.tsx +115 -14
- package/Application/BottomTab/BottomTabBar.tsx +6 -3
- package/Application/BottomTab/DuoBarCompression.tsx +135 -0
- package/Application/BottomTab/DuoTabBar.tsx +462 -0
- package/Application/BottomTab/index.tsx +150 -134
- package/Application/Components/DuoGlassBackground.tsx +179 -0
- package/Application/Components/DuoQuickReturnHeader.tsx +42 -0
- package/Application/Components/HeaderExtendHeader.tsx +75 -28
- package/Application/Components/HeaderLeft.tsx +38 -0
- package/Application/Components/HeaderRight.tsx +142 -9
- package/Application/Components/HeaderTitle.tsx +67 -17
- package/Application/Components/OptionalGlassView.ts +11 -0
- package/Application/Components/OptionalScrollObserver.ts +12 -0
- package/Application/Components/SearchHeader.tsx +22 -15
- package/Application/Components/SideBarButton.tsx +139 -0
- package/Application/ModalScreen.tsx +16 -2
- package/Application/NavigationContainer.tsx +36 -45
- package/Application/Navigator.ts +1 -3
- package/Application/StackScreen.tsx +63 -9
- package/Application/WidgetContainer.tsx +9 -7
- package/Application/device.ts +29 -0
- package/Application/types.ts +27 -1
- package/Application/utils.tsx +255 -7
- package/Consts/index.ts +0 -1
- package/Consts/theme.ts +5 -6
- package/Input/Input.tsx +3 -4
- package/Input/InputMoney.tsx +31 -51
- package/Input/InputPhoneNumber.tsx +3 -4
- package/Input/InputSearch.tsx +3 -4
- package/Input/InputTextArea.tsx +3 -4
- package/Layout/Card.tsx +8 -26
- package/Layout/FloatingButton.tsx +3 -3
- package/Layout/Item.tsx +1 -10
- package/Layout/ReservedRegions.tsx +47 -0
- package/Layout/Screen.tsx +199 -52
- package/Layout/Section.tsx +5 -18
- package/Layout/index.ts +4 -4
- package/Layout/styles.ts +5 -0
- package/Layout/utils.ts +465 -5
- package/Popup/PopupNotify.tsx +33 -22
- package/Popup/PopupPromotion.tsx +20 -8
- package/Popup/utils.ts +32 -0
- package/Text/index.tsx +2 -11
- package/Text/utils.ts +1 -2
- package/package.json +3 -2
- package/Consts/designSystemFont.ts +0 -38
- package/Layout/GridSystem.tsx +0 -137
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
import 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,38 @@ 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
|
|
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 heightDevice = isDuo
|
|
56
|
+
? windowSize.height
|
|
57
|
+
: Dimensions.get('screen').height;
|
|
37
58
|
const action = useRef<undefined | string>(undefined);
|
|
38
59
|
const insets = useSafeAreaInsets();
|
|
39
60
|
const heightHeader = useHeaderHeight();
|
|
40
|
-
const
|
|
61
|
+
const topGap = Math.max(
|
|
62
|
+
getDuoTopInset(insets, windowSize.width, windowSize.height),
|
|
63
|
+
Spacing.XL,
|
|
64
|
+
);
|
|
65
|
+
const bottomInset =
|
|
66
|
+
Platform.OS === 'ios' ? Math.min(insets.bottom, 21) : insets.bottom;
|
|
41
67
|
const keyboardOffset = heightHeader - bottomInset;
|
|
42
68
|
|
|
43
69
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
@@ -58,6 +84,43 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
58
84
|
}: BottomSheetParams = props.route.params;
|
|
59
85
|
|
|
60
86
|
const translateY = useSharedValue(heightDevice);
|
|
87
|
+
const [keyboardLift, setKeyboardLift] = useState(0);
|
|
88
|
+
|
|
89
|
+
// KeyboardAvoidingView lifts the whole sheet past the top on iPhone Duo,
|
|
90
|
+
// so lift it here and shrink maxHeight by the same amount instead
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!isDuo || !useKeyboardAvoidingView) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const onKeyboardChange = (e: KeyboardEvent) => {
|
|
96
|
+
const overlap = windowSize.height - e.endCoordinates.screenY;
|
|
97
|
+
const lift = Math.max(
|
|
98
|
+
overlap - bottomInset - (keyboardVerticalOffset ?? 0),
|
|
99
|
+
0,
|
|
100
|
+
);
|
|
101
|
+
if (e.duration) {
|
|
102
|
+
LayoutAnimation.configureNext({
|
|
103
|
+
duration: e.duration,
|
|
104
|
+
update: {
|
|
105
|
+
duration: e.duration,
|
|
106
|
+
type: LayoutAnimation.Types[e.easing] || 'keyboard',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
setKeyboardLift(lift);
|
|
111
|
+
};
|
|
112
|
+
const subscriptions = [
|
|
113
|
+
Keyboard.addListener('keyboardWillChangeFrame', onKeyboardChange),
|
|
114
|
+
Keyboard.addListener('keyboardWillHide', onKeyboardChange),
|
|
115
|
+
];
|
|
116
|
+
return () => subscriptions.forEach(sub => sub.remove());
|
|
117
|
+
}, [
|
|
118
|
+
bottomInset,
|
|
119
|
+
isDuo,
|
|
120
|
+
keyboardVerticalOffset,
|
|
121
|
+
useKeyboardAvoidingView,
|
|
122
|
+
windowSize.height,
|
|
123
|
+
]);
|
|
61
124
|
|
|
62
125
|
const openAnimation = useCallback(() => {
|
|
63
126
|
translateY.value = withTiming(0, {
|
|
@@ -74,7 +137,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
74
137
|
duration: 200,
|
|
75
138
|
easing: Easing.bezier(0.3, 0.0, 0.8, 0.15),
|
|
76
139
|
},
|
|
77
|
-
() =>
|
|
140
|
+
() => scheduleOnRN(callback),
|
|
78
141
|
);
|
|
79
142
|
},
|
|
80
143
|
[heightDevice, translateY],
|
|
@@ -137,9 +200,9 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
137
200
|
}
|
|
138
201
|
closeAnimation(() => {
|
|
139
202
|
navigator?.pop();
|
|
140
|
-
|
|
203
|
+
scheduleOnRN(() => {
|
|
141
204
|
callback?.();
|
|
142
|
-
})
|
|
205
|
+
});
|
|
143
206
|
});
|
|
144
207
|
},
|
|
145
208
|
[barrierDismissible, closeAnimation, navigator],
|
|
@@ -248,6 +311,23 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
248
311
|
};
|
|
249
312
|
});
|
|
250
313
|
|
|
314
|
+
// landscape side buttons span the full height, so inset the content; in
|
|
315
|
+
// portrait the side status bar only covers the top corner
|
|
316
|
+
const sideInsetStyle = isDuo &&
|
|
317
|
+
!isDuoCentered &&
|
|
318
|
+
!isPortrait && {
|
|
319
|
+
paddingLeft: insets.left,
|
|
320
|
+
paddingRight: insets.right,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const screen = (
|
|
324
|
+
<Screen
|
|
325
|
+
{...props}
|
|
326
|
+
{...props.route.params}
|
|
327
|
+
onRequestClose={onRequestClose}
|
|
328
|
+
/>
|
|
329
|
+
);
|
|
330
|
+
|
|
251
331
|
return (
|
|
252
332
|
<Container
|
|
253
333
|
transparent
|
|
@@ -264,7 +344,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
264
344
|
android: undefined,
|
|
265
345
|
})}
|
|
266
346
|
keyboardVerticalOffset={keyboardVerticalOffset ?? keyboardOffset}
|
|
267
|
-
enabled={useKeyboardAvoidingView}
|
|
347
|
+
enabled={useKeyboardAvoidingView && !isDuo}
|
|
268
348
|
contentContainerStyle={styles.container}
|
|
269
349
|
style={styles.container}
|
|
270
350
|
>
|
|
@@ -287,15 +367,34 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
287
367
|
borderTopRightRadius: Radius.M,
|
|
288
368
|
overflow: 'hidden',
|
|
289
369
|
},
|
|
370
|
+
isDuo && {
|
|
371
|
+
maxHeight: windowSize.height - topGap - keyboardLift,
|
|
372
|
+
marginBottom: keyboardLift,
|
|
373
|
+
},
|
|
374
|
+
isDuoCentered && {
|
|
375
|
+
// keeps the portrait width in landscape, like system sheets
|
|
376
|
+
width: Math.min(
|
|
377
|
+
Math.min(windowSize.width, windowSize.height) - Spacing.L * 2,
|
|
378
|
+
windowSize.width -
|
|
379
|
+
Math.max(insets.left, insets.right, Spacing.L) * 2,
|
|
380
|
+
),
|
|
381
|
+
alignSelf: 'center',
|
|
382
|
+
},
|
|
290
383
|
showBaseLineDebug && styles.debugBaseLine,
|
|
291
384
|
]}
|
|
292
385
|
>
|
|
293
|
-
{
|
|
294
|
-
|
|
295
|
-
{
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
386
|
+
{isDuo ? (
|
|
387
|
+
// content shrinks so a tall sheet keeps its header and footer
|
|
388
|
+
<View style={[styles.shrink, sideInsetStyle]}>
|
|
389
|
+
{renderHeader()}
|
|
390
|
+
<View style={styles.shrink}>{screen}</View>
|
|
391
|
+
</View>
|
|
392
|
+
) : (
|
|
393
|
+
<>
|
|
394
|
+
{renderHeader()}
|
|
395
|
+
{screen}
|
|
396
|
+
</>
|
|
397
|
+
)}
|
|
299
398
|
{footerComponent && (
|
|
300
399
|
<View
|
|
301
400
|
style={[
|
|
@@ -304,6 +403,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
304
403
|
paddingBottom: Spacing.S,
|
|
305
404
|
backgroundColor: theme.colors.background.surface,
|
|
306
405
|
},
|
|
406
|
+
sideInsetStyle,
|
|
307
407
|
]}
|
|
308
408
|
>
|
|
309
409
|
{footerComponent}
|
|
@@ -353,6 +453,7 @@ const styles = StyleSheet.create({
|
|
|
353
453
|
justifyContent: 'center',
|
|
354
454
|
marginHorizontal: Spacing.M,
|
|
355
455
|
},
|
|
456
|
+
shrink: { flexShrink: 1 },
|
|
356
457
|
debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
|
|
357
458
|
});
|
|
358
459
|
|
|
@@ -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 =
|
|
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
|
-
|
|
251
|
+
paddingLeft: insets.left,
|
|
252
|
+
paddingRight: insets.right,
|
|
250
253
|
},
|
|
251
254
|
style,
|
|
252
255
|
]}
|
|
@@ -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
|
+
// Back alone, while a rail opened from the compressed state takes the toolbar slot
|
|
38
|
+
const BACK_ONLY_HEIGHT = DuoMetrics.controlSize;
|
|
39
|
+
|
|
40
|
+
const NO_COMPRESSION: DuoBarCompression = {
|
|
41
|
+
toolbarCompressed: false,
|
|
42
|
+
tabBarCompressed: false,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const DuoBarCompressionContext =
|
|
46
|
+
createContext<DuoBarCompression>(NO_COMPRESSION);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* height of the vertical tab rail, plus the QR button under it
|
|
50
|
+
*/
|
|
51
|
+
const getRailHeight = (tabCount: number, hasQr: boolean) =>
|
|
52
|
+
2 * RAIL_PADDING +
|
|
53
|
+
tabCount * DuoMetrics.controlSize +
|
|
54
|
+
Math.max(tabCount - 1, 0) * RAIL_GAP +
|
|
55
|
+
(hasQr ? Spacing.M + DuoMetrics.controlSize : 0);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* keeps the side bar's toolbar and tab bar clear of each other (HIG, Designing for
|
|
59
|
+
* iPhone Duo: Vertical controls):
|
|
60
|
+
* - both fit: both stay whole
|
|
61
|
+
* - one fits whole beside the other compressed: one is compressed, tapping it swaps them
|
|
62
|
+
* - otherwise both rest compressed; tapping one opens it for a moment (the rail closes
|
|
63
|
+
* again once a tab is picked)
|
|
64
|
+
*/
|
|
65
|
+
const DuoBarCompressionProvider: React.FC<{
|
|
66
|
+
behavior?: DuoBarCompressionBehavior;
|
|
67
|
+
tabCount: number;
|
|
68
|
+
hasQr: boolean;
|
|
69
|
+
children: ReactNode;
|
|
70
|
+
}> = ({ behavior = 'toolbar', tabCount, hasQr, children }) => {
|
|
71
|
+
const { height, sideBar } = useScreenLayout();
|
|
72
|
+
const [toolbarHeight, setToolbarHeight] = useState(DEFAULT_TOOLBAR_HEIGHT);
|
|
73
|
+
// 'toolbar' compression keeps the tab bar whole until the toolbar is tapped
|
|
74
|
+
const [expanded, setExpanded] = useState<'toolbar' | 'tabBar'>(
|
|
75
|
+
behavior === 'toolbar' ? 'tabBar' : 'toolbar',
|
|
76
|
+
);
|
|
77
|
+
// bar opened for a moment while both rest compressed
|
|
78
|
+
const [opened, setOpened] = useState<'toolbar' | 'tabBar'>();
|
|
79
|
+
|
|
80
|
+
const value = useMemo(() => {
|
|
81
|
+
if (!sideBar) {
|
|
82
|
+
return NO_COMPRESSION;
|
|
83
|
+
}
|
|
84
|
+
const room = height - sideBar.bottom - sideBar.top - GROUP_GAP;
|
|
85
|
+
const tabBar = getRailHeight(tabCount, hasQr);
|
|
86
|
+
const compressedTabBar = getRailHeight(1, hasQr) - 2 * RAIL_PADDING;
|
|
87
|
+
const fits = (toolbar: number, tabs: number) => toolbar + tabs <= room;
|
|
88
|
+
|
|
89
|
+
if (fits(toolbarHeight, tabBar)) {
|
|
90
|
+
return { ...NO_COMPRESSION, setToolbarHeight };
|
|
91
|
+
}
|
|
92
|
+
const canSwap =
|
|
93
|
+
fits(COMPRESSED_TOOLBAR_HEIGHT, tabBar) &&
|
|
94
|
+
fits(toolbarHeight, compressedTabBar);
|
|
95
|
+
if (canSwap) {
|
|
96
|
+
return {
|
|
97
|
+
toolbarCompressed: expanded === 'tabBar',
|
|
98
|
+
tabBarCompressed: expanded === 'toolbar',
|
|
99
|
+
expandToolbar: () => setExpanded('toolbar'),
|
|
100
|
+
expandTabBar: () => setExpanded('tabBar'),
|
|
101
|
+
setToolbarHeight,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const open = {
|
|
106
|
+
expandToolbar: () => setOpened('toolbar'),
|
|
107
|
+
expandTabBar: () => setOpened('tabBar'),
|
|
108
|
+
setToolbarHeight,
|
|
109
|
+
};
|
|
110
|
+
if (opened === 'tabBar' && fits(BACK_ONLY_HEIGHT, 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 };
|