]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(vapor): vapor transition + transition-group (#12962)
authoredison <daiwei521@126.com>
Mon, 20 Oct 2025 03:39:50 +0000 (11:39 +0800)
committerGitHub <noreply@github.com>
Mon, 20 Oct 2025 03:39:50 +0000 (11:39 +0800)
58 files changed:
packages-private/vapor-e2e-test/__tests__/transition-group.spec.ts [new file with mode: 0644]
packages-private/vapor-e2e-test/__tests__/transition.spec.ts [new file with mode: 0644]
packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts
packages-private/vapor-e2e-test/index.html
packages-private/vapor-e2e-test/interop/App.vue
packages-private/vapor-e2e-test/interop/main.ts
packages-private/vapor-e2e-test/transition-group/App.vue [new file with mode: 0644]
packages-private/vapor-e2e-test/transition-group/components/VaporComp.vue [new file with mode: 0644]
packages-private/vapor-e2e-test/transition-group/components/VdomComp.vue [new file with mode: 0644]
packages-private/vapor-e2e-test/transition-group/index.html [new file with mode: 0644]
packages-private/vapor-e2e-test/transition-group/main.ts [new file with mode: 0644]
packages-private/vapor-e2e-test/transition/App.vue [new file with mode: 0644]
packages-private/vapor-e2e-test/transition/components/VaporCompA.vue [new file with mode: 0644]
packages-private/vapor-e2e-test/transition/components/VaporCompB.vue [new file with mode: 0644]
packages-private/vapor-e2e-test/transition/components/VaporSlot.vue [new file with mode: 0644]
packages-private/vapor-e2e-test/transition/components/VdomComp.vue [new file with mode: 0644]
packages-private/vapor-e2e-test/transition/index.html [new file with mode: 0644]
packages-private/vapor-e2e-test/transition/main.ts [new file with mode: 0644]
packages-private/vapor-e2e-test/transition/style.css [new file with mode: 0644]
packages-private/vapor-e2e-test/vite.config.ts
packages/compiler-dom/src/index.ts
packages/compiler-dom/src/transforms/Transition.ts
packages/compiler-vapor/__tests__/transforms/TransformTransition.spec.ts [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/src/compile.ts
packages/compiler-vapor/src/generators/block.ts
packages/compiler-vapor/src/generators/component.ts
packages/compiler-vapor/src/generators/vShow.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/compiler-vapor/src/transforms/transformTransition.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/utils.ts
packages/compiler-vapor/src/transforms/vIf.ts
packages/compiler-vapor/src/transforms/vShow.ts
packages/compiler-vapor/src/transforms/vSlot.ts
packages/compiler-vapor/src/utils.ts
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/components/BaseTransition.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-dom/src/components/TransitionGroup.ts
packages/runtime-dom/src/index.ts
packages/runtime-vapor/src/apiCreateDynamicComponent.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/apiCreateFragment.ts [new file with mode: 0644]
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/components/Transition.ts [new file with mode: 0644]
packages/runtime-vapor/src/components/TransitionGroup.ts [new file with mode: 0644]
packages/runtime-vapor/src/directives/vShow.ts
packages/runtime-vapor/src/dom/node.ts
packages/runtime-vapor/src/dom/prop.ts
packages/runtime-vapor/src/dom/template.ts
packages/runtime-vapor/src/index.ts
packages/runtime-vapor/src/vdomInterop.ts
packages/vue/__tests__/e2e/e2eUtils.ts
packages/vue/__tests__/e2e/style.css [new file with mode: 0644]
packages/vue/__tests__/e2e/transition.html

diff --git a/packages-private/vapor-e2e-test/__tests__/transition-group.spec.ts b/packages-private/vapor-e2e-test/__tests__/transition-group.spec.ts
new file mode 100644 (file)
index 0000000..ba050f0
--- /dev/null
@@ -0,0 +1,406 @@
+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>`,
+    )
+  })
+})
diff --git a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts
new file mode 100644 (file)
index 0000000..0bfc305
--- /dev/null
@@ -0,0 +1,1660 @@
+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,
+    )
+  })
+})
index 360f48085a14e7ae4f587d9a89ffeae216f582fc..e05f06e1abd7b999664428deafca228b479a5d03 100644 (file)
@@ -5,10 +5,23 @@ import {
 } 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(() => {
@@ -22,12 +35,15 @@ describe('vdom / vapor interop', () => {
     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')
@@ -81,4 +97,163 @@ describe('vdom / vapor interop', () => {
     },
     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,
+    )
+  })
 })
index 7dc205e5ab024878395e5fb22c09679be565c6a3..09ea6aa607a4283ce54ff83a4fd91428153002ff 100644 (file)
@@ -1,2 +1,11 @@
 <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>
index 772a6989dd74c90f2e1cb10fbfed734996cc44fe..8cf42e475498f62f71155efdc4dc147117f23fa4 100644 (file)
@@ -1,9 +1,22 @@
 <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>
@@ -19,4 +32,41 @@ const passSlot = ref(true)
 
     <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>
index d5d6d7dcf8ca81835a59c39a7523229a80243f36..41155dc5cb2064900b734fc6240eb5184486c3e8 100644 (file)
@@ -1,4 +1,5 @@
 import { createApp, vaporInteropPlugin } from 'vue'
 import App from './App.vue'
+import '../transition/style.css'
 
 createApp(App).use(vaporInteropPlugin).mount('#app')
diff --git a/packages-private/vapor-e2e-test/transition-group/App.vue b/packages-private/vapor-e2e-test/transition-group/App.vue
new file mode 100644 (file)
index 0000000..5577574
--- /dev/null
@@ -0,0 +1,145 @@
+<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>
diff --git a/packages-private/vapor-e2e-test/transition-group/components/VaporComp.vue b/packages-private/vapor-e2e-test/transition-group/components/VaporComp.vue
new file mode 100644 (file)
index 0000000..906795d
--- /dev/null
@@ -0,0 +1,9 @@
+<script vapor>
+const msg = 'vapor comp'
+</script>
+
+<template>
+  <div>
+    <slot />
+  </div>
+</template>
diff --git a/packages-private/vapor-e2e-test/transition-group/components/VdomComp.vue b/packages-private/vapor-e2e-test/transition-group/components/VdomComp.vue
new file mode 100644 (file)
index 0000000..afd7d55
--- /dev/null
@@ -0,0 +1,9 @@
+<script setup>
+const msg = 'vdom comp'
+</script>
+
+<template>
+  <div>
+    <slot />
+  </div>
+</template>
diff --git a/packages-private/vapor-e2e-test/transition-group/index.html b/packages-private/vapor-e2e-test/transition-group/index.html
new file mode 100644 (file)
index 0000000..79052a0
--- /dev/null
@@ -0,0 +1,2 @@
+<script type="module" src="./main.ts"></script>
+<div id="app"></div>
diff --git a/packages-private/vapor-e2e-test/transition-group/main.ts b/packages-private/vapor-e2e-test/transition-group/main.ts
new file mode 100644 (file)
index 0000000..efa06a2
--- /dev/null
@@ -0,0 +1,5 @@
+import { createVaporApp, vaporInteropPlugin } from 'vue'
+import App from './App.vue'
+import '../../../packages/vue/__tests__/e2e/style.css'
+
+createVaporApp(App).use(vaporInteropPlugin).mount('#app')
diff --git a/packages-private/vapor-e2e-test/transition/App.vue b/packages-private/vapor-e2e-test/transition/App.vue
new file mode 100644 (file)
index 0000000..4855098
--- /dev/null
@@ -0,0 +1,528 @@
+<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>
diff --git a/packages-private/vapor-e2e-test/transition/components/VaporCompA.vue b/packages-private/vapor-e2e-test/transition/components/VaporCompA.vue
new file mode 100644 (file)
index 0000000..f6902d8
--- /dev/null
@@ -0,0 +1,6 @@
+<script setup vapor lang="ts">
+const msg = 'vapor compA'
+</script>
+<template>
+  <div>{{ msg }}</div>
+</template>
diff --git a/packages-private/vapor-e2e-test/transition/components/VaporCompB.vue b/packages-private/vapor-e2e-test/transition/components/VaporCompB.vue
new file mode 100644 (file)
index 0000000..db90f99
--- /dev/null
@@ -0,0 +1,6 @@
+<script setup vapor lang="ts">
+const msg = 'vapor compB'
+</script>
+<template>
+  <div>{{ msg }}</div>
+</template>
diff --git a/packages-private/vapor-e2e-test/transition/components/VaporSlot.vue b/packages-private/vapor-e2e-test/transition/components/VaporSlot.vue
new file mode 100644 (file)
index 0000000..f5eff01
--- /dev/null
@@ -0,0 +1,8 @@
+<script setup vapor lang="ts">
+const msg = 'vapor'
+</script>
+<template>
+  <div>
+    <slot></slot>
+  </div>
+</template>
diff --git a/packages-private/vapor-e2e-test/transition/components/VdomComp.vue b/packages-private/vapor-e2e-test/transition/components/VdomComp.vue
new file mode 100644 (file)
index 0000000..cb6ec7c
--- /dev/null
@@ -0,0 +1,6 @@
+<script setup lang="ts">
+const msg = 'vdom comp'
+</script>
+<template>
+  <div>{{ msg }}</div>
+</template>
diff --git a/packages-private/vapor-e2e-test/transition/index.html b/packages-private/vapor-e2e-test/transition/index.html
new file mode 100644 (file)
index 0000000..79052a0
--- /dev/null
@@ -0,0 +1,2 @@
+<script type="module" src="./main.ts"></script>
+<div id="app"></div>
diff --git a/packages-private/vapor-e2e-test/transition/main.ts b/packages-private/vapor-e2e-test/transition/main.ts
new file mode 100644 (file)
index 0000000..e77d51d
--- /dev/null
@@ -0,0 +1,6 @@
+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')
diff --git a/packages-private/vapor-e2e-test/transition/style.css b/packages-private/vapor-e2e-test/transition/style.css
new file mode 100644 (file)
index 0000000..e6faf6c
--- /dev/null
@@ -0,0 +1,35 @@
+.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;
+}
index 1e29a4dbd13f89e6d657f6804ec87af653c62924..f50fccea3cee448746785d46ced5fafcaa12925d 100644 (file)
@@ -14,6 +14,11 @@ export default defineConfig({
       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',
+        ),
       },
     },
   },
index 950901e7bf91c1d6ad97f8526ff788f538658adc..446a917ad7c51aad871475715bd1d910e8a06a51 100644 (file)
@@ -76,4 +76,5 @@ export {
 } from './errors'
 export { resolveModifiers } from './transforms/vOn'
 export { isValidHTMLNesting } from './htmlNesting'
+export { postTransformTransition } from './transforms/Transition'
 export * from '@vue/compiler-core'
index f6cf968e37263492a9348f7a9afcca03684a6948..30ea083d8fcad649bb3db63947ba85299669eb67 100644 (file)
@@ -1,4 +1,5 @@
 import {
+  type CompilerError,
   type ComponentNode,
   ElementTypes,
   type IfBranchNode,
@@ -15,47 +16,55 @@ export const transformTransition: NodeTransform = (node, context) => {
   ) {
     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 =>
@@ -66,6 +75,7 @@ function hasMultipleChildren(node: ComponentNode | IfBranchNode): boolean {
   return (
     children.length !== 1 ||
     child.type === NodeTypes.FOR ||
-    (child.type === NodeTypes.IF && child.branches.some(hasMultipleChildren))
+    (child.type === NodeTypes.IF &&
+      child.branches.some(defaultHasMultipleChildren))
   )
 }
diff --git a/packages/compiler-vapor/__tests__/transforms/TransformTransition.spec.ts b/packages/compiler-vapor/__tests__/transforms/TransformTransition.spec.ts
new file mode 100644 (file)
index 0000000..bcb7b44
--- /dev/null
@@ -0,0 +1,222 @@
+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()
+  })
+})
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap
new file mode 100644 (file)
index 0000000..12a3f2a
--- /dev/null
@@ -0,0 +1,128 @@
+// 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
+}"
+`;
index c39037a47d864367c1ac56ff39e359a1aa0a9e0e..8fa2e793321fbbd9ce17f25a47b53dddeed19614 100644 (file)
@@ -26,6 +26,7 @@ import { transformVFor } from './transforms/vFor'
 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'
@@ -54,6 +55,7 @@ export function compile(
     extend({}, resolvedOptions, {
       nodeTransforms: [
         ...nodeTransforms,
+        ...(__DEV__ ? [transformTransition] : []),
         ...(options.nodeTransforms || []), // user transforms
       ],
       directiveTransforms: extend(
index a4f98dfdffa3269074815cb6d8916be484aad95c..40fa8da6322d9cb80ce6ffe13b72efe041161943 100644 (file)
@@ -13,6 +13,7 @@ import type { CodegenContext } from '../generate'
 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,
@@ -39,9 +40,13 @@ export function genBlockContent(
   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')
@@ -73,6 +78,19 @@ export function genBlockContent(
   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}`)
index fb76abaa50571affcbf230be32b9a79d4475e6a2..aa2f6844c1391918c04dbfab18a030d4dbd5f863 100644 (file)
@@ -40,6 +40,8 @@ import { genDirectiveModifiers, genDirectivesForElement } from './directive'
 import { genBlock } from './block'
 import { genModelHandler } from './vModel'
 
+import { isBuiltInComponent } from '../utils'
+
 export function genCreateComponent(
   operation: CreateComponentIRNode,
   context: CodegenContext,
@@ -53,13 +55,12 @@ export function genCreateComponent(
   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,
@@ -92,8 +93,15 @@ export function genCreateComponent(
     } 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,
       )
     }
@@ -128,7 +136,10 @@ function processInlineHandlers(
         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
@@ -397,7 +408,7 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) {
   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) {
@@ -425,11 +436,28 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) {
         ? `${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
 }
index 9a6ccefcded93932fffa7d1adbf002182473b735..5ff6b257dc7f745741aac1f6983ecc23eebacb55 100644 (file)
@@ -7,12 +7,15 @@ export function genVShow(
   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,
   ]
 }
index 18f0139ab568e4f082c88241bca7f6a87b53bb98..a8130be3890982310973671630a58fd5ef986717 100644 (file)
@@ -39,6 +39,7 @@ export enum IRNodeTypes {
 
 export interface BaseIRNode {
   type: IRNodeTypes
+  key?: SimpleExpressionNode | undefined
 }
 
 export type CoreHelper = keyof typeof import('packages/runtime-dom/src')
@@ -53,6 +54,7 @@ export interface BlockIRNode extends BaseIRNode {
   effect: IREffect[]
   operation: OperationNode[]
   returns: number[]
+  hasDeferredVShow: boolean
 }
 
 export interface RootIRNode {
@@ -181,6 +183,7 @@ export interface DirectiveIRNode extends BaseIRNode {
   builtin?: boolean
   asset?: boolean
   modelType?: 'text' | 'dynamic' | 'radio' | 'checkbox' | 'select'
+  deferred?: boolean
 }
 
 export interface CreateComponentIRNode extends BaseIRNode {
@@ -259,6 +262,7 @@ export interface IRDynamicInfo {
   children: IRDynamicInfo[]
   template?: number
   hasDynamicChild?: boolean
+  needsKey?: boolean
   operation?: OperationNode
 }
 
index 05153e729aff475dd609bc8bb5bedfe3f1cde0d3..dcabe36093813f2e07a995c78cfe60c5fc3f744d 100644 (file)
@@ -1,4 +1,3 @@
-import { isValidHTMLNesting } from '@vue/compiler-dom'
 import {
   type AttributeNode,
   type ComponentNode,
@@ -11,6 +10,7 @@ import {
   createCompilerError,
   createSimpleExpression,
   isStaticArgOf,
+  isValidHTMLNesting,
 } from '@vue/compiler-dom'
 import {
   camelize,
@@ -36,7 +36,7 @@ import {
   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
@@ -122,6 +122,12 @@ function transformComponentElement(
       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)
@@ -437,7 +443,9 @@ function dedupeProperties(results: DirectiveTransformResult[]): IRProp[] {
     }
     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)
       }
diff --git a/packages/compiler-vapor/src/transforms/transformTransition.ts b/packages/compiler-vapor/src/transforms/transformTransition.ts
new file mode 100644 (file)
index 0000000..6014235
--- /dev/null
@@ -0,0 +1,65 @@
+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
+}
index f7d0594fe58151ac73eae39833d1978343d7116b..b746999a18a525db223388f96cc1dcf25ea83765 100644 (file)
@@ -30,6 +30,7 @@ export const newBlock = (node: BlockIRNode['node']): BlockIRNode => ({
   operation: [],
   returns: [],
   tempId: 0,
+  hasDeferredVShow: false,
 })
 
 export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {
index bae9f1aa23f81227c19914fc47b336f3f2db7f17..2426fa0215eeee6a0360b22f67c9562494016d5e 100644 (file)
@@ -18,7 +18,7 @@ import {
 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'],
@@ -135,5 +135,8 @@ export function createIfBranch(
   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]
 }
index f1135d6b0a5902f7d9602c00c2d43b2e3ea2b7d0..a60b20a71fabe83a5d890272190cd14525d899f6 100644 (file)
@@ -2,11 +2,13 @@ import {
   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
@@ -27,11 +29,26 @@ export const transformVShow: DirectiveTransform = (dir, node, context) => {
     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,
   })
 }
index 3e78913a23ef2fdd40c13a8fcb92a78e43225cb5..05aac4aee3c50cdfb86e693356d81d9f9bfa56eb 100644 (file)
@@ -23,7 +23,12 @@ import {
   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) => {
@@ -83,7 +88,18 @@ function transformComponentSlot(
     })
   }
 
-  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
 
@@ -244,9 +260,14 @@ function createSlotBlock(
   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]
 }
index 728281914fd786f67aee7e73d6e4edd38c0ef2b7..d2c7eca3bb1501bd2a1048fccd5e54655de18251 100644 (file)
@@ -15,6 +15,7 @@ import {
 } 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,
@@ -88,3 +89,36 @@ export function getLiteralExpressionValue(
   }
   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'
+  }
+}
index a1409a7fe442e2247b357e40a5449075f95ec471..4c18a11f493f2a726bb1a25f4420bafa36243edd 100644 (file)
@@ -27,7 +27,7 @@ import { warn } from './warning'
 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'
@@ -175,7 +175,6 @@ export interface AppConfig extends GenericAppConfig {
 
 /**
  * The vapor in vdom implementation is in runtime-vapor/src/vdomInterop.ts
- * @internal
  */
 export interface VaporInteropInterface {
   mount(
@@ -188,6 +187,10 @@ export interface VaporInteropInterface {
   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
index 698ed428d1ced079861522ac4efc0f4a29d5bc1c..dcfb81370a6f5a6428bc8037fcb3ef2f40e31c40 100644 (file)
@@ -1,6 +1,8 @@
 import {
   type ComponentInternalInstance,
   type ComponentOptions,
+  type ConcreteComponent,
+  type GenericComponentInstance,
   type SetupContext,
   getCurrentInstance,
 } from '../component'
@@ -19,7 +21,7 @@ import { ErrorCodes, callWithAsyncErrorHandling } from '../errorHandling'
 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[]
@@ -87,7 +89,7 @@ export interface TransitionState {
   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 {
@@ -103,7 +105,7 @@ export function useTransitionState(): TransitionState {
     isMounted: false,
     isLeaving: false,
     isUnmounting: false,
-    leavingVNodes: new Map(),
+    leavingNodes: new Map(),
   }
   onMounted(() => {
     state.isMounted = true
@@ -138,7 +140,9 @@ export const BaseTransitionPropsValidators: Record<string, any> = {
 }
 
 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
 }
 
@@ -164,15 +168,7 @@ const BaseTransitionImpl: ComponentOptions = {
       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)
@@ -309,24 +305,83 @@ function getLeavingNodesForType(
   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,
@@ -344,8 +399,6 @@ export function resolveTransitionHooks(
     onAfterAppear,
     onAppearCancelled,
   } = props
-  const key = String(vnode.key)
-  const leavingVNodesCache = getLeavingNodesForType(state, vnode)
 
   const callHook: TransitionHookCaller = (hook, args) => {
     hook &&
@@ -387,15 +440,7 @@ export function resolveTransitionHooks(
         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])
     },
 
@@ -434,7 +479,7 @@ export function resolveTransitionHooks(
     },
 
     leave(el, remove) {
-      const key = String(vnode.key)
+      // const key = String(vnode.key)
       if (el[enterCbKey]) {
         el[enterCbKey](true /* cancelled */)
       }
@@ -453,11 +498,9 @@ export function resolveTransitionHooks(
           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 {
@@ -465,16 +508,8 @@ export function resolveTransitionHooks(
       }
     },
 
-    clone(vnode) {
-      const hooks = resolveTransitionHooks(
-        vnode,
-        props,
-        state,
-        instance,
-        postClone,
-      )
-      if (postClone) postClone(hooks)
-      return hooks
+    clone(node) {
+      return cloneHooks(node)
     },
   }
 
@@ -524,8 +559,15 @@ function getInnerChild(vnode: VNode): VNode | undefined {
 
 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!)
@@ -571,3 +613,18 @@ export function getTransitionRawChildren(
   }
   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}`)
+  }
+}
index 9d97bb18593ca93a388011aedabb0de5c5b4ac72..aec569123252fc6ee75f1907ac581984f66bfc79 100644 (file)
@@ -118,6 +118,7 @@ export { KeepAlive, type KeepAliveProps } from './components/KeepAlive'
 export {
   BaseTransition,
   BaseTransitionPropsValidators,
+  checkTransitionMode,
   type BaseTransitionProps,
 } from './components/BaseTransition'
 // For using custom directives
@@ -150,8 +151,10 @@ export { registerRuntimeCompiler, isRuntimeOnly } from './component'
 export {
   useTransitionState,
   resolveTransitionHooks,
+  baseResolveTransitionHooks,
   setTransitionHooks,
   getTransitionRawChildren,
+  leaveCbKey,
 } from './components/BaseTransition'
 export { initCustomFormatter } from './customFormatter'
 
@@ -335,6 +338,8 @@ export type { SuspenseBoundary } from './components/Suspense'
 export type {
   TransitionState,
   TransitionHooks,
+  TransitionHooksContext,
+  TransitionElement,
 } from './components/BaseTransition'
 export type {
   AsyncComponentOptions,
@@ -558,6 +563,10 @@ export { startMeasure, endMeasure } from './profiling'
  * @internal
  */
 export { initFeatureFlags } from './featureFlags'
+/**
+ * @internal
+ */
+export { performTransitionEnter, performTransitionLeave } from './renderer'
 /**
  * @internal
  */
index 17ae7c6aba3376f21953eb6dea9579fd48b097cf..7a4e16f8781ca70c8cecb74a05f7137a76858e67 100644 (file)
@@ -738,20 +738,21 @@ function baseCreateRenderer(
     }
     // #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,
@@ -2188,12 +2189,12 @@ function baseCreateRenderer(
       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!
@@ -2387,27 +2388,15 @@ function baseCreateRenderer(
       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!)
     }
   }
 
@@ -2707,7 +2696,7 @@ export function traverseStaticChildren(
 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
@@ -2724,7 +2713,51 @@ export function invalidateMount(hooks: LifecycleHook | undefined): void {
   }
 }
 
-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 {
index 72af535d385e0a837df6fccea72e9f0bae6e7213..abf3e0954243dec714c336f73c3801f35af57543 100644 (file)
@@ -32,7 +32,7 @@ import { extend } from '@vue/shared'
 
 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'> & {
@@ -88,7 +88,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
 
       // 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)
 
@@ -97,20 +97,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
 
       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 = []
     })
@@ -179,8 +166,7 @@ export const TransitionGroup = TransitionGroupImpl as unknown as {
   }
 }
 
-function callPendingCbs(c: VNode) {
-  const el = c.el as any
+export function callPendingCbs(el: any): void {
   if (el[moveCbKey]) {
     el[moveCbKey]()
   }
@@ -194,19 +180,36 @@ function recordPosition(c: VNode) {
 }
 
 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,
@@ -233,3 +236,24 @@ function hasCSSTransform(
   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)
+}
index 64ba93e0e3e98e8d86b2d6d080d4a0e3a0c3afcb..2ead79760ccc406db1d87d97625c4b0c2248688d 100644 (file)
@@ -348,6 +348,27 @@ export {
   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
  */
index 945e0f38d8729b9f97fb2ef42296d8dcf62b453f..8a127c2daf196ce32dc8fa386b19fe1e4ad58798 100644 (file)
@@ -1,6 +1,6 @@
-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'
@@ -31,6 +31,8 @@ export function createDynamicComponent(
 
   renderEffect(() => {
     const value = getter()
+    const appContext =
+      (currentInstance && currentInstance.appContext) || emptyContext
     frag.update(
       () =>
         createComponentWithFallback(
@@ -38,6 +40,7 @@ export function createDynamicComponent(
           rawProps,
           rawSlots,
           isSingleRoot,
+          appContext,
         ),
       value,
     )
index ffe91a87c4983fa22f4a7b07f428a8bab3a3cd05..763bb1defed23dd7b90eb9b34a31da8ba69be440 100644 (file)
@@ -24,6 +24,7 @@ import { currentInstance, isVaporComponent } from './component'
 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,
@@ -387,6 +388,11 @@ export const createFor = (
       key2,
     ))
 
+    // apply transition for new nodes
+    if (frag.$transition) {
+      applyTransitionHooks(block.nodes, frag.$transition, false)
+    }
+
     if (parent) insert(block.nodes, parent, anchor)
 
     return block
@@ -596,3 +602,7 @@ export function getRestElement(val: any, keys: string[]): any {
 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
+}
diff --git a/packages/runtime-vapor/src/apiCreateFragment.ts b/packages/runtime-vapor/src/apiCreateFragment.ts
new file mode 100644 (file)
index 0000000..50179b8
--- /dev/null
@@ -0,0 +1,10 @@
+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
+}
index ba84161a71b2430314f5e87244b848a059350c61..a45e98ca6a323eb2e5f1ae0e29f1e5d7af66059f 100644 (file)
@@ -8,21 +8,52 @@ import {
 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
@@ -49,21 +80,38 @@ export class DynamicFragment extends VaporFragment {
 
     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)
@@ -107,11 +155,26 @@ export function insert(
   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) {
@@ -127,9 +190,9 @@ export function insert(
     // 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)
   }
@@ -144,7 +207,15 @@ export function prepend(parent: ParentNode, ...blocks: Block[]): void {
 
 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)) {
@@ -154,7 +225,7 @@ export function remove(block: Block, parent?: ParentNode): void {
   } else {
     // fragment
     if (block.remove) {
-      block.remove(parent)
+      block.remove(parent, (block as TransitionBlock).$transition)
     } else {
       remove(block.nodes, parent)
     }
index 08fd881e9596ea578114a8c6d1b64ce28bddde10..4f8a2f5b2d7f447f383b84f3d1f4490184608cd1 100644 (file)
@@ -57,6 +57,7 @@ import {
   getSlot,
 } from './componentSlots'
 import { hmrReload, hmrRerender } from './hmr'
+import { createElement } from './dom/node'
 import { isHydrating, locateHydrationNode } from './dom/hydration'
 import {
   insertionAnchor,
@@ -257,11 +258,7 @@ export function createComponent(
   ) {
     const el = getRootElement(instance)
     if (el) {
-      renderEffect(() => {
-        isApplyingFallthroughProps = true
-        setDynamicProps(el, [instance.attrs])
-        isApplyingFallthroughProps = false
-      })
+      renderEffect(() => applyFallthroughProps(el, instance.attrs))
     }
   }
 
@@ -284,6 +281,15 @@ export function createComponent(
 
 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
  */
@@ -318,7 +324,7 @@ export function devRender(instance: VaporComponentInstance): void {
         )) || []
 }
 
-const emptyContext: GenericAppContext = {
+export const emptyContext: GenericAppContext = {
   app: null as any,
   config: {},
   provides: /*@__PURE__*/ Object.create(null),
@@ -486,11 +492,13 @@ export function createComponentWithFallback(
   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) {
@@ -499,7 +507,6 @@ export function createComponentWithFallback(
     resetInsertionState()
   }
 
-  const el = document.createElement(comp)
   // mark single root
   ;(el as any).$root = isSingleRoot
 
diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts
new file mode 100644 (file)
index 0000000..017cb0f
--- /dev/null
@@ -0,0 +1,326 @@
+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
+}
diff --git a/packages/runtime-vapor/src/components/TransitionGroup.ts b/packages/runtime-vapor/src/components/TransitionGroup.ts
new file mode 100644 (file)
index 0000000..074a28c
--- /dev/null
@@ -0,0 +1,227 @@
+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
+  }
+}
index 6ed28dfddfedff207e33d050c94a80b87430e0fd..bb94acf95c25364e1476cdbdb28c68f20a320d26 100644 (file)
@@ -6,7 +6,12 @@ import {
 } 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 {
@@ -49,13 +54,33 @@ function setDisplay(target: Block, value: unknown): 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(
index 83bc32c57f0b0eef5c82cc6da675e3a244ce3edb..26cb66c462cf6634b1b3f54c1b20881cfc557350 100644 (file)
@@ -1,3 +1,8 @@
+/*! #__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)
index 346bea53eb16659eaea357db91a26bbef4e6cae8..71aaaf2853237b346328b94538e7f653034964e7 100644 (file)
@@ -270,6 +270,9 @@ export function optimizePropertyLookup(): void {
   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 =
index b78ca4e52cfb8fa988d1de60ad7879a00f8f3566..7bfbca4e52b862a303f3f0ead9af091053201ba2 100644 (file)
@@ -1,5 +1,5 @@
+import { child, createElement, createTextNode } from './node'
 import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration'
-import { child, createTextNode } from './node'
 
 let t: HTMLTemplateElement
 
@@ -19,7 +19,7 @@ export function template(html: string, root?: boolean) {
       return createTextNode(html)
     }
     if (!node) {
-      t = t || document.createElement('template')
+      t = t || createElement('template')
       t.innerHTML = html
       node = child(t.content)
     }
index 7a8aea5a0d71778852b4eb97a8b470016c30340e..bad4b4f343a190e8a097f88439b3b5fea7175a4c 100644 (file)
@@ -29,6 +29,7 @@ export {
 } from './dom/prop'
 export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
 export { createIf } from './apiCreateIf'
+export { createKeyedFragment } from './apiCreateFragment'
 export {
   createFor,
   createForSlots,
@@ -46,3 +47,5 @@ export {
   applyDynamicModel,
 } from './directives/vModel'
 export { withVaporDirectives } from './directives/custom'
+export { VaporTransition } from './components/Transition'
+export { VaporTransitionGroup } from './components/TransitionGroup'
index adc54526175c09642e2bb5f0d759e6835a63778a..8458198e7cc4cb3d9c1e4499e70625365185586b 100644 (file)
@@ -7,6 +7,7 @@ import {
   type RendererInternals,
   type ShallowRef,
   type Slots,
+  type TransitionHooks,
   type VNode,
   type VaporInteropInterface,
   createInternalObject,
@@ -16,6 +17,7 @@ import {
   isEmitListener,
   onScopeDispose,
   renderSlot,
+  setTransitionHooks as setVNodeTransitionHooks,
   shallowReactive,
   shallowRef,
   simpleSetCurrentInstance,
@@ -29,13 +31,20 @@ import {
   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`)
 
@@ -50,7 +59,15 @@ const vaporInteropImpl: Omit<
     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 } = [
@@ -70,6 +87,12 @@ const vaporInteropImpl: Omit<
     ))
     instance.rawPropsRef = propsRef
     instance.rawSlotsRef = slotsRef
+    if (vnode.transition) {
+      setVaporTransitionHooks(
+        instance,
+        vnode.transition as VaporTransitionHooks,
+      )
+    }
     mountComponent(instance, container, selfAnchor)
     simpleSetCurrentInstance(prev)
     return instance
@@ -123,6 +146,10 @@ const vaporInteropImpl: Omit<
     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<
@@ -189,12 +216,16 @@ function createVDOMComponent(
 
   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,
@@ -218,6 +249,7 @@ function createVDOMComponent(
     }
 
     frag.nodes = vnode.el as Block
+    simpleSetCurrentInstance(prev)
   }
 
   frag.remove = unmount
index 2ffebeb59508ff2d62c02b75ae99d33e8c976243..ac05a47e7e0e6f3addc996b4a3450eba328358b0 100644 (file)
@@ -50,6 +50,16 @@ interface PuppeteerUtils {
   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 {
@@ -200,6 +210,43 @@ 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,
@@ -219,5 +266,7 @@ export function setupPuppeteer(args?: string[]): PuppeteerUtils {
     clearValue,
     timeout,
     nextFrame,
+    transitionStart,
+    waitForElement,
   }
 }
diff --git a/packages/vue/__tests__/e2e/style.css b/packages/vue/__tests__/e2e/style.css
new file mode 100644 (file)
index 0000000..ae6749b
--- /dev/null
@@ -0,0 +1,77 @@
+.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;
+  }
+}
index ab404d67dc7286fbc05e20c7bf47c9a5bd5529da..7f5fce9e34a8a90e979b707551d548243bc1489a 100644 (file)
@@ -1,82 +1,4 @@
 <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" />