@seed-design/stackflow 1.1.19 → 1.1.21
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/lib/_virtual/_rolldown/runtime.cjs +34 -0
- package/lib/_virtual/_rolldown/runtime.js +14 -0
- package/lib/components/AppBar/AppBar.cjs +88 -87
- package/lib/components/AppBar/AppBar.d.ts +3 -0
- package/lib/components/AppBar/AppBar.js +86 -84
- package/lib/components/AppBar/AppBar.namespace.cjs +20 -15
- package/lib/components/AppBar/AppBar.namespace.d.ts +2 -2
- package/lib/components/AppBar/AppBar.namespace.js +15 -1
- package/lib/components/AppBar/index.cjs +17 -17
- package/lib/components/AppBar/index.d.ts +2 -2
- package/lib/components/AppBar/index.js +3 -3
- package/lib/components/AppScreen/AppScreen.cjs +46 -61
- package/lib/components/AppScreen/AppScreen.js +45 -57
- package/lib/components/AppScreen/AppScreen.namespace.cjs +17 -13
- package/lib/components/AppScreen/AppScreen.namespace.js +12 -1
- package/lib/components/AppScreen/index.cjs +14 -15
- package/lib/components/AppScreen/index.js +3 -3
- package/lib/components/index.cjs +30 -25
- package/lib/components/index.js +5 -6
- package/lib/hooks/index.cjs +3 -9
- package/lib/hooks/index.js +2 -1
- package/lib/hooks/useActivityZIndexBase.cjs +12 -11
- package/lib/hooks/useActivityZIndexBase.js +11 -7
- package/lib/index.cjs +41 -31
- package/lib/index.js +8 -10
- package/lib/plugin.cjs +16 -16
- package/lib/plugin.js +14 -12
- package/lib/primitive/AppBar/AppBar.cjs +41 -33
- package/lib/primitive/AppBar/AppBar.js +40 -29
- package/lib/primitive/AppBar/AppBar.namespace.cjs +16 -12
- package/lib/primitive/AppBar/AppBar.namespace.js +11 -1
- package/lib/primitive/AppBar/anatomy.cjs +18 -0
- package/lib/primitive/AppBar/anatomy.d.ts +15 -0
- package/lib/primitive/AppBar/anatomy.js +18 -0
- package/lib/primitive/AppBar/index.cjs +13 -14
- package/lib/primitive/AppBar/index.js +3 -3
- package/lib/primitive/AppBar/useAppBar.cjs +29 -36
- package/lib/primitive/AppBar/useAppBar.js +28 -32
- package/lib/primitive/AppBar/useAppBarContext.cjs +11 -18
- package/lib/primitive/AppBar/useAppBarContext.js +10 -14
- package/lib/primitive/AppScreen/AppScreen.cjs +49 -52
- package/lib/primitive/AppScreen/AppScreen.js +48 -48
- package/lib/primitive/AppScreen/AppScreen.namespace.cjs +16 -12
- package/lib/primitive/AppScreen/AppScreen.namespace.js +11 -1
- package/lib/primitive/AppScreen/anatomy.cjs +17 -0
- package/lib/primitive/AppScreen/anatomy.d.ts +14 -0
- package/lib/primitive/AppScreen/anatomy.js +17 -0
- package/lib/primitive/AppScreen/index.cjs +15 -16
- package/lib/primitive/AppScreen/index.js +4 -4
- package/lib/primitive/AppScreen/useAppScreen.cjs +60 -63
- package/lib/primitive/AppScreen/useAppScreen.js +59 -59
- package/lib/primitive/AppScreen/useAppScreenContext.cjs +11 -18
- package/lib/primitive/AppScreen/useAppScreenContext.js +10 -14
- package/lib/primitive/GlobalInteraction/GlobalInteraction.cjs +21 -26
- package/lib/primitive/GlobalInteraction/GlobalInteraction.js +20 -22
- package/lib/primitive/GlobalInteraction/animation.cjs +303 -0
- package/lib/primitive/GlobalInteraction/animation.d.ts +16 -0
- package/lib/primitive/GlobalInteraction/animation.js +299 -0
- package/lib/primitive/GlobalInteraction/constants.cjs +9 -0
- package/lib/primitive/GlobalInteraction/constants.d.ts +19 -0
- package/lib/primitive/GlobalInteraction/constants.js +8 -0
- package/lib/primitive/GlobalInteraction/dom.cjs +147 -0
- package/lib/primitive/GlobalInteraction/dom.d.ts +44 -0
- package/lib/primitive/GlobalInteraction/dom.js +140 -0
- package/lib/primitive/GlobalInteraction/index.cjs +5 -11
- package/lib/primitive/GlobalInteraction/index.js +3 -2
- package/lib/primitive/GlobalInteraction/useGlobalInteraction.cjs +220 -133
- package/lib/primitive/GlobalInteraction/useGlobalInteraction.d.ts +10 -26
- package/lib/primitive/GlobalInteraction/useGlobalInteraction.js +219 -129
- package/lib/primitive/GlobalInteraction/useGlobalInteractionContext.cjs +11 -18
- package/lib/primitive/GlobalInteraction/useGlobalInteractionContext.js +10 -14
- package/lib/primitive/GlobalInteraction/useSwipeBack.cjs +71 -55
- package/lib/primitive/GlobalInteraction/useSwipeBack.d.ts +19 -2
- package/lib/primitive/GlobalInteraction/useSwipeBack.js +70 -51
- package/lib/primitive/index.cjs +55 -27
- package/lib/primitive/index.js +26 -9
- package/lib/primitive/private/useElementOffset.cjs +29 -51
- package/lib/primitive/private/useElementOffset.js +27 -28
- package/lib/primitive/private/useTopActivity.cjs +22 -31
- package/lib/primitive/private/useTopActivity.js +21 -27
- package/lib/utils/createStyleContext.cjs +91 -74
- package/lib/utils/createStyleContext.js +89 -70
- package/lib/utils/styled.cjs +13 -20
- package/lib/utils/styled.js +12 -16
- package/package.json +4 -4
- package/src/components/AppBar/AppBar.namespace.ts +2 -0
- package/src/components/AppBar/AppBar.tsx +29 -3
- package/src/components/AppBar/index.ts +2 -0
- package/src/primitive/AppBar/AppBar.tsx +4 -1
- package/src/primitive/AppBar/anatomy.ts +16 -0
- package/src/primitive/AppBar/useAppBar.tsx +2 -1
- package/src/primitive/AppScreen/anatomy.ts +15 -0
- package/src/primitive/AppScreen/useAppScreen.tsx +5 -4
- package/src/primitive/GlobalInteraction/animation.ts +447 -0
- package/src/primitive/GlobalInteraction/constants.ts +24 -0
- package/src/primitive/GlobalInteraction/dom.ts +211 -0
- package/src/primitive/GlobalInteraction/useGlobalInteraction.ts +302 -129
- package/src/primitive/GlobalInteraction/useSwipeBack.ts +64 -26
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useCallback, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
|
3
2
|
import { useTopActivity } from "../private/useTopActivity";
|
|
3
|
+
import {
|
|
4
|
+
type TransitionStyle,
|
|
5
|
+
type TransitionTargets,
|
|
6
|
+
findTransitionTargets,
|
|
7
|
+
readTransitionStyle,
|
|
8
|
+
applySwipeStyles,
|
|
9
|
+
clearAllStyles,
|
|
10
|
+
setIdlePositions,
|
|
11
|
+
setPostExitPositions,
|
|
12
|
+
} from "./dom";
|
|
13
|
+
import {
|
|
14
|
+
cancelAll,
|
|
15
|
+
animateTransition,
|
|
16
|
+
animateSwipeComplete,
|
|
17
|
+
animateSwipeCancel,
|
|
18
|
+
scrubAppBarBackground,
|
|
19
|
+
} from "./animation";
|
|
4
20
|
|
|
5
21
|
export type SwipeBackState = "idle" | "swiping" | "canceling" | "completing";
|
|
6
22
|
|
|
@@ -14,22 +30,18 @@ export interface SwipeBackContext {
|
|
|
14
30
|
velocity: number;
|
|
15
31
|
}
|
|
16
32
|
|
|
17
|
-
export interface
|
|
33
|
+
export interface SwipeBackThresholds {
|
|
18
34
|
/**
|
|
19
|
-
* The threshold to determine whether the swipe back is intentional.
|
|
35
|
+
* The threshold to determine whether the swipe back is intentional, by displacement ratio.
|
|
20
36
|
* @default 0.4
|
|
21
37
|
*/
|
|
22
|
-
|
|
38
|
+
displacementRatioThreshold?: number;
|
|
23
39
|
|
|
24
40
|
/**
|
|
25
|
-
* The threshold to determine whether the swipe back is intentional.
|
|
41
|
+
* The threshold to determine whether the swipe back is intentional, by velocity.
|
|
26
42
|
* @default 1
|
|
27
43
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
onSwipeBackStart?: () => void;
|
|
31
|
-
onSwipeBackMove?: (props: { displacement: number; displacementRatio: number }) => void;
|
|
32
|
-
onSwipeBackEnd?: (props: { swiped: boolean }) => void;
|
|
44
|
+
velocityThreshold?: number;
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
export interface StartSwipeBackProps {
|
|
@@ -42,147 +54,308 @@ export interface MoveSwipeBackProps {
|
|
|
42
54
|
t: number;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
const IDLE_CONTEXT: SwipeBackContext = Object.freeze({
|
|
58
|
+
x0: 0,
|
|
59
|
+
t0: 0,
|
|
60
|
+
displacement: 0,
|
|
61
|
+
displacementRatio: 0,
|
|
62
|
+
velocity: 0,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const DEFAULT_DISPLACEMENT_RATIO_THRESHOLD = 0.4;
|
|
66
|
+
const DEFAULT_VELOCITY_THRESHOLD = 1;
|
|
47
67
|
|
|
48
68
|
export function useGlobalInteraction() {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
const swipeBackContextRef = useRef<SwipeBackContext>({
|
|
52
|
-
x0: 0,
|
|
53
|
-
t0: 0,
|
|
54
|
-
displacement: 0,
|
|
55
|
-
displacementRatio: 0,
|
|
56
|
-
velocity: 0,
|
|
57
|
-
});
|
|
69
|
+
const swipeBackStateRef = useRef<SwipeBackState>("idle");
|
|
70
|
+
|
|
71
|
+
const swipeBackContextRef = useRef<SwipeBackContext>({ ...IDLE_CONTEXT });
|
|
58
72
|
const stackRef = useRef<HTMLDivElement>(null);
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
// Cached transition targets — populated once per gesture/transition
|
|
75
|
+
const cachedTargetsRef = useRef<TransitionTargets | null>(null);
|
|
76
|
+
|
|
77
|
+
// Running WAAPI animations — for cancellation on new transitions
|
|
78
|
+
const runningAnimsRef = useRef<Animation[]>([]);
|
|
79
|
+
|
|
80
|
+
// Current scrub animation on the top AppBar background element. Retained
|
|
81
|
+
// across touchmoves so setKeyframes can replace it in place — recreating
|
|
82
|
+
// on every move would cancel-then-recreate and flicker.
|
|
83
|
+
const appBarBgScrubAnimRef = useRef<Animation | null>(null);
|
|
84
|
+
|
|
85
|
+
// Transition style of the top activity at swipe start. Non-iOS styles
|
|
86
|
+
// (fadeFromBottomAndroid / fadeIn) do not track finger displacement —
|
|
87
|
+
// their exit is driven by stackflow's normal exit-active lifecycle.
|
|
88
|
+
const swipeStyleRef = useRef<TransitionStyle | null>(null);
|
|
89
|
+
|
|
90
|
+
// Defer push animation one frame so stackflow has committed the new top
|
|
91
|
+
// activity's DOM (data-activity-is-top + layer/appBar subtree) before we
|
|
92
|
+
// query targets. Running sync in useLayoutEffect turned out to race with
|
|
93
|
+
// stackflow's internal subscription updates, leaving targets empty and
|
|
94
|
+
// the enter animation never firing.
|
|
95
|
+
const pendingPushRAFRef = useRef<number | null>(null);
|
|
96
|
+
|
|
97
|
+
// Skip the next exit-active transition after swipe completing,
|
|
98
|
+
// because stackflow fires exit-active as part of its normal lifecycle
|
|
99
|
+
// but we've already animated the exit via WAAPI.
|
|
100
|
+
const skipNextExitRef = useRef(false);
|
|
101
|
+
|
|
102
|
+
/** Cancel any running WAAPI animations and clear the ref. */
|
|
103
|
+
const stopRunningAnims = useCallback(() => {
|
|
104
|
+
cancelAll(runningAnimsRef.current);
|
|
105
|
+
runningAnimsRef.current = [];
|
|
70
106
|
}, []);
|
|
71
107
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
} = props;
|
|
78
|
-
const onSwipeStart = useCallbackRef(props.onSwipeBackStart);
|
|
79
|
-
const onSwipeMove = useCallbackRef(props.onSwipeBackMove);
|
|
80
|
-
const onSwipeEnd = useCallbackRef(props.onSwipeBackEnd);
|
|
81
|
-
|
|
82
|
-
const startSwipeBack = useCallback(
|
|
83
|
-
({ x0, t0 }: StartSwipeBackProps) => {
|
|
84
|
-
setSwipeBackContext({
|
|
85
|
-
x0,
|
|
86
|
-
t0,
|
|
87
|
-
displacement: 0,
|
|
88
|
-
displacementRatio: 0,
|
|
89
|
-
velocity: 0,
|
|
90
|
-
});
|
|
91
|
-
setSwipeBackState((prev) => (prev === "swiping" ? prev : "swiping"));
|
|
92
|
-
onSwipeStart?.();
|
|
93
|
-
},
|
|
94
|
-
[onSwipeStart],
|
|
95
|
-
);
|
|
108
|
+
/** Cancel the in-place scrub animation on the AppBar background. */
|
|
109
|
+
const stopAppBarBgScrub = useCallback(() => {
|
|
110
|
+
appBarBgScrubAnimRef.current?.cancel();
|
|
111
|
+
appBarBgScrubAnimRef.current = null;
|
|
112
|
+
}, []);
|
|
96
113
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
displacement,
|
|
105
|
-
displacementRatio,
|
|
106
|
-
velocity,
|
|
107
|
-
});
|
|
108
|
-
setSwipeBackState((prev) => (prev === "swiping" ? prev : "swiping"));
|
|
109
|
-
onSwipeMove?.({ displacement, displacementRatio });
|
|
110
|
-
},
|
|
111
|
-
[onSwipeMove],
|
|
112
|
-
);
|
|
114
|
+
/** Cancel a pending deferred push animation, if any. */
|
|
115
|
+
const cancelPendingPushRAF = useCallback(() => {
|
|
116
|
+
if (pendingPushRAFRef.current !== null) {
|
|
117
|
+
cancelAnimationFrame(pendingPushRAFRef.current);
|
|
118
|
+
pendingPushRAFRef.current = null;
|
|
119
|
+
}
|
|
120
|
+
}, []);
|
|
113
121
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
/** Update swipe-back-state attribute on the stack DOM element. */
|
|
123
|
+
const setSwipeBackState = useCallback((state: SwipeBackState) => {
|
|
124
|
+
swipeBackStateRef.current = state;
|
|
125
|
+
if (stackRef.current) {
|
|
126
|
+
stackRef.current.dataset["swipeBackState"] = state;
|
|
127
|
+
}
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
const startSwipeBack = useCallback(
|
|
131
|
+
({ x0, t0 }: StartSwipeBackProps) => {
|
|
132
|
+
// Cancel pending push rAF and any running animations
|
|
133
|
+
cancelPendingPushRAF();
|
|
134
|
+
stopRunningAnims();
|
|
135
|
+
stopAppBarBgScrub();
|
|
136
|
+
|
|
137
|
+
swipeBackContextRef.current = { ...IDLE_CONTEXT, x0, t0 };
|
|
127
138
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
// Cache target elements and transition style once per gesture
|
|
140
|
+
if (stackRef.current) {
|
|
141
|
+
cachedTargetsRef.current = findTransitionTargets(stackRef.current);
|
|
142
|
+
swipeStyleRef.current = readTransitionStyle(stackRef.current);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
setSwipeBackState("swiping");
|
|
146
|
+
},
|
|
147
|
+
[cancelPendingPushRAF, stopRunningAnims, stopAppBarBgScrub, setSwipeBackState],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const moveSwipeBack = useCallback(({ x, t }: MoveSwipeBackProps): SwipeBackContext => {
|
|
151
|
+
const ctx = swipeBackContextRef.current;
|
|
152
|
+
const displacement = Math.max(0, x - ctx.x0);
|
|
153
|
+
const displacementRatio = displacement / window.innerWidth;
|
|
154
|
+
const velocity = displacement / (t - ctx.t0);
|
|
155
|
+
|
|
156
|
+
// Mutate in place — this runs at 60fps inside touchmove rAF.
|
|
157
|
+
ctx.displacement = displacement;
|
|
158
|
+
ctx.displacementRatio = displacementRatio;
|
|
159
|
+
ctx.velocity = velocity;
|
|
160
|
+
|
|
161
|
+
// Only iOS slide tracks finger displacement; other styles stay put
|
|
162
|
+
// and let stackflow's own exit transition play on pop.
|
|
163
|
+
const targets = cachedTargetsRef.current;
|
|
164
|
+
if (targets && swipeStyleRef.current === "slideFromRightIOS") {
|
|
165
|
+
applySwipeStyles(targets, displacement, displacementRatio);
|
|
166
|
+
appBarBgScrubAnimRef.current = scrubAppBarBackground(
|
|
167
|
+
targets.topAppBarBackground,
|
|
168
|
+
`translate3d(${displacement}px, 0, 0)`,
|
|
169
|
+
appBarBgScrubAnimRef.current,
|
|
131
170
|
);
|
|
171
|
+
}
|
|
132
172
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
173
|
+
return ctx;
|
|
174
|
+
}, []);
|
|
175
|
+
|
|
176
|
+
const endSwipeBack = useCallback(
|
|
177
|
+
(thresholds: SwipeBackThresholds = {}): boolean => {
|
|
178
|
+
const {
|
|
179
|
+
displacementRatioThreshold = DEFAULT_DISPLACEMENT_RATIO_THRESHOLD,
|
|
180
|
+
velocityThreshold = DEFAULT_VELOCITY_THRESHOLD,
|
|
181
|
+
} = thresholds;
|
|
182
|
+
|
|
183
|
+
const ctx = swipeBackContextRef.current;
|
|
184
|
+
const swiped =
|
|
185
|
+
ctx.displacementRatio > displacementRatioThreshold || ctx.velocity > velocityThreshold;
|
|
186
|
+
|
|
187
|
+
const targets = cachedTargetsRef.current;
|
|
188
|
+
if (!targets) {
|
|
142
189
|
setSwipeBackState("idle");
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
190
|
+
return swiped;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Non-iOS styles do not track finger motion — let stackflow's normal
|
|
194
|
+
// exit-active lifecycle drive the pop animation when `swiped` is true.
|
|
195
|
+
if (swipeStyleRef.current !== "slideFromRightIOS") {
|
|
196
|
+
cachedTargetsRef.current = null;
|
|
197
|
+
swipeStyleRef.current = null;
|
|
198
|
+
setSwipeBackState("idle");
|
|
199
|
+
return swiped;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Keep swipe-time inline styles intact — WAAPI keyframe[0] matches the
|
|
203
|
+
// current displacement, and the inline values cover the 1-frame gap
|
|
204
|
+
// before the animation's first paint. Clearing here would cause a
|
|
205
|
+
// single-frame snap to default (transform: 0) on heavy main-thread
|
|
206
|
+
// sessions where reflow can interleave between this clear and animate().
|
|
207
|
+
stopAppBarBgScrub();
|
|
208
|
+
|
|
209
|
+
const onFinish = (animations: Animation[], pin: () => void) => {
|
|
210
|
+
// Always pin + cancel: skipping these leaves inline styles from the
|
|
211
|
+
// mid-gesture snapshot on screen when the animation gets cancelled
|
|
212
|
+
// by a newer transition (the cancel itself drops fill:forwards, so
|
|
213
|
+
// the stale inline styles become visible).
|
|
214
|
+
// Set inline styles BEFORE cancel to prevent flash.
|
|
215
|
+
pin();
|
|
216
|
+
cancelAll(animations);
|
|
217
|
+
// Only reset the running ref / per-gesture caches if no newer
|
|
218
|
+
// transition has claimed them — otherwise we'd clobber the next
|
|
219
|
+
// gesture's state.
|
|
220
|
+
if (runningAnimsRef.current === animations) {
|
|
221
|
+
runningAnimsRef.current = [];
|
|
222
|
+
cachedTargetsRef.current = null;
|
|
223
|
+
swipeStyleRef.current = null;
|
|
224
|
+
setSwipeBackState("idle");
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
if (swiped) {
|
|
229
|
+
setSwipeBackState("completing");
|
|
230
|
+
// Mark to skip the next exit-active from stackflow
|
|
231
|
+
skipNextExitRef.current = true;
|
|
232
|
+
const { animations, finished } = animateSwipeComplete(targets, ctx.displacement);
|
|
233
|
+
runningAnimsRef.current = animations;
|
|
234
|
+
finished.then(() => onFinish(animations, () => setPostExitPositions(targets)));
|
|
235
|
+
} else {
|
|
236
|
+
setSwipeBackState("canceling");
|
|
237
|
+
const { animations, finished } = animateSwipeCancel(targets, ctx.displacement);
|
|
238
|
+
runningAnimsRef.current = animations;
|
|
239
|
+
finished.then(() => onFinish(animations, () => setIdlePositions(targets)));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return swiped;
|
|
154
243
|
},
|
|
155
|
-
[
|
|
244
|
+
[setSwipeBackState, stopAppBarBgScrub],
|
|
156
245
|
);
|
|
157
246
|
|
|
247
|
+
const resetSwipeBack = useCallback(() => {
|
|
248
|
+
stopRunningAnims();
|
|
249
|
+
stopAppBarBgScrub();
|
|
250
|
+
if (cachedTargetsRef.current) {
|
|
251
|
+
clearAllStyles(cachedTargetsRef.current);
|
|
252
|
+
}
|
|
253
|
+
cachedTargetsRef.current = null;
|
|
254
|
+
swipeStyleRef.current = null;
|
|
255
|
+
swipeBackContextRef.current = { ...IDLE_CONTEXT };
|
|
256
|
+
setSwipeBackState("idle");
|
|
257
|
+
}, [stopRunningAnims, stopAppBarBgScrub, setSwipeBackState]);
|
|
258
|
+
|
|
158
259
|
const topActivity = useTopActivity();
|
|
159
260
|
|
|
261
|
+
// ── WAAPI push/pop transitions triggered by stackflow state changes ──
|
|
262
|
+
const prevTransitionStateRef = useRef<string>(topActivity.transitionState);
|
|
263
|
+
|
|
264
|
+
useLayoutEffect(() => {
|
|
265
|
+
const prev = prevTransitionStateRef.current;
|
|
266
|
+
const next = topActivity.transitionState;
|
|
267
|
+
prevTransitionStateRef.current = next;
|
|
268
|
+
|
|
269
|
+
const stackEl = stackRef.current;
|
|
270
|
+
if (!stackEl) return;
|
|
271
|
+
|
|
272
|
+
const swipeState = swipeBackStateRef.current;
|
|
273
|
+
|
|
274
|
+
if (next === "enter-active" && prev !== "enter-active") {
|
|
275
|
+
if (swipeState !== "idle") return;
|
|
276
|
+
|
|
277
|
+
stopRunningAnims();
|
|
278
|
+
cancelPendingPushRAF();
|
|
279
|
+
// Defer one frame so stackflow's new top activity subtree is reliably
|
|
280
|
+
// observable via data-activity-is-top. Sync dispatch inside
|
|
281
|
+
// useLayoutEffect raced with stackflow subscription updates and left
|
|
282
|
+
// findTransitionTargets empty on push.
|
|
283
|
+
pendingPushRAFRef.current = requestAnimationFrame(() => {
|
|
284
|
+
pendingPushRAFRef.current = null;
|
|
285
|
+
const style = readTransitionStyle(stackEl);
|
|
286
|
+
const targets = findTransitionTargets(stackEl);
|
|
287
|
+
const { animations, finished } = animateTransition(targets, "push", style);
|
|
288
|
+
runningAnimsRef.current = animations;
|
|
289
|
+
finished.then(() => {
|
|
290
|
+
// Always pin idle inline styles + cancel before checking the ref.
|
|
291
|
+
// Skipping these on cancel-by-newer-transition would leave the
|
|
292
|
+
// mid-flight inline styles (or none at all) on screen.
|
|
293
|
+
setIdlePositions(targets, style);
|
|
294
|
+
cancelAll(animations);
|
|
295
|
+
if (runningAnimsRef.current === animations) {
|
|
296
|
+
runningAnimsRef.current = [];
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (next === "exit-active" && prev !== "exit-active") {
|
|
303
|
+
if (skipNextExitRef.current) {
|
|
304
|
+
skipNextExitRef.current = false;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (swipeState !== "idle") return;
|
|
309
|
+
|
|
310
|
+
cancelPendingPushRAF();
|
|
311
|
+
stopRunningAnims();
|
|
312
|
+
const style = readTransitionStyle(stackEl);
|
|
313
|
+
const targets = findTransitionTargets(stackEl);
|
|
314
|
+
const { animations, finished } = animateTransition(targets, "pop", style);
|
|
315
|
+
runningAnimsRef.current = animations;
|
|
316
|
+
finished.then(() => {
|
|
317
|
+
// Always pin post-exit inline styles + cancel. If we bail here on
|
|
318
|
+
// cancel-by-newer-transition, the previous transition's idle inline
|
|
319
|
+
// styles (e.g. behind layer at -30%) stay on screen.
|
|
320
|
+
setPostExitPositions(targets, style);
|
|
321
|
+
cancelAll(animations);
|
|
322
|
+
if (runningAnimsRef.current === animations) {
|
|
323
|
+
runningAnimsRef.current = [];
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}, [topActivity.transitionState, stopRunningAnims, cancelPendingPushRAF]);
|
|
328
|
+
|
|
329
|
+
// Cancel any pending push rAF and running animations on unmount so
|
|
330
|
+
// late-firing finished handlers can't run against a torn-down stack.
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
return () => {
|
|
333
|
+
cancelPendingPushRAF();
|
|
334
|
+
stopRunningAnims();
|
|
335
|
+
stopAppBarBgScrub();
|
|
336
|
+
};
|
|
337
|
+
}, [cancelPendingPushRAF, stopRunningAnims, stopAppBarBgScrub]);
|
|
338
|
+
|
|
160
339
|
const stackProps = useMemo(
|
|
161
|
-
() =>
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
topActivity.activityType,
|
|
171
|
-
topActivity.transitionStyle,
|
|
172
|
-
],
|
|
173
|
-
) as React.HTMLAttributes<HTMLElement>;
|
|
340
|
+
() =>
|
|
341
|
+
({
|
|
342
|
+
"data-swipe-back-state": swipeBackStateRef.current,
|
|
343
|
+
"data-global-transition-state": topActivity.transitionState,
|
|
344
|
+
"data-top-activity-type": topActivity.activityType,
|
|
345
|
+
"data-top-transition-style": topActivity.transitionStyle,
|
|
346
|
+
}) as React.HTMLAttributes<HTMLElement>,
|
|
347
|
+
[topActivity.transitionState, topActivity.activityType, topActivity.transitionStyle],
|
|
348
|
+
);
|
|
174
349
|
|
|
175
350
|
return useMemo(
|
|
176
351
|
() => ({
|
|
177
352
|
stackRef,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
getSwipeBackEvents,
|
|
183
|
-
|
|
353
|
+
startSwipeBack,
|
|
354
|
+
moveSwipeBack,
|
|
355
|
+
endSwipeBack,
|
|
356
|
+
resetSwipeBack,
|
|
184
357
|
stackProps,
|
|
185
358
|
}),
|
|
186
|
-
[
|
|
359
|
+
[startSwipeBack, moveSwipeBack, endSwipeBack, resetSwipeBack, stackProps],
|
|
187
360
|
);
|
|
188
361
|
}
|
|
@@ -1,56 +1,94 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { useCallbackRef } from "@radix-ui/react-use-callback-ref";
|
|
2
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
3
3
|
import { useGlobalInteractionContext } from "./useGlobalInteractionContext";
|
|
4
4
|
|
|
5
|
-
export interface UseSwipeBackProps
|
|
5
|
+
export interface UseSwipeBackProps {
|
|
6
|
+
/**
|
|
7
|
+
* The threshold to determine whether the swipe back is intentional, by displacement ratio.
|
|
8
|
+
* @default 0.4
|
|
9
|
+
*/
|
|
10
|
+
swipeBackDisplacementRatioThreshold?: number;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The threshold to determine whether the swipe back is intentional, by velocity.
|
|
14
|
+
* @default 1
|
|
15
|
+
*/
|
|
16
|
+
swipeBackVelocityThreshold?: number;
|
|
17
|
+
|
|
18
|
+
onSwipeBackStart?: () => void;
|
|
19
|
+
onSwipeBackMove?: (props: { displacement: number; displacementRatio: number }) => void;
|
|
20
|
+
onSwipeBackEnd?: (props: { swiped: boolean }) => void;
|
|
21
|
+
}
|
|
6
22
|
|
|
7
23
|
export function useSwipeBack(props: UseSwipeBackProps) {
|
|
8
|
-
const
|
|
9
|
-
|
|
24
|
+
const {
|
|
25
|
+
swipeBackDisplacementRatioThreshold: displacementRatioThreshold,
|
|
26
|
+
swipeBackVelocityThreshold: velocityThreshold,
|
|
27
|
+
} = props;
|
|
28
|
+
|
|
29
|
+
const onSwipeStart = useCallbackRef(props.onSwipeBackStart);
|
|
30
|
+
const onSwipeMove = useCallbackRef(props.onSwipeBackMove);
|
|
31
|
+
const onSwipeEnd = useCallbackRef(props.onSwipeBackEnd);
|
|
32
|
+
|
|
33
|
+
const { startSwipeBack, moveSwipeBack, endSwipeBack, resetSwipeBack } =
|
|
34
|
+
useGlobalInteractionContext();
|
|
35
|
+
|
|
36
|
+
const rAFLockRef = useRef(false);
|
|
10
37
|
|
|
11
38
|
useEffect(() => {
|
|
12
39
|
return () => {
|
|
13
|
-
|
|
40
|
+
resetSwipeBack();
|
|
14
41
|
};
|
|
15
|
-
}, [
|
|
42
|
+
}, [resetSwipeBack]);
|
|
16
43
|
|
|
17
44
|
return useMemo(
|
|
18
45
|
() => ({
|
|
19
46
|
activityProps: {
|
|
20
47
|
"data-swipe-back": "",
|
|
21
48
|
} as React.HTMLAttributes<HTMLDivElement>,
|
|
22
|
-
layerProps: {
|
|
23
|
-
onAnimationEnd: (e) => {
|
|
24
|
-
if (e.target === e.currentTarget) {
|
|
25
|
-
events.reset();
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
onTransitionEnd: (e) => {
|
|
29
|
-
if (e.target === e.currentTarget) {
|
|
30
|
-
events.reset();
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
} as React.HTMLAttributes<HTMLDivElement>,
|
|
49
|
+
layerProps: {} as React.HTMLAttributes<HTMLDivElement>,
|
|
34
50
|
edgeProps: {
|
|
35
51
|
tabIndex: -1,
|
|
36
|
-
onTouchStart: (e) => {
|
|
52
|
+
onTouchStart: (e: React.TouchEvent) => {
|
|
37
53
|
const x0 = e.touches[0].clientX;
|
|
38
54
|
const t0 = Date.now();
|
|
39
|
-
|
|
55
|
+
startSwipeBack({ x0, t0 });
|
|
56
|
+
onSwipeStart();
|
|
40
57
|
},
|
|
41
|
-
onTouchMove: (e) => {
|
|
58
|
+
onTouchMove: (e: React.TouchEvent) => {
|
|
59
|
+
// rAF lock: process at most once per animation frame
|
|
60
|
+
if (rAFLockRef.current) return;
|
|
61
|
+
rAFLockRef.current = true;
|
|
42
62
|
const x = e.touches[0].clientX;
|
|
43
63
|
const t = Date.now();
|
|
44
|
-
|
|
64
|
+
requestAnimationFrame(() => {
|
|
65
|
+
const ctx = moveSwipeBack({ x, t });
|
|
66
|
+
rAFLockRef.current = false;
|
|
67
|
+
onSwipeMove({
|
|
68
|
+
displacement: ctx.displacement,
|
|
69
|
+
displacementRatio: ctx.displacementRatio,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
45
72
|
},
|
|
46
73
|
onTouchEnd: () => {
|
|
47
|
-
|
|
74
|
+
const swiped = endSwipeBack({ displacementRatioThreshold, velocityThreshold });
|
|
75
|
+
onSwipeEnd({ swiped });
|
|
48
76
|
},
|
|
49
77
|
onTouchCancel: () => {
|
|
50
|
-
|
|
78
|
+
const swiped = endSwipeBack({ displacementRatioThreshold, velocityThreshold });
|
|
79
|
+
onSwipeEnd({ swiped });
|
|
51
80
|
},
|
|
52
81
|
} as React.HTMLAttributes<HTMLElement>,
|
|
53
82
|
}),
|
|
54
|
-
[
|
|
83
|
+
[
|
|
84
|
+
startSwipeBack,
|
|
85
|
+
moveSwipeBack,
|
|
86
|
+
endSwipeBack,
|
|
87
|
+
onSwipeStart,
|
|
88
|
+
onSwipeMove,
|
|
89
|
+
onSwipeEnd,
|
|
90
|
+
displacementRatioThreshold,
|
|
91
|
+
velocityThreshold,
|
|
92
|
+
],
|
|
55
93
|
);
|
|
56
94
|
}
|