--- /dev/null
+import path from 'node:path'
+import {
+ E2E_TIMEOUT,
+ setupPuppeteer,
+} from '../../../packages/vue/__tests__/e2e/e2eUtils'
+import connect from 'connect'
+import sirv from 'sirv'
+import { expect } from 'vitest'
+const { page, nextFrame, timeout, html, transitionStart } = setupPuppeteer()
+
+const duration = process.env.CI ? 200 : 50
+const buffer = process.env.CI ? 50 : 20
+const transitionFinish = (time = duration) => timeout(time + buffer)
+
+describe('vapor transition-group', () => {
+ let server: any
+ const port = '8196'
+ beforeAll(() => {
+ server = connect()
+ .use(sirv(path.resolve(import.meta.dirname, '../dist')))
+ .listen(port)
+ process.on('SIGTERM', () => server && server.close())
+ })
+
+ afterAll(() => {
+ server.close()
+ })
+
+ beforeEach(async () => {
+ const baseUrl = `http://localhost:${port}/transition-group/`
+ await page().goto(baseUrl)
+ await page().waitForSelector('#app')
+ })
+
+ test(
+ 'enter',
+ async () => {
+ const btnSelector = '.enter > button'
+ const containerSelector = '.enter > 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">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-from test-enter-active">d</div>` +
+ `<div class="test test-enter-from test-enter-active">e</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-active test-enter-to">d</div>` +
+ `<div class="test test-enter-active test-enter-to">e</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test">d</div>` +
+ `<div class="test">e</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'leave',
+ async () => {
+ const btnSelector = '.leave > button'
+ const containerSelector = '.leave > 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 test-leave-from test-leave-active">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test test-leave-from test-leave-active">c</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-leave-active test-leave-to">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test test-leave-active test-leave-to">c</div>`,
+ )
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(`<div class="test">b</div>`)
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'enter + leave',
+ async () => {
+ const btnSelector = '.enter-leave > button'
+ const containerSelector = '.enter-leave > 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 test-leave-from test-leave-active">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-from test-enter-active">d</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-leave-active test-leave-to">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-active test-enter-to">d</div>`,
+ )
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test">d</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'appear',
+ async () => {
+ const btnSelector = '.appear > button'
+ const containerSelector = '.appear > div'
+
+ expect(await html('.appear')).toBe(`<button>appear button</button>`)
+
+ await page().evaluate(() => {
+ return (window as any).setAppear()
+ })
+
+ // appear
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-appear-from test-appear-active">a</div>` +
+ `<div class="test test-appear-from test-appear-active">b</div>` +
+ `<div class="test test-appear-from test-appear-active">c</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-appear-active test-appear-to">a</div>` +
+ `<div class="test test-appear-active test-appear-to">b</div>` +
+ `<div class="test test-appear-active test-appear-to">c</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-from test-enter-active">d</div>` +
+ `<div class="test test-enter-from test-enter-active">e</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-active test-enter-to">d</div>` +
+ `<div class="test test-enter-active test-enter-to">e</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test">d</div>` +
+ `<div class="test">e</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'move',
+ async () => {
+ const btnSelector = '.move > button'
+ const containerSelector = '.move > 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('dynamic name', async () => {
+ const btnSelector = '.dynamic-name button.toggleBtn'
+ const btnChangeName = '.dynamic-name button.changeNameBtn'
+ const containerSelector = '.dynamic-name > div'
+
+ expect(await html(containerSelector)).toBe(
+ `<div>a</div>` + `<div>b</div>` + `<div>c</div>`,
+ )
+
+ // invalid name
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div>b</div>` + `<div>c</div>` + `<div>a</div>`)
+
+ // change name
+ expect(
+ (await transitionStart(btnChangeName, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="group-move" style="">a</div>` +
+ `<div class="group-move" style="">b</div>` +
+ `<div class="group-move" style="">c</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="" style="">a</div>` +
+ `<div class="" style="">b</div>` +
+ `<div class="" style="">c</div>`,
+ )
+ })
+
+ test('events', async () => {
+ const btnSelector = '.events > button'
+ const containerSelector = '.events > div'
+
+ expect(await html('.events')).toBe(`<button>events button</button>`)
+
+ await page().evaluate(() => {
+ return (window as any).setAppear()
+ })
+
+ // appear
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-appear-from test-appear-active">a</div>` +
+ `<div class="test test-appear-from test-appear-active">b</div>` +
+ `<div class="test test-appear-from test-appear-active">c</div>`,
+ )
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-appear-active test-appear-to">a</div>` +
+ `<div class="test test-appear-active test-appear-to">b</div>` +
+ `<div class="test test-appear-active test-appear-to">c</div>`,
+ )
+
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls()
+ })
+ expect(calls).toContain('beforeAppear')
+ expect(calls).toContain('onAppear')
+ expect(calls).not.toContain('afterAppear')
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ expect(
+ await page().evaluate(() => {
+ return (window as any).getCalls()
+ }),
+ ).toContain('afterAppear')
+
+ // enter + leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test test-leave-from test-leave-active">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-from test-enter-active">d</div>`,
+ )
+
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls()
+ })
+ expect(calls).toContain('beforeLeave')
+ expect(calls).toContain('onLeave')
+ expect(calls).not.toContain('afterLeave')
+ expect(calls).toContain('beforeEnter')
+ expect(calls).toContain('onEnter')
+ expect(calls).not.toContain('afterEnter')
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-leave-active test-leave-to">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-active test-enter-to">d</div>`,
+ )
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls()
+ })
+ expect(calls).not.toContain('afterLeave')
+ expect(calls).not.toContain('afterEnter')
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test">d</div>`,
+ )
+
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls()
+ })
+ expect(calls).toContain('afterLeave')
+ expect(calls).toContain('afterEnter')
+ })
+
+ test('interop: render vdom component', async () => {
+ const btnSelector = '.interop > button'
+ const containerSelector = '.interop > div'
+
+ expect(await html(containerSelector)).toBe(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test-leave-from test-leave-active"><div>a</div></div>` +
+ `<div class="test-move" style=""><div>b</div></div>` +
+ `<div class="test-move" style=""><div>c</div></div>` +
+ `<div class="test-enter-from test-enter-active"><div>d</div></div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test-leave-active test-leave-to"><div>a</div></div>` +
+ `<div class="test-move" style=""><div>b</div></div>` +
+ `<div class="test-move" style=""><div>c</div></div>` +
+ `<div class="test-enter-active test-enter-to"><div>d</div></div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="" style=""><div>b</div></div>` +
+ `<div class="" style=""><div>c</div></div>` +
+ `<div class=""><div>d</div></div>`,
+ )
+ })
+})
--- /dev/null
+import path from 'node:path'
+import {
+ E2E_TIMEOUT,
+ setupPuppeteer,
+} from '../../../packages/vue/__tests__/e2e/e2eUtils'
+import connect from 'connect'
+import sirv from 'sirv'
+import { nextTick } from 'vue'
+const {
+ page,
+ classList,
+ text,
+ nextFrame,
+ timeout,
+ isVisible,
+ count,
+ html,
+ transitionStart,
+ waitForElement,
+ click,
+} = setupPuppeteer()
+
+const duration = process.env.CI ? 200 : 50
+const buffer = process.env.CI ? 50 : 20
+const transitionFinish = (time = duration) => timeout(time + buffer)
+
+describe('vapor transition', () => {
+ let server: any
+ const port = '8195'
+ beforeAll(() => {
+ server = connect()
+ .use(sirv(path.resolve(import.meta.dirname, '../dist')))
+ .listen(port)
+ process.on('SIGTERM', () => server && server.close())
+ })
+
+ afterAll(() => {
+ server.close()
+ })
+
+ beforeEach(async () => {
+ const baseUrl = `http://localhost:${port}/transition/`
+ await page().goto(baseUrl)
+ await page().waitForSelector('#app')
+ })
+
+ describe('transition with v-if', () => {
+ test(
+ 'basic transition',
+ async () => {
+ const btnSelector = '.if-basic > button'
+ const containerSelector = '.if-basic > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">content</div>`,
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'v-leave-from', 'v-leave-active'])
+
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'v-leave-active',
+ 'v-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'v-enter-from', 'v-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'v-enter-active',
+ 'v-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'named transition',
+ async () => {
+ const btnSelector = '.if-named > button'
+ const containerSelector = '.if-named > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'custom transition classes',
+ async () => {
+ const btnSelector = '.if-custom-classes > button'
+ const containerSelector = '.if-custom-classes > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'bye-from', 'bye-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'bye-active',
+ 'bye-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'hello-from', 'hello-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'hello-active',
+ 'hello-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition with dynamic name',
+ async () => {
+ const btnSelector = '.if-dynamic-name > button.toggle'
+ const btnChangeNameSelector = '.if-dynamic-name > button.change'
+ const containerSelector = '.if-dynamic-name > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ await click(btnChangeNameSelector)
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'changed-enter-from', 'changed-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'changed-enter-active',
+ 'changed-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition events without appear',
+ async () => {
+ const btnSelector = '.if-events-without-appear > button'
+ const containerSelector = '.if-events-without-appear > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('withoutAppear')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+
+ expect(
+ await page().evaluate(() => {
+ return (window as any).getCalls('withoutAppear')
+ }),
+ ).not.contain('afterLeave')
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+ expect(
+ await page().evaluate(() => {
+ return (window as any).getCalls('withoutAppear')
+ }),
+ ).toStrictEqual(['beforeLeave', 'onLeave', 'afterLeave'])
+
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('withoutAppear')
+ })
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withoutAppear')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ expect(
+ await page().evaluate(() => {
+ return (window as any).getCalls('withoutAppear')
+ }),
+ ).not.contain('afterEnter')
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ expect(
+ await page().evaluate(() => {
+ return (window as any).getCalls('withoutAppear')
+ }),
+ ).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'events with arguments',
+ async () => {
+ const btnSelector = '.if-events-with-args > button'
+ const containerSelector = '.if-events-with-args > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+
+ // leave
+ await click(btnSelector)
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('withArgs')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave'])
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'before-leave',
+ 'leave',
+ ])
+
+ await timeout(200 + buffer)
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withArgs')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave', 'afterLeave'])
+ expect(await html(containerSelector)).toBe('')
+
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('withArgs')
+ })
+
+ // enter
+ await click(btnSelector)
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withArgs')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'before-enter',
+ 'enter',
+ ])
+
+ await timeout(200 + buffer)
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withArgs')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+ expect(await html(containerSelector)).toBe(
+ '<div class="test before-enter enter after-enter">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'onEnterCancelled',
+ async () => {
+ const btnSelector = '.if-enter-cancelled > button'
+ const containerSelector = '.if-enter-cancelled > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+
+ // cancel (leave)
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('enterCancel')
+ })
+ expect(calls).toStrictEqual(['enterCancelled'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition on appear',
+ async () => {
+ const btnSelector = '.if-appear > button'
+ const containerSelector = '.if-appear > div'
+ const childSelector = `${containerSelector} > div`
+
+ // appear
+ expect(await classList(childSelector)).contains('test-appear-active')
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition events with appear',
+ async () => {
+ const btnSelector = '.if-events-with-appear > button'
+ const containerSelector = '.if-events-with-appear > div'
+ const childSelector = `${containerSelector} > div`
+ // appear
+ expect(await classList(childSelector)).contains('test-appear-active')
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).toStrictEqual(['beforeAppear', 'onAppear'])
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).toStrictEqual(['beforeAppear', 'onAppear', 'afterAppear'])
+
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('withAppear')
+ })
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave'])
+
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).not.contain('afterLeave')
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave', 'afterLeave'])
+
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('withAppear')
+ })
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).not.contain('afterEnter')
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('withAppear')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+ },
+ E2E_TIMEOUT,
+ )
+ test(
+ 'css: false',
+ async () => {
+ const btnSelector = '.if-css-false > button'
+ const containerSelector = '.if-css-false > div'
+ const childSelector = `${containerSelector} > div`
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+
+ // leave
+ await click(btnSelector)
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('cssFalse')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave', 'afterLeave'])
+ expect(await html(containerSelector)).toBe('')
+
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('cssFalse')
+ })
+
+ // enter
+ await transitionStart(btnSelector, childSelector)
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('cssFalse')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'no transition detected',
+ async () => {
+ const btnSelector = '.if-no-trans > button'
+ const containerSelector = '.if-no-trans > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe('<div>content</div>')
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['noop-leave-from', 'noop-leave-active'])
+ await nextFrame()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['noop-enter-from', 'noop-enter-active'])
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ '<div class="">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'animations',
+ async () => {
+ const btnSelector = '.if-ani > button'
+ const containerSelector = '.if-ani > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe('<div>content</div>')
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test-anim-leave-from', 'test-anim-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test-anim-leave-active',
+ 'test-anim-leave-to',
+ ])
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test-anim-enter-from', 'test-anim-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test-anim-enter-active',
+ 'test-anim-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'explicit transition type',
+ async () => {
+ const btnSelector = '.if-ani-explicit-type > button'
+ const containerSelector = '.if-ani-explicit-type > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe('<div>content</div>')
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual([
+ 'test-anim-long-leave-from',
+ 'test-anim-long-leave-active',
+ ])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test-anim-long-leave-active',
+ 'test-anim-long-leave-to',
+ ])
+
+ if (!process.env.CI) {
+ await new Promise(r => {
+ setTimeout(r, duration - buffer)
+ })
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test-anim-long-leave-active',
+ 'test-anim-long-leave-to',
+ ])
+ }
+
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual([
+ 'test-anim-long-enter-from',
+ 'test-anim-long-enter-active',
+ ])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test-anim-long-enter-active',
+ 'test-anim-long-enter-to',
+ ])
+
+ if (!process.env.CI) {
+ await new Promise(r => {
+ setTimeout(r, duration - buffer)
+ })
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test-anim-long-enter-active',
+ 'test-anim-long-enter-to',
+ ])
+ }
+
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe(
+ '<div class="">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test.todo('transition on SVG elements', async () => {}, E2E_TIMEOUT)
+
+ test(
+ 'custom transition higher-order component',
+ async () => {
+ const btnSelector = '.if-high-order > button'
+ const containerSelector = '.if-high-order > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition on child components with empty root node',
+ async () => {
+ const btnSelector = '.if-empty-root > button.toggle'
+ const btnChangeSelector = '.if-empty-root > button.change'
+ const containerSelector = '.if-empty-root > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe('')
+
+ // change view -> 'two'
+ await click(btnChangeSelector)
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">two</div>',
+ )
+
+ // change view -> 'one'
+ await click(btnChangeSelector)
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition with v-if at component root-level',
+ async () => {
+ const btnSelector = '.if-at-component-root-level > button.toggle'
+ const btnChangeSelector = '.if-at-component-root-level > button.change'
+ const containerSelector = '.if-at-component-root-level > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe('')
+
+ // change view -> 'two'
+ await click(btnChangeSelector)
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">two</div>',
+ )
+
+ // change view -> 'one'
+ await click(btnChangeSelector)
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'wrapping transition + fallthrough attrs',
+ async () => {
+ const btnSelector = '.if-fallthrough-attr > button'
+ const containerSelector = '.if-fallthrough-attr > div'
+
+ expect(await html(containerSelector)).toBe('<div foo="1">content</div>')
+
+ await click(btnSelector)
+ // toggle again before leave finishes
+ await nextTick()
+ await click(btnSelector)
+
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe(
+ '<div foo="1" class="">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition + fallthrough attrs (in-out mode)',
+ async () => {
+ const btnSelector = '.if-fallthrough-attr-in-out > button'
+ const containerSelector = '.if-fallthrough-attr-in-out > div'
+
+ expect(await html(containerSelector)).toBe('<div foo="1">one</div>')
+
+ // toggle
+ await click(btnSelector)
+ await nextTick()
+ await transitionFinish(duration * 3)
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('ifInOut')
+ })
+ expect(calls).toStrictEqual([
+ 'beforeEnter',
+ 'onEnter',
+ 'afterEnter',
+ 'beforeLeave',
+ 'onLeave',
+ 'afterLeave',
+ ])
+
+ expect(await html(containerSelector)).toBe(
+ '<div foo="1" class="">two</div>',
+ )
+
+ // clear calls
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('ifInOut')
+ })
+
+ // toggle back
+ await click(btnSelector)
+ await nextTick()
+ await transitionFinish(duration * 3)
+
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('ifInOut')
+ })
+ expect(calls).toStrictEqual([
+ 'beforeEnter',
+ 'onEnter',
+ 'afterEnter',
+ 'beforeLeave',
+ 'onLeave',
+ 'afterLeave',
+ ])
+
+ expect(await html(containerSelector)).toBe(
+ '<div foo="1" class="">one</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+ })
+
+ describe.todo('transition with KeepAlive', () => {})
+ describe.todo('transition with Suspense', () => {})
+ describe.todo('transition with Teleport', () => {})
+
+ describe('transition with v-show', () => {
+ test(
+ 'named transition with v-show',
+ async () => {
+ const btnSelector = '.show-named > button'
+ const containerSelector = '.show-named > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ expect(await isVisible(childSelector)).toBe(true)
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await isVisible(childSelector)).toBe(false)
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test" style="">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition events with v-show',
+ async () => {
+ const btnSelector = '.show-events > button'
+ const containerSelector = '.show-events > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('show')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('show')
+ })
+ expect(calls).not.contain('afterLeave')
+ await transitionFinish()
+ expect(await isVisible(childSelector)).toBe(false)
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('show')
+ })
+ expect(calls).toStrictEqual(['beforeLeave', 'onLeave', 'afterLeave'])
+
+ // clear calls
+ await page().evaluate(() => {
+ ;(window as any).resetCalls('show')
+ })
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('show')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test" style="">content</div>',
+ )
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('show')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'onLeaveCancelled (v-show only)',
+ async () => {
+ const btnSelector = '.show-leave-cancelled > button'
+ const containerSelector = '.show-leave-cancelled > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+
+ // cancel (enter)
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('showLeaveCancel')
+ })
+ expect(calls).toStrictEqual(['leaveCancelled'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await isVisible(childSelector)).toBe(true)
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition on appear with v-show',
+ async () => {
+ const btnSelector = '.show-appear > button'
+ const containerSelector = '.show-appear > div'
+ const childSelector = `${containerSelector} > div`
+
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('showAppear')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
+
+ // appear
+ expect(await classList(childSelector)).contains('test-appear-active')
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('showAppear')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await isVisible(childSelector)).toBe(false)
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test" style="">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'transition events should not call onEnter with v-show false',
+ async () => {
+ const btnSelector = '.show-appear-not-enter > button'
+ const containerSelector = '.show-appear-not-enter > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await isVisible(childSelector)).toBe(false)
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls('notEnter')
+ })
+ expect(calls).toStrictEqual([])
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('notEnter')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('notEnter')
+ })
+ expect(calls).not.contain('afterEnter')
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test" style="">content</div>',
+ )
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls('notEnter')
+ })
+ expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
+ },
+ E2E_TIMEOUT,
+ )
+ })
+
+ describe('explicit durations', () => {
+ test(
+ 'single value',
+ async () => {
+ const btnSelector = '.duration-single-value > button'
+ const containerSelector = '.duration-single-value > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'enter with explicit durations',
+ async () => {
+ const btnSelector = '.duration-enter > button'
+ const containerSelector = '.duration-enter > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'leave with explicit durations',
+ async () => {
+ const btnSelector = '.duration-leave > button'
+ const containerSelector = '.duration-leave > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'separate enter and leave',
+ async () => {
+ const btnSelector = '.duration-enter-leave > button'
+ const containerSelector = '.duration-enter-leave > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+
+ // leave
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-leave-active',
+ 'test-leave-to',
+ ])
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe('')
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, childSelector)).classNames,
+ ).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
+ await nextFrame()
+ expect(await classList(childSelector)).toStrictEqual([
+ 'test',
+ 'test-enter-active',
+ 'test-enter-to',
+ ])
+ await transitionFinish(duration * 4)
+ expect(await html(containerSelector)).toBe(
+ '<div class="test">content</div>',
+ )
+ },
+ E2E_TIMEOUT,
+ )
+ })
+
+ test(
+ 'should work with keyed element',
+ async () => {
+ const btnSelector = '.keyed > button'
+ const containerSelector = '.keyed > h1'
+
+ expect(await text(containerSelector)).toContain('0')
+
+ // change key
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).classNames,
+ ).toStrictEqual(['v-leave-from', 'v-leave-active'])
+
+ await nextFrame()
+ expect(await classList(containerSelector)).toStrictEqual([
+ 'v-leave-active',
+ 'v-leave-to',
+ ])
+
+ await transitionFinish()
+ await nextFrame()
+ expect(await text(containerSelector)).toContain('1')
+
+ // change key again
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).classNames,
+ ).toStrictEqual(['v-leave-from', 'v-leave-active'])
+
+ await nextFrame()
+ expect(await classList(containerSelector)).toStrictEqual([
+ 'v-leave-active',
+ 'v-leave-to',
+ ])
+
+ await transitionFinish()
+ await nextFrame()
+ expect(await text(containerSelector)).toContain('2')
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'should work with out-in mode',
+ async () => {
+ const btnSelector = '.out-in > button'
+ const containerSelector = '.out-in > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(`<div>vapor compB</div>`)
+
+ // compB -> compA
+ // compB leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div class="fade-leave-from fade-leave-active">vapor compB</div>`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vapor compB</div>`,
+ )
+
+ // compA enter
+ await waitForElement(childSelector, 'vapor compA', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-enter-active fade-enter-to">vapor compA</div>`,
+ )
+
+ await transitionFinish()
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compA</div>`,
+ )
+
+ // compA -> compB
+ // compA leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div class="fade-leave-from fade-leave-active">vapor compA</div>`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vapor compA</div>`,
+ )
+
+ // compB enter
+ await waitForElement(childSelector, 'vapor compB', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-enter-active fade-enter-to">vapor compB</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compB</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'should work with in-out mode',
+ async () => {
+ const btnSelector = '.in-out > button'
+ const containerSelector = '.in-out > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(`<div>vapor compB</div>`)
+
+ // compA enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div>vapor compB</div><div class="fade-enter-from fade-enter-active">vapor compA</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div>vapor compB</div><div class="fade-enter-active fade-enter-to">vapor compA</div>`,
+ )
+
+ // compB leave
+ await waitForElement(childSelector, 'vapor compB', [
+ 'fade-leave-from',
+ 'fade-leave-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vapor compB</div><div class="">vapor compA</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compA</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ // tests for using vdom component in createVaporApp + vaporInteropPlugin
+ describe('interop', () => {
+ test(
+ 'render vdom component',
+ async () => {
+ const btnSelector = '.vdom > button'
+ const containerSelector = '.vdom > div'
+
+ expect(await html(containerSelector)).toBe(`<div>vdom comp</div>`)
+
+ // comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div class="v-leave-from v-leave-active">vdom comp</div>`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="v-leave-active v-leave-to">vdom comp</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(``)
+
+ // comp enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div class="v-enter-from v-enter-active">vdom comp</div>`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="v-enter-active v-enter-to">vdom comp</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vdom comp</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'switch between vdom/vapor component (out-in mode)',
+ async () => {
+ const btnSelector = '.vdom-vapor-out-in > button'
+ const containerSelector = '.vdom-vapor-out-in > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(`<div>vdom comp</div>`)
+
+ // switch to vapor comp
+ // vdom comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div class="fade-leave-from fade-leave-active">vdom comp</div>`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vdom comp</div>`,
+ )
+
+ // vapor comp enter
+ await waitForElement(childSelector, 'vapor compA', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-enter-active fade-enter-to">vapor compA</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compA</div>`,
+ )
+
+ // switch to vdom comp
+ // vapor comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="fade-leave-from fade-leave-active">vapor compA</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vapor compA</div>`,
+ )
+
+ // vdom comp enter
+ await waitForElement(childSelector, 'vdom comp', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-enter-active fade-enter-to">vdom comp</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vdom comp</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'switch between vdom/vapor component (in-out mode)',
+ async () => {
+ const btnSelector = '.vdom-vapor-in-out > button'
+ const containerSelector = '.vdom-vapor-in-out > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(`<div>vapor compA</div>`)
+
+ // switch to vdom comp
+ // vdom comp enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div>vapor compA</div><div class="fade-enter-from fade-enter-active">vdom comp</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div>vapor compA</div><div class="fade-enter-active fade-enter-to">vdom comp</div>`,
+ )
+
+ // vapor comp leave
+ await waitForElement(childSelector, 'vapor compA', [
+ 'fade-leave-from',
+ 'fade-leave-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vapor compA</div><div class="">vdom comp</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vdom comp</div>`,
+ )
+
+ // switch to vapor comp
+ // vapor comp enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="">vdom comp</div><div class="fade-enter-from fade-enter-active">vapor compA</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vdom comp</div><div class="fade-enter-active fade-enter-to">vapor compA</div>`,
+ )
+
+ // vdom comp leave
+ await waitForElement(childSelector, 'vdom comp', [
+ 'fade-leave-from',
+ 'fade-leave-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vdom comp</div><div class="">vapor compA</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compA</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+ })
+})
} from '../../../packages/vue/__tests__/e2e/e2eUtils'
import connect from 'connect'
import sirv from 'sirv'
+const {
+ page,
+ click,
+ text,
+ enterValue,
+ html,
+ transitionStart,
+ waitForElement,
+ nextFrame,
+ timeout,
+} = setupPuppeteer()
-describe('vdom / vapor interop', () => {
- const { page, click, text, enterValue } = setupPuppeteer()
+const duration = process.env.CI ? 200 : 50
+const buffer = process.env.CI ? 50 : 20
+const transitionFinish = (time = duration) => timeout(time + buffer)
+describe('vdom / vapor interop', () => {
let server: any
const port = '8193'
beforeAll(() => {
server.close()
})
+ beforeEach(async () => {
+ const baseUrl = `http://localhost:${port}/interop/`
+ await page().goto(baseUrl)
+ await page().waitForSelector('#app')
+ })
+
test(
'should work',
async () => {
- const baseUrl = `http://localhost:${port}/interop/`
- await page().goto(baseUrl)
-
expect(await text('.vapor > h2')).toContain('Vapor component in VDOM')
expect(await text('.vapor-prop')).toContain('hello')
},
E2E_TIMEOUT,
)
+
+ describe('vdom transition', () => {
+ test(
+ 'render vapor component',
+ async () => {
+ const btnSelector = '.trans-vapor > button'
+ const containerSelector = '.trans-vapor > div'
+
+ expect(await html(containerSelector)).toBe(`<div>vapor compA</div>`)
+
+ // comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="v-leave-from v-leave-active">vapor compA</div><!---->`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="v-leave-active v-leave-to">vapor compA</div><!---->`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(`<!---->`)
+
+ // comp enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div class="v-enter-from v-enter-active">vapor compA</div>`)
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="v-enter-active v-enter-to">vapor compA</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compA</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'switch between vdom/vapor component (out-in mode)',
+ async () => {
+ const btnSelector = '.trans-vdom-vapor-out-in > button'
+ const containerSelector = '.trans-vdom-vapor-out-in > div'
+ const childSelector = `${containerSelector} > div`
+
+ expect(await html(containerSelector)).toBe(`<div>vdom comp</div>`)
+
+ // switch to vapor comp
+ // vdom comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="fade-leave-from fade-leave-active">vdom comp</div><!---->`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vdom comp</div><!---->`,
+ )
+
+ // vapor comp enter
+ await waitForElement(childSelector, 'vapor compA', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-enter-active fade-enter-to">vapor compA</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vapor compA</div>`,
+ )
+
+ // switch to vdom comp
+ // vapor comp leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="fade-leave-from fade-leave-active">vapor compA</div><!---->`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-leave-active fade-leave-to">vapor compA</div><!---->`,
+ )
+
+ // vdom comp enter
+ await waitForElement(childSelector, 'vdom comp', [
+ 'fade-enter-from',
+ 'fade-enter-active',
+ ])
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="fade-enter-active fade-enter-to">vdom comp</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="">vdom comp</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+ })
+
+ describe('vdom transition-group', () => {
+ test(
+ 'render vapor component',
+ async () => {
+ const btnSelector = '.trans-group-vapor > button'
+ const containerSelector = '.trans-group-vapor > div'
+
+ expect(await html(containerSelector)).toBe(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>`,
+ )
+
+ // insert
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>` +
+ `<div class="test-enter-from test-enter-active"><div>d</div></div>` +
+ `<div class="test-enter-from test-enter-active"><div>e</div></div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>` +
+ `<div class="test-enter-active test-enter-to"><div>d</div></div>` +
+ `<div class="test-enter-active test-enter-to"><div>e</div></div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>` +
+ `<div class=""><div>d</div></div>` +
+ `<div class=""><div>e</div></div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+ })
})
<a href="/interop/">VDOM / Vapor interop</a>
<a href="/todomvc/">Vapor TodoMVC</a>
+<a href="/transition/">Vapor Transition</a>
+<a href="/transition-group/">Vapor TransitionGroup</a>
+
+<style>
+ a {
+ display: block;
+ margin: 10px;
+ }
+</style>
<script setup lang="ts">
-import { ref } from 'vue'
+import { ref, shallowRef } from 'vue'
import VaporComp from './VaporComp.vue'
+import VaporCompA from '../transition/components/VaporCompA.vue'
+import VdomComp from '../transition/components/VdomComp.vue'
+import VaporSlot from '../transition/components/VaporSlot.vue'
const msg = ref('hello')
const passSlot = ref(true)
+
+const toggleVapor = ref(true)
+const interopComponent = shallowRef(VdomComp)
+function toggleInteropComponent() {
+ interopComponent.value =
+ interopComponent.value === VaporCompA ? VdomComp : VaporCompA
+}
+
+const items = ref(['a', 'b', 'c'])
+const enterClick = () => items.value.push('d', 'e')
</script>
<template>
<template #test v-if="passSlot">A test slot</template>
</VaporComp>
+
+ <!-- transition interop -->
+ <div>
+ <div class="trans-vapor">
+ <button @click="toggleVapor = !toggleVapor">
+ toggle vapor component
+ </button>
+ <div>
+ <Transition>
+ <VaporCompA v-if="toggleVapor" />
+ </Transition>
+ </div>
+ </div>
+ <div class="trans-vdom-vapor-out-in">
+ <button @click="toggleInteropComponent">
+ switch between vdom/vapor component out-in mode
+ </button>
+ <div>
+ <Transition name="fade" mode="out-in">
+ <component :is="interopComponent"></component>
+ </Transition>
+ </div>
+ </div>
+ </div>
+ <!-- transition-group interop -->
+ <div>
+ <div class="trans-group-vapor">
+ <button @click="enterClick">insert items</button>
+ <div>
+ <transition-group name="test">
+ <VaporSlot v-for="item in items" :key="item">
+ <div>{{ item }}</div>
+ </VaporSlot>
+ </transition-group>
+ </div>
+ </div>
+ </div>
</template>
import { createApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'
+import '../transition/style.css'
createApp(App).use(vaporInteropPlugin).mount('#app')
--- /dev/null
+<script setup vapor>
+import { ref } from 'vue'
+import VdomComp from './components/VdomComp.vue'
+
+const items = ref(['a', 'b', 'c'])
+const enterClick = () => items.value.push('d', 'e')
+const leaveClick = () => (items.value = ['b'])
+const enterLeaveClick = () => (items.value = ['b', 'c', 'd'])
+const appear = ref(false)
+window.setAppear = () => (appear.value = true)
+const moveClick = () => (items.value = ['d', 'b', 'a'])
+
+const name = ref('invalid')
+const dynamicClick = () => (items.value = ['b', 'c', 'a'])
+const changeName = () => {
+ name.value = 'group'
+ items.value = ['a', 'b', 'c']
+}
+
+let calls = []
+window.getCalls = () => {
+ const ret = calls.slice()
+ calls = []
+ return ret
+}
+const eventsClick = () => (items.value = ['b', 'c', 'd'])
+
+const interopClick = () => (items.value = ['b', 'c', 'd'])
+</script>
+
+<template>
+ <div class="transition-group-container">
+ <div class="enter">
+ <button @click="enterClick">enter button</button>
+ <div>
+ <transition-group name="test">
+ <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="leave">
+ <button @click="leaveClick">leave button</button>
+ <div>
+ <transition-group name="test">
+ <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="enter-leave">
+ <button @click="enterLeaveClick">enter-leave button</button>
+ <div>
+ <transition-group name="test">
+ <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="appear">
+ <button @click="enterClick">appear button</button>
+ <div v-if="appear">
+ <transition-group
+ appear
+ appear-from-class="test-appear-from"
+ appear-to-class="test-appear-to"
+ appear-active-class="test-appear-active"
+ name="test"
+ >
+ <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="move">
+ <button @click="moveClick">move button</button>
+ <div>
+ <transition-group name="group">
+ <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="dynamic-name">
+ <button class="toggleBtn" @click="dynamicClick">dynamic button</button>
+ <button class="changeNameBtn" @click="changeName">change name</button>
+ <div>
+ <transition-group :name="name">
+ <div v-for="item in items" :key="item">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="events">
+ <button @click="eventsClick">events button</button>
+ <div v-if="appear">
+ <transition-group
+ name="test"
+ appear
+ appear-from-class="test-appear-from"
+ appear-to-class="test-appear-to"
+ appear-active-class="test-appear-active"
+ @beforeEnter="() => calls.push('beforeEnter')"
+ @enter="() => calls.push('onEnter')"
+ @afterEnter="() => calls.push('afterEnter')"
+ @beforeLeave="() => calls.push('beforeLeave')"
+ @leave="() => calls.push('onLeave')"
+ @afterLeave="() => calls.push('afterLeave')"
+ @beforeAppear="() => calls.push('beforeAppear')"
+ @appear="() => calls.push('onAppear')"
+ @afterAppear="() => calls.push('afterAppear')"
+ >
+ <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="interop">
+ <button @click="interopClick">interop button</button>
+ <div>
+ <transition-group name="test">
+ <VdomComp v-for="item in items" :key="item">
+ <div>{{ item }}</div>
+ </VdomComp>
+ </transition-group>
+ </div>
+ </div>
+ </div>
+</template>
+<style>
+.transition-group-container > div {
+ padding: 15px;
+ border: 1px solid #f7f7f7;
+ margin-top: 15px;
+}
+
+.test-move,
+.test-enter-active,
+.test-leave-active {
+ transition: all 50ms cubic-bezier(0.55, 0, 0.1, 1);
+}
+
+.test-enter-from,
+.test-leave-to {
+ opacity: 0;
+ transform: scaleY(0.01) translate(30px, 0);
+}
+
+.test-leave-active {
+ position: absolute;
+}
+</style>
--- /dev/null
+<script vapor>
+const msg = 'vapor comp'
+</script>
+
+<template>
+ <div>
+ <slot />
+ </div>
+</template>
--- /dev/null
+<script setup>
+const msg = 'vdom comp'
+</script>
+
+<template>
+ <div>
+ <slot />
+ </div>
+</template>
--- /dev/null
+<script type="module" src="./main.ts"></script>
+<div id="app"></div>
--- /dev/null
+import { createVaporApp, vaporInteropPlugin } from 'vue'
+import App from './App.vue'
+import '../../../packages/vue/__tests__/e2e/style.css'
+
+createVaporApp(App).use(vaporInteropPlugin).mount('#app')
--- /dev/null
+<script vapor>
+import {
+ createComponent,
+ defineVaporComponent,
+ ref,
+ shallowRef,
+ VaporTransition,
+ createIf,
+ template,
+} from 'vue'
+const show = ref(true)
+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
+
+let calls = {
+ basic: [],
+ withoutAppear: [],
+ withArgs: [],
+ enterCancel: [],
+ withAppear: [],
+ cssFalse: [],
+ ifInOut: [],
+
+ show: [],
+ showLeaveCancel: [],
+ showAppear: [],
+ notEnter: [],
+}
+window.getCalls = key => calls[key]
+window.resetCalls = key => (calls[key] = [])
+
+import VaporCompA from './components/VaporCompA.vue'
+import VaporCompB from './components/VaporCompB.vue'
+const activeComponent = shallowRef(VaporCompB)
+function toggleComponent() {
+ activeComponent.value =
+ activeComponent.value === VaporCompA ? VaporCompB : VaporCompA
+}
+
+const toggleVdom = ref(true)
+import VDomComp from './components/VdomComp.vue'
+
+const interopComponent = shallowRef(VDomComp)
+function toggleInteropComponent() {
+ interopComponent.value =
+ interopComponent.value === VaporCompA ? VDomComp : VaporCompA
+}
+
+const name = ref('test')
+const MyTransition = defineVaporComponent((props, { slots }) => {
+ return createComponent(VaporTransition, { name: () => 'test' }, slots)
+})
+
+const MyTransitionFallthroughAttr = defineVaporComponent((props, { slots }) => {
+ return createComponent(
+ VaporTransition,
+ { foo: () => 1, name: () => 'test' },
+ slots,
+ )
+})
+
+const One = defineVaporComponent({
+ setup() {
+ return createIf(
+ () => false,
+ () => template('<div>one</div>', true)(),
+ )
+ },
+})
+
+const Two = defineVaporComponent({
+ setup() {
+ return template('<div>two</div>', true)()
+ },
+})
+const view = shallowRef(One)
+function changeView() {
+ view.value = view.value === One ? Two : One
+}
+
+const SimpleOne = defineVaporComponent({
+ setup() {
+ return template('<div>one</div>', true)()
+ },
+})
+const viewInOut = shallowRef(SimpleOne)
+function changeViewInOut() {
+ viewInOut.value = viewInOut.value === SimpleOne ? Two : SimpleOne
+}
+</script>
+
+<template>
+ <div class="transition-container">
+ <!-- work with vif -->
+ <div class="if-basic">
+ <div>
+ <transition>
+ <div v-if="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">basic toggle</button>
+ </div>
+ <div class="if-named">
+ <div>
+ <transition name="test">
+ <div v-if="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="if-custom-classes">
+ <div>
+ <transition
+ enter-from-class="hello-from"
+ enter-active-class="hello-active"
+ enter-to-class="hello-to"
+ leave-from-class="bye-from"
+ leave-active-class="bye-active"
+ leave-to-class="bye-to"
+ >
+ <div v-if="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="if-dynamic-name">
+ <div>
+ <transition :name="name">
+ <div v-if="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button class="toggle" @click="toggle = !toggle">button</button>
+ <button class="change" @click="name = 'changed'">{{ name }}</button>
+ </div>
+ <div class="if-events-without-appear">
+ <div>
+ <transition
+ name="test"
+ @before-enter="() => calls.withoutAppear.push('beforeEnter')"
+ @enter="() => calls.withoutAppear.push('onEnter')"
+ @after-enter="() => calls.withoutAppear.push('afterEnter')"
+ @beforeLeave="() => calls.withoutAppear.push('beforeLeave')"
+ @leave="() => calls.withoutAppear.push('onLeave')"
+ @afterLeave="() => calls.withoutAppear.push('afterLeave')"
+ >
+ <div v-if="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="if-events-with-args">
+ <div>
+ <transition
+ :css="false"
+ name="test"
+ @before-enter="
+ el => {
+ calls.withArgs.push('beforeEnter')
+ el.classList.add('before-enter')
+ }
+ "
+ @enter="
+ (el, done) => {
+ calls.withArgs.push('onEnter')
+ el.classList.add('enter')
+ timeout(done, 200)
+ }
+ "
+ @after-enter="
+ el => {
+ calls.withArgs.push('afterEnter')
+ el.classList.add('after-enter')
+ }
+ "
+ @before-leave="
+ el => {
+ calls.withArgs.push('beforeLeave')
+ el.classList.add('before-leave')
+ }
+ "
+ @leave="
+ (el, done) => {
+ calls.withArgs.push('onLeave')
+ el.classList.add('leave')
+ timeout(done, 200)
+ }
+ "
+ @after-leave="
+ () => {
+ calls.withArgs.push('afterLeave')
+ }
+ "
+ >
+ <div v-if="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="if-enter-cancelled">
+ <div>
+ <transition
+ name="test"
+ @enter-cancelled="
+ () => {
+ calls.enterCancel.push('enterCancelled')
+ }
+ "
+ >
+ <div v-if="!toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">cancelled</button>
+ </div>
+ <div class="if-appear">
+ <div>
+ <transition
+ name="test"
+ appear
+ appear-from-class="test-appear-from"
+ appear-to-class="test-appear-to"
+ appear-active-class="test-appear-active"
+ >
+ <div v-if="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="if-events-with-appear">
+ <div>
+ <transition
+ name="test"
+ appear
+ appear-from-class="test-appear-from"
+ appear-to-class="test-appear-to"
+ appear-active-class="test-appear-active"
+ @beforeEnter="() => calls.withAppear.push('beforeEnter')"
+ @enter="() => calls.withAppear.push('onEnter')"
+ @afterEnter="() => calls.withAppear.push('afterEnter')"
+ @beforeLeave="() => calls.withAppear.push('beforeLeave')"
+ @leave="() => calls.withAppear.push('onLeave')"
+ @afterLeave="() => calls.withAppear.push('afterLeave')"
+ @beforeAppear="() => calls.withAppear.push('beforeAppear')"
+ @appear="() => calls.withAppear.push('onAppear')"
+ @afterAppear="() => calls.withAppear.push('afterAppear')"
+ >
+ <div v-if="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="if-css-false">
+ <div>
+ <transition
+ :css="false"
+ name="test"
+ @beforeEnter="() => calls.cssFalse.push('beforeEnter')"
+ @enter="() => calls.cssFalse.push('onEnter')"
+ @afterEnter="() => calls.cssFalse.push('afterEnter')"
+ @beforeLeave="() => calls.cssFalse.push('beforeLeave')"
+ @leave="() => calls.cssFalse.push('onLeave')"
+ @afterLeave="() => calls.cssFalse.push('afterLeave')"
+ >
+ <div v-if="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle"></button>
+ </div>
+ <div class="if-no-trans">
+ <div>
+ <transition name="noop">
+ <div v-if="toggle">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="if-ani">
+ <div>
+ <transition name="test-anim">
+ <div v-if="toggle">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="if-ani-explicit-type">
+ <div>
+ <transition name="test-anim-long" type="animation">
+ <div v-if="toggle">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="if-high-order">
+ <div>
+ <MyTransition>
+ <div v-if="toggle" class="test">content</div>
+ </MyTransition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="if-empty-root">
+ <div>
+ <transition name="test">
+ <component class="test" :is="view"></component>
+ </transition>
+ </div>
+ <button class="toggle" @click="toggle = !toggle">button</button>
+ <button class="change" @click="changeView">changeView button</button>
+ </div>
+ <div class="if-at-component-root-level">
+ <div>
+ <transition name="test" mode="out-in">
+ <component class="test" :is="view"></component>
+ </transition>
+ </div>
+ <button class="toggle" @click="toggle = !toggle">button</button>
+ <button class="change" @click="changeView">changeView button</button>
+ </div>
+ <div class="if-fallthrough-attr">
+ <div>
+ <MyTransitionFallthroughAttr>
+ <div v-if="toggle">content</div>
+ </MyTransitionFallthroughAttr>
+ </div>
+ <button @click="toggle = !toggle">button fallthrough</button>
+ </div>
+ <div class="if-fallthrough-attr-in-out">
+ <div>
+ <transition
+ foo="1"
+ name="test"
+ mode="in-out"
+ @beforeEnter="() => calls.ifInOut.push('beforeEnter')"
+ @enter="() => calls.ifInOut.push('onEnter')"
+ @afterEnter="() => calls.ifInOut.push('afterEnter')"
+ @beforeLeave="() => calls.ifInOut.push('beforeLeave')"
+ @leave="() => calls.ifInOut.push('onLeave')"
+ @afterLeave="() => calls.ifInOut.push('afterLeave')"
+ >
+ <component :is="viewInOut"></component>
+ </transition>
+ </div>
+ <button @click="changeViewInOut">button</button>
+ </div>
+ <!-- work with vif end -->
+
+ <!-- work with vshow -->
+ <div class="show-named">
+ <div>
+ <transition name="test">
+ <div v-show="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="show-events">
+ <div>
+ <transition
+ name="test"
+ @beforeEnter="() => calls.show.push('beforeEnter')"
+ @enter="() => calls.show.push('onEnter')"
+ @afterEnter="() => calls.show.push('afterEnter')"
+ @beforeLeave="() => calls.show.push('beforeLeave')"
+ @leave="() => calls.show.push('onLeave')"
+ @afterLeave="() => calls.show.push('afterLeave')"
+ >
+ <div v-show="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="show-leave-cancelled">
+ <div>
+ <transition
+ name="test"
+ @leave-cancelled="() => calls.showLeaveCancel.push('leaveCancelled')"
+ >
+ <div v-show="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">leave cancelled</button>
+ </div>
+ <div class="show-appear">
+ <div>
+ <transition
+ name="test"
+ appear
+ appear-from-class="test-appear-from"
+ appear-to-class="test-appear-to"
+ appear-active-class="test-appear-active"
+ @beforeEnter="() => calls.showAppear.push('beforeEnter')"
+ @enter="() => calls.showAppear.push('onEnter')"
+ @afterEnter="() => calls.showAppear.push('afterEnter')"
+ >
+ <div v-show="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="show-appear-not-enter">
+ <div>
+ <transition
+ name="test"
+ appear
+ @beforeEnter="() => calls.notEnter.push('beforeEnter')"
+ @enter="() => calls.notEnter.push('onEnter')"
+ @afterEnter="() => calls.notEnter.push('afterEnter')"
+ >
+ <div v-show="!toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <!-- work with vshow end -->
+
+ <!-- explicit durations -->
+ <div class="duration-single-value">
+ <div>
+ <transition name="test" :duration="duration * 2">
+ <div v-if="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="duration-enter">
+ <div>
+ <transition name="test" :duration="{ enter: duration * 2 }">
+ <div v-if="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="duration-leave">
+ <div>
+ <transition name="test" :duration="{ leave: duration * 2 }">
+ <div v-if="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <div class="duration-enter-leave">
+ <div>
+ <transition
+ name="test"
+ :duration="{ enter: duration * 4, leave: duration * 2 }"
+ >
+ <div v-if="toggle" class="test">content</div>
+ </transition>
+ </div>
+ <button @click="toggle = !toggle">button</button>
+ </div>
+ <!-- explicit durations end -->
+
+ <!-- keyed fragment -->
+ <div class="keyed">
+ <button @click="count++">inc</button>
+ <Transition>
+ <h1 style="position: absolute" :key="count">{{ count }}</h1>
+ </Transition>
+ </div>
+ <!-- keyed fragment end -->
+
+ <!-- mode -->
+ <div class="out-in">
+ <button @click="toggleComponent">toggle out-in</button>
+ <div>
+ <Transition name="fade" mode="out-in">
+ <component :is="activeComponent"></component>
+ </Transition>
+ </div>
+ </div>
+ <div class="in-out">
+ <button @click="toggleComponent">toggle in-out</button>
+ <div>
+ <Transition name="fade" mode="in-out">
+ <component :is="activeComponent"></component>
+ </Transition>
+ </div>
+ </div>
+ <!-- mode end -->
+
+ <!-- vdom interop -->
+ <div class="vdom">
+ <button @click="toggleVdom = !toggleVdom">toggle vdom component</button>
+ <div>
+ <Transition>
+ <VDomComp v-if="toggleVdom" />
+ </Transition>
+ </div>
+ </div>
+ <div class="vdom-vapor-out-in">
+ <button @click="toggleInteropComponent">
+ switch between vdom/vapor component out-in mode
+ </button>
+ <div>
+ <Transition name="fade" mode="out-in">
+ <component :is="interopComponent"></component>
+ </Transition>
+ </div>
+ </div>
+ <div class="vdom-vapor-in-out">
+ <button @click="toggleVdom = !toggleVdom">
+ switch between vdom/vapor component in-out mode
+ </button>
+ <div>
+ <Transition name="fade" mode="in-out">
+ <VaporCompA v-if="toggleVdom" />
+ <VDomComp v-else></VDomComp>
+ </Transition>
+ </div>
+ </div>
+ <!-- vdom interop end -->
+ </div>
+</template>
+<style>
+.keyed {
+ height: 100px;
+}
+</style>
+<style>
+.transition-container > div {
+ padding: 15px;
+ border: 1px solid #f7f7f7;
+ margin-top: 15px;
+}
+</style>
--- /dev/null
+<script setup vapor lang="ts">
+const msg = 'vapor compA'
+</script>
+<template>
+ <div>{{ msg }}</div>
+</template>
--- /dev/null
+<script setup vapor lang="ts">
+const msg = 'vapor compB'
+</script>
+<template>
+ <div>{{ msg }}</div>
+</template>
--- /dev/null
+<script setup vapor lang="ts">
+const msg = 'vapor'
+</script>
+<template>
+ <div>
+ <slot></slot>
+ </div>
+</template>
--- /dev/null
+<script setup lang="ts">
+const msg = 'vdom comp'
+</script>
+<template>
+ <div>{{ msg }}</div>
+</template>
--- /dev/null
+<script type="module" src="./main.ts"></script>
+<div id="app"></div>
--- /dev/null
+import { createVaporApp, vaporInteropPlugin } from 'vue'
+import App from './App.vue'
+import '../../../packages/vue/__tests__/e2e/style.css'
+import './style.css'
+
+createVaporApp(App).use(vaporInteropPlugin).mount('#app')
--- /dev/null
+.v-enter-active,
+.v-leave-active {
+ transition: opacity 50ms ease;
+}
+
+.v-enter-from,
+.v-leave-to {
+ opacity: 0;
+}
+
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 50ms ease;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+}
+
+.test-move,
+.test-enter-active,
+.test-leave-active {
+ transition: all 50ms cubic-bezier(0.55, 0, 0.1, 1);
+}
+
+.test-enter-from,
+.test-leave-to {
+ opacity: 0;
+ transform: scaleY(0.01) translate(30px, 0);
+}
+
+.test-leave-active {
+ position: absolute;
+}
input: {
interop: resolve(import.meta.dirname, 'interop/index.html'),
todomvc: resolve(import.meta.dirname, 'todomvc/index.html'),
+ transition: resolve(import.meta.dirname, 'transition/index.html'),
+ transitionGroup: resolve(
+ import.meta.dirname,
+ 'transition-group/index.html',
+ ),
},
},
},
} from './errors'
export { resolveModifiers } from './transforms/vOn'
export { isValidHTMLNesting } from './htmlNesting'
+export { postTransformTransition } from './transforms/Transition'
export * from '@vue/compiler-core'
import {
+ type CompilerError,
type ComponentNode,
ElementTypes,
type IfBranchNode,
) {
const component = context.isBuiltInComponent(node.tag)
if (component === TRANSITION) {
- return () => {
- if (!node.children.length) {
- return
- }
+ return postTransformTransition(node, context.onError)
+ }
+ }
+}
- // warn multiple transition children
- if (hasMultipleChildren(node)) {
- context.onError(
- createDOMCompilerError(
- DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN,
- {
- start: node.children[0].loc.start,
- end: node.children[node.children.length - 1].loc.end,
- source: '',
- },
- ),
- )
- }
+export function postTransformTransition(
+ node: ComponentNode,
+ onError: (error: CompilerError) => void,
+ hasMultipleChildren: (
+ node: ComponentNode,
+ ) => boolean = defaultHasMultipleChildren,
+): () => void {
+ return () => {
+ if (!node.children.length) {
+ return
+ }
+
+ if (hasMultipleChildren(node)) {
+ onError(
+ createDOMCompilerError(DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN, {
+ start: node.children[0].loc.start,
+ end: node.children[node.children.length - 1].loc.end,
+ source: '',
+ }),
+ )
+ }
- // check if it's s single child w/ v-show
- // if yes, inject "persisted: true" to the transition props
- const child = node.children[0]
- if (child.type === NodeTypes.ELEMENT) {
- for (const p of child.props) {
- if (p.type === NodeTypes.DIRECTIVE && p.name === 'show') {
- node.props.push({
- type: NodeTypes.ATTRIBUTE,
- name: 'persisted',
- nameLoc: node.loc,
- value: undefined,
- loc: node.loc,
- })
- }
- }
+ // check if it's s single child w/ v-show
+ // if yes, inject "persisted: true" to the transition props
+ const child = node.children[0]
+ if (child.type === NodeTypes.ELEMENT) {
+ for (const p of child.props) {
+ if (p.type === NodeTypes.DIRECTIVE && p.name === 'show') {
+ node.props.push({
+ type: NodeTypes.ATTRIBUTE,
+ name: 'persisted',
+ nameLoc: node.loc,
+ value: undefined,
+ loc: node.loc,
+ })
}
}
}
}
}
-function hasMultipleChildren(node: ComponentNode | IfBranchNode): boolean {
+function defaultHasMultipleChildren(
+ node: ComponentNode | IfBranchNode,
+): boolean {
// #1352 filter out potential comment nodes.
const children = (node.children = node.children.filter(
c =>
return (
children.length !== 1 ||
child.type === NodeTypes.FOR ||
- (child.type === NodeTypes.IF && child.branches.some(hasMultipleChildren))
+ (child.type === NodeTypes.IF &&
+ child.branches.some(defaultHasMultipleChildren))
)
}
--- /dev/null
+import { makeCompile } from './_utils'
+import {
+ transformChildren,
+ transformElement,
+ transformText,
+ transformVBind,
+ transformVIf,
+ transformVShow,
+ transformVSlot,
+} from '@vue/compiler-vapor'
+import { transformTransition } from '../../src/transforms/transformTransition'
+import { DOMErrorCodes } from '@vue/compiler-dom'
+
+const compileWithElementTransform = makeCompile({
+ nodeTransforms: [
+ transformText,
+ transformVIf,
+ transformElement,
+ transformVSlot,
+ transformChildren,
+ transformTransition,
+ ],
+ directiveTransforms: {
+ bind: transformVBind,
+ show: transformVShow,
+ },
+})
+
+describe('compiler: transition', () => {
+ test('basic', () => {
+ const { code } = compileWithElementTransform(
+ `<Transition><h1 v-show="show">foo</h1></Transition>`,
+ )
+ expect(code).toMatchSnapshot()
+ })
+
+ test('v-show + appear', () => {
+ const { code } = compileWithElementTransform(
+ `<Transition appear><h1 v-show="show">foo</h1></Transition>`,
+ )
+ expect(code).toMatchSnapshot()
+ })
+
+ test('work with v-if', () => {
+ const { code } = compileWithElementTransform(
+ `<Transition><h1 v-if="show">foo</h1></Transition>`,
+ )
+
+ expect(code).toMatchSnapshot()
+ // n2 should have a key
+ expect(code).contains('n2.$key = 2')
+ })
+
+ test('work with dynamic keyed children', () => {
+ const { code } = compileWithElementTransform(
+ `<Transition>
+ <h1 :key="key">foo</h1>
+ </Transition>`,
+ )
+
+ expect(code).toMatchSnapshot()
+ expect(code).contains('_createKeyedFragment(() => _ctx.key')
+ // should preserve key
+ expect(code).contains('n0.$key = _ctx.key')
+ })
+
+ function checkWarning(template: string, shouldWarn = true) {
+ const onError = vi.fn()
+ compileWithElementTransform(template, { onError })
+ if (shouldWarn) {
+ expect(onError).toHaveBeenCalled()
+ expect(onError.mock.calls).toMatchObject([
+ [{ code: DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN }],
+ ])
+ } else {
+ expect(onError).not.toHaveBeenCalled()
+ }
+ }
+
+ test('warns if multiple children', () => {
+ checkWarning(
+ `<Transition>
+ <h1>foo</h1>
+ <h2>bar</h2>
+ </Transition>`,
+ true,
+ )
+ })
+
+ test('warns with v-for', () => {
+ checkWarning(
+ `
+ <transition>
+ <div v-for="i in items">hey</div>
+ </transition>
+ `,
+ true,
+ )
+ })
+
+ test('warns with multiple v-if + v-for', () => {
+ checkWarning(
+ `
+ <transition>
+ <div v-if="a" v-for="i in items">hey</div>
+ <div v-else v-for="i in items">hey</div>
+ </transition>
+ `,
+ true,
+ )
+ })
+
+ test('warns with template v-if', () => {
+ checkWarning(
+ `
+ <transition>
+ <template v-if="ok"></template>
+ </transition>
+ `,
+ true,
+ )
+ })
+
+ test('warns with multiple templates', () => {
+ checkWarning(
+ `
+ <transition>
+ <template v-if="a"></template>
+ <template v-else></template>
+ </transition>
+ `,
+ true,
+ )
+ })
+
+ test('warns if multiple children with v-if', () => {
+ checkWarning(
+ `
+ <transition>
+ <div v-if="one">hey</div>
+ <div v-if="other">hey</div>
+ </transition>
+ `,
+ true,
+ )
+ })
+
+ test('does not warn with regular element', () => {
+ checkWarning(
+ `
+ <transition>
+ <div>hey</div>
+ </transition>
+ `,
+ false,
+ )
+ })
+
+ test('does not warn with one single v-if', () => {
+ checkWarning(
+ `
+ <transition>
+ <div v-if="a">hey</div>
+ </transition>
+ `,
+ false,
+ )
+ })
+
+ test('does not warn with v-if v-else-if v-else', () => {
+ checkWarning(
+ `
+ <transition>
+ <div v-if="a">hey</div>
+ <div v-else-if="b">hey</div>
+ <div v-else>hey</div>
+ </transition>
+ `,
+ false,
+ )
+ })
+
+ test('does not warn with v-if v-else', () => {
+ checkWarning(
+ `
+ <transition>
+ <div v-if="a">hey</div>
+ <div v-else>hey</div>
+ </transition>
+ `,
+ false,
+ )
+ })
+
+ test('inject persisted when child has v-show', () => {
+ expect(
+ compileWithElementTransform(`
+ <Transition>
+ <div v-show="ok" />
+ </Transition>
+ `).code,
+ ).toMatchSnapshot()
+ })
+
+ test('the v-if/else-if/else branches in Transition should ignore comments', () => {
+ expect(
+ compileWithElementTransform(`
+ <transition>
+ <div v-if="a">hey</div>
+ <!-- this should be ignored -->
+ <div v-else-if="b">hey</div>
+ <!-- this should be ignored -->
+ <div v-else>
+ <p v-if="c"/>
+ <!-- this should not be ignored -->
+ <p v-else/>
+ </div>
+ </transition>
+ `).code,
+ ).toMatchSnapshot()
+ })
+})
--- /dev/null
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: transition > basic 1`] = `
+"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, createComponent as _createComponent, template as _template } from 'vue';
+const t0 = _template("<h1>foo</h1>")
+
+export function render(_ctx) {
+ const n1 = _createComponent(_VaporTransition, { persisted: () => ("") }, {
+ "default": () => {
+ const n0 = t0()
+ _applyVShow(n0, () => (_ctx.show))
+ return n0
+ }
+ }, true)
+ return n1
+}"
+`;
+
+exports[`compiler: transition > inject persisted when child has v-show 1`] = `
+"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, createComponent as _createComponent, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+ const n1 = _createComponent(_VaporTransition, { persisted: () => ("") }, {
+ "default": () => {
+ const n0 = t0()
+ _applyVShow(n0, () => (_ctx.ok))
+ return n0
+ }
+ }, true)
+ return n1
+}"
+`;
+
+exports[`compiler: transition > the v-if/else-if/else branches in Transition should ignore comments 1`] = `
+"import { VaporTransition as _VaporTransition, setInsertionState as _setInsertionState, createIf as _createIf, createComponent as _createComponent, template as _template } from 'vue';
+const t0 = _template("<div>hey</div>")
+const t1 = _template("<p></p>")
+const t2 = _template("<div></div>")
+
+export function render(_ctx) {
+ const n16 = _createComponent(_VaporTransition, null, {
+ "default": () => {
+ const n0 = _createIf(() => (_ctx.a), () => {
+ const n2 = t0()
+ n2.$key = 2
+ return n2
+ }, () => _createIf(() => (_ctx.b), () => {
+ const n5 = t0()
+ n5.$key = 5
+ return n5
+ }, () => {
+ const n14 = t2()
+ _setInsertionState(n14, 0)
+ const n9 = _createIf(() => (_ctx.c), () => {
+ const n11 = t1()
+ return n11
+ }, () => {
+ const n13 = t1()
+ return n13
+ })
+ n14.$key = 14
+ return n14
+ }))
+ return [n0, n3, n7]
+ }
+ }, true)
+ return n16
+}"
+`;
+
+exports[`compiler: transition > v-show + appear 1`] = `
+"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, createComponent as _createComponent, template as _template } from 'vue';
+const t0 = _template("<h1>foo</h1>")
+
+export function render(_ctx) {
+ const deferredApplyVShows = []
+ const n1 = _createComponent(_VaporTransition, {
+ appear: () => (""),
+ persisted: () => ("")
+ }, {
+ "default": () => {
+ const n0 = t0()
+ deferredApplyVShows.push(() => _applyVShow(n0, () => (_ctx.show)))
+ return n0
+ }
+ }, true)
+ deferredApplyVShows.forEach(fn => fn())
+ return n1
+}"
+`;
+
+exports[`compiler: transition > work with dynamic keyed children 1`] = `
+"import { VaporTransition as _VaporTransition, createKeyedFragment as _createKeyedFragment, createComponent as _createComponent, template as _template } from 'vue';
+const t0 = _template("<h1>foo</h1>")
+
+export function render(_ctx) {
+ const n1 = _createComponent(_VaporTransition, null, {
+ "default": () => {
+ return _createKeyedFragment(() => _ctx.key, () => {
+ const n0 = t0()
+ n0.$key = _ctx.key
+ return n0
+ })
+ }
+ }, true)
+ return n1
+}"
+`;
+
+exports[`compiler: transition > work with v-if 1`] = `
+"import { VaporTransition as _VaporTransition, createIf as _createIf, createComponent as _createComponent, template as _template } from 'vue';
+const t0 = _template("<h1>foo</h1>")
+
+export function render(_ctx) {
+ const n3 = _createComponent(_VaporTransition, null, {
+ "default": () => {
+ const n0 = _createIf(() => (_ctx.show), () => {
+ const n2 = t0()
+ n2.$key = 2
+ return n2
+ })
+ return n0
+ }
+ }, true)
+ return n3
+}"
+`;
import { transformComment } from './transforms/transformComment'
import { transformSlotOutlet } from './transforms/transformSlotOutlet'
import { transformVSlot } from './transforms/vSlot'
+import { transformTransition } from './transforms/transformTransition'
import type { HackOptions } from './ir'
export { wrapTemplate } from './transforms/utils'
extend({}, resolvedOptions, {
nodeTransforms: [
...nodeTransforms,
+ ...(__DEV__ ? [transformTransition] : []),
...(options.nodeTransforms || []), // user transforms
],
directiveTransforms: extend(
import { genEffects, genOperations } from './operation'
import { genChildren, genSelf } from './template'
import { toValidAssetId } from '@vue/compiler-dom'
+import { genExpression } from './expression'
export function genBlock(
oper: BlockIRNode,
genEffectsExtraFrag?: () => CodeFragment[],
): CodeFragment[] {
const [frag, push] = buildCodeFragment()
- const { dynamic, effect, operation, returns } = block
+ const { dynamic, effect, operation, returns, key } = block
const resetBlock = context.enterBlock(block)
+ if (block.hasDeferredVShow) {
+ push(NEWLINE, `const deferredApplyVShows = []`)
+ }
+
if (root) {
for (let name of context.ir.component) {
const id = toValidAssetId(name, 'component')
push(...genOperations(operation, context))
push(...genEffects(effect, context, genEffectsExtraFrag))
+ if (block.hasDeferredVShow) {
+ push(NEWLINE, `deferredApplyVShows.forEach(fn => fn())`)
+ }
+
+ if (dynamic.needsKey) {
+ for (const child of dynamic.children) {
+ const keyValue = key
+ ? genExpression(key, context)
+ : JSON.stringify(child.id)
+ push(NEWLINE, `n${child.id}.$key = `, ...keyValue)
+ }
+ }
+
push(NEWLINE, `return `)
const returnNodes = returns.map(n => `n${n}`)
import { genBlock } from './block'
import { genModelHandler } from './vModel'
+import { isBuiltInComponent } from '../utils'
+
export function genCreateComponent(
operation: CreateComponentIRNode,
context: CodegenContext,
const rawProps = context.withId(() => genRawProps(props, context), ids)
const inlineHandlers: CodeFragment[] = handlers.reduce<CodeFragment[]>(
- (acc, { name, value }) => {
+ (acc, { name, value }: InlineHandler) => {
const handler = genEventHandler(context, value, undefined, false)
return [...acc, `const ${name} = `, ...handler, NEWLINE]
},
[],
)
-
return [
NEWLINE,
...inlineHandlers,
} else if (operation.asset) {
return toValidAssetId(operation.tag, 'component')
} else {
+ const { tag } = operation
+ const builtInTag = isBuiltInComponent(tag)
+ if (builtInTag) {
+ // @ts-expect-error
+ helper(builtInTag)
+ return `_${builtInTag}`
+ }
return genExpression(
- extend(createSimpleExpression(operation.tag, false), { ast: null }),
+ extend(createSimpleExpression(tag, false), { ast: null }),
context,
)
}
const isMemberExp = isMemberExpression(value, context.options)
// cache inline handlers (fn expression or inline statement)
if (!isMemberExp) {
- const name = getUniqueHandlerName(context, `_on_${prop.key.content}`)
+ const name = getUniqueHandlerName(
+ context,
+ `_on_${prop.key.content.replace(/-/g, '_')}`,
+ )
handlers.push({ name, value })
ids[name] = null
// replace the original prop value with the handler name
let propsName: string | undefined
let exitScope: (() => void) | undefined
let depth: number | undefined
- const { props } = oper
+ const { props, key } = oper
const idsOfProps = new Set<string>()
if (props) {
? `${propsName}[${JSON.stringify(id)}]`
: null),
)
- const blockFn = context.withId(
+ let blockFn = context.withId(
() => genBlock(oper, context, [propsName]),
idMap,
)
exitScope && exitScope()
+ if (key) {
+ blockFn = [
+ `() => {`,
+ INDENT_START,
+ NEWLINE,
+ `return `,
+ ...genCall(
+ context.helper('createKeyedFragment'),
+ [`() => `, ...genExpression(key, context)],
+ blockFn,
+ ),
+ INDENT_END,
+ NEWLINE,
+ `}`,
+ ]
+ }
+
return blockFn
}
oper: DirectiveIRNode,
context: CodegenContext,
): CodeFragment[] {
+ const { deferred, element } = oper
return [
NEWLINE,
- ...genCall(context.helper('applyVShow'), `n${oper.element}`, [
+ deferred ? `deferredApplyVShows.push(() => ` : undefined,
+ ...genCall(context.helper('applyVShow'), `n${element}`, [
`() => (`,
...genExpression(oper.dir.exp!, context),
`)`,
]),
+ deferred ? `)` : undefined,
]
}
export interface BaseIRNode {
type: IRNodeTypes
+ key?: SimpleExpressionNode | undefined
}
export type CoreHelper = keyof typeof import('packages/runtime-dom/src')
effect: IREffect[]
operation: OperationNode[]
returns: number[]
+ hasDeferredVShow: boolean
}
export interface RootIRNode {
builtin?: boolean
asset?: boolean
modelType?: 'text' | 'dynamic' | 'radio' | 'checkbox' | 'select'
+ deferred?: boolean
}
export interface CreateComponentIRNode extends BaseIRNode {
children: IRDynamicInfo[]
template?: number
hasDynamicChild?: boolean
+ needsKey?: boolean
operation?: OperationNode
}
-import { isValidHTMLNesting } from '@vue/compiler-dom'
import {
type AttributeNode,
type ComponentNode,
createCompilerError,
createSimpleExpression,
isStaticArgOf,
+ isValidHTMLNesting,
} from '@vue/compiler-dom'
import {
camelize,
type VaporDirectiveNode,
} from '../ir'
import { EMPTY_EXPRESSION } from './utils'
-import { findProp } from '../utils'
+import { findProp, isBuiltInComponent } from '../utils'
export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
asset = false
}
+ const builtInTag = isBuiltInComponent(tag)
+ if (builtInTag) {
+ tag = builtInTag
+ asset = false
+ }
+
const dotIndex = tag.indexOf('.')
if (dotIndex > 0) {
const ns = resolveSetupReference(tag.slice(0, dotIndex), context)
}
const name = prop.key.content
const existing = knownProps.get(name)
- if (existing) {
+ // prop names and event handler names can be the same but serve different purposes
+ // e.g. `:appear="true"` is a prop while `@appear="handler"` is an event handler
+ if (existing && existing.handler === prop.handler) {
if (name === 'style' || name === 'class') {
mergePropValues(existing, prop)
}
--- /dev/null
+import type { NodeTransform } from '@vue/compiler-vapor'
+import { findDir, isTransitionTag } from '../utils'
+import {
+ type ElementNode,
+ ElementTypes,
+ NodeTypes,
+ isTemplateNode,
+ postTransformTransition,
+} from '@vue/compiler-dom'
+
+export const transformTransition: NodeTransform = (node, context) => {
+ if (
+ node.type === NodeTypes.ELEMENT &&
+ node.tagType === ElementTypes.COMPONENT
+ ) {
+ if (isTransitionTag(node.tag)) {
+ return postTransformTransition(
+ node,
+ context.options.onError,
+ hasMultipleChildren,
+ )
+ }
+ }
+}
+
+function hasMultipleChildren(node: ElementNode): boolean {
+ const children = (node.children = node.children.filter(
+ c =>
+ c.type !== NodeTypes.COMMENT &&
+ !(c.type === NodeTypes.TEXT && !c.content.trim()),
+ ))
+
+ const first = children[0]
+
+ // has v-for
+ if (
+ children.length === 1 &&
+ first.type === NodeTypes.ELEMENT &&
+ (findDir(first, 'for') || isTemplateNode(first))
+ ) {
+ return true
+ }
+
+ const hasElse = (node: ElementNode) =>
+ findDir(node, 'else-if') || findDir(node, 'else', true)
+
+ // has v-if/v-else-if/v-else
+ if (
+ children.every(
+ (c, index) =>
+ c.type === NodeTypes.ELEMENT &&
+ // not template
+ !isTemplateNode(c) &&
+ // not has v-for
+ !findDir(c, 'for') &&
+ // if the first child has v-if, the rest should also have v-else-if/v-else
+ (index === 0 ? findDir(c, 'if') : hasElse(c)) &&
+ !hasMultipleChildren(c),
+ )
+ ) {
+ return false
+ }
+
+ return children.length > 1
+}
operation: [],
returns: [],
tempId: 0,
+ hasDeferredVShow: false,
})
export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {
import { extend } from '@vue/shared'
import { newBlock, wrapTemplate } from './utils'
import { getSiblingIf } from './transformComment'
-import { isStaticExpression } from '../utils'
+import { isInTransition, isStaticExpression } from '../utils'
export const transformVIf: NodeTransform = createStructuralDirectiveTransform(
['if', 'else', 'else-if'],
const branch: BlockIRNode = newBlock(node)
const exitBlock = context.enterBlock(branch)
context.reference()
+ // generate key for branch result when it's in transition
+ // the key will be used to track node leaving at runtime
+ branch.dynamic.needsKey = isInTransition(context)
return [branch, exitBlock]
}
DOMErrorCodes,
ElementTypes,
ErrorCodes,
+ NodeTypes,
createCompilerError,
createDOMCompilerError,
} from '@vue/compiler-dom'
import type { DirectiveTransform } from '../transform'
import { IRNodeTypes } from '../ir'
+import { findProp, isTransitionTag } from '../utils'
export const transformVShow: DirectiveTransform = (dir, node, context) => {
const { exp, loc } = dir
return
}
+ // lazy apply vshow if the node is inside a transition with appear
+ let shouldDeferred = false
+ const parentNode = context.parent && context.parent.node
+ if (parentNode && parentNode.type === NodeTypes.ELEMENT) {
+ shouldDeferred = !!(
+ isTransitionTag(parentNode.tag) &&
+ findProp(parentNode, 'appear', false, true)
+ )
+
+ if (shouldDeferred) {
+ context.parent!.parent!.block.hasDeferredVShow = true
+ }
+ }
+
context.registerOperation({
type: IRNodeTypes.DIRECTIVE,
element: context.reference(),
dir,
name: 'show',
builtin: true,
+ deferred: shouldDeferred,
})
}
type SlotBlockIRNode,
type VaporDirectiveNode,
} from '../ir'
-import { findDir, resolveExpression } from '../utils'
+import {
+ findDir,
+ findProp,
+ isTransitionNode,
+ resolveExpression,
+} from '../utils'
import { markNonTemplate } from './transformText'
export const transformVSlot: NodeTransform = (node, context) => {
})
}
- const [block, onExit] = createSlotBlock(node, dir, context)
+ let slotKey
+ if (isTransitionNode(node) && nonSlotTemplateChildren.length) {
+ const keyProp = findProp(
+ nonSlotTemplateChildren[0] as ElementNode,
+ 'key',
+ ) as VaporDirectiveNode
+ if (keyProp) {
+ slotKey = keyProp.exp
+ }
+ }
+
+ const [block, onExit] = createSlotBlock(node, dir, context, slotKey)
const { slots } = context
slotNode: ElementNode,
dir: VaporDirectiveNode | undefined,
context: TransformContext<ElementNode>,
+ key: SimpleExpressionNode | undefined = undefined,
): [SlotBlockIRNode, () => void] {
const block: SlotBlockIRNode = newBlock(slotNode)
block.props = dir && dir.exp
+ if (key) {
+ block.key = key
+ block.dynamic.needsKey = true
+ }
const exitBlock = context.enterBlock(block)
return [block, exitBlock]
}
} from '@vue/compiler-dom'
import type { VaporDirectiveNode } from './ir'
import { EMPTY_EXPRESSION } from './transforms/utils'
+import type { TransformContext } from './transform'
export const findProp = _findProp as (
node: ElementNode,
}
return exp.isStatic ? exp.content : null
}
+
+export function isInTransition(
+ context: TransformContext<ElementNode>,
+): boolean {
+ const parentNode = context.parent && context.parent.node
+ return !!(parentNode && isTransitionNode(parentNode as ElementNode))
+}
+
+export function isTransitionNode(node: ElementNode): boolean {
+ return node.type === NodeTypes.ELEMENT && isTransitionTag(node.tag)
+}
+
+export function isTransitionGroupNode(node: ElementNode): boolean {
+ return node.type === NodeTypes.ELEMENT && isTransitionGroupTag(node.tag)
+}
+
+export function isTransitionTag(tag: string): boolean {
+ tag = tag.toLowerCase()
+ return tag === 'transition' || tag === 'vaportransition'
+}
+
+export function isTransitionGroupTag(tag: string): boolean {
+ tag = tag.toLowerCase().replace(/-/g, '')
+ return tag === 'transitiongroup' || tag === 'vaportransitiongroup'
+}
+
+export function isBuiltInComponent(tag: string): string | undefined {
+ if (isTransitionTag(tag)) {
+ return 'VaporTransition'
+ } else if (isTransitionGroupTag(tag)) {
+ return 'VaporTransitionGroup'
+ }
+}
import type { VNode } from './vnode'
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
import { NO, extend, hasOwn, isFunction, isObject } from '@vue/shared'
-import { version } from '.'
+import { type TransitionHooks, version } from '.'
import { installAppCompatProperties } from './compat/global'
import type { NormalizedPropsOptions } from './componentProps'
import type { ObjectEmitsOptions } from './componentEmits'
/**
* The vapor in vdom implementation is in runtime-vapor/src/vdomInterop.ts
- * @internal
*/
export interface VaporInteropInterface {
mount(
unmount(vnode: VNode, doRemove?: boolean): void
move(vnode: VNode, container: any, anchor: any): void
slot(n1: VNode | null, n2: VNode, container: any, anchor: any): void
+ setTransitionHooks(
+ component: ComponentInternalInstance,
+ transition: TransitionHooks,
+ ): void
vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any
vdomUnmount: UnmountComponentFn
import {
type ComponentInternalInstance,
type ComponentOptions,
+ type ConcreteComponent,
+ type GenericComponentInstance,
type SetupContext,
getCurrentInstance,
} from '../component'
import { PatchFlags, ShapeFlags, isArray, isFunction } from '@vue/shared'
import { onBeforeUnmount, onMounted } from '../apiLifecycle'
import { isTeleport } from './Teleport'
-import type { RendererElement } from '../renderer'
+import { type RendererElement, getVaporInterface } from '../renderer'
import { SchedulerJobFlags } from '../scheduler'
type Hook<T = () => void> = T | T[]
isUnmounting: boolean
// Track pending leave callbacks for children of the same key.
// This is used to force remove leaving a child when a new copy is entering.
- leavingVNodes: Map<any, Record<string, VNode>>
+ leavingNodes: Map<any, Record<string, any>>
}
export interface TransitionElement {
isMounted: false,
isLeaving: false,
isUnmounting: false,
- leavingVNodes: new Map(),
+ leavingNodes: new Map(),
}
onMounted(() => {
state.isMounted = true
}
const recursiveGetSubtree = (instance: ComponentInternalInstance): VNode => {
- const subTree = instance.subTree
+ const subTree = instance.type.__vapor
+ ? (instance as any).block
+ : instance.subTree
return subTree.component ? recursiveGetSubtree(subTree.component) : subTree
}
const rawProps = toRaw(props)
const { mode } = rawProps
// check mode
- if (
- __DEV__ &&
- mode &&
- mode !== 'in-out' &&
- mode !== 'out-in' &&
- mode !== 'default'
- ) {
- warn(`invalid <transition> mode: ${mode}`)
- }
+ checkTransitionMode(mode)
if (state.isLeaving) {
return emptyPlaceholder(child)
state: TransitionState,
vnode: VNode,
): Record<string, VNode> {
- const { leavingVNodes } = state
- let leavingVNodesCache = leavingVNodes.get(vnode.type)!
+ const { leavingNodes } = state
+ let leavingVNodesCache = leavingNodes.get(vnode.type)!
if (!leavingVNodesCache) {
leavingVNodesCache = Object.create(null)
- leavingVNodes.set(vnode.type, leavingVNodesCache)
+ leavingNodes.set(vnode.type, leavingVNodesCache)
}
return leavingVNodesCache
}
+export interface TransitionHooksContext {
+ setLeavingNodeCache: (node: any) => void
+ unsetLeavingNodeCache: (node: any) => void
+ earlyRemove: () => void
+ cloneHooks: (node: any) => TransitionHooks
+}
+
// The transition hooks are attached to the vnode as vnode.transition
// and will be called at appropriate timing in the renderer.
export function resolveTransitionHooks(
vnode: VNode,
props: BaseTransitionProps<any>,
state: TransitionState,
- instance: ComponentInternalInstance,
+ instance: GenericComponentInstance,
postClone?: (hooks: TransitionHooks) => void,
+): TransitionHooks {
+ const key = String(vnode.key)
+ const leavingVNodesCache = getLeavingNodesForType(state, vnode)
+ const context: TransitionHooksContext = {
+ setLeavingNodeCache: () => {
+ leavingVNodesCache[key] = vnode
+ },
+ unsetLeavingNodeCache: () => {
+ if (leavingVNodesCache[key] === vnode) {
+ delete leavingVNodesCache[key]
+ }
+ },
+ earlyRemove: () => {
+ const leavingVNode = leavingVNodesCache[key]
+ if (
+ leavingVNode &&
+ isSameVNodeType(vnode, leavingVNode) &&
+ (leavingVNode.el as TransitionElement)[leaveCbKey]
+ ) {
+ // force early removal (not cancelled)
+ ;(leavingVNode.el as TransitionElement)[leaveCbKey]!()
+ }
+ },
+ cloneHooks: vnode => {
+ const hooks = resolveTransitionHooks(
+ vnode,
+ props,
+ state,
+ instance,
+ postClone,
+ )
+ if (postClone) postClone(hooks)
+ return hooks
+ },
+ }
+
+ return baseResolveTransitionHooks(context, props, state, instance)
+}
+
+// shared between vdom and vapor
+export function baseResolveTransitionHooks(
+ context: TransitionHooksContext,
+ props: BaseTransitionProps<any>,
+ state: TransitionState,
+ instance: GenericComponentInstance,
): TransitionHooks {
+ const {
+ setLeavingNodeCache,
+ unsetLeavingNodeCache,
+ earlyRemove,
+ cloneHooks,
+ } = context
+
const {
appear,
mode,
onAfterAppear,
onAppearCancelled,
} = props
- const key = String(vnode.key)
- const leavingVNodesCache = getLeavingNodesForType(state, vnode)
const callHook: TransitionHookCaller = (hook, args) => {
hook &&
el[leaveCbKey](true /* cancelled */)
}
// for toggled element with same key (v-if)
- const leavingVNode = leavingVNodesCache[key]
- if (
- leavingVNode &&
- isSameVNodeType(vnode, leavingVNode) &&
- (leavingVNode.el as TransitionElement)[leaveCbKey]
- ) {
- // force early removal (not cancelled)
- ;(leavingVNode.el as TransitionElement)[leaveCbKey]!()
- }
+ earlyRemove()
callHook(hook, [el])
},
},
leave(el, remove) {
- const key = String(vnode.key)
+ // const key = String(vnode.key)
if (el[enterCbKey]) {
el[enterCbKey](true /* cancelled */)
}
callHook(onAfterLeave, [el])
}
el[leaveCbKey] = undefined
- if (leavingVNodesCache[key] === vnode) {
- delete leavingVNodesCache[key]
- }
+ unsetLeavingNodeCache(el)
})
- leavingVNodesCache[key] = vnode
+ setLeavingNodeCache(el)
if (onLeave) {
callAsyncHook(onLeave, [el, done])
} else {
}
},
- clone(vnode) {
- const hooks = resolveTransitionHooks(
- vnode,
- props,
- state,
- instance,
- postClone,
- )
- if (postClone) postClone(hooks)
- return hooks
+ clone(node) {
+ return cloneHooks(node)
},
}
export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks): void {
if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) {
- vnode.transition = hooks
- setTransitionHooks(vnode.component.subTree, hooks)
+ if ((vnode.type as ConcreteComponent).__vapor) {
+ getVaporInterface(vnode.component, vnode).setTransitionHooks(
+ vnode.component,
+ hooks,
+ )
+ } else {
+ vnode.transition = hooks
+ setTransitionHooks(vnode.component.subTree, hooks)
+ }
} else if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
vnode.ssContent!.transition = hooks.clone(vnode.ssContent!)
vnode.ssFallback!.transition = hooks.clone(vnode.ssFallback!)
}
return ret
}
+
+/**
+ * dev-only
+ */
+export function checkTransitionMode(mode: string | undefined): void {
+ if (
+ __DEV__ &&
+ mode &&
+ mode !== 'in-out' &&
+ mode !== 'out-in' &&
+ mode !== 'default'
+ ) {
+ warn(`invalid <transition> mode: ${mode}`)
+ }
+}
export {
BaseTransition,
BaseTransitionPropsValidators,
+ checkTransitionMode,
type BaseTransitionProps,
} from './components/BaseTransition'
// For using custom directives
export {
useTransitionState,
resolveTransitionHooks,
+ baseResolveTransitionHooks,
setTransitionHooks,
getTransitionRawChildren,
+ leaveCbKey,
} from './components/BaseTransition'
export { initCustomFormatter } from './customFormatter'
export type {
TransitionState,
TransitionHooks,
+ TransitionHooksContext,
+ TransitionElement,
} from './components/BaseTransition'
export type {
AsyncComponentOptions,
* @internal
*/
export { initFeatureFlags } from './featureFlags'
+/**
+ * @internal
+ */
+export { performTransitionEnter, performTransitionLeave } from './renderer'
/**
* @internal
*/
}
// #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
// #1689 For inside suspense + suspense resolved case, just call it
- const needCallTransitionHooks = needTransition(parentSuspense, transition)
- if (needCallTransitionHooks) {
- transition!.beforeEnter(el)
+ if (transition) {
+ performTransitionEnter(
+ el,
+ transition,
+ () => hostInsert(el, container, anchor),
+ parentSuspense,
+ )
+ } else {
+ hostInsert(el, container, anchor)
}
- hostInsert(el, container, anchor)
- if (
- (vnodeHook = props && props.onVnodeMounted) ||
- needCallTransitionHooks ||
- dirs
- ) {
+
+ if ((vnodeHook = props && props.onVnodeMounted) || dirs) {
queuePostRenderEffect(
() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
- needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
},
undefined,
transition
if (needTransition) {
if (moveType === MoveType.ENTER) {
- transition!.beforeEnter(el!)
- hostInsert(el!, container, anchor)
- queuePostRenderEffect(
- () => transition!.enter(el!),
- undefined,
+ performTransitionEnter(
+ el!,
+ transition,
+ () => hostInsert(el!, container, anchor),
parentSuspense,
+ true,
)
} else {
const { leave, delayLeave, afterLeave } = transition!
return
}
- const performRemove = () => {
- hostRemove(el!)
- if (transition && !transition.persisted && transition.afterLeave) {
- transition.afterLeave()
- }
- }
-
- if (
- vnode.shapeFlag & ShapeFlags.ELEMENT &&
- transition &&
- !transition.persisted
- ) {
- const { leave, delayLeave } = transition
- const performLeave = () => leave(el!, performRemove)
- if (delayLeave) {
- delayLeave(vnode.el!, performRemove, performLeave)
- } else {
- performLeave()
- }
+ if (transition) {
+ performTransitionLeave(
+ el!,
+ transition,
+ () => hostRemove(el!),
+ !!(vnode.shapeFlag & ShapeFlags.ELEMENT),
+ )
} else {
- performRemove()
+ hostRemove(el!)
}
}
function locateNonHydratedAsyncRoot(
instance: ComponentInternalInstance,
): ComponentInternalInstance | undefined {
- const subComponent = instance.vapor ? null : instance.subTree.component
+ const subComponent = instance.subTree && instance.subTree.component
if (subComponent) {
if (subComponent.asyncDep && !subComponent.asyncResolved) {
return subComponent
}
}
-function getVaporInterface(
+// shared between vdom and vapor
+export function performTransitionEnter(
+ el: RendererElement,
+ transition: TransitionHooks,
+ insert: () => void,
+ parentSuspense: SuspenseBoundary | null,
+ force: boolean = false,
+): void {
+ if (force || needTransition(parentSuspense, transition)) {
+ transition.beforeEnter(el)
+ insert()
+ queuePostRenderEffect(() => transition.enter(el), undefined, parentSuspense)
+ } else {
+ insert()
+ }
+}
+
+// shared between vdom and vapor
+export function performTransitionLeave(
+ el: RendererElement,
+ transition: TransitionHooks,
+ remove: () => void,
+ isElement: boolean = true,
+): void {
+ const performRemove = () => {
+ remove()
+ if (transition && !transition.persisted && transition.afterLeave) {
+ transition.afterLeave()
+ }
+ }
+
+ if (isElement && transition && !transition.persisted) {
+ const { leave, delayLeave } = transition
+ const performLeave = () => leave(el, performRemove)
+ if (delayLeave) {
+ delayLeave(el, performRemove, performLeave)
+ } else {
+ performLeave()
+ }
+ } else {
+ performRemove()
+ }
+}
+
+export function getVaporInterface(
instance: ComponentInternalInstance | null,
vnode: VNode,
): VaporInteropInterface {
const positionMap = new WeakMap<VNode, DOMRect>()
const newPositionMap = new WeakMap<VNode, DOMRect>()
-const moveCbKey = Symbol('_moveCb')
+export const moveCbKey: symbol = Symbol('_moveCb')
const enterCbKey = Symbol('_enterCb')
export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
// we divide the work into three loops to avoid mixing DOM reads and writes
// in each iteration - which helps prevent layout thrashing.
- prevChildren.forEach(callPendingCbs)
+ prevChildren.forEach(vnode => callPendingCbs(vnode.el))
prevChildren.forEach(recordPosition)
const movedChildren = prevChildren.filter(applyTranslation)
movedChildren.forEach(c => {
const el = c.el as ElementWithTransition
- const style = el.style
- addTransitionClass(el, moveClass)
- style.transform = style.webkitTransform = style.transitionDuration = ''
- const cb = ((el as any)[moveCbKey] = (e: TransitionEvent) => {
- if (e && e.target !== el) {
- return
- }
- if (!e || /transform$/.test(e.propertyName)) {
- el.removeEventListener('transitionend', cb)
- ;(el as any)[moveCbKey] = null
- removeTransitionClass(el, moveClass)
- }
- })
- el.addEventListener('transitionend', cb)
+ handleMovedChildren(el, moveClass)
})
prevChildren = []
})
}
}
-function callPendingCbs(c: VNode) {
- const el = c.el as any
+export function callPendingCbs(el: any): void {
if (el[moveCbKey]) {
el[moveCbKey]()
}
}
function applyTranslation(c: VNode): VNode | undefined {
- const oldPos = positionMap.get(c)!
- const newPos = newPositionMap.get(c)!
+ if (
+ baseApplyTranslation(
+ positionMap.get(c)!,
+ newPositionMap.get(c)!,
+ c.el as ElementWithTransition,
+ )
+ ) {
+ return c
+ }
+}
+
+// shared between vdom and vapor
+export function baseApplyTranslation(
+ oldPos: DOMRect,
+ newPos: DOMRect,
+ el: ElementWithTransition,
+): boolean {
const dx = oldPos.left - newPos.left
const dy = oldPos.top - newPos.top
if (dx || dy) {
- const s = (c.el as HTMLElement).style
+ const s = (el as HTMLElement).style
s.transform = s.webkitTransform = `translate(${dx}px,${dy}px)`
s.transitionDuration = '0s'
- return c
+ return true
}
+ return false
}
-function hasCSSTransform(
+// shared between vdom and vapor
+export function hasCSSTransform(
el: ElementWithTransition,
root: Node,
moveClass: string,
container.removeChild(clone)
return hasTransform
}
+
+// shared between vdom and vapor
+export const handleMovedChildren = (
+ el: ElementWithTransition,
+ moveClass: string,
+): void => {
+ const style = el.style
+ addTransitionClass(el, moveClass)
+ style.transform = style.webkitTransform = style.transitionDuration = ''
+ const cb = ((el as any)[moveCbKey] = (e: TransitionEvent) => {
+ if (e && e.target !== el) {
+ return
+ }
+ if (!e || /transform$/.test(e.propertyName)) {
+ el.removeEventListener('transitionend', cb)
+ ;(el as any)[moveCbKey] = null
+ removeTransitionClass(el, moveClass)
+ }
+ })
+ el.addEventListener('transitionend', cb)
+}
vModelSelectInit,
vModelSetSelected,
} from './directives/vModel'
+/**
+ * @internal
+ */
+export {
+ resolveTransitionProps,
+ TransitionPropsValidators,
+ forceReflow,
+ addTransitionClass,
+ removeTransitionClass,
+ type ElementWithTransition,
+} from './components/Transition'
+/**
+ * @internal
+ */
+export {
+ hasCSSTransform,
+ callPendingCbs,
+ moveCbKey,
+ handleMovedChildren,
+ baseApplyTranslation,
+} from './components/TransitionGroup'
/**
* @internal
*/
-import { resolveDynamicComponent } from '@vue/runtime-dom'
+import { currentInstance, resolveDynamicComponent } from '@vue/runtime-dom'
import { DynamicFragment, type VaporFragment, insert } from './block'
-import { createComponentWithFallback } from './component'
+import { createComponentWithFallback, emptyContext } from './component'
import { renderEffect } from './renderEffect'
import type { RawProps } from './componentProps'
import type { RawSlots } from './componentSlots'
renderEffect(() => {
const value = getter()
+ const appContext =
+ (currentInstance && currentInstance.appContext) || emptyContext
frag.update(
() =>
createComponentWithFallback(
rawProps,
rawSlots,
isSingleRoot,
+ appContext,
),
value,
)
import type { DynamicSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
import { VaporVForFlags } from '../../shared/src/vaporFlags'
+import { applyTransitionHooks } from './components/Transition'
import { isHydrating, locateHydrationNode } from './dom/hydration'
import {
insertionAnchor,
key2,
))
+ // apply transition for new nodes
+ if (frag.$transition) {
+ applyTransitionHooks(block.nodes, frag.$transition, false)
+ }
+
if (parent) insert(block.nodes, parent, anchor)
return block
export function getDefaultValue(val: any, defaultVal: any): any {
return val === undefined ? defaultVal : val
}
+
+export function isForBlock(block: Block): block is ForBlock {
+ return block instanceof ForBlock
+}
--- /dev/null
+import { type Block, type BlockFn, DynamicFragment } from './block'
+import { renderEffect } from './renderEffect'
+
+export function createKeyedFragment(key: () => any, render: BlockFn): Block {
+ const frag = __DEV__ ? new DynamicFragment('keyed') : new DynamicFragment()
+ renderEffect(() => {
+ frag.update(render, key())
+ })
+ return frag
+}
import { createComment, createTextNode } from './dom/node'
import { EffectScope, setActiveSub } from '@vue/reactivity'
import { isHydrating } from './dom/hydration'
+import {
+ type TransitionHooks,
+ type TransitionProps,
+ type TransitionState,
+ performTransitionEnter,
+ performTransitionLeave,
+} from '@vue/runtime-dom'
+import {
+ applyTransitionHooks,
+ applyTransitionLeaveHooks,
+} from './components/Transition'
+
+export interface TransitionOptions {
+ $key?: any
+ $transition?: VaporTransitionHooks
+}
+
+export interface VaporTransitionHooks extends TransitionHooks {
+ state: TransitionState
+ props: TransitionProps
+ instance: VaporComponentInstance
+ // mark transition hooks as disabled so that it skips during
+ // inserting
+ disabled?: boolean
+}
+
+export type TransitionBlock =
+ | (Node & TransitionOptions)
+ | (VaporFragment & TransitionOptions)
+ | (DynamicFragment & TransitionOptions)
-export type Block =
- | Node
- | VaporFragment
- | DynamicFragment
- | VaporComponentInstance
- | Block[]
+export type Block = TransitionBlock | VaporComponentInstance | Block[]
export type BlockFn = (...args: any[]) => Block
-export class VaporFragment {
+export class VaporFragment implements TransitionOptions {
+ $key?: any
+ $transition?: VaporTransitionHooks | undefined
nodes: Block
anchor?: Node
- insert?: (parent: ParentNode, anchor: Node | null) => void
- remove?: (parent?: ParentNode) => void
+ insert?: (
+ parent: ParentNode,
+ anchor: Node | null,
+ transitionHooks?: TransitionHooks,
+ ) => void
+ remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void
constructor(nodes: Block) {
this.nodes = nodes
const prevSub = setActiveSub()
const parent = this.anchor.parentNode
+ const transition = this.$transition
+ const renderBranch = () => {
+ if (render) {
+ this.scope = new EffectScope()
+ this.nodes = this.scope.run(render) || []
+ if (transition) {
+ this.$transition = applyTransitionHooks(this.nodes, transition)
+ }
+ if (parent) insert(this.nodes, parent, this.anchor)
+ } else {
+ this.scope = undefined
+ this.nodes = []
+ }
+ }
// teardown previous branch
if (this.scope) {
this.scope.stop()
- parent && remove(this.nodes, parent)
+ const mode = transition && transition.mode
+ if (mode) {
+ applyTransitionLeaveHooks(this.nodes, transition, renderBranch)
+ parent && remove(this.nodes, parent)
+ if (mode === 'out-in') {
+ setActiveSub(prevSub)
+ return
+ }
+ } else {
+ parent && remove(this.nodes, parent)
+ }
}
- if (render) {
- this.scope = new EffectScope()
- this.nodes = this.scope.run(render) || []
- if (parent) insert(this.nodes, parent, this.anchor)
- } else {
- this.scope = undefined
- this.nodes = []
- }
+ renderBranch()
if (this.fallback && !isValidBlock(this.nodes)) {
parent && remove(this.nodes, parent)
block: Block,
parent: ParentNode & { $anchor?: Node | null },
anchor: Node | null | 0 = null, // 0 means prepend
+ parentSuspense?: any, // TODO Suspense
): void {
anchor = anchor === 0 ? parent.$anchor || parent.firstChild : anchor
if (block instanceof Node) {
if (!isHydrating) {
- parent.insertBefore(block, anchor)
+ // only apply transition on Element nodes
+ if (
+ block instanceof Element &&
+ (block as TransitionBlock).$transition &&
+ !(block as TransitionBlock).$transition!.disabled
+ ) {
+ performTransitionEnter(
+ block,
+ (block as TransitionBlock).$transition as TransitionHooks,
+ () => parent.insertBefore(block, anchor),
+ parentSuspense,
+ )
+ } else {
+ parent.insertBefore(block, anchor)
+ }
}
} else if (isVaporComponent(block)) {
if (block.isMounted) {
// fragment
if (block.insert) {
// TODO handle hydration for vdom interop
- block.insert(parent, anchor)
+ block.insert(parent, anchor, (block as TransitionBlock).$transition)
} else {
- insert(block.nodes, parent, anchor)
+ insert(block.nodes, parent, anchor, parentSuspense)
}
if (block.anchor) insert(block.anchor, parent, anchor)
}
export function remove(block: Block, parent?: ParentNode): void {
if (block instanceof Node) {
- parent && parent.removeChild(block)
+ if ((block as TransitionBlock).$transition && block instanceof Element) {
+ performTransitionLeave(
+ block,
+ (block as TransitionBlock).$transition as TransitionHooks,
+ () => parent && parent.removeChild(block),
+ )
+ } else {
+ parent && parent.removeChild(block)
+ }
} else if (isVaporComponent(block)) {
unmountComponent(block, parent)
} else if (isArray(block)) {
} else {
// fragment
if (block.remove) {
- block.remove(parent)
+ block.remove(parent, (block as TransitionBlock).$transition)
} else {
remove(block.nodes, parent)
}
getSlot,
} from './componentSlots'
import { hmrReload, hmrRerender } from './hmr'
+import { createElement } from './dom/node'
import { isHydrating, locateHydrationNode } from './dom/hydration'
import {
insertionAnchor,
) {
const el = getRootElement(instance)
if (el) {
- renderEffect(() => {
- isApplyingFallthroughProps = true
- setDynamicProps(el, [instance.attrs])
- isApplyingFallthroughProps = false
- })
+ renderEffect(() => applyFallthroughProps(el, instance.attrs))
}
}
export let isApplyingFallthroughProps = false
+export function applyFallthroughProps(
+ block: Block,
+ attrs: Record<string, any>,
+): void {
+ isApplyingFallthroughProps = true
+ setDynamicProps(block as Element, [attrs])
+ isApplyingFallthroughProps = false
+}
+
/**
* dev only
*/
)) || []
}
-const emptyContext: GenericAppContext = {
+export const emptyContext: GenericAppContext = {
app: null as any,
config: {},
provides: /*@__PURE__*/ Object.create(null),
rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null,
isSingleRoot?: boolean,
+ appContext?: GenericAppContext,
): HTMLElement | VaporComponentInstance {
if (!isString(comp)) {
- return createComponent(comp, rawProps, rawSlots, isSingleRoot)
+ return createComponent(comp, rawProps, rawSlots, isSingleRoot, appContext)
}
+ const el = createElement(comp)
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
if (isHydrating) {
resetInsertionState()
}
- const el = document.createElement(comp)
// mark single root
;(el as any).$root = isSingleRoot
--- /dev/null
+import {
+ type GenericComponentInstance,
+ type TransitionElement,
+ type TransitionHooks,
+ type TransitionHooksContext,
+ type TransitionProps,
+ TransitionPropsValidators,
+ type TransitionState,
+ baseResolveTransitionHooks,
+ checkTransitionMode,
+ currentInstance,
+ leaveCbKey,
+ resolveTransitionProps,
+ useTransitionState,
+ warn,
+} from '@vue/runtime-dom'
+import {
+ type Block,
+ type TransitionBlock,
+ type VaporTransitionHooks,
+ isFragment,
+} from '../block'
+import {
+ type FunctionalVaporComponent,
+ type VaporComponentInstance,
+ applyFallthroughProps,
+ isVaporComponent,
+} from '../component'
+import { extend, isArray } from '@vue/shared'
+import { renderEffect } from '../renderEffect'
+
+const decorate = (t: typeof VaporTransition) => {
+ t.displayName = 'VaporTransition'
+ t.props = TransitionPropsValidators
+ t.__vapor = true
+ return t
+}
+
+export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
+ (props, { slots, attrs }) => {
+ const children = (slots.default && slots.default()) as any as Block
+ if (!children) return
+
+ const instance = currentInstance! as VaporComponentInstance
+ const { mode } = props
+ checkTransitionMode(mode)
+
+ let resolvedProps
+ let isMounted = false
+ renderEffect(() => {
+ resolvedProps = resolveTransitionProps(props)
+ if (isMounted) {
+ // only update props for Fragment block, for later reusing
+ if (isFragment(children)) {
+ children.$transition!.props = resolvedProps
+ } else {
+ const child = findTransitionBlock(children)
+ if (child) {
+ // replace existing transition hooks
+ child.$transition!.props = resolvedProps
+ applyTransitionHooks(child, child.$transition!)
+ }
+ }
+ } else {
+ isMounted = true
+ }
+ })
+
+ // fallthrough attrs
+ let fallthroughAttrs = true
+ if (instance.hasFallthrough) {
+ renderEffect(() => {
+ // attrs are accessed in advance
+ const resolvedAttrs = extend({}, attrs)
+ const child = findTransitionBlock(children)
+ if (child) {
+ // mark single root
+ ;(child as any).$root = true
+
+ applyFallthroughProps(child, resolvedAttrs)
+ // ensure fallthrough attrs are not happened again in
+ // applyTransitionHooks
+ fallthroughAttrs = false
+ }
+ })
+ }
+
+ applyTransitionHooks(
+ children,
+ {
+ state: useTransitionState(),
+ props: resolvedProps!,
+ instance: instance,
+ } as VaporTransitionHooks,
+ fallthroughAttrs,
+ )
+
+ return children
+ },
+)
+
+const getTransitionHooksContext = (
+ key: String,
+ props: TransitionProps,
+ state: TransitionState,
+ instance: GenericComponentInstance,
+ postClone: ((hooks: TransitionHooks) => void) | undefined,
+) => {
+ const { leavingNodes } = state
+ const context: TransitionHooksContext = {
+ setLeavingNodeCache: el => {
+ leavingNodes.set(key, el)
+ },
+ unsetLeavingNodeCache: el => {
+ const leavingNode = leavingNodes.get(key)
+ if (leavingNode === el) {
+ leavingNodes.delete(key)
+ }
+ },
+ earlyRemove: () => {
+ const leavingNode = leavingNodes.get(key)
+ if (leavingNode && (leavingNode as TransitionElement)[leaveCbKey]) {
+ // force early removal (not cancelled)
+ ;(leavingNode as TransitionElement)[leaveCbKey]!()
+ }
+ },
+ cloneHooks: block => {
+ const hooks = resolveTransitionHooks(
+ block,
+ props,
+ state,
+ instance,
+ postClone,
+ )
+ if (postClone) postClone(hooks)
+ return hooks
+ },
+ }
+ return context
+}
+
+export function resolveTransitionHooks(
+ block: TransitionBlock,
+ props: TransitionProps,
+ state: TransitionState,
+ instance: GenericComponentInstance,
+ postClone?: (hooks: TransitionHooks) => void,
+): VaporTransitionHooks {
+ const context = getTransitionHooksContext(
+ String(block.$key),
+ props,
+ state,
+ instance,
+ postClone,
+ )
+ const hooks = baseResolveTransitionHooks(
+ context,
+ props,
+ state,
+ instance,
+ ) as VaporTransitionHooks
+ hooks.state = state
+ hooks.props = props
+ hooks.instance = instance as VaporComponentInstance
+ return hooks
+}
+
+export function applyTransitionHooks(
+ block: Block,
+ hooks: VaporTransitionHooks,
+ fallthroughAttrs: boolean = true,
+): VaporTransitionHooks {
+ const isFrag = isFragment(block)
+ const child = findTransitionBlock(block)
+ if (!child) {
+ // set transition hooks on fragment for reusing during it's updating
+ if (isFrag) setTransitionHooksOnFragment(block, hooks)
+ return hooks
+ }
+
+ const { props, instance, state, delayedLeave } = hooks
+ let resolvedHooks = resolveTransitionHooks(
+ child,
+ props,
+ state,
+ instance,
+ hooks => (resolvedHooks = hooks as VaporTransitionHooks),
+ )
+ resolvedHooks.delayedLeave = delayedLeave
+ setTransitionHooks(child, resolvedHooks)
+ if (isFrag) setTransitionHooksOnFragment(block, resolvedHooks)
+
+ // fallthrough attrs
+ if (fallthroughAttrs && instance.hasFallthrough) {
+ // mark single root
+ ;(child as any).$root = true
+ applyFallthroughProps(child, instance.attrs)
+ }
+
+ return resolvedHooks
+}
+
+export function applyTransitionLeaveHooks(
+ block: Block,
+ enterHooks: VaporTransitionHooks,
+ afterLeaveCb: () => void,
+): void {
+ const leavingBlock = findTransitionBlock(block)
+ if (!leavingBlock) return undefined
+
+ const { props, state, instance } = enterHooks
+ const leavingHooks = resolveTransitionHooks(
+ leavingBlock,
+ props,
+ state,
+ instance,
+ )
+ setTransitionHooks(leavingBlock, leavingHooks)
+
+ const { mode } = props
+ if (mode === 'out-in') {
+ state.isLeaving = true
+ leavingHooks.afterLeave = () => {
+ state.isLeaving = false
+ afterLeaveCb()
+ leavingBlock.$transition = undefined
+ delete leavingHooks.afterLeave
+ }
+ } else if (mode === 'in-out') {
+ leavingHooks.delayLeave = (
+ block: TransitionElement,
+ earlyRemove,
+ delayedLeave,
+ ) => {
+ state.leavingNodes.set(String(leavingBlock.$key), leavingBlock)
+ // early removal callback
+ block[leaveCbKey] = () => {
+ earlyRemove()
+ block[leaveCbKey] = undefined
+ leavingBlock.$transition = undefined
+ delete enterHooks.delayedLeave
+ }
+ enterHooks.delayedLeave = () => {
+ delayedLeave()
+ leavingBlock.$transition = undefined
+ delete enterHooks.delayedLeave
+ }
+ }
+ }
+}
+
+const transitionBlockCache = new WeakMap<Block, TransitionBlock>()
+export function findTransitionBlock(
+ block: Block,
+ inFragment: boolean = false,
+): TransitionBlock | undefined {
+ if (transitionBlockCache.has(block)) {
+ return transitionBlockCache.get(block)
+ }
+
+ let isFrag = false
+ 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)) {
+ child = findTransitionBlock(block.block)
+ // use component id as key
+ if (child && child.$key === undefined) child.$key = block.uid
+ } else if (isArray(block)) {
+ child = block[0] as TransitionBlock
+ let hasFound = false
+ for (const c of block) {
+ const item = findTransitionBlock(c)
+ if (item instanceof Element) {
+ if (__DEV__ && hasFound) {
+ // warn more than one non-comment child
+ warn(
+ '<transition> can only be used on a single element or component. ' +
+ 'Use <transition-group> for lists.',
+ )
+ break
+ }
+ child = item
+ hasFound = true
+ if (!__DEV__) break
+ }
+ }
+ } else if ((isFrag = isFragment(block))) {
+ if (block.insert) {
+ child = block
+ } else {
+ child = findTransitionBlock(block.nodes, true)
+ }
+ }
+
+ if (__DEV__ && !child && !inFragment && !isFrag) {
+ warn('Transition component has no valid child element')
+ }
+
+ return child
+}
+
+export function setTransitionHooksOnFragment(
+ block: Block,
+ hooks: VaporTransitionHooks,
+): void {
+ if (isFragment(block)) {
+ setTransitionHooks(block, hooks)
+ } else if (isArray(block)) {
+ for (let i = 0; i < block.length; i++) {
+ setTransitionHooksOnFragment(block[i], hooks)
+ }
+ }
+}
+
+export function setTransitionHooks(
+ block: TransitionBlock | VaporComponentInstance,
+ hooks: VaporTransitionHooks,
+): void {
+ if (isVaporComponent(block)) {
+ block = findTransitionBlock(block.block) as TransitionBlock
+ if (!block) return
+ }
+ block.$transition = hooks
+}
--- /dev/null
+import {
+ type ElementWithTransition,
+ type TransitionGroupProps,
+ TransitionPropsValidators,
+ baseApplyTranslation,
+ callPendingCbs,
+ currentInstance,
+ forceReflow,
+ handleMovedChildren,
+ hasCSSTransform,
+ onBeforeUpdate,
+ onUpdated,
+ resolveTransitionProps,
+ useTransitionState,
+ warn,
+} from '@vue/runtime-dom'
+import { extend, isArray } from '@vue/shared'
+import {
+ type Block,
+ DynamicFragment,
+ type TransitionBlock,
+ type VaporTransitionHooks,
+ insert,
+ isFragment,
+} from '../block'
+import {
+ resolveTransitionHooks,
+ setTransitionHooks,
+ setTransitionHooksOnFragment,
+} from './Transition'
+import {
+ type ObjectVaporComponent,
+ type VaporComponentInstance,
+ applyFallthroughProps,
+ isVaporComponent,
+} from '../component'
+import { isForBlock } from '../apiCreateFor'
+import { renderEffect } from '../renderEffect'
+import { createElement } from '../dom/node'
+
+const positionMap = new WeakMap<TransitionBlock, DOMRect>()
+const newPositionMap = new WeakMap<TransitionBlock, DOMRect>()
+
+const decorate = (t: typeof VaporTransitionGroup) => {
+ delete (t.props! as any).mode
+ t.__vapor = true
+ return t
+}
+
+export const VaporTransitionGroup: ObjectVaporComponent = decorate({
+ name: 'VaporTransitionGroup',
+
+ props: /*@__PURE__*/ extend({}, TransitionPropsValidators, {
+ tag: String,
+ moveClass: String,
+ }),
+
+ setup(props: TransitionGroupProps, { slots }) {
+ const instance = currentInstance as VaporComponentInstance
+ const state = useTransitionState()
+ const cssTransitionProps = resolveTransitionProps(props)
+
+ let prevChildren: TransitionBlock[]
+ let children: TransitionBlock[]
+ let slottedBlock: Block
+
+ onBeforeUpdate(() => {
+ prevChildren = []
+ children = getTransitionBlocks(slottedBlock)
+ if (children) {
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ if (isValidTransitionBlock(child)) {
+ prevChildren.push(child)
+ // disabled transition during enter, so the children will be
+ // inserted into the correct position immediately. this prevents
+ // `recordPosition` from getting incorrect positions in `onUpdated`
+ child.$transition!.disabled = true
+ positionMap.set(
+ child,
+ getTransitionElement(child).getBoundingClientRect(),
+ )
+ }
+ }
+ }
+ })
+
+ onUpdated(() => {
+ if (!prevChildren.length) {
+ return
+ }
+
+ const moveClass = props.moveClass || `${props.name || 'v'}-move`
+ const firstChild = getFirstConnectedChild(prevChildren)
+ if (
+ !firstChild ||
+ !hasCSSTransform(
+ firstChild as ElementWithTransition,
+ firstChild.parentNode as Node,
+ moveClass,
+ )
+ ) {
+ prevChildren = []
+ return
+ }
+
+ prevChildren.forEach(callPendingCbs)
+ prevChildren.forEach(child => {
+ child.$transition!.disabled = false
+ recordPosition(child)
+ })
+ const movedChildren = prevChildren.filter(applyTranslation)
+
+ // force reflow to put everything in position
+ forceReflow()
+
+ movedChildren.forEach(c =>
+ handleMovedChildren(
+ getTransitionElement(c) as ElementWithTransition,
+ moveClass,
+ ),
+ )
+ prevChildren = []
+ })
+
+ slottedBlock = slots.default && slots.default()
+
+ // store props and state on fragment for reusing during insert new items
+ setTransitionHooksOnFragment(slottedBlock, {
+ props: cssTransitionProps,
+ state,
+ instance,
+ } as VaporTransitionHooks)
+
+ children = getTransitionBlocks(slottedBlock)
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ if (isValidTransitionBlock(child)) {
+ if (child.$key != null) {
+ setTransitionHooks(
+ child,
+ resolveTransitionHooks(child, cssTransitionProps, state, instance!),
+ )
+ } else if (__DEV__ && child.$key == null) {
+ warn(`<transition-group> children must be keyed`)
+ }
+ }
+ }
+
+ const tag = props.tag
+ if (tag) {
+ const container = createElement(tag)
+ insert(slottedBlock, container)
+ // fallthrough attrs
+ if (instance!.hasFallthrough) {
+ ;(container as any).$root = true
+ renderEffect(() => applyFallthroughProps(container, instance!.attrs))
+ }
+ return container
+ } else {
+ const frag = __DEV__
+ ? new DynamicFragment('transition-group')
+ : new DynamicFragment()
+ renderEffect(() => frag.update(() => slottedBlock))
+ return frag
+ }
+ },
+})
+
+function getTransitionBlocks(block: Block) {
+ let children: TransitionBlock[] = []
+ if (block instanceof Node) {
+ children.push(block)
+ } else if (isVaporComponent(block)) {
+ children.push(...getTransitionBlocks(block.block))
+ } else if (isArray(block)) {
+ for (let i = 0; i < block.length; i++) {
+ const b = block[i]
+ const blocks = getTransitionBlocks(b)
+ if (isForBlock(b)) blocks.forEach(block => (block.$key = b.key))
+ children.push(...blocks)
+ }
+ } else if (isFragment(block)) {
+ if (block.insert) {
+ // vdom component
+ children.push(block)
+ } else {
+ children.push(...getTransitionBlocks(block.nodes))
+ }
+ }
+
+ return children
+}
+
+function isValidTransitionBlock(block: Block): boolean {
+ return !!(block instanceof Element || (isFragment(block) && block.insert))
+}
+
+function getTransitionElement(c: TransitionBlock): Element {
+ return (isFragment(c) ? (c.nodes as Element) : c) as Element
+}
+
+function recordPosition(c: TransitionBlock) {
+ newPositionMap.set(c, getTransitionElement(c).getBoundingClientRect())
+}
+
+function applyTranslation(c: TransitionBlock): TransitionBlock | undefined {
+ if (
+ baseApplyTranslation(
+ positionMap.get(c)!,
+ newPositionMap.get(c)!,
+ getTransitionElement(c) as ElementWithTransition,
+ )
+ ) {
+ return c
+ }
+}
+
+function getFirstConnectedChild(
+ children: TransitionBlock[],
+): Element | undefined {
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ const el = getTransitionElement(child)
+ if (el.isConnected) return el
+ }
+}
} from '@vue/runtime-dom'
import { renderEffect } from '../renderEffect'
import { isVaporComponent } from '../component'
-import { type Block, DynamicFragment, VaporFragment } from '../block'
+import {
+ type Block,
+ DynamicFragment,
+ type TransitionBlock,
+ VaporFragment,
+} from '../block'
import { isArray } from '@vue/shared'
export function applyVShow(target: Block, source: () => any): void {
if (target instanceof VaporFragment && target.insert) {
return setDisplay(target.nodes, value)
}
+
+ const { $transition } = target as TransitionBlock
if (target instanceof Element) {
const el = target as VShowElement
if (!(vShowOriginalDisplay in el)) {
el[vShowOriginalDisplay] =
el.style.display === 'none' ? '' : el.style.display
}
- el.style.display = value ? el[vShowOriginalDisplay]! : 'none'
+ if ($transition) {
+ if (value) {
+ $transition.beforeEnter(target)
+ el.style.display = el[vShowOriginalDisplay]!
+ $transition.enter(target)
+ } else {
+ // during initial render, the element is not yet inserted into the
+ // DOM, and it is hidden, no need to trigger transition
+ if (target.isConnected) {
+ $transition.leave(target, () => {
+ el.style.display = 'none'
+ })
+ } else {
+ el.style.display = 'none'
+ }
+ }
+ } else {
+ el.style.display = value ? el[vShowOriginalDisplay]! : 'none'
+ }
el[vShowHidden] = !value
} else if (__DEV__) {
warn(
+/*! #__NO_SIDE_EFFECTS__ */
+export function createElement(tagName: string): HTMLElement {
+ return document.createElement(tagName)
+}
+
/*! #__NO_SIDE_EFFECTS__ */
export function createTextNode(value = ''): Text {
return document.createTextNode(value)
if (isOptimized) return
isOptimized = true
const proto = Element.prototype as any
+ proto.$transition = undefined
+ proto.$key = undefined
+ proto.$evtclick = undefined
proto.$anchor = proto.$evtclick = undefined
proto.$root = false
proto.$html =
+import { child, createElement, createTextNode } from './node'
import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration'
-import { child, createTextNode } from './node'
let t: HTMLTemplateElement
return createTextNode(html)
}
if (!node) {
- t = t || document.createElement('template')
+ t = t || createElement('template')
t.innerHTML = html
node = child(t.content)
}
} from './dom/prop'
export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
export { createIf } from './apiCreateIf'
+export { createKeyedFragment } from './apiCreateFragment'
export {
createFor,
createForSlots,
applyDynamicModel,
} from './directives/vModel'
export { withVaporDirectives } from './directives/custom'
+export { VaporTransition } from './components/Transition'
+export { VaporTransitionGroup } from './components/TransitionGroup'
type RendererInternals,
type ShallowRef,
type Slots,
+ type TransitionHooks,
type VNode,
type VaporInteropInterface,
createInternalObject,
isEmitListener,
onScopeDispose,
renderSlot,
+ setTransitionHooks as setVNodeTransitionHooks,
shallowReactive,
shallowRef,
simpleSetCurrentInstance,
mountComponent,
unmountComponent,
} from './component'
-import { type Block, VaporFragment, insert, remove } from './block'
-import { EMPTY_OBJ, extend, isFunction } from '@vue/shared'
+import {
+ type Block,
+ VaporFragment,
+ type VaporTransitionHooks,
+ insert,
+ remove,
+} from './block'
+import { EMPTY_OBJ, extend, isFunction, isReservedProp } from '@vue/shared'
import { type RawProps, rawPropsProxyHandlers } from './componentProps'
import type { RawSlots, VaporSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
import { createTextNode } from './dom/node'
import { optimizePropertyLookup } from './dom/prop'
+import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition'
export const interopKey: unique symbol = Symbol(`interop`)
const prev = currentInstance
simpleSetCurrentInstance(parentComponent)
- const propsRef = shallowRef(vnode.props)
+ // filter out reserved props
+ const props: VNode['props'] = {}
+ for (const key in vnode.props) {
+ if (!isReservedProp(key)) {
+ props[key] = vnode.props[key]
+ }
+ }
+
+ const propsRef = shallowRef(props)
const slotsRef = shallowRef(vnode.children)
const dynamicPropSource: (() => any)[] & { [interopKey]?: boolean } = [
))
instance.rawPropsRef = propsRef
instance.rawSlotsRef = slotsRef
+ if (vnode.transition) {
+ setVaporTransitionHooks(
+ instance,
+ vnode.transition as VaporTransitionHooks,
+ )
+ }
mountComponent(instance, container, selfAnchor)
simpleSetCurrentInstance(prev)
return instance
insert(vnode.vb || (vnode.component as any), container, anchor)
insert(vnode.anchor as any, container, anchor)
},
+
+ setTransitionHooks(component, hooks) {
+ setVaporTransitionHooks(component as any, hooks as VaporTransitionHooks)
+ },
}
const vaporSlotPropsProxyHandler: ProxyHandler<
let isMounted = false
const parentInstance = currentInstance as VaporComponentInstance
- const unmount = (parentNode?: ParentNode) => {
+ const unmount = (parentNode?: ParentNode, transition?: TransitionHooks) => {
+ if (transition) setVNodeTransitionHooks(vnode, transition)
internals.umt(vnode.component!, null, !!parentNode)
}
- frag.insert = (parentNode, anchor) => {
+ frag.insert = (parentNode, anchor, transition) => {
+ const prev = currentInstance
+ simpleSetCurrentInstance(parentInstance)
if (!isMounted) {
+ if (transition) setVNodeTransitionHooks(vnode, transition)
internals.mt(
vnode,
parentNode,
}
frag.nodes = vnode.el as Block
+ simpleSetCurrentInstance(prev)
}
frag.remove = unmount
clearValue(selector: string): Promise<any>
timeout(time: number): Promise<any>
nextFrame(): Promise<any>
+ transitionStart(
+ btnSelector: string,
+ containerSelector: string,
+ ): Promise<{ classNames: string[]; innerHTML: string }>
+ waitForElement(
+ selector: string,
+ text: string,
+ classNames: string[],
+ timeout?: number,
+ ): Promise<any>
}
export function setupPuppeteer(args?: string[]): PuppeteerUtils {
})
}
+ const transitionStart = (btnSelector: string, containerSelector: string) =>
+ page.evaluate(
+ ([btnSel, containerSel]) => {
+ ;(document.querySelector(btnSel) as HTMLElement)!.click()
+ return Promise.resolve().then(() => {
+ const container = document.querySelector(containerSel)!
+ return {
+ classNames: container.className.split(/\s+/g),
+ innerHTML: container.innerHTML,
+ }
+ })
+ },
+ [btnSelector, containerSelector],
+ )
+
+ const waitForElement = (
+ selector: string,
+ text: string,
+ classNames: string[], // if empty, check for no classes
+ timeout = 2000,
+ ) =>
+ page.waitForFunction(
+ (sel, expectedText, expectedClasses) => {
+ const el = document.querySelector(sel)
+ const hasClasses =
+ expectedClasses.length === 0
+ ? el?.classList.length === 0
+ : expectedClasses.every(c => el?.classList.contains(c))
+ const hasText = el?.textContent?.includes(expectedText)
+ return !!el && hasClasses && hasText
+ },
+ { timeout },
+ selector,
+ text,
+ classNames,
+ )
+
return {
page: () => page,
click,
clearValue,
timeout,
nextFrame,
+ transitionStart,
+ waitForElement,
}
}
--- /dev/null
+.test {
+ -webkit-transition: opacity 50ms ease;
+ transition: opacity 50ms ease;
+}
+.group-move {
+ -webkit-transition: -webkit-transform 50ms ease;
+ transition: transform 50ms ease;
+}
+.v-appear,
+.v-enter,
+.v-leave-active,
+.test-appear,
+.test-enter,
+.test-leave-active,
+.test-reflow-enter,
+.test-reflow-leave-to,
+.hello,
+.bye.active,
+.changed-enter {
+ opacity: 0;
+}
+.test-reflow-leave-active,
+.test-reflow-enter-active {
+ -webkit-transition: opacity 50ms ease;
+ transition: opacity 50ms ease;
+}
+.test-reflow-leave-from {
+ opacity: 0.9;
+}
+.test-anim-enter-active {
+ animation: test-enter 50ms;
+ -webkit-animation: test-enter 50ms;
+}
+.test-anim-leave-active {
+ animation: test-leave 50ms;
+ -webkit-animation: test-leave 50ms;
+}
+.test-anim-long-enter-active {
+ animation: test-enter 100ms;
+ -webkit-animation: test-enter 100ms;
+}
+.test-anim-long-leave-active {
+ animation: test-leave 100ms;
+ -webkit-animation: test-leave 100ms;
+}
+@keyframes test-enter {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@-webkit-keyframes test-enter {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@keyframes test-leave {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+@-webkit-keyframes test-leave {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
<script src="../../dist/vue.global.js"></script>
<div id="app"></div>
-<style>
- .test {
- -webkit-transition: opacity 50ms ease;
- transition: opacity 50ms ease;
- }
- .group-move {
- -webkit-transition: -webkit-transform 50ms ease;
- transition: transform 50ms ease;
- }
- .v-appear,
- .v-enter,
- .v-leave-active,
- .test-appear,
- .test-enter,
- .test-leave-active,
- .test-reflow-enter,
- .test-reflow-leave-to,
- .hello,
- .bye.active,
- .changed-enter {
- opacity: 0;
- }
- .test-reflow-leave-active,
- .test-reflow-enter-active {
- -webkit-transition: opacity 50ms ease;
- transition: opacity 50ms ease;
- }
- .test-reflow-leave-from {
- opacity: 0.9;
- }
- .test-anim-enter-active {
- animation: test-enter 50ms;
- -webkit-animation: test-enter 50ms;
- }
- .test-anim-leave-active {
- animation: test-leave 50ms;
- -webkit-animation: test-leave 50ms;
- }
- .test-anim-long-enter-active {
- animation: test-enter 100ms;
- -webkit-animation: test-enter 100ms;
- }
- .test-anim-long-leave-active {
- animation: test-leave 100ms;
- -webkit-animation: test-leave 100ms;
- }
- @keyframes test-enter {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
- }
- @-webkit-keyframes test-enter {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
- }
- @keyframes test-leave {
- from {
- opacity: 1;
- }
- to {
- opacity: 0;
- }
- }
- @-webkit-keyframes test-leave {
- from {
- opacity: 1;
- }
- to {
- opacity: 0;
- }
- }
-</style>
+<link rel="stylesheet" href="style.css" />