Jelajahi Sumber

fix(transition-group): support reusable transition group (#14077)

edison 5 bulan lalu
induk
melakukan
171f3f5694

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

@@ -369,6 +369,44 @@ describe('vapor transition-group', () => {
     expect(calls).toContain('afterEnter')
   })
 
+  test(
+    'reusable transition group',
+    async () => {
+      const btnSelector = '.reusable-transition-group > button'
+      const containerSelector = '.reusable-transition-group > div'
+
+      expect(await html(containerSelector)).toBe(
+        `<div class="test">a</div>` +
+          `<div class="test">b</div>` +
+          `<div class="test">c</div>`,
+      )
+
+      expect(
+        (await transitionStart(btnSelector, containerSelector)).innerHTML,
+      ).toBe(
+        `<div class="test group-enter-from group-enter-active">d</div>` +
+          `<div class="test">b</div>` +
+          `<div class="test group-move" style="">a</div>` +
+          `<div class="test group-leave-from group-leave-active group-move" style="">c</div>`,
+      )
+
+      await nextFrame()
+      expect(await html(containerSelector)).toBe(
+        `<div class="test group-enter-active group-enter-to">d</div>` +
+          `<div class="test">b</div>` +
+          `<div class="test group-move" style="">a</div>` +
+          `<div class="test group-leave-active group-move group-leave-to" style="">c</div>`,
+      )
+      await transitionFinish(duration * 2)
+      expect(await html(containerSelector)).toBe(
+        `<div class="test">d</div>` +
+          `<div class="test">b</div>` +
+          `<div class="test" style="">a</div>`,
+      )
+    },
+    E2E_TIMEOUT,
+  )
+
   test('interop: render vdom component', async () => {
     const btnSelector = '.interop > button'
     const containerSelector = '.interop > div'

+ 9 - 0
packages-private/vapor-e2e-test/transition-group/App.vue

@@ -1,6 +1,7 @@
 <script setup vapor>
 import { ref } from 'vue'
 import VdomComp from './components/VdomComp.vue'
+import MyTransitionGroup from './components/MyTransitionGroup.vue'
 
 const items = ref(['a', 'b', 'c'])
 const enterClick = () => items.value.push('d', 'e')
@@ -108,6 +109,14 @@ const interopClick = () => (items.value = ['b', 'c', 'd'])
         </transition-group>
       </div>
     </div>
+    <div class="reusable-transition-group">
+      <button @click="moveClick">reusable button</button>
+      <div>
+        <MyTransitionGroup name="group">
+          <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+        </MyTransitionGroup>
+      </div>
+    </div>
     <div class="interop">
       <button @click="interopClick">interop button</button>
       <div>

+ 7 - 0
packages-private/vapor-e2e-test/transition-group/components/MyTransitionGroup.vue

@@ -0,0 +1,7 @@
+<script setup vapor></script>
+
+<template>
+  <TransitionGroup>
+    <slot />
+  </TransitionGroup>
+</template>

+ 2 - 15
packages/compiler-vapor/src/generators/component.ts

@@ -40,12 +40,7 @@ import { genEventHandler } from './event'
 import { genDirectiveModifiers, genDirectivesForElement } from './directive'
 import { genBlock } from './block'
 import { genModelHandler } from './vModel'
-import {
-  isBuiltInComponent,
-  isKeepAliveTag,
-  isTeleportTag,
-  isTransitionGroupTag,
-} from '../utils'
+import { isBuiltInComponent } from '../utils'
 
 export function genCreateComponent(
   operation: CreateComponentIRNode,
@@ -465,15 +460,7 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) {
     ]
   }
 
-  if (
-    node.type === NodeTypes.ELEMENT &&
-    // Not a real component
-    !isTeleportTag(node.tag) &&
-    // Needs to determine whether to activate/deactivate based on instance.parent being KeepAlive
-    !isKeepAliveTag(node.tag) &&
-    // Slot updates need to trigger TransitionGroup's onBeforeUpdate/onUpdated hook
-    !isTransitionGroupTag(node.tag)
-  ) {
+  if (node.type === NodeTypes.ELEMENT) {
     // wrap with withVaporCtx to ensure correct currentInstance inside slot
     blockFn = [`${context.helper('withVaporCtx')}(`, ...blockFn, `)`]
   }

+ 7 - 0
packages/runtime-vapor/src/apiCreateFor.ts

@@ -39,6 +39,7 @@ import {
   isLastInsertion,
   resetInsertionState,
 } from './insertionState'
+import { triggerTransitionGroupUpdate } from './components/TransitionGroup'
 
 class ForBlock extends VaporFragment {
   scope: EffectScope | undefined
@@ -130,6 +131,12 @@ export const createFor = (
     newBlocks = new Array(newLength)
     let isFallback = false
 
+    // trigger TransitionGroup update hooks
+    const transitionHooks = frag.$transition
+    if (transitionHooks && transitionHooks.group) {
+      triggerTransitionGroupUpdate(transitionHooks)
+    }
+
     const prevSub = setActiveSub()
 
     if (!isMounted) {

+ 3 - 0
packages/runtime-vapor/src/block.ts

@@ -29,6 +29,9 @@ export interface VaporTransitionHooks extends TransitionHooks {
   // mark transition hooks as disabled so that it skips during
   // inserting
   disabled?: boolean
+  // mark transition hooks as group so that it triggers TransitionGroup update hooks
+  // in vFor renderList function
+  group?: boolean
 }
 
 export interface TransitionOptions {

+ 5 - 1
packages/runtime-vapor/src/components/Transition.ts

@@ -233,7 +233,7 @@ export function applyTransitionHooks(
     return hooks
   }
 
-  const { props, instance, state, delayedLeave } = hooks
+  const { props, instance, state, delayedLeave, group } = hooks
   let resolvedHooks = resolveTransitionHooks(
     child,
     props,
@@ -242,6 +242,7 @@ export function applyTransitionHooks(
     hooks => (resolvedHooks = hooks as VaporTransitionHooks),
   )
   resolvedHooks.delayedLeave = delayedLeave
+  resolvedHooks.group = group
   child.$transition = resolvedHooks
   if (isFrag) setTransitionHooksOnFragment(block, resolvedHooks)
 
@@ -365,6 +366,9 @@ export function setTransitionHooksOnFragment(
 ): void {
   if (isFragment(block)) {
     block.$transition = hooks
+    if (block.nodes && isFragment(block.nodes)) {
+      setTransitionHooksOnFragment(block.nodes, hooks)
+    }
   } else if (isArray(block)) {
     for (let i = 0; i < block.length; i++) {
       setTransitionHooksOnFragment(block[i], hooks)

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

@@ -10,11 +10,12 @@ import {
   hasCSSTransform,
   onBeforeUpdate,
   onUpdated,
+  queuePostFlushCb,
   resolveTransitionProps,
   useTransitionState,
   warn,
 } from '@vue/runtime-dom'
-import { extend, isArray } from '@vue/shared'
+import { extend, invokeArrayFns, isArray } from '@vue/shared'
 import {
   type Block,
   type TransitionBlock,
@@ -126,6 +127,7 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({
       props: cssTransitionProps,
       state,
       instance,
+      group: true,
     } as VaporTransitionHooks)
 
     children = getTransitionBlocks(slottedBlock)
@@ -133,10 +135,14 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({
       const child = children[i]
       if (isValidTransitionBlock(child)) {
         if (child.$key != null) {
-          setTransitionHooks(
+          const hooks = resolveTransitionHooks(
             child,
-            resolveTransitionHooks(child, cssTransitionProps, state, instance!),
+            cssTransitionProps,
+            state,
+            instance!,
           )
+          hooks.group = true
+          setTransitionHooks(child, hooks)
         } else if (__DEV__ && child.$key == null) {
           warn(`<transition-group> children must be keyed`)
         }
@@ -221,3 +227,23 @@ function getFirstConnectedChild(
     if (el.isConnected) return el
   }
 }
+
+/**
+ * The implementation of TransitionGroup relies on the onBeforeUpdate and onUpdated hooks.
+ * However, when the slot content of TransitionGroup updates, it does not trigger the
+ * onBeforeUpdate and onUpdated hooks. Therefore, it is necessary to manually trigger
+ * the TransitionGroup update hooks to ensure its proper work.
+ */
+export function triggerTransitionGroupUpdate(
+  transition: VaporTransitionHooks,
+): void {
+  const { instance } = transition
+  if (!instance.isUpdating) {
+    instance.isUpdating = true
+    if (instance.bu) invokeArrayFns(instance.bu)
+    queuePostFlushCb(() => {
+      instance.isUpdating = false
+      if (instance.u) invokeArrayFns(instance.u)
+    })
+  }
+}