diff --git a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts index 180b0157aaa..1dce93782e1 100644 --- a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts @@ -13,7 +13,6 @@ const { nextFrame, timeout, isVisible, - count, html, transitionStart, waitForElement, @@ -40,6 +39,9 @@ describe('vapor transition', () => { beforeEach(async () => { const baseUrl = `http://localhost:${port}/transition/` + await page().evaluateOnNewDocument(dur => { + ;(window as any).__TRANSITION_DURATION__ = dur + }, duration) await page().goto(baseUrl) await page().waitForSelector('#app') }) @@ -972,6 +974,65 @@ describe('vapor transition', () => { ) }) + describe('transition with AsyncComponent', () => { + test('apply transition to inner component', async () => { + const btnSelector = '.async > button' + const containerSelector = '.async > div' + + expect(await html(containerSelector)).toBe('') + + // toggle + await click(btnSelector) + await nextTick() + // not yet resolved + expect(await html(containerSelector)).toBe('') + + // wait resolving + await timeout(50) + + // enter (resolved) + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + await nextFrame() + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + await transitionFinish() + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + + // leave + await click(btnSelector) + await nextTick() + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + await nextFrame() + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + await transitionFinish() + expect(await html(containerSelector)).toBe('') + + // enter again + await click(btnSelector) + // use the already resolved component + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + await nextFrame() + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + await transitionFinish() + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + }) + }) + describe('transition with v-show', () => { test( 'named transition with v-show', diff --git a/packages-private/vapor-e2e-test/transition/App.vue b/packages-private/vapor-e2e-test/transition/App.vue index 8b07a5ac4be..e7227ded01e 100644 --- a/packages-private/vapor-e2e-test/transition/App.vue +++ b/packages-private/vapor-e2e-test/transition/App.vue @@ -7,6 +7,7 @@ import { VaporTransition, createIf, template, + defineVaporAsyncComponent, onUnmounted, } from 'vue' const show = ref(true) @@ -14,7 +15,7 @@ const toggle = ref(true) const count = ref(0) const timeout = (fn, time) => setTimeout(fn, time) -const duration = typeof process !== 'undefined' && process.env.CI ? 200 : 50 +const duration = window.__TRANSITION_DURATION__ || 50 let calls = { basic: [], @@ -94,6 +95,10 @@ function changeViewInOut() { viewInOut.value = viewInOut.value === SimpleOne ? Two : SimpleOne } +const AsyncComp = defineVaporAsyncComponent(() => { + return new Promise(resolve => setTimeout(() => resolve(VaporCompA), 50)) +}) + const TrueBranch = defineVaporComponent({ name: 'TrueBranch', setup() { @@ -503,6 +508,17 @@ const click = () => { + +
+
+ + + +
+ +
+ +
diff --git a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts index dd6143950e3..4cde9454f09 100644 --- a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts +++ b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts @@ -26,8 +26,9 @@ import { removeFragmentNodes, } from './dom/hydration' import { invokeArrayFns } from '@vue/shared' -import { insert, remove } from './block' +import { type TransitionOptions, insert, remove } from './block' import { parentNode } from './dom/node' +import { setTransitionHooks } from './components/Transition' /*@ __NO_SIDE_EFFECTS__ */ export function defineVaporAsyncComponent( @@ -109,7 +110,8 @@ export function defineVaporAsyncComponent( }, setup() { - const instance = currentInstance as VaporComponentInstance + const instance = currentInstance as VaporComponentInstance & + TransitionOptions markAsyncBoundary(instance) const frag = @@ -166,6 +168,8 @@ export function defineVaporAsyncComponent( } else if (loadingComponent && !delayed.value) { render = () => createComponent(loadingComponent) } + + if (instance.$transition) frag!.$transition = instance.$transition frag!.update(render) }) @@ -176,10 +180,10 @@ export function defineVaporAsyncComponent( function createInnerComp( comp: VaporComponent, - parent: VaporComponentInstance, + parent: VaporComponentInstance & TransitionOptions, frag?: DynamicFragment, ): VaporComponentInstance { - const { rawProps, rawSlots, isSingleRoot, appContext } = parent + const { rawProps, rawSlots, isSingleRoot, appContext, $transition } = parent const instance = createComponent( comp, rawProps, @@ -189,6 +193,9 @@ function createInnerComp( appContext, ) + // set transition hooks + if ($transition) setTransitionHooks(instance, $transition) + // set ref // @ts-expect-error frag && frag.setRef && frag.setRef(instance) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 729ebee16fc..0f6d32ae8a5 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -36,12 +36,20 @@ export interface TransitionOptions { $transition?: VaporTransitionHooks } -export type TransitionBlock = - | (Node & TransitionOptions) - | (VaporFragment & TransitionOptions) - | (DynamicFragment & TransitionOptions) +export type TransitionBlock = ( + | Node + | VaporFragment + | DynamicFragment + | VaporComponentInstance +) & + TransitionOptions -export type Block = TransitionBlock | VaporComponentInstance | Block[] +export type Block = + | Node + | VaporFragment + | DynamicFragment + | VaporComponentInstance + | Block[] export type BlockFn = (...args: any[]) => Block export function isBlock(val: NonNullable): val is Block { diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index 6b1580a9221..131154e2b6c 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -11,6 +11,7 @@ import { checkTransitionMode, currentInstance, getComponentName, + isAsyncWrapper, isTemplateNode, leaveCbKey, queuePostFlushCb, @@ -92,7 +93,7 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate( if (child) { // replace existing transition hooks child.$transition!.props = resolvedProps - applyTransitionHooks(child, child.$transition!) + applyTransitionHooks(child, child.$transition!, undefined, true) } } } else { @@ -141,7 +142,7 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate( ) const getTransitionHooksContext = ( - key: String, + key: string, props: TransitionProps, state: TransitionState, instance: GenericComponentInstance, @@ -210,6 +211,7 @@ export function applyTransitionHooks( block: Block, hooks: VaporTransitionHooks, fallthroughAttrs: boolean = true, + isResolved: boolean = false, ): VaporTransitionHooks { // filter out comment nodes if (isArray(block)) { @@ -222,7 +224,9 @@ export function applyTransitionHooks( } const isFrag = isFragment(block) - const child = findTransitionBlock(block, isFrag) + const child = isResolved + ? (block as TransitionBlock) + : findTransitionBlock(block, isFrag) if (!child) { // set transition hooks on fragment for reusing during it's updating if (isFrag) setTransitionHooksOnFragment(block, hooks) @@ -238,7 +242,7 @@ export function applyTransitionHooks( hooks => (resolvedHooks = hooks as VaporTransitionHooks), ) resolvedHooks.delayedLeave = delayedLeave - setTransitionHooks(child, resolvedHooks) + child.$transition = resolvedHooks if (isFrag) setTransitionHooksOnFragment(block, resolvedHooks) // fallthrough attrs @@ -266,7 +270,7 @@ export function applyTransitionLeaveHooks( state, instance, ) - setTransitionHooks(leavingBlock, leavingHooks) + leavingBlock.$transition = leavingHooks const { mode } = props if (mode === 'out-in') { @@ -300,25 +304,25 @@ export function applyTransitionLeaveHooks( } } -const transitionBlockCache = new WeakMap() export function findTransitionBlock( block: Block, inFragment: boolean = false, ): TransitionBlock | undefined { - if (transitionBlockCache.has(block)) { - return transitionBlockCache.get(block) - } - let child: TransitionBlock | undefined if (block instanceof Node) { // transition can only be applied on Element child if (block instanceof Element) child = block } else if (isVaporComponent(block)) { - // stop searching if encountering nested Transition component - if (getComponentName(block.type) === displayName) return undefined - child = findTransitionBlock(block.block, inFragment) - // use component id as key - if (child && child.$key === undefined) child.$key = block.uid + // should save hooks on unresolved async wrapper, so that it can be applied after resolved + if (isAsyncWrapper(block) && !block.type.__asyncResolved) { + child = block + } else { + // stop searching if encountering nested Transition component + if (getComponentName(block.type) === displayName) return undefined + child = findTransitionBlock(block.block, inFragment) + // use component id as key + if (child && child.$key === undefined) child.$key = block.uid + } } else if (isArray(block)) { let hasFound = false for (const c of block) { @@ -369,7 +373,7 @@ export function setTransitionHooksOnFragment( } export function setTransitionHooks( - block: TransitionBlock | VaporComponentInstance, + block: TransitionBlock, hooks: VaporTransitionHooks, ): void { if (isVaporComponent(block)) {