@momo-kits/foundation 0.164.1-beta.7 → 0.164.1-beta.8
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/StackScreen.tsx +4 -1
- package/Application/utils.tsx +107 -0
- package/package.json +1 -1
|
@@ -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 { SwipeBackHaptic, useAppState } from './utils';
|
|
10
10
|
import { TooltipPortalHost, TooltipPortalProvider } from './TooltipPortal';
|
|
11
11
|
|
|
12
12
|
const runAfterInteractions = InteractionManager.runAfterInteractions;
|
|
@@ -413,6 +413,9 @@ const StackScreen: React.FC<any> = props => {
|
|
|
413
413
|
<Component heightHeader={heightHeader} {...data} />
|
|
414
414
|
{showGrid && <GridSystem />}
|
|
415
415
|
<TooltipPortalHost />
|
|
416
|
+
{/* a leaf on purpose: it subscribes to CardAnimationContext, which changes on every
|
|
417
|
+
Card render, and must not pull this screen into those renders */}
|
|
418
|
+
<SwipeBackHaptic />
|
|
416
419
|
</TooltipPortalProvider>
|
|
417
420
|
</ScreenContext.Provider>
|
|
418
421
|
);
|
package/Application/utils.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import React, {
|
|
|
6
6
|
useRef,
|
|
7
7
|
} from 'react';
|
|
8
8
|
import {
|
|
9
|
+
CardAnimationContext,
|
|
9
10
|
CardStyleInterpolators,
|
|
10
11
|
StackNavigationOptions,
|
|
11
12
|
TransitionPresets,
|
|
@@ -16,6 +17,7 @@ import type {
|
|
|
16
17
|
} from '@react-navigation/stack';
|
|
17
18
|
import type { NavigationState, PartialState } from '@react-navigation/native';
|
|
18
19
|
import type { HeaderTitleProps, NavigationOptions } from './types';
|
|
20
|
+
import { ApplicationContext } from '../Context';
|
|
19
21
|
import { Colors, Spacing } from '../Consts';
|
|
20
22
|
import { Animated, AppState, NativeModules, Platform } from 'react-native';
|
|
21
23
|
import type { SharedValue } from 'react-native-reanimated';
|
|
@@ -369,6 +371,110 @@ const useNormalizedSharedValue = (
|
|
|
369
371
|
return fallback;
|
|
370
372
|
};
|
|
371
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Travel, in points, at which the swipe back gesture ticks. Flat and well short of
|
|
376
|
+
* react-navigation's own `distance / 2` commit rule: the tick marks "the gesture is
|
|
377
|
+
* recognised", not "you are past the point of no return".
|
|
378
|
+
*/
|
|
379
|
+
const SWIPE_BACK_HAPTIC_THRESHOLD = 50;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* `current.progress` is `gesture.interpolate(...)` built by CardStack
|
|
383
|
+
* (@react-navigation/stack 7.4.2, views/Stack/CardStack.tsx `getProgressFromGesture`).
|
|
384
|
+
* Only `Animated.Value.addListener` bridges native driver updates back to js, so the raw
|
|
385
|
+
* translation has to be read off the interpolation's parent.
|
|
386
|
+
*
|
|
387
|
+
* Reaches into a private field. Every hop is optional and duck typed, so a shape change in
|
|
388
|
+
* a future @react-navigation/stack degrades to "no haptic" rather than a crash.
|
|
389
|
+
*/
|
|
390
|
+
const getSwipeGestureValue = (
|
|
391
|
+
progress: Animated.AnimatedInterpolation<number> | undefined,
|
|
392
|
+
): Animated.Value | undefined => {
|
|
393
|
+
const parent = (progress as { _parent?: unknown } | undefined)?._parent as
|
|
394
|
+
| Animated.Value
|
|
395
|
+
| undefined;
|
|
396
|
+
|
|
397
|
+
return typeof parent?.addListener === 'function' &&
|
|
398
|
+
typeof parent?.removeListener === 'function'
|
|
399
|
+
? parent
|
|
400
|
+
: undefined;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const readAnimatedValue = (node: unknown, fallback: number): number => {
|
|
404
|
+
const value = (node as { __getValue?: () => unknown } | undefined)?.__getValue?.();
|
|
405
|
+
return typeof value === 'number' ? value : fallback;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Haptic tick for the iOS swipe back gesture.
|
|
410
|
+
*
|
|
411
|
+
* Deliberately a leaf that renders nothing. Consuming `CardAnimationContext` re-renders on
|
|
412
|
+
* every `Card` render, and an earlier version put that on `StackScreen` itself — which then
|
|
413
|
+
* re-rendered the whole screen mid gesture and cost the pop: `Card` defers `onClose()` by
|
|
414
|
+
* 32ms and clears that timer from `animate()`, which `componentDidUpdate` re-enters on any
|
|
415
|
+
* render in the window. Keeping the subscription in a null leaf isolates it.
|
|
416
|
+
*
|
|
417
|
+
* Both listeners are installed on mount, never lazily inside the gesture: on the New
|
|
418
|
+
* Architecture `addListener` queues `startListeningToAnimatedNodeValue` via
|
|
419
|
+
* `queueOperationBlock`, and that queue is only drained by a React mount commit — which
|
|
420
|
+
* never happens during a native driven pan.
|
|
421
|
+
*/
|
|
422
|
+
const SwipeBackHaptic = () => {
|
|
423
|
+
const { navigator } = useContext<any>(ApplicationContext);
|
|
424
|
+
const animation = useContext(CardAnimationContext);
|
|
425
|
+
const hasFired = useRef(false);
|
|
426
|
+
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
if (Platform.OS !== 'ios' || !animation) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const { swiping, inverted, current } = animation;
|
|
433
|
+
const gesture = getSwipeGestureValue(current?.progress);
|
|
434
|
+
if (!gesture || typeof swiping?.addListener !== 'function') {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let isSwiping = readAnimatedValue(swiping, 0) === 1;
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* sign, not magnitude: a leftward overdrag reports a negative translation and must
|
|
442
|
+
* not count as progress toward dismissal
|
|
443
|
+
*/
|
|
444
|
+
const direction = readAnimatedValue(inverted, 1);
|
|
445
|
+
|
|
446
|
+
const swipingListener = swiping.addListener(({ value }) => {
|
|
447
|
+
isSwiping = value === 1;
|
|
448
|
+
if (!isSwiping) {
|
|
449
|
+
hasFired.current = false;
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const gestureListener = gesture.addListener(({ value: translation }) => {
|
|
454
|
+
if (!isSwiping) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (translation * direction < SWIPE_BACK_HAPTIC_THRESHOLD) {
|
|
459
|
+
hasFired.current = false;
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!hasFired.current) {
|
|
464
|
+
hasFired.current = true;
|
|
465
|
+
navigator?.maxApi?.triggerEventVibration?.('light');
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
return () => {
|
|
470
|
+
gesture.removeListener(gestureListener);
|
|
471
|
+
swiping.removeListener(swipingListener);
|
|
472
|
+
};
|
|
473
|
+
}, [animation, navigator]);
|
|
474
|
+
|
|
475
|
+
return null;
|
|
476
|
+
};
|
|
477
|
+
|
|
372
478
|
export {
|
|
373
479
|
getStackOptions,
|
|
374
480
|
getModalOptions,
|
|
@@ -380,4 +486,5 @@ export {
|
|
|
380
486
|
useAppState,
|
|
381
487
|
useNativeCanGoBack,
|
|
382
488
|
useNormalizedSharedValue,
|
|
489
|
+
SwipeBackHaptic,
|
|
383
490
|
};
|