@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.
@@ -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
  );
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.164.1-beta.7",
3
+ "version": "0.164.1-beta.8",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},