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'
<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')
</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>
--- /dev/null
+<script setup vapor></script>
+
+<template>
+ <TransitionGroup>
+ <slot />
+ </TransitionGroup>
+</template>
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,
]
}
- 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, `)`]
}
isLastInsertion,
resetInsertionState,
} from './insertionState'
+import { triggerTransitionGroupUpdate } from './components/TransitionGroup'
class ForBlock extends VaporFragment {
scope: EffectScope | undefined
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) {
// 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 {
return hooks
}
- const { props, instance, state, delayedLeave } = hooks
+ const { props, instance, state, delayedLeave, group } = hooks
let resolvedHooks = resolveTransitionHooks(
child,
props,
hooks => (resolvedHooks = hooks as VaporTransitionHooks),
)
resolvedHooks.delayedLeave = delayedLeave
+ resolvedHooks.group = group
child.$transition = resolvedHooks
if (isFrag) setTransitionHooksOnFragment(block, resolvedHooks)
): 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)
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,
props: cssTransitionProps,
state,
instance,
+ group: true,
} as VaporTransitionHooks)
children = getTransitionBlocks(slottedBlock)
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`)
}
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)
+ })
+ }
+}