@@ -7,7 +7,14 @@ import { useMotionImperativeRef } from '../hooks/useMotionImperativeRef';
7
7
import { useMountedState } from '../hooks/useMountedState' ;
8
8
import { useIsReducedMotion } from '../hooks/useIsReducedMotion' ;
9
9
import { getChildElement } from '../utils/getChildElement' ;
10
- import type { MotionParam , PresenceMotion , MotionImperativeRef , PresenceMotionFn , PresenceDirection } from '../types' ;
10
+ import type {
11
+ MotionParam ,
12
+ PresenceMotion ,
13
+ MotionImperativeRef ,
14
+ PresenceMotionFn ,
15
+ PresenceDirection ,
16
+ AnimationHandle ,
17
+ } from '../types' ;
11
18
import { useMotionBehaviourContext } from '../contexts/MotionBehaviourContext' ;
12
19
13
20
/**
@@ -72,6 +79,8 @@ export type PresenceComponent<MotionParams extends Record<string, MotionParam> =
72
79
[ MOTION_DEFINITION ] : PresenceMotionFn < MotionParams > ;
73
80
} ;
74
81
82
+ const INTERRUPTABLE_MOTION_SYMBOL = Symbol . for ( 'interruptablePresence' ) ;
83
+
75
84
export function createPresenceComponent < MotionParams extends Record < string , MotionParam > = { } > (
76
85
value : PresenceMotion | PresenceMotionFn < MotionParams > ,
77
86
) : PresenceComponent < MotionParams > {
@@ -143,8 +152,40 @@ export function createPresenceComponent<MotionParams extends Record<string, Moti
143
152
return ;
144
153
}
145
154
155
+ let handle : AnimationHandle | undefined ;
156
+
157
+ function cleanup ( ) {
158
+ if ( ! handle ) {
159
+ return ;
160
+ }
161
+
162
+ // Heads up!
163
+ //
164
+ // If the animation is interruptible & is running, we don't want to cancel it as it will be reversed in
165
+ // the next effect.
166
+ if ( IS_EXPERIMENTAL_INTERRUPTIBLE_MOTION && handle . isRunning ( ) ) {
167
+ return ;
168
+ }
169
+
170
+ handle . cancel ( ) ;
171
+ handleRef . current = undefined ;
172
+ }
173
+
146
174
const presenceMotion =
147
175
typeof value === 'function' ? value ( { element, ...optionsRef . current . params } ) : ( value as PresenceMotion ) ;
176
+ const IS_EXPERIMENTAL_INTERRUPTIBLE_MOTION = (
177
+ presenceMotion as PresenceMotion & { [ INTERRUPTABLE_MOTION_SYMBOL ] ?: boolean }
178
+ ) [ INTERRUPTABLE_MOTION_SYMBOL ] ;
179
+
180
+ if ( IS_EXPERIMENTAL_INTERRUPTIBLE_MOTION ) {
181
+ handle = handleRef . current ;
182
+
183
+ if ( handle && handle . isRunning ( ) ) {
184
+ handle . reverse ( ) ;
185
+
186
+ return cleanup ;
187
+ }
188
+ }
148
189
149
190
const atoms = visible ? presenceMotion . enter : presenceMotion . exit ;
150
191
const direction : PresenceDirection = visible ? 'enter' : 'exit' ;
@@ -158,13 +199,14 @@ export function createPresenceComponent<MotionParams extends Record<string, Moti
158
199
handleMotionStart ( direction ) ;
159
200
}
160
201
161
- const handle = animateAtoms ( element , atoms , { isReducedMotion : isReducedMotion ( ) } ) ;
202
+ handle = animateAtoms ( element , atoms , { isReducedMotion : isReducedMotion ( ) } ) ;
162
203
163
204
if ( applyInitialStyles ) {
164
205
// Heads up!
165
206
// .finish() is used in this case to skip animation and apply animation styles immediately
166
207
handle . finish ( ) ;
167
- return ;
208
+
209
+ return cleanup ;
168
210
}
169
211
170
212
handleRef . current = handle ;
@@ -177,9 +219,7 @@ export function createPresenceComponent<MotionParams extends Record<string, Moti
177
219
handle . finish ( ) ;
178
220
}
179
221
180
- return ( ) => {
181
- handle . cancel ( ) ;
182
- } ;
222
+ return cleanup ;
183
223
} ,
184
224
// Excluding `isFirstMount` from deps to prevent re-triggering the animation on subsequent renders
185
225
// eslint-disable-next-line react-hooks/exhaustive-deps
0 commit comments