Bladeren bron

fix(runtime-vapor): defer TransitionGroup moves until child updates flush

daiwei 1 week geleden
bovenliggende
commit
711213f7e5

+ 37 - 0
packages-private/vapor-e2e-test/__tests__/transition-group.spec.ts

@@ -641,6 +641,43 @@ describe('vapor transition-group', () => {
       )
   })
 
+  test('same-key component move after prop change', async () => {
+    const btnSelector = '.same-key-component-move-after-prop-change > button'
+    const containerSelector = '.same-key-component-move-after-prop-change > div'
+
+    await expect
+      .element(css(containerSelector))
+      .toContainHTML(
+        `<div class="item-wrapper">` +
+          `<div class="item closed" id="item-1"><div class="item-inner">item 1</div></div>` +
+          `<div class="item closed" id="item-2"><div class="item-inner">item 2</div></div>` +
+          `<!--for--></div><!--transition-group-->`,
+      )
+
+    click(btnSelector)
+    await nextTick()
+    await nextFrame()
+
+    await expect
+      .element(css(containerSelector))
+      .toContainHTML(
+        `<div class="item-wrapper">` +
+          `<div class="item opened" id="item-1"><div class="item-inner">item 1</div></div>` +
+          `<div class="item closed group-move" id="item-2" style=""><div class="item-inner">item 2</div></div>` +
+          `<!--for--></div><!--transition-group-->`,
+      )
+
+    await transitionFinish(350)
+    await expect
+      .element(css(containerSelector))
+      .toContainHTML(
+        `<div class="item-wrapper">` +
+          `<div class="item opened" id="item-1"><div class="item-inner">item 1</div></div>` +
+          `<div class="item closed" id="item-2" style=""><div class="item-inner">item 2</div></div>` +
+          `<!--for--></div><!--transition-group-->`,
+      )
+  })
+
   test('dynamic name', async () => {
     const btnSelector = '.dynamic-name button.toggleBtn'
     const btnChangeName = '.dynamic-name button.changeNameBtn'

+ 47 - 0
packages-private/vapor-e2e-test/transition-group/cases/vapor-transition-group/same-key-component-move-after-prop-change.vue

@@ -0,0 +1,47 @@
+<script setup vapor>
+import { ref } from 'vue'
+import VaporExpandingItem from '../../components/VaporExpandingItem.vue'
+
+const items = ref(
+  [...Array(2)].map((_, i) => ({
+    id: i + 1,
+    isOpened: false,
+  })),
+)
+
+function toggleExpansion() {
+  items.value = [
+    { id: 1, isOpened: true },
+    { id: 2, isOpened: false },
+  ]
+}
+</script>
+
+<template>
+  <div class="same-key-component-move-after-prop-change">
+    <button @click="toggleExpansion">toggle expansion of first element</button>
+    <div>
+      <transition-group name="group" tag="div" class="item-wrapper">
+        <VaporExpandingItem
+          v-for="i in items"
+          :key="i.id"
+          :id="i.id"
+          :is-opened="i.isOpened"
+        />
+      </transition-group>
+    </div>
+  </div>
+</template>
+
+<style>
+.item-wrapper {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 5px;
+  width: 430px;
+}
+
+.same-key-component-move-after-prop-change .group-move {
+  transition: transform 300ms ease;
+}
+</style>

+ 11 - 3
packages/runtime-vapor/src/components/TransitionGroup.ts

@@ -149,9 +149,10 @@ const VaporTransitionGroupImpl = defineVaporComponent({
 
     let prevChildren: ResolvedTransitionBlock[] = []
     // Multiple child owners can update in the same flush (e.g. a VDOM child
-    // props update plus the surrounding v-for keyed diff). Keep the first
-    // position snapshot until the matching updated hook applies the move.
+    // props update plus the surrounding v-for keyed diff). Keep the first old
+    // position snapshot, then apply moves after child render jobs have flushed.
     let isUpdatePending = false
+    let isUpdatedPending = false
     let slottedBlock: Block = []
 
     const beforeUpdate = () => {
@@ -176,7 +177,8 @@ const VaporTransitionGroupImpl = defineVaporComponent({
       }
     }
 
-    const updated = () => {
+    const flushUpdated = () => {
+      isUpdatedPending = false
       if (!isUpdatePending) return
       isUpdatePending = false
       if (!prevChildren.length) {
@@ -215,6 +217,12 @@ const VaporTransitionGroupImpl = defineVaporComponent({
       prevChildren = []
     }
 
+    const updated = () => {
+      if (!isUpdatePending || isUpdatedPending) return
+      isUpdatedPending = true
+      queuePostFlushCb(flushUpdated)
+    }
+
     onBeforeUpdate(beforeUpdate)
     onUpdated(updated)