From 721afd10b2cf3eef588aea9b4e6eb08c7be7a2f2 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 12 Nov 2025 11:07:09 +0800 Subject: [PATCH 1/2] feat(transition-group): enhance TransitionGroup to trigger update hooks manually --- .../src/generators/component.ts | 17 ++-------- packages/runtime-vapor/src/apiCreateFor.ts | 7 ++++ packages/runtime-vapor/src/block.ts | 3 ++ .../src/components/Transition.ts | 6 +++- .../src/components/TransitionGroup.ts | 32 +++++++++++++++++-- 5 files changed, 46 insertions(+), 19 deletions(-) diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index c2da7a3f22e..cd842a91887 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/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, `)`] } diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 5c4e598b910..5bc4473997c 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/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) { diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 628e6b61c7b..76a5edb2f69 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/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 { diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index 131154e2b6c..3f945e838f5 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/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) diff --git a/packages/runtime-vapor/src/components/TransitionGroup.ts b/packages/runtime-vapor/src/components/TransitionGroup.ts index b1e86c87b24..48dab603211 100644 --- a/packages/runtime-vapor/src/components/TransitionGroup.ts +++ b/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(` 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) + }) + } +} From 968dcbe5def54730842772108a63895d0dcb2936 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 12 Nov 2025 11:18:07 +0800 Subject: [PATCH 2/2] test: add reusable transition group tests --- .../__tests__/transition-group.spec.ts | 38 +++++++++++++++++++ .../vapor-e2e-test/transition-group/App.vue | 9 +++++ .../components/MyTransitionGroup.vue | 7 ++++ 3 files changed, 54 insertions(+) create mode 100644 packages-private/vapor-e2e-test/transition-group/components/MyTransitionGroup.vue diff --git a/packages-private/vapor-e2e-test/__tests__/transition-group.spec.ts b/packages-private/vapor-e2e-test/__tests__/transition-group.spec.ts index ba050f0f263..80563244733 100644 --- a/packages-private/vapor-e2e-test/__tests__/transition-group.spec.ts +++ b/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( + `
a
` + + `
b
` + + `
c
`, + ) + + expect( + (await transitionStart(btnSelector, containerSelector)).innerHTML, + ).toBe( + `
d
` + + `
b
` + + `
a
` + + `
c
`, + ) + + await nextFrame() + expect(await html(containerSelector)).toBe( + `
d
` + + `
b
` + + `
a
` + + `
c
`, + ) + await transitionFinish(duration * 2) + expect(await html(containerSelector)).toBe( + `
d
` + + `
b
` + + `
a
`, + ) + }, + E2E_TIMEOUT, + ) + test('interop: render vdom component', async () => { const btnSelector = '.interop > button' const containerSelector = '.interop > div' diff --git a/packages-private/vapor-e2e-test/transition-group/App.vue b/packages-private/vapor-e2e-test/transition-group/App.vue index 55775743c56..5cc6903a985 100644 --- a/packages-private/vapor-e2e-test/transition-group/App.vue +++ b/packages-private/vapor-e2e-test/transition-group/App.vue @@ -1,6 +1,7 @@ + +