@momo-kits/foundation 0.168.1-beta.11 → 0.168.1-beta.13
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 +142 -37
- package/Application/BottomTab/DuoTabBar.tsx +191 -151
- package/Application/BottomTab/useLiquidSelection.ts +374 -0
- package/Application/ModalScreen.tsx +12 -8
- package/Application/device.ts +24 -1
- package/Input/InputMoney.tsx +6 -15
- package/Input/styles.ts +1 -2
- package/Layout/utils.ts +1 -0
- package/Popup/PopupNotify.tsx +20 -10
- package/Popup/utils.ts +9 -15
- package/package.json +1 -1
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
|
2
2
|
import { CommonActions, Route } from '@react-navigation/native';
|
|
3
|
-
import React, { useContext,
|
|
3
|
+
import React, { ReactNode, useContext, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import {
|
|
5
5
|
LayoutChangeEvent,
|
|
6
|
+
StyleProp,
|
|
6
7
|
StyleSheet,
|
|
7
8
|
TouchableOpacity,
|
|
8
9
|
View,
|
|
10
|
+
ViewStyle,
|
|
9
11
|
} from 'react-native';
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
useSharedValue,
|
|
13
|
-
withSpring,
|
|
14
|
-
} from 'react-native-reanimated';
|
|
12
|
+
import { GestureDetector } from 'react-native-gesture-handler';
|
|
13
|
+
import Animated from 'react-native-reanimated';
|
|
15
14
|
import { Colors, Radius, Spacing } from '../../Consts';
|
|
16
15
|
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
17
16
|
import { Icon } from '../../Icon';
|
|
@@ -20,6 +19,12 @@ import { Text } from '../../Text';
|
|
|
20
19
|
import { useDuoBarCompression } from './DuoBarCompression';
|
|
21
20
|
import { FloatingButtonProps } from '../types';
|
|
22
21
|
import { TabBarIcon } from './TabBarIcon';
|
|
22
|
+
import {
|
|
23
|
+
LiquidSelection,
|
|
24
|
+
useLiquidItem,
|
|
25
|
+
useLiquidSelection,
|
|
26
|
+
usePressLift,
|
|
27
|
+
} from './useLiquidSelection';
|
|
23
28
|
|
|
24
29
|
type Props = BottomTabBarProps & {
|
|
25
30
|
activeTintColor: string;
|
|
@@ -40,11 +45,7 @@ const FLOATING_HEIGHT = DuoMetrics.floatingTabBarHeight;
|
|
|
40
45
|
const FLOATING_PADDING = Spacing.XS;
|
|
41
46
|
// the system bar sizes its pill to the tabs instead of stretching it across the display
|
|
42
47
|
const FLOATING_ITEM_WIDTH = 80;
|
|
43
|
-
// while pressed the selection bubble grows into a lens past the pill's edges, wider and taller (80x54 to about 96x70)
|
|
44
|
-
const LENS_SCALE_X = 1.2;
|
|
45
|
-
const LENS_SCALE_Y = 1.3;
|
|
46
48
|
const FLOATING_BUBBLE_RADIUS = (FLOATING_HEIGHT - 2 * FLOATING_PADDING) / 2;
|
|
47
|
-
const SPRING = { damping: 18, stiffness: 220, mass: 0.8 };
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* distance from the screen bottom to the floating tab bar, overlapping the home
|
|
@@ -121,11 +122,7 @@ const TabLabel: React.FC<{
|
|
|
121
122
|
fontScale: { useOSScaleRate: false, userScaleRate: 1 },
|
|
122
123
|
}}
|
|
123
124
|
>
|
|
124
|
-
<Text
|
|
125
|
-
numberOfLines={1}
|
|
126
|
-
typography={typography}
|
|
127
|
-
color={color}
|
|
128
|
-
>
|
|
125
|
+
<Text numberOfLines={1} typography={typography} color={color}>
|
|
129
126
|
{label}
|
|
130
127
|
</Text>
|
|
131
128
|
</MiniAppContext.Provider>
|
|
@@ -162,21 +159,22 @@ const getTabItems = ({
|
|
|
162
159
|
navigation.emit({ type: 'tabLongPress', target: route.key });
|
|
163
160
|
};
|
|
164
161
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
162
|
+
const renderIcon = (active: boolean) =>
|
|
163
|
+
options.tabBarIcon && (
|
|
164
|
+
<TabBarIcon
|
|
165
|
+
route={route}
|
|
166
|
+
badge={options.tabBarBadge}
|
|
167
|
+
badgeStyle={options.tabBarBadgeStyle}
|
|
168
|
+
activeOpacity={active ? 1 : 0}
|
|
169
|
+
inactiveOpacity={active ? 0 : 1}
|
|
170
|
+
activeTintColor={activeTintColor}
|
|
171
|
+
inactiveTintColor={inactiveTintColor}
|
|
172
|
+
renderIcon={options.tabBarIcon}
|
|
173
|
+
style={styles.icon}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return { route, focused, label, onPress, onLongPress, renderIcon };
|
|
180
178
|
});
|
|
181
179
|
};
|
|
182
180
|
|
|
@@ -192,6 +190,58 @@ const QrIcon: React.FC<{ icon: FloatingButtonProps['icon']; size: number }> = ({
|
|
|
192
190
|
|
|
193
191
|
type TabItem = ReturnType<typeof getTabItems>[number];
|
|
194
192
|
|
|
193
|
+
/**
|
|
194
|
+
* tab item: a tap is handled here as before, a drag over the bar is handled by the bar's
|
|
195
|
+
* gesture. The active layer fades in as the lens passes over it
|
|
196
|
+
*/
|
|
197
|
+
const LiquidTabItem: React.FC<{
|
|
198
|
+
item: TabItem;
|
|
199
|
+
itemIndex: number;
|
|
200
|
+
selection: LiquidSelection;
|
|
201
|
+
onSelect: () => void;
|
|
202
|
+
renderContent: (active: boolean) => ReactNode;
|
|
203
|
+
style: StyleProp<ViewStyle>;
|
|
204
|
+
}> = ({ item, itemIndex, selection, onSelect, renderContent, style }) => {
|
|
205
|
+
const { activeStyle, popStyle } = useLiquidItem(selection, itemIndex);
|
|
206
|
+
const activeLayerStyle = useMemo(
|
|
207
|
+
() => [StyleSheet.absoluteFill, styles.itemLayer, activeStyle],
|
|
208
|
+
[activeStyle],
|
|
209
|
+
);
|
|
210
|
+
// like the system tab bar, a held tab is still picked when released
|
|
211
|
+
const longPressed = useRef(false);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<TouchableOpacity
|
|
215
|
+
accessibilityRole={'tab'}
|
|
216
|
+
accessibilityLabel={item.label}
|
|
217
|
+
accessibilityState={{ selected: item.focused }}
|
|
218
|
+
activeOpacity={1}
|
|
219
|
+
onPressIn={() => {
|
|
220
|
+
longPressed.current = false;
|
|
221
|
+
selection.pressIn(itemIndex);
|
|
222
|
+
}}
|
|
223
|
+
onPressOut={() => {
|
|
224
|
+
if (selection.pressOut() && longPressed.current) {
|
|
225
|
+
onSelect();
|
|
226
|
+
}
|
|
227
|
+
}}
|
|
228
|
+
onPress={onSelect}
|
|
229
|
+
onLongPress={() => {
|
|
230
|
+
longPressed.current = true;
|
|
231
|
+
item.onLongPress();
|
|
232
|
+
}}
|
|
233
|
+
style={style}
|
|
234
|
+
>
|
|
235
|
+
<Animated.View style={popStyle}>
|
|
236
|
+
<View style={styles.itemLayer}>{renderContent(false)}</View>
|
|
237
|
+
<Animated.View style={activeLayerStyle}>
|
|
238
|
+
{renderContent(true)}
|
|
239
|
+
</Animated.View>
|
|
240
|
+
</Animated.View>
|
|
241
|
+
</TouchableOpacity>
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
|
|
195
245
|
/**
|
|
196
246
|
* column of icon-only tab items with the sliding selection bubble
|
|
197
247
|
*/
|
|
@@ -200,56 +250,47 @@ const Rail: React.FC<{
|
|
|
200
250
|
index: number;
|
|
201
251
|
onItemPress?: () => void;
|
|
202
252
|
}> = ({ items, index, onItemPress }) => {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
253
|
+
const onSelect = (itemIndex: number) => {
|
|
254
|
+
items[itemIndex]?.onPress();
|
|
255
|
+
onItemPress?.();
|
|
256
|
+
};
|
|
257
|
+
const selection = useLiquidSelection({
|
|
258
|
+
count: items.length,
|
|
259
|
+
pitch: RAIL_PITCH,
|
|
260
|
+
firstCenter: RAIL_PADDING + ITEM_SIZE / 2,
|
|
261
|
+
axis: 'y',
|
|
262
|
+
index,
|
|
263
|
+
onSelect,
|
|
264
|
+
});
|
|
265
|
+
const { barStyle, bubbleStyle } = selection;
|
|
266
|
+
// stable style arrays: the tab switch re-renders the rail while the lens is still
|
|
267
|
+
// springing back, a new array would re-attach the animated style mid animation
|
|
268
|
+
const railStyle = useMemo(() => [styles.rail, barStyle], [barStyle]);
|
|
269
|
+
const railBubbleStyle = useMemo(
|
|
270
|
+
() => [styles.railBubble, bubbleStyle],
|
|
271
|
+
[bubbleStyle],
|
|
272
|
+
);
|
|
217
273
|
|
|
218
274
|
return (
|
|
219
|
-
<
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
pointerEvents={'none'}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
275
|
+
<GestureDetector gesture={selection.gesture}>
|
|
276
|
+
<Animated.View style={railStyle} accessibilityLabel={'bottom_tab_bar'}>
|
|
277
|
+
<BarMaterial radius={ITEM_SIZE / 2} />
|
|
278
|
+
<Animated.View pointerEvents={'none'} style={railBubbleStyle}>
|
|
279
|
+
<SelectionMaterial radius={RAIL_BUBBLE_WIDTH / 2} />
|
|
280
|
+
</Animated.View>
|
|
281
|
+
{items.map((item, itemIndex) => (
|
|
282
|
+
<LiquidTabItem
|
|
283
|
+
key={item.route.key}
|
|
284
|
+
item={item}
|
|
285
|
+
itemIndex={itemIndex}
|
|
286
|
+
selection={selection}
|
|
287
|
+
onSelect={() => onSelect(itemIndex)}
|
|
288
|
+
renderContent={item.renderIcon}
|
|
289
|
+
style={styles.railItem}
|
|
290
|
+
/>
|
|
291
|
+
))}
|
|
226
292
|
</Animated.View>
|
|
227
|
-
|
|
228
|
-
<TouchableOpacity
|
|
229
|
-
key={item.route.key}
|
|
230
|
-
accessibilityRole={'tab'}
|
|
231
|
-
accessibilityLabel={item.label}
|
|
232
|
-
accessibilityState={{ selected: item.focused }}
|
|
233
|
-
activeOpacity={1}
|
|
234
|
-
onPressIn={() => {
|
|
235
|
-
bubbleOffset.value = withSpring(itemIndex * RAIL_PITCH, SPRING);
|
|
236
|
-
lens.value = withSpring(1, SPRING);
|
|
237
|
-
}}
|
|
238
|
-
onPressOut={() => {
|
|
239
|
-
bubbleOffset.value = withSpring(index * RAIL_PITCH, SPRING);
|
|
240
|
-
lens.value = withSpring(0, SPRING);
|
|
241
|
-
}}
|
|
242
|
-
onPress={() => {
|
|
243
|
-
item.onPress();
|
|
244
|
-
onItemPress?.();
|
|
245
|
-
}}
|
|
246
|
-
onLongPress={item.onLongPress}
|
|
247
|
-
style={styles.railItem}
|
|
248
|
-
>
|
|
249
|
-
{item.icon}
|
|
250
|
-
</TouchableOpacity>
|
|
251
|
-
))}
|
|
252
|
-
</View>
|
|
293
|
+
</GestureDetector>
|
|
253
294
|
);
|
|
254
295
|
};
|
|
255
296
|
|
|
@@ -263,6 +304,7 @@ const DuoRailTabBar: React.FC<Props> = props => {
|
|
|
263
304
|
const { sideBar } = useScreenLayout();
|
|
264
305
|
const { tabBarCompressed, expandTabBar, onTabSelected } =
|
|
265
306
|
useDuoBarCompression();
|
|
307
|
+
const compactLift = usePressLift();
|
|
266
308
|
const { state, floatingButton } = props;
|
|
267
309
|
const items = getTabItems(props);
|
|
268
310
|
const current = items[state.index];
|
|
@@ -303,16 +345,21 @@ const DuoRailTabBar: React.FC<Props> = props => {
|
|
|
303
345
|
|
|
304
346
|
return (
|
|
305
347
|
<View pointerEvents={'box-none'} style={columnStyle}>
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
348
|
+
<Animated.View style={compactLift.style}>
|
|
349
|
+
<TouchableOpacity
|
|
350
|
+
accessibilityRole={'button'}
|
|
351
|
+
accessibilityLabel={current?.label}
|
|
352
|
+
accessibilityHint={'bottom_tab_bar'}
|
|
353
|
+
activeOpacity={1}
|
|
354
|
+
onPressIn={compactLift.onPressIn}
|
|
355
|
+
onPressOut={compactLift.onPressOut}
|
|
356
|
+
onPress={expandTabBar}
|
|
357
|
+
style={styles.railCompact}
|
|
358
|
+
>
|
|
359
|
+
<BarMaterial radius={Radius.XL} />
|
|
360
|
+
{current?.renderIcon(true)}
|
|
361
|
+
</TouchableOpacity>
|
|
362
|
+
</Animated.View>
|
|
316
363
|
{qr}
|
|
317
364
|
</View>
|
|
318
365
|
);
|
|
@@ -330,24 +377,29 @@ const DuoFloatingTabBar: React.FC<Props> = props => {
|
|
|
330
377
|
const [rowWidth, setRowWidth] = useState(0);
|
|
331
378
|
const cellWidth = rowWidth / state.routes.length;
|
|
332
379
|
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
{
|
|
348
|
-
|
|
380
|
+
const selection = useLiquidSelection({
|
|
381
|
+
count: items.length,
|
|
382
|
+
pitch: cellWidth,
|
|
383
|
+
firstCenter: cellWidth / 2,
|
|
384
|
+
axis: 'x',
|
|
385
|
+
index: state.index,
|
|
386
|
+
onSelect: itemIndex => items[itemIndex]?.onPress(),
|
|
387
|
+
});
|
|
388
|
+
const { barStyle, bubbleStyle } = selection;
|
|
389
|
+
const tabCount = state.routes.length;
|
|
390
|
+
// stable style arrays, see Rail
|
|
391
|
+
const floatingBarStyle = useMemo(
|
|
392
|
+
() => [
|
|
393
|
+
styles.floatingBar,
|
|
394
|
+
{ width: tabCount * FLOATING_ITEM_WIDTH + 2 * FLOATING_PADDING },
|
|
395
|
+
barStyle,
|
|
349
396
|
],
|
|
350
|
-
|
|
397
|
+
[tabCount, barStyle],
|
|
398
|
+
);
|
|
399
|
+
const floatingBubbleStyle = useMemo(
|
|
400
|
+
() => [styles.floatingBubble, { width: cellWidth }, bubbleStyle],
|
|
401
|
+
[cellWidth, bubbleStyle],
|
|
402
|
+
);
|
|
351
403
|
|
|
352
404
|
const onRowLayout = (e: LayoutChangeEvent) =>
|
|
353
405
|
setRowWidth(e.nativeEvent.layout.width);
|
|
@@ -364,57 +416,41 @@ const DuoFloatingTabBar: React.FC<Props> = props => {
|
|
|
364
416
|
},
|
|
365
417
|
]}
|
|
366
418
|
>
|
|
367
|
-
<View
|
|
368
|
-
style={
|
|
369
|
-
styles.floatingBar,
|
|
370
|
-
{
|
|
371
|
-
width:
|
|
372
|
-
state.routes.length * FLOATING_ITEM_WIDTH + 2 * FLOATING_PADDING,
|
|
373
|
-
},
|
|
374
|
-
]}
|
|
419
|
+
<Animated.View
|
|
420
|
+
style={floatingBarStyle}
|
|
375
421
|
accessibilityLabel={'bottom_tab_bar'}
|
|
376
422
|
>
|
|
377
423
|
<BarMaterial radius={FLOATING_HEIGHT / 2} />
|
|
378
|
-
<
|
|
379
|
-
{
|
|
380
|
-
|
|
381
|
-
pointerEvents={'none'}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}}
|
|
404
|
-
onPress={item.onPress}
|
|
405
|
-
onLongPress={item.onLongPress}
|
|
406
|
-
style={styles.floatingItem}
|
|
407
|
-
>
|
|
408
|
-
{item.icon}
|
|
409
|
-
<TabLabel
|
|
410
|
-
label={item.label}
|
|
411
|
-
typography={'label_xs_medium'}
|
|
412
|
-
color={item.focused ? activeTintColor : inactiveTintColor}
|
|
424
|
+
<GestureDetector gesture={selection.gesture}>
|
|
425
|
+
<View style={styles.floatingRow} onLayout={onRowLayout}>
|
|
426
|
+
{rowWidth > 0 && (
|
|
427
|
+
<Animated.View pointerEvents={'none'} style={floatingBubbleStyle}>
|
|
428
|
+
<SelectionMaterial radius={FLOATING_BUBBLE_RADIUS} />
|
|
429
|
+
</Animated.View>
|
|
430
|
+
)}
|
|
431
|
+
{items.map((item, index) => (
|
|
432
|
+
<LiquidTabItem
|
|
433
|
+
key={item.route.key}
|
|
434
|
+
item={item}
|
|
435
|
+
itemIndex={index}
|
|
436
|
+
selection={selection}
|
|
437
|
+
onSelect={item.onPress}
|
|
438
|
+
renderContent={active => (
|
|
439
|
+
<>
|
|
440
|
+
{item.renderIcon(active)}
|
|
441
|
+
<TabLabel
|
|
442
|
+
label={item.label}
|
|
443
|
+
typography={'label_xs_medium'}
|
|
444
|
+
color={active ? activeTintColor : inactiveTintColor}
|
|
445
|
+
/>
|
|
446
|
+
</>
|
|
447
|
+
)}
|
|
448
|
+
style={styles.floatingItem}
|
|
413
449
|
/>
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
</
|
|
417
|
-
</View>
|
|
450
|
+
))}
|
|
451
|
+
</View>
|
|
452
|
+
</GestureDetector>
|
|
453
|
+
</Animated.View>
|
|
418
454
|
{floatingButton && (
|
|
419
455
|
<TouchableOpacity
|
|
420
456
|
accessibilityRole={'button'}
|
|
@@ -518,6 +554,10 @@ const styles = StyleSheet.create({
|
|
|
518
554
|
flex: 1,
|
|
519
555
|
alignItems: 'center',
|
|
520
556
|
justifyContent: 'center',
|
|
557
|
+
},
|
|
558
|
+
itemLayer: {
|
|
559
|
+
alignItems: 'center',
|
|
560
|
+
justifyContent: 'center',
|
|
521
561
|
gap: Spacing.XXS,
|
|
522
562
|
},
|
|
523
563
|
// pink fills the whole pinned platter
|