diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 848369074..c3a10f492 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -203,11 +203,13 @@ const BaseTransitionImpl: ComponentOptions = { return emptyPlaceholder(child) } - const enterHooks = resolveTransitionHooks( + let enterHooks = resolveTransitionHooks( innerChild, rawProps, state, instance, + // #11061, ensure enterHooks is fresh after clone + hooks => (enterHooks = hooks), ) setTransitionHooks(innerChild, enterHooks) @@ -305,6 +307,7 @@ export function resolveTransitionHooks( props: BaseTransitionProps, state: TransitionState, instance: ComponentInternalInstance, + postClone?: (hooks: TransitionHooks) => void, ): TransitionHooks { const { appear, @@ -445,7 +448,15 @@ export function resolveTransitionHooks( }, clone(vnode) { - return resolveTransitionHooks(vnode, props, state, instance) + const hooks = resolveTransitionHooks( + vnode, + props, + state, + instance, + postClone, + ) + if (postClone) postClone(hooks) + return hooks }, } diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 0e2a4bafc..3d30503e2 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -36,7 +36,10 @@ import { isSuspense, } from './components/Suspense' import type { DirectiveBinding } from './directives' -import type { TransitionHooks } from './components/BaseTransition' +import { + type TransitionHooks, + setTransitionHooks, +} from './components/BaseTransition' import { warn } from './warning' import { type Teleport, @@ -691,7 +694,10 @@ export function cloneVNode( // to clone the transition to ensure that the vnode referenced within // the transition hooks is fresh. if (transition && cloneTransition) { - cloned.transition = transition.clone(cloned as VNode) + setTransitionHooks( + cloned as VNode, + transition.clone(cloned as VNode) as TransitionHooks, + ) } if (__COMPAT__) { diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index 26c39a0ec..d005f78b8 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1340,6 +1340,98 @@ describe('e2e: Transition', () => { E2E_TIMEOUT, ) + // #11061 + test( + 'transition + fallthrough attrs (in-out mode)', + async () => { + const beforeLeaveSpy = vi.fn() + const onLeaveSpy = vi.fn() + const afterLeaveSpy = vi.fn() + const beforeEnterSpy = vi.fn() + const onEnterSpy = vi.fn() + const afterEnterSpy = vi.fn() + + await page().exposeFunction('onLeaveSpy', onLeaveSpy) + await page().exposeFunction('onEnterSpy', onEnterSpy) + await page().exposeFunction('beforeLeaveSpy', beforeLeaveSpy) + await page().exposeFunction('beforeEnterSpy', beforeEnterSpy) + await page().exposeFunction('afterLeaveSpy', afterLeaveSpy) + await page().exposeFunction('afterEnterSpy', afterEnterSpy) + + await page().evaluate(() => { + const { onEnterSpy, onLeaveSpy } = window as any + const { createApp, ref } = (window as any).Vue + createApp({ + components: { + one: { + template: '
one
', + }, + two: { + template: '
two
', + }, + }, + template: ` +
+ + + +
+ + `, + setup: () => { + const view = ref('one') + const click = () => + (view.value = view.value === 'one' ? 'two' : 'one') + return { + view, + click, + beforeEnterSpy, + onEnterSpy, + afterEnterSpy, + beforeLeaveSpy, + onLeaveSpy, + afterLeaveSpy, + } + }, + }).mount('#app') + }) + expect(await html('#container')).toBe('
one
') + + // toggle + await click('#toggleBtn') + await nextTick() + await transitionFinish() + expect(beforeEnterSpy).toBeCalledTimes(1) + expect(onEnterSpy).toBeCalledTimes(1) + expect(afterEnterSpy).toBeCalledTimes(1) + expect(beforeLeaveSpy).toBeCalledTimes(1) + expect(onLeaveSpy).toBeCalledTimes(1) + expect(afterLeaveSpy).toBeCalledTimes(1) + + expect(await html('#container')).toBe('
two
') + + // toggle back + await click('#toggleBtn') + await nextTick() + await transitionFinish() + expect(beforeEnterSpy).toBeCalledTimes(2) + expect(onEnterSpy).toBeCalledTimes(2) + expect(afterEnterSpy).toBeCalledTimes(2) + expect(beforeLeaveSpy).toBeCalledTimes(2) + expect(onLeaveSpy).toBeCalledTimes(2) + expect(afterLeaveSpy).toBeCalledTimes(2) + + expect(await html('#container')).toBe('
one
') + }, + E2E_TIMEOUT, + ) + test( 'w/ KeepAlive + unmount innerChild', async () => {