]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(transition): ensure Transition enterHooks are updated after clone (#11066)
authoredison <daiwei521@126.com>
Tue, 4 Jun 2024 14:07:42 +0000 (22:07 +0800)
committerGitHub <noreply@github.com>
Tue, 4 Jun 2024 14:07:42 +0000 (22:07 +0800)
close #11061

packages/runtime-core/src/components/BaseTransition.ts
packages/runtime-core/src/vnode.ts
packages/vue/__tests__/e2e/Transition.spec.ts

index 848369074ea92b07c8ca61121fc8a1871602578d..c3a10f492f3953f453a96d151ef7b2e44cfd2431 100644 (file)
@@ -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<any>,
   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
     },
   }
 
index 0e2a4bafcc5ae6d69cc7978a58dc10f30b69c15c..3d30503e20a90a8c404d3d1c657303c7988d727f 100644 (file)
@@ -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<T, U>(
   // 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__) {
index 26c39a0ecc085d8a2077f98dc04ad9b554062b6f..d005f78b84d4e6cdbbe7f284b80ba600e094e311 100644 (file)
@@ -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: '<div>one</div>',
+              },
+              two: {
+                template: '<div>two</div>',
+              },
+            },
+            template: `
+            <div id="container">
+              <transition foo="1" name="test" mode="in-out" 
+                @before-enter="beforeEnterSpy()"
+                @enter="onEnterSpy()"
+                @after-enter="afterEnterSpy()"
+                @before-leave="beforeLeaveSpy()"
+                @leave="onLeaveSpy()"
+                @after-leave="afterLeaveSpy()">
+                <component :is="view"></component>
+              </transition>
+            </div>
+            <button id="toggleBtn" @click="click">button</button>
+          `,
+            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('<div foo="1">one</div>')
+
+        // 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('<div foo="1" class="">two</div>')
+
+        // 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('<div foo="1" class="">one</div>')
+      },
+      E2E_TIMEOUT,
+    )
+
     test(
       'w/ KeepAlive + unmount innerChild',
       async () => {