@momo-kits/foundation 0.168.1-beta.9 → 0.168.1-duo.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.
- package/Application/BottomSheet.tsx +156 -38
- package/Application/BottomTab/ActiveIndicator.tsx +90 -0
- package/Application/BottomTab/BottomTabBar.tsx +264 -255
- package/Application/BottomTab/CurvedContainer.tsx +153 -0
- package/Application/BottomTab/CustomBottomTabItem.tsx +132 -108
- package/Application/BottomTab/DuoBarCompression.tsx +4 -12
- package/Application/BottomTab/DuoTabBar.tsx +206 -402
- package/Application/BottomTab/TabBarIcon.tsx +160 -72
- package/Application/BottomTab/index.tsx +26 -174
- package/Application/BottomTab/metrics.ts +145 -0
- package/Application/BottomTab/tabItems.ts +46 -0
- package/Application/BottomTab/useLiquidSelection.ts +374 -0
- package/Application/Components/DuoGlassBackground.tsx +5 -8
- package/Application/ModalScreen.tsx +12 -8
- package/Application/device.ts +24 -1
- package/Application/utils.tsx +5 -9
- package/Layout/Screen.tsx +18 -39
- package/Layout/utils.ts +1 -0
- package/Popup/PopupNotify.tsx +20 -10
- package/Popup/utils.ts +9 -15
- package/package.json +2 -1
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
PanResponder,
|
|
17
17
|
Platform,
|
|
18
18
|
Pressable,
|
|
19
|
+
StatusBar,
|
|
19
20
|
StyleSheet,
|
|
20
21
|
TouchableOpacity,
|
|
21
22
|
useWindowDimensions,
|
|
@@ -39,18 +40,39 @@ import Animated, {
|
|
|
39
40
|
} from 'react-native-reanimated';
|
|
40
41
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
41
42
|
import layoutStyles from '../Layout/styles';
|
|
43
|
+
import { DuoMetrics, InnerEdgeStyles, useScreenLayout } from '../Layout/utils';
|
|
42
44
|
import { getDuoTopInset, isIPhoneDuo } from './device';
|
|
45
|
+
import { SideBarButton } from './Components/SideBarButton';
|
|
43
46
|
|
|
44
47
|
// iPhone Duo inner display: center the sheet instead of spanning the width
|
|
45
48
|
const DUO_REGULAR_MIN_WIDTH = 600;
|
|
46
49
|
|
|
50
|
+
const INDICATOR_HEIGHT = 16;
|
|
51
|
+
const HEADER_HEIGHT = 56;
|
|
52
|
+
const DUO_ORIENTATIONS = ['portrait', 'landscape'];
|
|
53
|
+
const SIDE_COLUMN_OFFSET =
|
|
54
|
+
INDICATOR_HEIGHT + (HEADER_HEIGHT - DuoMetrics.controlSize) / 2;
|
|
55
|
+
|
|
56
|
+
const getSideInset = (inset: number) => Math.max(inset - Spacing.M, 0);
|
|
57
|
+
|
|
47
58
|
const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
48
59
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
49
60
|
const context = useContext<any>(MiniAppContext);
|
|
50
61
|
const windowSize = useWindowDimensions();
|
|
51
62
|
const isDuo = isIPhoneDuo();
|
|
52
|
-
const isDuoCentered =
|
|
53
|
-
|
|
63
|
+
const isDuoCentered =
|
|
64
|
+
isDuo &&
|
|
65
|
+
Math.min(windowSize.width, windowSize.height) >= DUO_REGULAR_MIN_WIDTH;
|
|
66
|
+
const isDuoFloating = isDuo && !isDuoCentered;
|
|
67
|
+
const { sideBar } = useScreenLayout();
|
|
68
|
+
const sideColumnWidth = (sideBar?.width ?? 0) - Spacing.M;
|
|
69
|
+
const sideColumn =
|
|
70
|
+
isDuoFloating &&
|
|
71
|
+
sideColumnWidth >= DuoMetrics.controlSize + Spacing.M &&
|
|
72
|
+
sideBar
|
|
73
|
+
? sideBar
|
|
74
|
+
: undefined;
|
|
75
|
+
const hasSideColumn = !!sideColumn;
|
|
54
76
|
// 'screen' is the outer display on iPhone Duo, even when unfolded
|
|
55
77
|
const screenHeight = Dimensions.get('screen').height;
|
|
56
78
|
const heightDevice = isDuo ? windowSize.height : screenHeight;
|
|
@@ -58,9 +80,14 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
58
80
|
const insets = useSafeAreaInsets();
|
|
59
81
|
const heightHeader = useHeaderHeight();
|
|
60
82
|
const topGap = Math.max(
|
|
61
|
-
|
|
83
|
+
sideColumn
|
|
84
|
+
? insets.top
|
|
85
|
+
: getDuoTopInset(insets, windowSize.width, windowSize.height),
|
|
62
86
|
Spacing.XL,
|
|
63
87
|
);
|
|
88
|
+
const sideInsetLeft = isDuoFloating ? getSideInset(insets.left) : 0;
|
|
89
|
+
const sideInsetRight = isDuoFloating ? getSideInset(insets.right) : 0;
|
|
90
|
+
const indicatorShift = (sideInsetRight - sideInsetLeft) / 2;
|
|
64
91
|
const isIOS = Platform.OS === 'ios';
|
|
65
92
|
const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
|
|
66
93
|
const keyboardOffset = heightHeader - bottomInset;
|
|
@@ -83,7 +110,10 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
83
110
|
}: BottomSheetParams = props.route.params;
|
|
84
111
|
|
|
85
112
|
const translateY = useSharedValue(heightDevice);
|
|
113
|
+
const onDismissRef = useRef(props.route.params?.onDismiss);
|
|
114
|
+
onDismissRef.current = props.route.params?.onDismiss;
|
|
86
115
|
const [keyboardLift, setKeyboardLift] = useState(0);
|
|
116
|
+
const [sheetTop, setSheetTop] = useState(windowSize.height);
|
|
87
117
|
|
|
88
118
|
// KeyboardAvoidingView lifts the whole sheet past the top on iPhone Duo,
|
|
89
119
|
// so lift it here and shrink maxHeight by the same amount instead
|
|
@@ -179,15 +209,15 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
179
209
|
}
|
|
180
210
|
|
|
181
211
|
/**
|
|
182
|
-
* emit dismiss event
|
|
212
|
+
* emit dismiss event, once: folding or rotating iPhone Duo changes the
|
|
213
|
+
* window height while the sheet stays open
|
|
183
214
|
*/
|
|
184
215
|
useEffect(() => {
|
|
185
|
-
translateY.value = heightDevice;
|
|
186
216
|
openAnimation();
|
|
187
217
|
return () => {
|
|
188
|
-
|
|
218
|
+
onDismissRef.current?.(action.current);
|
|
189
219
|
};
|
|
190
|
-
}, [
|
|
220
|
+
}, [openAnimation]);
|
|
191
221
|
|
|
192
222
|
/**
|
|
193
223
|
* handler dismiss
|
|
@@ -228,14 +258,50 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
228
258
|
return () => backHandler.remove();
|
|
229
259
|
}, [barrierDismissible, onDismiss]);
|
|
230
260
|
|
|
261
|
+
const onPressBack = useCallback(() => {
|
|
262
|
+
onDismiss(true, leftOptions?.onPressIconLeft);
|
|
263
|
+
}, [leftOptions, onDismiss]);
|
|
264
|
+
|
|
265
|
+
const onPressClose = useCallback(() => {
|
|
266
|
+
action.current = 'icon_close';
|
|
267
|
+
onDismiss(true);
|
|
268
|
+
}, [onDismiss]);
|
|
269
|
+
|
|
270
|
+
const showBack = !!leftOptions?.onPressIconLeft;
|
|
271
|
+
const iconBack = leftOptions?.iconLeft ?? 'ic_back';
|
|
272
|
+
const sideColumnCount = showBack ? 2 : 1;
|
|
273
|
+
const sideColumnHeight =
|
|
274
|
+
sideColumnCount * DuoMetrics.controlSize +
|
|
275
|
+
(sideColumnCount - 1) * Spacing.M;
|
|
276
|
+
const coversStatusBar = !!sideColumn && sheetTop < sideColumn.top;
|
|
277
|
+
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (!coversStatusBar) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const entry = StatusBar.pushStackEntry({
|
|
283
|
+
barStyle: theme.dark ? 'light-content' : 'dark-content',
|
|
284
|
+
animated: true,
|
|
285
|
+
});
|
|
286
|
+
return () => StatusBar.popStackEntry(entry);
|
|
287
|
+
}, [coversStatusBar, theme.dark]);
|
|
288
|
+
|
|
231
289
|
/**
|
|
232
290
|
* render header
|
|
233
291
|
*/
|
|
234
292
|
const renderHeader = useCallback(() => {
|
|
293
|
+
const showButtons = !hasSideColumn;
|
|
235
294
|
return (
|
|
236
295
|
<View {...panResponder.panHandlers}>
|
|
237
|
-
<View style={{ height:
|
|
238
|
-
<View
|
|
296
|
+
<View style={{ height: INDICATOR_HEIGHT }}>
|
|
297
|
+
<View
|
|
298
|
+
style={[
|
|
299
|
+
styles.indicator,
|
|
300
|
+
indicatorShift !== 0 && {
|
|
301
|
+
transform: [{ translateX: indicatorShift }],
|
|
302
|
+
},
|
|
303
|
+
]}
|
|
304
|
+
/>
|
|
239
305
|
</View>
|
|
240
306
|
<View
|
|
241
307
|
style={[
|
|
@@ -246,14 +312,9 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
246
312
|
},
|
|
247
313
|
]}
|
|
248
314
|
>
|
|
249
|
-
{
|
|
250
|
-
<TouchableOpacity
|
|
251
|
-
|
|
252
|
-
onPress={() => {
|
|
253
|
-
onDismiss(true, leftOptions.onPressIconLeft);
|
|
254
|
-
}}
|
|
255
|
-
>
|
|
256
|
-
<Icon source={leftOptions.iconLeft ?? 'ic_back'} />
|
|
315
|
+
{showButtons && showBack ? (
|
|
316
|
+
<TouchableOpacity style={styles.iconButton} onPress={onPressBack}>
|
|
317
|
+
<Icon source={iconBack} />
|
|
257
318
|
</TouchableOpacity>
|
|
258
319
|
) : null}
|
|
259
320
|
|
|
@@ -261,9 +322,15 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
261
322
|
<View style={Styles.flex}>{options.header}</View>
|
|
262
323
|
) : (
|
|
263
324
|
<>
|
|
264
|
-
{!leftOptions ?
|
|
325
|
+
{showButtons && !leftOptions ? (
|
|
326
|
+
<View style={styles.iconButton} />
|
|
327
|
+
) : null}
|
|
265
328
|
<Text
|
|
266
|
-
style={[
|
|
329
|
+
style={[
|
|
330
|
+
Styles.flex,
|
|
331
|
+
styles.title,
|
|
332
|
+
!showButtons && styles.titleInset,
|
|
333
|
+
]}
|
|
267
334
|
numberOfLines={1}
|
|
268
335
|
typography="header_default_bold"
|
|
269
336
|
>
|
|
@@ -271,28 +338,64 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
271
338
|
</Text>
|
|
272
339
|
</>
|
|
273
340
|
)}
|
|
274
|
-
|
|
275
|
-
style={styles.iconButton}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}}
|
|
280
|
-
>
|
|
281
|
-
<Icon source="navigation_close" />
|
|
282
|
-
</TouchableOpacity>
|
|
341
|
+
{showButtons ? (
|
|
342
|
+
<TouchableOpacity style={styles.iconButton} onPress={onPressClose}>
|
|
343
|
+
<Icon source="navigation_close" />
|
|
344
|
+
</TouchableOpacity>
|
|
345
|
+
) : null}
|
|
283
346
|
</View>
|
|
284
347
|
</View>
|
|
285
348
|
);
|
|
286
349
|
}, [
|
|
287
|
-
|
|
350
|
+
hasSideColumn,
|
|
351
|
+
iconBack,
|
|
352
|
+
indicatorShift,
|
|
288
353
|
leftOptions,
|
|
354
|
+
onPressBack,
|
|
355
|
+
onPressClose,
|
|
289
356
|
options.header,
|
|
290
357
|
options.title,
|
|
291
358
|
panResponder.panHandlers,
|
|
359
|
+
showBack,
|
|
292
360
|
theme.colors.border.default,
|
|
293
361
|
useDivider,
|
|
294
362
|
]);
|
|
295
363
|
|
|
364
|
+
/**
|
|
365
|
+
* iPhone Duo folded: back and close move to the side bar column, drawn on
|
|
366
|
+
* the part of the sheet under the side safe area. They line up with the
|
|
367
|
+
* header row, stay below the status bar cluster on a tall sheet and above a
|
|
368
|
+
* camera at the bottom end of the strip.
|
|
369
|
+
*/
|
|
370
|
+
const renderSideColumn = () => {
|
|
371
|
+
if (!sideColumn) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
const lowestTop =
|
|
375
|
+
windowSize.height - sideColumn.bottom - sideColumnHeight - sheetTop;
|
|
376
|
+
const top = Math.min(
|
|
377
|
+
Math.max(sideColumn.top - sheetTop, SIDE_COLUMN_OFFSET),
|
|
378
|
+
lowestTop,
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<View
|
|
383
|
+
pointerEvents={'box-none'}
|
|
384
|
+
style={[
|
|
385
|
+
styles.sideColumn,
|
|
386
|
+
sideColumn.edge === 'right' ? styles.alignRight : styles.alignLeft,
|
|
387
|
+
InnerEdgeStyles[sideColumn.edge],
|
|
388
|
+
{ top, width: sideColumnWidth },
|
|
389
|
+
]}
|
|
390
|
+
>
|
|
391
|
+
{showBack ? (
|
|
392
|
+
<SideBarButton icon={iconBack} onPress={onPressBack} />
|
|
393
|
+
) : null}
|
|
394
|
+
<SideBarButton icon="navigation_close" onPress={onPressClose} />
|
|
395
|
+
</View>
|
|
396
|
+
);
|
|
397
|
+
};
|
|
398
|
+
|
|
296
399
|
const animatedStyle = useAnimatedStyle(() => {
|
|
297
400
|
return {
|
|
298
401
|
transform: [{ translateY: translateY.value }],
|
|
@@ -310,12 +413,9 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
310
413
|
};
|
|
311
414
|
});
|
|
312
415
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const sideInsetStyle = needsSideInset && {
|
|
317
|
-
paddingLeft: insets.left,
|
|
318
|
-
paddingRight: insets.right,
|
|
416
|
+
const sideInsetStyle = isDuoFloating && {
|
|
417
|
+
paddingLeft: sideInsetLeft,
|
|
418
|
+
paddingRight: sideInsetRight,
|
|
319
419
|
};
|
|
320
420
|
|
|
321
421
|
const screen = (
|
|
@@ -335,6 +435,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
335
435
|
}}
|
|
336
436
|
style={StyleSheet.absoluteFillObject}
|
|
337
437
|
isModalKit={true}
|
|
438
|
+
supportedOrientations={isDuo ? DUO_ORIENTATIONS : undefined}
|
|
338
439
|
>
|
|
339
440
|
<KeyboardAvoidingView
|
|
340
441
|
behavior={Platform.select({
|
|
@@ -357,18 +458,26 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
357
458
|
</Pressable>
|
|
358
459
|
|
|
359
460
|
<Animated.View
|
|
461
|
+
onLayout={
|
|
462
|
+
isDuoFloating ? e => setSheetTop(e.nativeEvent.layout.y) : undefined
|
|
463
|
+
}
|
|
360
464
|
style={[
|
|
361
465
|
animatedStyle,
|
|
362
466
|
{
|
|
363
467
|
backgroundColor,
|
|
364
|
-
borderTopLeftRadius: Radius.M,
|
|
365
|
-
borderTopRightRadius: Radius.M,
|
|
468
|
+
borderTopLeftRadius: isDuo ? Radius.XL : Radius.M,
|
|
469
|
+
borderTopRightRadius: isDuo ? Radius.XL : Radius.M,
|
|
366
470
|
overflow: 'hidden',
|
|
367
471
|
},
|
|
368
472
|
isDuo && {
|
|
369
473
|
maxHeight: windowSize.height - topGap - keyboardLift,
|
|
370
474
|
marginBottom: keyboardLift,
|
|
371
475
|
},
|
|
476
|
+
isDuoFloating && { marginHorizontal: Spacing.M },
|
|
477
|
+
sideColumn && {
|
|
478
|
+
minHeight:
|
|
479
|
+
SIDE_COLUMN_OFFSET + sideColumnHeight + sideColumn.bottom,
|
|
480
|
+
},
|
|
372
481
|
isDuoCentered && {
|
|
373
482
|
// keeps the portrait width in landscape, like system sheets
|
|
374
483
|
width: Math.min(
|
|
@@ -415,6 +524,7 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
|
|
|
415
524
|
}}
|
|
416
525
|
/>
|
|
417
526
|
)}
|
|
527
|
+
{renderSideColumn()}
|
|
418
528
|
</Animated.View>
|
|
419
529
|
</KeyboardAvoidingView>
|
|
420
530
|
</Container>
|
|
@@ -440,10 +550,12 @@ const styles = StyleSheet.create({
|
|
|
440
550
|
top: Spacing.S,
|
|
441
551
|
},
|
|
442
552
|
header: {
|
|
443
|
-
height:
|
|
553
|
+
height: HEADER_HEIGHT,
|
|
444
554
|
flexDirection: 'row',
|
|
445
555
|
alignItems: 'center',
|
|
446
556
|
},
|
|
557
|
+
title: { textAlign: 'center' },
|
|
558
|
+
titleInset: { paddingHorizontal: Spacing.M },
|
|
447
559
|
iconButton: {
|
|
448
560
|
width: 24,
|
|
449
561
|
height: 24,
|
|
@@ -452,6 +564,12 @@ const styles = StyleSheet.create({
|
|
|
452
564
|
marginHorizontal: Spacing.M,
|
|
453
565
|
},
|
|
454
566
|
shrink: { flexShrink: 1 },
|
|
567
|
+
sideColumn: {
|
|
568
|
+
position: 'absolute',
|
|
569
|
+
gap: Spacing.M,
|
|
570
|
+
},
|
|
571
|
+
alignRight: { right: 0 },
|
|
572
|
+
alignLeft: { left: 0 },
|
|
455
573
|
debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
|
|
456
574
|
});
|
|
457
575
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React, { useContext, useEffect, useRef } from 'react';
|
|
2
|
+
import { StyleProp, StyleSheet, ViewStyle } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withTiming,
|
|
7
|
+
} from 'react-native-reanimated';
|
|
8
|
+
import { ApplicationContext } from '../../Context';
|
|
9
|
+
import { getIndicatorShadow, TabBarMotion } from './metrics';
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
axis: 'x' | 'y';
|
|
13
|
+
// position of the selected item along the axis, undefined until it is measured
|
|
14
|
+
offset?: number;
|
|
15
|
+
// horizontal bar: width of the selected item
|
|
16
|
+
extent?: number;
|
|
17
|
+
radius: number;
|
|
18
|
+
style: StyleProp<ViewStyle>;
|
|
19
|
+
};
|
|
20
|
+
|
|
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
|
|
24
|
+
*/
|
|
25
|
+
const ActiveIndicator: React.FC<Props> = ({
|
|
26
|
+
axis,
|
|
27
|
+
offset,
|
|
28
|
+
extent,
|
|
29
|
+
radius,
|
|
30
|
+
style,
|
|
31
|
+
}) => {
|
|
32
|
+
const { theme } = useContext(ApplicationContext);
|
|
33
|
+
const position = useSharedValue(offset ?? 0);
|
|
34
|
+
const size = useSharedValue(extent ?? 0);
|
|
35
|
+
const visible = useSharedValue(offset === undefined ? 0 : 1);
|
|
36
|
+
const placed = useRef(offset !== undefined);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (offset === undefined) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
size.value = extent ?? 0;
|
|
43
|
+
if (!placed.current) {
|
|
44
|
+
placed.current = true;
|
|
45
|
+
position.value = offset;
|
|
46
|
+
visible.value = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
position.value = withTiming(offset, TabBarMotion.indicator);
|
|
50
|
+
}, [offset, extent, position, size, visible]);
|
|
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
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Animated.View
|
|
67
|
+
pointerEvents={'none'}
|
|
68
|
+
style={[
|
|
69
|
+
styles.indicator,
|
|
70
|
+
{
|
|
71
|
+
borderRadius: radius,
|
|
72
|
+
backgroundColor: theme.colors.background.tonal,
|
|
73
|
+
boxShadow: getIndicatorShadow(theme.colors.primary),
|
|
74
|
+
},
|
|
75
|
+
style,
|
|
76
|
+
animatedStyle,
|
|
77
|
+
]}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const styles = StyleSheet.create({
|
|
83
|
+
indicator: {
|
|
84
|
+
position: 'absolute',
|
|
85
|
+
left: 0,
|
|
86
|
+
top: 0,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export { ActiveIndicator };
|