Преглед изворни кода

fix(runtime-vapor): animate vdom component moves in vapor TransitionGroup

daiwei пре 3 недеља
родитељ
комит
81422697a8

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

@@ -1134,5 +1134,43 @@ describe('vapor transition-group', () => {
             `<div class=""><div>d</div></div>`,
         )
     })
+
+    test('keyed vdom component move after key change', async () => {
+      const btnSelector = '.keyed-vdom-component-move-after-key-change > button'
+      const containerSelector =
+        '.keyed-vdom-component-move-after-key-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-->`,
+        )
+    })
   })
 })

+ 44 - 0
packages-private/vapor-e2e-test/transition-group/cases/interop/keyed-vdom-component-move-after-key-change.vue

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

+ 27 - 0
packages-private/vapor-e2e-test/transition-group/components/VdomExpandingItem.vue

@@ -0,0 +1,27 @@
+<script setup lang="ts">
+defineProps<{
+  id: number
+  isOpened: boolean
+}>()
+</script>
+
+<template>
+  <div class="item" :class="isOpened ? 'opened' : 'closed'" :id="`item-${id}`">
+    <div class="item-inner">item {{ id }}</div>
+  </div>
+</template>
+
+<style>
+.item {
+  border: 1px solid black;
+  width: 100px;
+  height: 100px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.item.opened {
+  width: 420px;
+}
+</style>

+ 2 - 2
packages/runtime-vapor/src/components/TransitionGroup.ts

@@ -406,12 +406,12 @@ function getTransitionBlocks(
       children.push(...blocks)
     }
   } else if (isFragment(block)) {
+    if (onFragment) onFragment(block)
+    if (onUpdateOwner) onUpdateOwner(block)
     if (isInteropEnabled && block.vnode) {
       // vdom component
       children.push(block)
     } else {
-      if (onFragment) onFragment(block)
-      if (onUpdateOwner) onUpdateOwner(block)
       const blocks = getTransitionBlocks(block.nodes, onFragment, onUpdateOwner)
       inheritKey(blocks, block.$key)
       children.push(...blocks)

+ 20 - 2
packages/runtime-vapor/src/vdomInterop.ts

@@ -820,13 +820,31 @@ function appendVnodeUpdatedHook(vnode: VNode, hook: () => void): void {
     : hook
 }
 
+function appendVnodeBeforeUpdateHook(vnode: VNode, hook: () => void): void {
+  const props = (vnode.props ||= {})
+  const existing = props.onVnodeBeforeUpdate
+  props.onVnodeBeforeUpdate = existing
+    ? isArray(existing)
+      ? [...existing, hook]
+      : [existing, hook]
+    : hook
+}
+
 function trackFragmentVNodeUpdates(frag: VaporFragment, vnode: VNode): void {
-  const refresh = () => {
+  const beforeUpdate = () => {
+    if (frag.onBeforeUpdate) {
+      for (let i = 0; i < frag.onBeforeUpdate.length; i++) {
+        frag.onBeforeUpdate[i]()
+      }
+    }
+  }
+  const updated = () => {
     frag.nodes = resolveVNodeNodes(vnode)
     frag.validityPending = false
     if (frag.onUpdated) frag.onUpdated.forEach(m => m())
   }
-  appendVnodeUpdatedHook(vnode, refresh)
+  appendVnodeBeforeUpdateHook(vnode, beforeUpdate)
+  appendVnodeUpdatedHook(vnode, updated)
 }
 
 /**