Skip to content

Commit c42dfaa

Browse files
committed
feat: add onSettleCurrent to useSpring
1 parent cb5a580 commit c42dfaa

File tree

6 files changed

+148
-7
lines changed

6 files changed

+148
-7
lines changed

README.ja.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ const { style, realValue, realVelocity } = useSpring(
324324
</template>
325325
```
326326

327-
`useSpring` が返す値には、現在のアニメーションが完了するまで待つ `onFinishCurrent` 関数があります。この関数には、進行中のアニメーションが完了したときに呼ばれるコールバック関数を登録できます
327+
`useSpring` が返す値には、現在のアニメーションが完了、もしくは、settle するまで待つ `onFinishCurrent``onSettleCurrent` 関数があります。この関数には、進行中のアニメーションが完了・settle したときに呼ばれるコールバック関数を登録できます
328328

329329
```vue
330330
<script setup>
@@ -343,11 +343,16 @@ function move() {
343343
// 100px まで移動
344344
position.value = 100
345345
346-
// 上記の position の更新によってトリガーされたアニメーションが完了するまで待つ
346+
// 上記の position の更新によってトリガーされたアニメーションが完了(duration が経過)するまで待つ
347347
onFinishCurrent(() => {
348348
// 0px まで移動
349349
position.value = 0
350350
})
351+
352+
// 上記の position の更新によってトリガーされたアニメーションが settle(見た目が停止)するまで待つ
353+
onSettleCurrent(() => {
354+
console.log('settled')
355+
})
351356
}
352357
</script>
353358
```

README.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ const { style, realValue, realVelocity } = useSpring(
324324
</template>
325325
```
326326

327-
`useSpring` provides `onFinishCurrent` function that is for waiting until the current animation is finished. You can register a callback function that will be called when an ongoing animation is finished.
327+
`useSpring` provides `onFinishCurrent` and `onSettleCurrent` functions for waiting until the current animation is finished or settled. You can register a callback function that will be called when an ongoing animation is finished/settled.
328328

329329
```vue
330330
<script setup>
@@ -343,11 +343,16 @@ function move() {
343343
// Move to 100px
344344
position.value = 100
345345
346-
// Wait for the animation is finished triggered by the above position update
346+
// Wait for the animation is finished (duration passed) triggered by the above position update
347347
onFinishCurrent(() => {
348348
// Move to 0px
349349
position.value = 0
350350
})
351+
352+
// Wait for the animation is settled (visually stopped) triggered by the above position update
353+
onSettleCurrent(() => {
354+
console.log('settled')
355+
})
351356
}
352357
</script>
353358
```

src/core/controller.ts

+16
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface AnimationController<
3333
stop: (options?: StopOptions) => void
3434

3535
onFinishCurrent: (fn: (data: { stopped: boolean }) => void) => void
36+
onSettleCurrent: (fn: (data: { stopped: boolean }) => void) => void
3637
}
3738

3839
interface ValueHistoryItem<Key extends keyof any> {
@@ -193,6 +194,20 @@ export function createAnimateController<
193194
})
194195
}
195196

197+
function onSettleCurrent(fn: (data: { stopped: boolean }) => void): void {
198+
if (!ctx) {
199+
fn({ stopped: false })
200+
return
201+
}
202+
203+
// Must store ctx to local variable because ctx in the callback of `then` can be
204+
// different from when the time onFinishCurrent
205+
const _ctx = ctx
206+
_ctx.settlingPromise.then(() => {
207+
fn({ stopped: _ctx.stoppedDuration !== undefined })
208+
})
209+
}
210+
196211
return {
197212
get realValue() {
198213
if (ctx) {
@@ -220,6 +235,7 @@ export function createAnimateController<
220235
setStyle,
221236
setOptions,
222237
onFinishCurrent,
238+
onSettleCurrent,
223239
}
224240
}
225241

src/vue/use-spring.ts

+15
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface UseSpringResult<Values extends Record<string, number[]>> {
2727
realValue: DeepReadonly<Ref<Values>>
2828
realVelocity: DeepReadonly<Ref<Values>>
2929
onFinishCurrent: (fn: (data: { stopped: boolean }) => void) => void
30+
onSettleCurrent: (fn: (data: { stopped: boolean }) => void) => void
3031
}
3132

3233
export function useSpring<Style extends Record<string, AnimateValue>>(
@@ -64,6 +65,19 @@ export function useSpring<Style extends Record<string, AnimateValue>>(
6465
})
6566
}
6667

68+
function onSettleCurrent(fn: (data: { stopped: boolean }) => void): void {
69+
// Wait for the next tick to ensure that input changes in the same tick
70+
// triggers a new animation that is a case like:
71+
//
72+
// springStyle.value = { width: '100px' }
73+
// onSettleCurrent(() => {
74+
// ...
75+
// })
76+
nextTick().then(() => {
77+
controller.onSettleCurrent(fn)
78+
})
79+
}
80+
6781
watch(
6882
[stopped, () => ({ ...input.value }), () => ({ ...optionsRef.value })],
6983
([stopped, input, options], [prevStopped, prevInput]) => {
@@ -86,5 +100,6 @@ export function useSpring<Style extends Record<string, AnimateValue>>(
86100
realValue: readonly(realValue),
87101
realVelocity: readonly(realVelocity),
88102
onFinishCurrent,
103+
onSettleCurrent,
89104
}
90105
}

test/core/controller.test.ts

+54-2
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,25 @@ describe('AnimationController', () => {
235235
})
236236
})
237237

238-
test('stopped == true when animation is stopped by stop()', () => {
238+
test('register settle listener for the current animation', () => {
239+
let style: any
240+
const controller = createAnimateController((_style) => {
241+
style = _style
242+
})
243+
controller.setOptions({ duration: 100 })
244+
controller.setStyle({ width: '100px' }, { animate: false })
245+
controller.setStyle({ width: '200px' })
246+
247+
return new Promise<void>((resolve) => {
248+
controller.onSettleCurrent(({ stopped }) => {
249+
expect(stopped).toBe(false)
250+
expect(style.width).toBe('200px')
251+
resolve()
252+
})
253+
})
254+
})
255+
256+
test('stopped == true in onFinishCurrent when animation is stopped by stop()', () => {
239257
let style: any
240258
const controller = createAnimateController((_style) => {
241259
style = _style
@@ -254,7 +272,7 @@ describe('AnimationController', () => {
254272
})
255273
})
256274

257-
test('stopped == true when animation is stopped by a new animation', () => {
275+
test('stopped == true in onFinishCurrent when animation is stopped by a new animation', () => {
258276
const controller = createAnimateController(() => {})
259277
controller.setOptions({ duration: 100 })
260278
controller.setStyle({ width: '100px' }, { animate: false })
@@ -268,4 +286,38 @@ describe('AnimationController', () => {
268286
controller.setStyle({ width: '300px' })
269287
})
270288
})
289+
290+
test('stopped == true in onSettleCurrent when animation is stopped by stop()', () => {
291+
let style: any
292+
const controller = createAnimateController((_style) => {
293+
style = _style
294+
})
295+
controller.setOptions({ duration: 100 })
296+
controller.setStyle({ width: '100px' }, { animate: false })
297+
controller.setStyle({ width: '200px' })
298+
299+
return new Promise<void>((resolve) => {
300+
controller.onSettleCurrent(({ stopped }) => {
301+
expect(stopped).toBe(true)
302+
expect(style.width).toBe(controller.realValue.width![0] + 'px')
303+
resolve()
304+
})
305+
controller.stop()
306+
})
307+
})
308+
309+
test('stopped == true in onSettleCurrent when animation is stopped by a new animation', () => {
310+
const controller = createAnimateController(() => {})
311+
controller.setOptions({ duration: 100 })
312+
controller.setStyle({ width: '100px' }, { animate: false })
313+
controller.setStyle({ width: '200px' })
314+
315+
return new Promise<void>((resolve) => {
316+
controller.onSettleCurrent(({ stopped }) => {
317+
expect(stopped).toBe(true)
318+
resolve()
319+
})
320+
controller.setStyle({ width: '300px' })
321+
})
322+
})
271323
})

test/vue/use-spring.test.ts

+49-1
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,29 @@ describe('useSpring', () => {
249249
})
250250
})
251251

252-
test('stopped == true when animation is stopped', () => {
252+
test('register settle listener for the current animation', () => {
253+
const value = ref(10)
254+
255+
const { style, onSettleCurrent } = useSpring(
256+
() => ({
257+
width: `${value.value}px`,
258+
}),
259+
{
260+
duration: 100,
261+
},
262+
)
263+
264+
return new Promise<void>((resolve) => {
265+
value.value = 20
266+
onSettleCurrent(({ stopped }) => {
267+
expect(stopped).toBe(false)
268+
expect(style.value.width).toBe('20px')
269+
resolve()
270+
})
271+
})
272+
})
273+
274+
test('stopped == true in onFinishCurrent when animation is stopped', () => {
253275
const value = ref(10)
254276
const disabled = ref(false)
255277

@@ -274,4 +296,30 @@ describe('useSpring', () => {
274296
})
275297
})
276298
})
299+
300+
test('stopped == true in onSettleCurrent when animation is stopped', () => {
301+
const value = ref(10)
302+
const disabled = ref(false)
303+
304+
const { style, realValue, onSettleCurrent } = useSpring(
305+
() => ({
306+
width: `${value.value}px`,
307+
}),
308+
() => ({
309+
duration: 100,
310+
disabled: disabled.value,
311+
}),
312+
)
313+
314+
value.value = 20
315+
316+
return new Promise<void>((resolve) => {
317+
disabled.value = true
318+
onSettleCurrent(({ stopped }) => {
319+
expect(stopped).toBe(true)
320+
expect(style.value.width).toBe(realValue.value.width[0] + 'px')
321+
resolve()
322+
})
323+
})
324+
})
277325
})

0 commit comments

Comments
 (0)