@momo-kits/foundation 0.164.1-beta.6 → 0.164.1-beta.7
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 +1 -2
- package/Application/utils.tsx +0 -165
- 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
|
|
9
|
+
import { useAppState } from './utils';
|
|
10
10
|
import { TooltipPortalHost, TooltipPortalProvider } from './TooltipPortal';
|
|
11
11
|
|
|
12
12
|
const runAfterInteractions = InteractionManager.runAfterInteractions;
|
|
@@ -51,7 +51,6 @@ 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();
|
|
55
54
|
|
|
56
55
|
const data = {
|
|
57
56
|
...initialParams,
|
package/Application/utils.tsx
CHANGED
|
@@ -6,7 +6,6 @@ import React, {
|
|
|
6
6
|
useRef,
|
|
7
7
|
} from 'react';
|
|
8
8
|
import {
|
|
9
|
-
CardAnimationContext,
|
|
10
9
|
CardStyleInterpolators,
|
|
11
10
|
StackNavigationOptions,
|
|
12
11
|
TransitionPresets,
|
|
@@ -22,7 +21,6 @@ import { Animated, AppState, NativeModules, Platform } from 'react-native';
|
|
|
22
21
|
import type { SharedValue } from 'react-native-reanimated';
|
|
23
22
|
import { useSharedValue } from 'react-native-reanimated';
|
|
24
23
|
import {
|
|
25
|
-
ApplicationContext,
|
|
26
24
|
MiniAppContext,
|
|
27
25
|
ScreenContext,
|
|
28
26
|
TrackingScopeContext,
|
|
@@ -334,168 +332,6 @@ const useNativeCanGoBack = () => {
|
|
|
334
332
|
);
|
|
335
333
|
};
|
|
336
334
|
|
|
337
|
-
/**
|
|
338
|
-
* fraction of screen width at which releasing commits the pop. deliberately
|
|
339
|
-
* pinned to react-navigation's own rule: Card.tsx (GestureState.END,
|
|
340
|
-
* @react-navigation/stack 7.4.2) closes the card when
|
|
341
|
-
* `(translation + velocity * gestureVelocityImpact) * inverted > distance / 2`
|
|
342
|
-
* with `distance = layout.width` for a horizontal gesture, and exposes no
|
|
343
|
-
* option to override it. so the tick means "release now and you go back"
|
|
344
|
-
* instead of marking a distance of its own — keep the two in sync.
|
|
345
|
-
*
|
|
346
|
-
* mid drag only translation is known, velocity exists solely at release, so a
|
|
347
|
-
* short fast flick can commit without ever crossing this and pops with no
|
|
348
|
-
* tick. that asymmetry is intended: modelling velocity per frame would tick
|
|
349
|
-
* on swipes that then do not commit, which is the worse error
|
|
350
|
-
*/
|
|
351
|
-
const SWIPE_BACK_HAPTIC_COMMIT_RATIO = 0.5;
|
|
352
|
-
|
|
353
|
-
const readAnimatedValue = (node: unknown, fallback: number): number => {
|
|
354
|
-
const getValue = (node as { __getValue?: () => unknown } | undefined)
|
|
355
|
-
?.__getValue;
|
|
356
|
-
if (typeof getValue !== 'function') {
|
|
357
|
-
return fallback;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const value = getValue.call(node);
|
|
361
|
-
return typeof value === 'number' ? value : fallback;
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* `current.progress` is built as `gesture.interpolate(...)`, so its parent is
|
|
366
|
-
* the raw pan translation the finger drives. Private react-navigation / RN
|
|
367
|
-
* shape, verified against @react-navigation/stack 7.4.2 + react-native 0.80.1
|
|
368
|
-
* — duck-typed so a rename degrades to "no haptic" instead of a crash.
|
|
369
|
-
*/
|
|
370
|
-
const getSwipeGestureValue = (
|
|
371
|
-
progress?: Animated.AnimatedInterpolation<number>,
|
|
372
|
-
): Animated.Value | undefined => {
|
|
373
|
-
const parent = (progress as { _parent?: Animated.Value } | undefined)
|
|
374
|
-
?._parent;
|
|
375
|
-
|
|
376
|
-
return typeof parent?.addListener === 'function' &&
|
|
377
|
-
typeof parent?.removeListener === 'function'
|
|
378
|
-
? parent
|
|
379
|
-
: undefined;
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* one light tick once the swipe back passes the point where releasing commits
|
|
384
|
-
* the pop. iOS only, mirroring `gestureEnabled` in getStackOptions.
|
|
385
|
-
*
|
|
386
|
-
* both listeners are installed on mount and never lazily inside the gesture:
|
|
387
|
-
* on the New Architecture `addListener` queues `startListeningToAnimatedNodeValue`
|
|
388
|
-
* via `queueOperationBlock`, and that queue is only drained by a React mount
|
|
389
|
-
* commit — which never happens during a native driven pan. subscribing when the
|
|
390
|
-
* swipe starts installs an observer that the release flush tears down again
|
|
391
|
-
* before a single frame is delivered, so it must stay on mount.
|
|
392
|
-
*
|
|
393
|
-
* `gesture` also drives the programmatic push and pop animations, so it is the
|
|
394
|
-
* JS `isSwiping` flag, not the subscription lifetime, that keeps the tick
|
|
395
|
-
* exclusive to a real finger drag.
|
|
396
|
-
*/
|
|
397
|
-
const useSwipeBackHaptic = () => {
|
|
398
|
-
const { navigator } = useContext<any>(ApplicationContext);
|
|
399
|
-
const animation = useContext(CardAnimationContext);
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* the latch has to outlive the effect. `animation` identity changes on every
|
|
403
|
-
* navigator render, so a `hasFired` local re-arms several times per drag and
|
|
404
|
-
* the tick then repeats on every frame past the threshold
|
|
405
|
-
*/
|
|
406
|
-
const hasFired = useRef(false);
|
|
407
|
-
|
|
408
|
-
useEffect(() => {
|
|
409
|
-
if (Platform.OS !== 'ios' || !animation) {
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const { swiping, inverted, current, layouts } = animation;
|
|
414
|
-
const gesture = getSwipeGestureValue(current?.progress);
|
|
415
|
-
if (!gesture || typeof swiping?.addListener !== 'function') {
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* `animation` identity is not stable, so this effect can re-run mid swipe.
|
|
421
|
-
* seed from the live value instead of waiting for a 0 -> 1 transition that
|
|
422
|
-
* has already been missed
|
|
423
|
-
*/
|
|
424
|
-
let isSwiping = readAnimatedValue(swiping, 0) === 1;
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* sign, not magnitude: a leftward overdrag reports a negative
|
|
428
|
-
* translation and must not count as progress toward dismissal
|
|
429
|
-
*/
|
|
430
|
-
const direction = readAnimatedValue(inverted, 1);
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* plain JS numbers, not animated nodes (`StackCardInterpolationProps`).
|
|
434
|
-
* screen layout can still be unmeasured on an early render, and a zero
|
|
435
|
-
* width would put the threshold at 0 and tick the moment the finger moves
|
|
436
|
-
*/
|
|
437
|
-
const commitThreshold =
|
|
438
|
-
(layouts?.screen?.width ?? 0) * SWIPE_BACK_HAPTIC_COMMIT_RATIO;
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* TEMPORARY DIAGNOSTIC (DS-760) — remove once the swipe-back no-pop is understood.
|
|
442
|
-
*
|
|
443
|
-
* The test device cannot run Xcode, so this borrows the one channel proven to reach it.
|
|
444
|
-
* On release past the commit threshold react-navigation should pop; if this screen is
|
|
445
|
-
* still mounted shortly after, the pop was requested and then swallowed — `Card` clears
|
|
446
|
-
* its 32ms `pendingGestureCallback` from `animate()`, and `componentDidUpdate` re-enters
|
|
447
|
-
* `animate()` for any re-render in that window.
|
|
448
|
-
*
|
|
449
|
-
* A `heavy` tick after release therefore means "gesture completed, pop lost". Silence
|
|
450
|
-
* means the gesture never reached the threshold, or was cancelled before END.
|
|
451
|
-
*/
|
|
452
|
-
let lastTravel = 0;
|
|
453
|
-
let pendingPopProbe: ReturnType<typeof setTimeout> | undefined;
|
|
454
|
-
|
|
455
|
-
const swipingListener = swiping.addListener(({ value }) => {
|
|
456
|
-
const wasSwiping = isSwiping;
|
|
457
|
-
isSwiping = value === 1;
|
|
458
|
-
if (isSwiping) {
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
hasFired.current = false;
|
|
462
|
-
if (wasSwiping && commitThreshold > 0 && lastTravel >= commitThreshold) {
|
|
463
|
-
pendingPopProbe = setTimeout(() => {
|
|
464
|
-
navigator?.maxApi?.triggerEventVibration?.('heavy');
|
|
465
|
-
}, 500);
|
|
466
|
-
}
|
|
467
|
-
lastTravel = 0;
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
const gestureListener = gesture.addListener(({ value: translation }) => {
|
|
471
|
-
if (!isSwiping || commitThreshold <= 0) {
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
lastTravel = translation * direction;
|
|
476
|
-
|
|
477
|
-
if (lastTravel < commitThreshold) {
|
|
478
|
-
hasFired.current = false;
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (!hasFired.current) {
|
|
483
|
-
hasFired.current = true;
|
|
484
|
-
navigator?.maxApi?.triggerEventVibration?.('light');
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
return () => {
|
|
489
|
-
// unmount cancels the probe: the screen popped, which is the success case
|
|
490
|
-
if (pendingPopProbe !== undefined) {
|
|
491
|
-
clearTimeout(pendingPopProbe);
|
|
492
|
-
}
|
|
493
|
-
gesture.removeListener(gestureListener);
|
|
494
|
-
swiping.removeListener(swipingListener);
|
|
495
|
-
};
|
|
496
|
-
}, [animation, navigator]);
|
|
497
|
-
};
|
|
498
|
-
|
|
499
335
|
/**
|
|
500
336
|
* Union of the two animation runtimes an internal foundation component might
|
|
501
337
|
* receive. Public Screen props still accept React Native Animated.Value only;
|
|
@@ -544,5 +380,4 @@ export {
|
|
|
544
380
|
useAppState,
|
|
545
381
|
useNativeCanGoBack,
|
|
546
382
|
useNormalizedSharedValue,
|
|
547
|
-
useSwipeBackHaptic,
|
|
548
383
|
};
|