]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(suspense): fix anchor for suspense with transition out-in (#9999)
author白雾三语 <32354856+baiwusanyu-c@users.noreply.github.com>
Mon, 8 Jan 2024 07:57:14 +0000 (15:57 +0800)
committerGitHub <noreply@github.com>
Mon, 8 Jan 2024 07:57:14 +0000 (15:57 +0800)
close #9996

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

index 8fbb07de9a122cfcd17c95e7eff3bb978d34e87b..8d6ee16410ae0cf81589e80233d3bcf9a2756515 100644 (file)
@@ -400,7 +400,6 @@ export interface SuspenseBoundary {
   namespace: ElementNamespace
   container: RendererElement
   hiddenContainer: RendererElement
-  anchor: RendererNode | null
   activeBranch: VNode | null
   pendingBranch: VNode | null
   deps: number
@@ -473,6 +472,7 @@ function createSuspenseBoundary(
     assertNumber(timeout, `Suspense timeout`)
   }
 
+  const initialAnchor = anchor
   const suspense: SuspenseBoundary = {
     vnode,
     parent: parentSuspense,
@@ -480,7 +480,6 @@ function createSuspenseBoundary(
     namespace,
     container,
     hiddenContainer,
-    anchor,
     deps: 0,
     pendingId: suspenseId++,
     timeout: typeof timeout === 'number' ? timeout : -1,
@@ -529,20 +528,28 @@ function createSuspenseBoundary(
               move(
                 pendingBranch!,
                 container,
-                next(activeBranch!),
+                anchor === initialAnchor ? next(activeBranch!) : anchor,
                 MoveType.ENTER,
               )
               queuePostFlushCb(effects)
             }
           }
         }
-        // this is initial anchor on mount
-        let { anchor } = suspense
         // unmount current active tree
         if (activeBranch) {
           // if the fallback tree was mounted, it may have been moved
           // as part of a parent suspense. get the latest anchor for insertion
-          anchor = next(activeBranch)
+          // #8105 if `delayEnter` is true, it means that the mounting of
+          // `activeBranch` will be delayed. if the branch switches before
+          // transition completes, both `activeBranch` and `pendingBranch` may
+          // coexist in the `hiddenContainer`. This could result in
+          // `next(activeBranch!)` obtaining an incorrect anchor
+          // (got `pendingBranch.el`).
+          // Therefore, after the mounting of activeBranch is completed,
+          // it is necessary to get the latest anchor.
+          if (parentNode(activeBranch.el!) !== suspense.hiddenContainer) {
+            anchor = next(activeBranch)
+          }
           unmount(activeBranch, parentComponent, suspense, true)
         }
         if (!delayEnter) {
index 24c818e2eede83c7ded8992aa7dc701109f36caf..e8d6d1e049ec1def817fa4a8e908daba4e6fcb41 100644 (file)
@@ -1652,6 +1652,77 @@ describe('e2e: Transition', () => {
       },
       E2E_TIMEOUT,
     )
+
+    // #9996
+    test(
+      'trigger again when transition is not finished & correctly anchor',
+      async () => {
+        await page().evaluate(duration => {
+          const { createApp, shallowRef, h } = (window as any).Vue
+          const One = {
+            async setup() {
+              return () => h('div', { class: 'test' }, 'one')
+            },
+          }
+          const Two = {
+            async setup() {
+              return () => h('div', { class: 'test' }, 'two')
+            },
+          }
+          createApp({
+            template: `
+            <div id="container">
+              <div>Top</div>
+              <transition name="test" mode="out-in" :duration="${duration}">
+                <Suspense>
+                  <component :is="view"/>
+                </Suspense>
+              </transition>
+              <div>Bottom</div>
+            </div>
+            <button id="toggleBtn" @click="click">button</button>
+          `,
+            setup: () => {
+              const view = shallowRef(One)
+              const click = () => {
+                view.value = view.value === One ? Two : One
+              }
+              return { view, click }
+            },
+          }).mount('#app')
+        }, duration)
+
+        await nextFrame()
+        expect(await html('#container')).toBe(
+          '<div>Top</div><div class="test test-enter-active test-enter-to">one</div><div>Bottom</div>',
+        )
+
+        await transitionFinish()
+        expect(await html('#container')).toBe(
+          '<div>Top</div><div class="test">one</div><div>Bottom</div>',
+        )
+
+        // trigger twice
+        classWhenTransitionStart()
+        await nextFrame()
+        expect(await html('#container')).toBe(
+          '<div>Top</div><div class="test test-leave-active test-leave-to">one</div><div>Bottom</div>',
+        )
+
+        await transitionFinish()
+        await nextFrame()
+        expect(await html('#container')).toBe(
+          '<div>Top</div><div class="test test-enter-active test-enter-to">two</div><div>Bottom</div>',
+        )
+
+        await transitionFinish()
+        await nextFrame()
+        expect(await html('#container')).toBe(
+          '<div>Top</div><div class="test">two</div><div>Bottom</div>',
+        )
+      },
+      E2E_TIMEOUT,
+    )
   })
 
   describe('transition with v-show', () => {