@momo-kits/foundation 0.164.1-beta.1 → 0.164.1-beta.3
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.
|
@@ -16,7 +16,7 @@ import { DeviceEventEmitter } from 'react-native';
|
|
|
16
16
|
import StackScreen from './StackScreen';
|
|
17
17
|
import ModalScreen from './ModalScreen';
|
|
18
18
|
import Navigator from './Navigator';
|
|
19
|
-
import { getModalOptions, getStackOptions } from './utils';
|
|
19
|
+
import { getModalOptions, getStackOptions, useNativeCanGoBack } from './utils';
|
|
20
20
|
import { NavigationContainerProps } from './types';
|
|
21
21
|
import { ApplicationContext, MiniAppContext } from '../Context';
|
|
22
22
|
import Localize from './Localize';
|
|
@@ -93,6 +93,7 @@ const Navigation: React.FC<any> = ({
|
|
|
93
93
|
const navigator = useRef(new Navigator(navigationRef, isReady));
|
|
94
94
|
const [showGrid, setShowGrid] = useState(false);
|
|
95
95
|
const insets = useSafeAreaInsets();
|
|
96
|
+
const syncCanGoBack = useNativeCanGoBack();
|
|
96
97
|
|
|
97
98
|
let headerBackground = context?.designSystemConfig?.config?.headerBar;
|
|
98
99
|
let headerGradient = theme.colors?.gradient;
|
|
@@ -192,7 +193,9 @@ const Navigation: React.FC<any> = ({
|
|
|
192
193
|
ref={navigationRef}
|
|
193
194
|
onReady={() => {
|
|
194
195
|
isReady.current = true;
|
|
196
|
+
syncCanGoBack(navigationRef.current?.getRootState?.());
|
|
195
197
|
}}
|
|
198
|
+
onStateChange={syncCanGoBack}
|
|
196
199
|
>
|
|
197
200
|
<Stack.Navigator
|
|
198
201
|
initialRouteName="Stack"
|
|
@@ -6,7 +6,7 @@ import Navigation from './Navigation';
|
|
|
6
6
|
import { ApplicationContext, MiniAppContext, ScreenContext } from '../Context';
|
|
7
7
|
import { GridSystem } from '../Layout';
|
|
8
8
|
import { version } from '../package.json';
|
|
9
|
-
import { useAppState } from './utils';
|
|
9
|
+
import { useAppState, useSwipeBackHaptic } from './utils';
|
|
10
10
|
import { TooltipPortalHost, TooltipPortalProvider } from './TooltipPortal';
|
|
11
11
|
|
|
12
12
|
const runAfterInteractions = InteractionManager.runAfterInteractions;
|
|
@@ -51,6 +51,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
51
51
|
const navigation = useRef(new Navigation(props.navigation, context)).current;
|
|
52
52
|
const heightHeader = useHeaderHeight();
|
|
53
53
|
const { isBackgroundToForeground } = useAppState();
|
|
54
|
+
useSwipeBackHaptic();
|
|
54
55
|
|
|
55
56
|
const data = {
|
|
56
57
|
...initialParams,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
3
3
|
import Navigator from './Navigator';
|
|
4
|
-
import ScaleSizeProvider from './ScaleSizeProvider';
|
|
5
4
|
import { WidgetContainerProps } from './types';
|
|
6
5
|
import { ApplicationContext, MiniAppContext } from '../Context';
|
|
7
6
|
import Localize from './Localize';
|
package/Application/utils.tsx
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import {
|
|
9
|
+
CardAnimationContext,
|
|
10
|
+
CardStyleInterpolators,
|
|
3
11
|
StackNavigationOptions,
|
|
4
12
|
TransitionPresets,
|
|
5
13
|
} from '@react-navigation/stack';
|
|
14
|
+
import type {
|
|
15
|
+
StackCardInterpolatedStyle,
|
|
16
|
+
StackCardInterpolationProps,
|
|
17
|
+
} from '@react-navigation/stack';
|
|
18
|
+
import type { NavigationState, PartialState } from '@react-navigation/native';
|
|
6
19
|
import type { HeaderTitleProps, NavigationOptions } from './types';
|
|
7
20
|
import { Colors, Spacing } from '../Consts';
|
|
8
|
-
import { Animated, AppState, Platform } from 'react-native';
|
|
21
|
+
import { Animated, AppState, NativeModules, Platform } from 'react-native';
|
|
9
22
|
import type { SharedValue } from 'react-native-reanimated';
|
|
10
23
|
import { useSharedValue } from 'react-native-reanimated';
|
|
11
24
|
import {
|
|
25
|
+
ApplicationContext,
|
|
12
26
|
MiniAppContext,
|
|
13
27
|
ScreenContext,
|
|
14
28
|
TrackingScopeContext,
|
|
@@ -18,6 +32,30 @@ import { HeaderBackground } from './Components/HeaderBackground';
|
|
|
18
32
|
import { HeaderLeft } from './Components/HeaderLeft';
|
|
19
33
|
import { HeaderRight } from './Components/HeaderRight';
|
|
20
34
|
|
|
35
|
+
const forStackTransitionWithOverlay = (
|
|
36
|
+
props: StackCardInterpolationProps,
|
|
37
|
+
): StackCardInterpolatedStyle => {
|
|
38
|
+
const { cardStyle } = CardStyleInterpolators.forHorizontalIOS(props);
|
|
39
|
+
return {
|
|
40
|
+
cardStyle,
|
|
41
|
+
shadowStyle: {
|
|
42
|
+
shadowOpacity: props.current.progress.interpolate({
|
|
43
|
+
inputRange: [0, 1],
|
|
44
|
+
outputRange: [0, 0.3],
|
|
45
|
+
extrapolate: 'clamp',
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
overlayStyle: {
|
|
49
|
+
backgroundColor: '#FDEAF4',
|
|
50
|
+
opacity: props.current.progress.interpolate({
|
|
51
|
+
inputRange: [0, 1],
|
|
52
|
+
outputRange: [0, 0.6],
|
|
53
|
+
extrapolate: 'clamp',
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
21
59
|
/**
|
|
22
60
|
* default options for stack screen
|
|
23
61
|
*/
|
|
@@ -38,8 +76,10 @@ const getStackOptions = (): StackNavigationOptions => {
|
|
|
38
76
|
headerLeft: (props: any) => <HeaderLeft {...props} />,
|
|
39
77
|
headerRight: (props: any) => <HeaderRight {...props} />,
|
|
40
78
|
headerTintColor: Colors.black_17,
|
|
41
|
-
gestureEnabled:
|
|
79
|
+
gestureEnabled: Platform.OS === 'ios',
|
|
42
80
|
...TransitionPresets.SlideFromRightIOS,
|
|
81
|
+
cardStyleInterpolator: forStackTransitionWithOverlay,
|
|
82
|
+
cardOverlayEnabled: true,
|
|
43
83
|
};
|
|
44
84
|
};
|
|
45
85
|
|
|
@@ -54,9 +94,17 @@ const getModalOptions = (): StackNavigationOptions => {
|
|
|
54
94
|
cardOverlayEnabled: true,
|
|
55
95
|
animation: 'none',
|
|
56
96
|
presentation: 'transparentModal',
|
|
97
|
+
gestureEnabled: false,
|
|
57
98
|
};
|
|
58
99
|
};
|
|
59
100
|
|
|
101
|
+
/**
|
|
102
|
+
* screen owns its back flow (guard popup, custom handler or no back button),
|
|
103
|
+
* so swipe back must not bypass it
|
|
104
|
+
*/
|
|
105
|
+
const hasCustomBackHandling = (params: NavigationOptions) =>
|
|
106
|
+
params.preventBack !== undefined || params.hiddenBack === true;
|
|
107
|
+
|
|
60
108
|
/**
|
|
61
109
|
* build react-navigation options
|
|
62
110
|
*/
|
|
@@ -125,6 +173,13 @@ const getOptions = (
|
|
|
125
173
|
};
|
|
126
174
|
}
|
|
127
175
|
|
|
176
|
+
/**
|
|
177
|
+
* only ever turns swipe back off, screens opt in via gestureEnabled
|
|
178
|
+
*/
|
|
179
|
+
if (params.gestureEnabled === undefined && hasCustomBackHandling(params)) {
|
|
180
|
+
options.gestureEnabled = false;
|
|
181
|
+
}
|
|
182
|
+
|
|
128
183
|
return {
|
|
129
184
|
...params,
|
|
130
185
|
...options,
|
|
@@ -230,6 +285,165 @@ const useAppState = () => {
|
|
|
230
285
|
};
|
|
231
286
|
};
|
|
232
287
|
|
|
288
|
+
/**
|
|
289
|
+
* screens the focused navigation branch can still pop.
|
|
290
|
+
* only stack navigators count, switching bottom tab is not a back step
|
|
291
|
+
*/
|
|
292
|
+
const getStackDepth = (
|
|
293
|
+
state?: NavigationState | PartialState<NavigationState>,
|
|
294
|
+
): number => {
|
|
295
|
+
const routes = state?.routes;
|
|
296
|
+
if (!routes?.length) {
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const index = state?.index ?? routes.length - 1;
|
|
301
|
+
const depth = state?.type === 'stack' ? index : 0;
|
|
302
|
+
|
|
303
|
+
return depth + getStackDepth(routes[index]?.state);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* report back ownership to the native container: `true` when the mini app sits
|
|
308
|
+
* on its root screen and native may dismiss the rootview, `false` while
|
|
309
|
+
* react-navigation still has screens (or a modal / bottom sheet) to pop.
|
|
310
|
+
*/
|
|
311
|
+
const useNativeCanGoBack = () => {
|
|
312
|
+
const lastValue = useRef<boolean | undefined>(undefined);
|
|
313
|
+
|
|
314
|
+
return useCallback(
|
|
315
|
+
(state?: NavigationState | PartialState<NavigationState>) => {
|
|
316
|
+
const canDismiss = getStackDepth(state) === 0;
|
|
317
|
+
if (lastValue.current === canDismiss) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const setCanGoBack = NativeModules?.MiniAppModule?.setCanGoBack;
|
|
322
|
+
if (typeof setCanGoBack !== 'function') {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
lastValue.current = canDismiss;
|
|
327
|
+
try {
|
|
328
|
+
setCanGoBack(canDismiss);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.warn('[NavigationContainer] setCanGoBack failed:', error);
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
[],
|
|
334
|
+
);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* finger travel that reads as "swipe back recognised". flat px on purpose:
|
|
339
|
+
* scaling it by screen width would move the tick to a different physical
|
|
340
|
+
* thumb distance on every device
|
|
341
|
+
*/
|
|
342
|
+
const SWIPE_BACK_HAPTIC_THRESHOLD = 100;
|
|
343
|
+
|
|
344
|
+
const readAnimatedValue = (node: unknown, fallback: number): number => {
|
|
345
|
+
const getValue = (node as { __getValue?: () => unknown } | undefined)
|
|
346
|
+
?.__getValue;
|
|
347
|
+
if (typeof getValue !== 'function') {
|
|
348
|
+
return fallback;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const value = getValue.call(node);
|
|
352
|
+
return typeof value === 'number' ? value : fallback;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* `current.progress` is built as `gesture.interpolate(...)`, so its parent is
|
|
357
|
+
* the raw pan translation the finger drives. Private react-navigation / RN
|
|
358
|
+
* shape, verified against @react-navigation/stack 7.4.2 + react-native 0.80.1
|
|
359
|
+
* — duck-typed so a rename degrades to "no haptic" instead of a crash.
|
|
360
|
+
*/
|
|
361
|
+
const getSwipeGestureValue = (
|
|
362
|
+
progress?: Animated.AnimatedInterpolation<number>,
|
|
363
|
+
): Animated.Value | undefined => {
|
|
364
|
+
const parent = (progress as { _parent?: Animated.Value } | undefined)
|
|
365
|
+
?._parent;
|
|
366
|
+
|
|
367
|
+
return typeof parent?.addListener === 'function' &&
|
|
368
|
+
typeof parent?.removeListener === 'function'
|
|
369
|
+
? parent
|
|
370
|
+
: undefined;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* one light tick once the swipe back has travelled far enough to read as
|
|
375
|
+
* recognised. iOS only, mirroring `gestureEnabled` in getStackOptions.
|
|
376
|
+
*
|
|
377
|
+
* both listeners are installed on mount and never lazily inside the gesture:
|
|
378
|
+
* on the New Architecture `addListener` queues `startListeningToAnimatedNodeValue`
|
|
379
|
+
* via `queueOperationBlock`, and that queue is only drained by a React mount
|
|
380
|
+
* commit — which never happens during a native driven pan. subscribing when the
|
|
381
|
+
* swipe starts installs an observer that the release flush tears down again
|
|
382
|
+
* before a single frame is delivered, so it must stay on mount.
|
|
383
|
+
*
|
|
384
|
+
* `gesture` also drives the programmatic push and pop animations, so it is the
|
|
385
|
+
* JS `isSwiping` flag, not the subscription lifetime, that keeps the tick
|
|
386
|
+
* exclusive to a real finger drag.
|
|
387
|
+
*/
|
|
388
|
+
const useSwipeBackHaptic = () => {
|
|
389
|
+
const { navigator } = useContext<any>(ApplicationContext);
|
|
390
|
+
const animation = useContext(CardAnimationContext);
|
|
391
|
+
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
if (Platform.OS !== 'ios' || !animation) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const { swiping, inverted, current } = animation;
|
|
398
|
+
const gesture = getSwipeGestureValue(current?.progress);
|
|
399
|
+
if (!gesture || typeof swiping?.addListener !== 'function') {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* `animation` identity is not stable, so this effect can re-run mid swipe.
|
|
405
|
+
* seed from the live value instead of waiting for a 0 -> 1 transition that
|
|
406
|
+
* has already been missed
|
|
407
|
+
*/
|
|
408
|
+
let isSwiping = readAnimatedValue(swiping, 0) === 1;
|
|
409
|
+
let hasFired = false;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* sign, not magnitude: a leftward overdrag reports a negative
|
|
413
|
+
* translation and must not count as progress toward dismissal
|
|
414
|
+
*/
|
|
415
|
+
const direction = readAnimatedValue(inverted, 1);
|
|
416
|
+
|
|
417
|
+
const swipingListener = swiping.addListener(({ value }) => {
|
|
418
|
+
isSwiping = value === 1;
|
|
419
|
+
if (!isSwiping) {
|
|
420
|
+
hasFired = false;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const gestureListener = gesture.addListener(({ value: translation }) => {
|
|
425
|
+
if (!isSwiping) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (translation * direction < SWIPE_BACK_HAPTIC_THRESHOLD) {
|
|
430
|
+
hasFired = false;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (!hasFired) {
|
|
435
|
+
hasFired = true;
|
|
436
|
+
navigator?.maxApi?.triggerEventVibration?.('light');
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
return () => {
|
|
441
|
+
gesture.removeListener(gestureListener);
|
|
442
|
+
swiping.removeListener(swipingListener);
|
|
443
|
+
};
|
|
444
|
+
}, [animation, navigator]);
|
|
445
|
+
};
|
|
446
|
+
|
|
233
447
|
/**
|
|
234
448
|
* Union of the two animation runtimes an internal foundation component might
|
|
235
449
|
* receive. Public Screen props still accept React Native Animated.Value only;
|
|
@@ -271,9 +485,12 @@ export {
|
|
|
271
485
|
getStackOptions,
|
|
272
486
|
getModalOptions,
|
|
273
487
|
getOptions,
|
|
488
|
+
getStackDepth,
|
|
274
489
|
exportHeaderTitle,
|
|
275
490
|
setAutomationID,
|
|
276
491
|
useComponentId,
|
|
277
492
|
useAppState,
|
|
493
|
+
useNativeCanGoBack,
|
|
278
494
|
useNormalizedSharedValue,
|
|
495
|
+
useSwipeBackHaptic,
|
|
279
496
|
};
|