--- /dev/null
+import path from 'node:path'
+import {
+ E2E_TIMEOUT,
+ setupPuppeteer,
+} from '../../../packages/vue/__tests__/e2e/e2eUtils'
+import connect from 'connect'
+import sirv from 'sirv'
+import { expect } from 'vitest'
+const { page, nextFrame, timeout, html, transitionStart } = setupPuppeteer()
+
+const duration = process.env.CI ? 200 : 50
+const buffer = process.env.CI ? 50 : 20
+const transitionFinish = (time = duration) => timeout(time + buffer)
+
+describe('vapor transition-group', () => {
+ let server: any
+ const port = '8196'
+ beforeAll(() => {
+ server = connect()
+ .use(sirv(path.resolve(import.meta.dirname, '../dist')))
+ .listen(port)
+ process.on('SIGTERM', () => server && server.close())
+ })
+
+ afterAll(() => {
+ server.close()
+ })
+
+ beforeEach(async () => {
+ const baseUrl = `http://localhost:${port}/transition-group/`
+ await page().goto(baseUrl)
+ await page().waitForSelector('#app')
+ })
+
+ test(
+ 'enter',
+ async () => {
+ const btnSelector = '.enter > button'
+ const containerSelector = '.enter > div'
+
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-from test-enter-active">d</div>` +
+ `<div class="test test-enter-from test-enter-active">e</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-active test-enter-to">d</div>` +
+ `<div class="test test-enter-active test-enter-to">e</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test">d</div>` +
+ `<div class="test">e</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'leave',
+ async () => {
+ const btnSelector = '.leave > button'
+ const containerSelector = '.leave > div'
+
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test test-leave-from test-leave-active">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test test-leave-from test-leave-active">c</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-leave-active test-leave-to">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test test-leave-active test-leave-to">c</div>`,
+ )
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(`<div class="test">b</div>`)
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'enter + leave',
+ async () => {
+ const btnSelector = '.enter-leave > button'
+ const containerSelector = '.enter-leave > div'
+
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test test-leave-from test-leave-active">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-from test-enter-active">d</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-leave-active test-leave-to">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-active test-enter-to">d</div>`,
+ )
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test">d</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'appear',
+ async () => {
+ const btnSelector = '.appear > button'
+ const containerSelector = '.appear > div'
+
+ expect(await html('.appear')).toBe(`<button>appear button</button>`)
+
+ await page().evaluate(() => {
+ return (window as any).setAppear()
+ })
+
+ // appear
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-appear-from test-appear-active">a</div>` +
+ `<div class="test test-appear-from test-appear-active">b</div>` +
+ `<div class="test test-appear-from test-appear-active">c</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-appear-active test-appear-to">a</div>` +
+ `<div class="test test-appear-active test-appear-to">b</div>` +
+ `<div class="test test-appear-active test-appear-to">c</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ // enter
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-from test-enter-active">d</div>` +
+ `<div class="test test-enter-from test-enter-active">e</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-active test-enter-to">d</div>` +
+ `<div class="test test-enter-active test-enter-to">e</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test">d</div>` +
+ `<div class="test">e</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test(
+ 'move',
+ async () => {
+ const btnSelector = '.move > button'
+ const containerSelector = '.move > div'
+
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test group-enter-from group-enter-active">d</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test group-move" style="">a</div>` +
+ `<div class="test group-leave-from group-leave-active group-move" style="">c</div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test group-enter-active group-enter-to">d</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test group-move" style="">a</div>` +
+ `<div class="test group-leave-active group-move group-leave-to" style="">c</div>`,
+ )
+ await transitionFinish(duration * 2)
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">d</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test" style="">a</div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+
+ test('dynamic name', async () => {
+ const btnSelector = '.dynamic-name button.toggleBtn'
+ const btnChangeName = '.dynamic-name button.changeNameBtn'
+ const containerSelector = '.dynamic-name > div'
+
+ expect(await html(containerSelector)).toBe(
+ `<div>a</div>` + `<div>b</div>` + `<div>c</div>`,
+ )
+
+ // invalid name
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(`<div>b</div>` + `<div>c</div>` + `<div>a</div>`)
+
+ // change name
+ expect(
+ (await transitionStart(btnChangeName, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="group-move" style="">a</div>` +
+ `<div class="group-move" style="">b</div>` +
+ `<div class="group-move" style="">c</div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="" style="">a</div>` +
+ `<div class="" style="">b</div>` +
+ `<div class="" style="">c</div>`,
+ )
+ })
+
+ test('events', async () => {
+ const btnSelector = '.events > button'
+ const containerSelector = '.events > div'
+
+ expect(await html('.events')).toBe(`<button>events button</button>`)
+
+ await page().evaluate(() => {
+ return (window as any).setAppear()
+ })
+
+ // appear
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-appear-from test-appear-active">a</div>` +
+ `<div class="test test-appear-from test-appear-active">b</div>` +
+ `<div class="test test-appear-from test-appear-active">c</div>`,
+ )
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-appear-active test-appear-to">a</div>` +
+ `<div class="test test-appear-active test-appear-to">b</div>` +
+ `<div class="test test-appear-active test-appear-to">c</div>`,
+ )
+
+ let calls = await page().evaluate(() => {
+ return (window as any).getCalls()
+ })
+ expect(calls).toContain('beforeAppear')
+ expect(calls).toContain('onAppear')
+ expect(calls).not.toContain('afterAppear')
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>`,
+ )
+
+ expect(
+ await page().evaluate(() => {
+ return (window as any).getCalls()
+ }),
+ ).toContain('afterAppear')
+
+ // enter + leave
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test test-leave-from test-leave-active">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-from test-enter-active">d</div>`,
+ )
+
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls()
+ })
+ expect(calls).toContain('beforeLeave')
+ expect(calls).toContain('onLeave')
+ expect(calls).not.toContain('afterLeave')
+ expect(calls).toContain('beforeEnter')
+ expect(calls).toContain('onEnter')
+ expect(calls).not.toContain('afterEnter')
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test test-leave-active test-leave-to">a</div>` +
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test test-enter-active test-enter-to">d</div>`,
+ )
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls()
+ })
+ expect(calls).not.toContain('afterLeave')
+ expect(calls).not.toContain('afterEnter')
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test">b</div>` +
+ `<div class="test">c</div>` +
+ `<div class="test">d</div>`,
+ )
+
+ calls = await page().evaluate(() => {
+ return (window as any).getCalls()
+ })
+ expect(calls).toContain('afterLeave')
+ expect(calls).toContain('afterEnter')
+ })
+
+ test('interop: render vdom component', async () => {
+ const btnSelector = '.interop > button'
+ const containerSelector = '.interop > div'
+
+ expect(await html(containerSelector)).toBe(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>`,
+ )
+
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div class="test-leave-from test-leave-active"><div>a</div></div>` +
+ `<div class="test-move" style=""><div>b</div></div>` +
+ `<div class="test-move" style=""><div>c</div></div>` +
+ `<div class="test-enter-from test-enter-active"><div>d</div></div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div class="test-leave-active test-leave-to"><div>a</div></div>` +
+ `<div class="test-move" style=""><div>b</div></div>` +
+ `<div class="test-move" style=""><div>c</div></div>` +
+ `<div class="test-enter-active test-enter-to"><div>d</div></div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div class="" style=""><div>b</div></div>` +
+ `<div class="" style=""><div>c</div></div>` +
+ `<div class=""><div>d</div></div>`,
+ )
+ })
+})
expect(calls).toStrictEqual([
'beforeAppear',
- 'onEnter',
+ 'onAppear',
'afterAppear',
'beforeLeave',
'onLeave',
const btnSelector = '.trans-vapor > button'
const containerSelector = '.trans-vapor > div'
- expect(await html(containerSelector)).toBe(
- `<div key="0">vapor compA</div>`,
- )
+ expect(await html(containerSelector)).toBe(`<div>vapor compA</div>`)
// comp leave
expect(
(await transitionStart(btnSelector, containerSelector)).innerHTML,
).toBe(
- `<div key="0" class="v-leave-from v-leave-active">vapor compA</div><!---->`,
+ `<div class="v-leave-from v-leave-active">vapor compA</div><!---->`,
)
await nextFrame()
expect(await html(containerSelector)).toBe(
- `<div key="0" class="v-leave-active v-leave-to">vapor compA</div><!---->`,
+ `<div class="v-leave-active v-leave-to">vapor compA</div><!---->`,
)
await transitionFinish()
// comp enter
expect(
(await transitionStart(btnSelector, containerSelector)).innerHTML,
- ).toBe(
- `<div key="0" class="v-enter-from v-enter-active">vapor compA</div>`,
- )
+ ).toBe(`<div class="v-enter-from v-enter-active">vapor compA</div>`)
await nextFrame()
expect(await html(containerSelector)).toBe(
- `<div key="0" class="v-enter-active v-enter-to">vapor compA</div>`,
+ `<div class="v-enter-active v-enter-to">vapor compA</div>`,
)
await transitionFinish()
expect(await html(containerSelector)).toBe(
- `<div key="0" class="">vapor compA</div>`,
+ `<div class="">vapor compA</div>`,
)
},
E2E_TIMEOUT,
E2E_TIMEOUT,
)
})
+
+ describe('vdom transition-group', () => {
+ test(
+ 'render vapor component',
+ async () => {
+ const btnSelector = '.trans-group-vapor > button'
+ const containerSelector = '.trans-group-vapor > div'
+
+ expect(await html(containerSelector)).toBe(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>`,
+ )
+
+ // insert
+ expect(
+ (await transitionStart(btnSelector, containerSelector)).innerHTML,
+ ).toBe(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>` +
+ `<div class="test-enter-from test-enter-active"><div>d</div></div>` +
+ `<div class="test-enter-from test-enter-active"><div>e</div></div>`,
+ )
+
+ await nextFrame()
+ expect(await html(containerSelector)).toBe(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>` +
+ `<div class="test-enter-active test-enter-to"><div>d</div></div>` +
+ `<div class="test-enter-active test-enter-to"><div>e</div></div>`,
+ )
+
+ await transitionFinish()
+ expect(await html(containerSelector)).toBe(
+ `<div><div>a</div></div>` +
+ `<div><div>b</div></div>` +
+ `<div><div>c</div></div>` +
+ `<div class=""><div>d</div></div>` +
+ `<div class=""><div>e</div></div>`,
+ )
+ },
+ E2E_TIMEOUT,
+ )
+ })
})
<a href="/interop/">VDOM / Vapor interop</a>
<a href="/todomvc/">Vapor TodoMVC</a>
<a href="/transition/">Vapor Transition</a>
+<a href="/transition-group/">Vapor TransitionGroup</a>
+
+<style>
+ a {
+ display: block;
+ margin: 10px;
+ }
+</style>
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)
interopComponent.value =
interopComponent.value === VaporCompA ? VdomComp : VaporCompA
}
+
+const items = ref(['a', 'b', 'c'])
+const enterClick = () => items.value.push('d', 'e')
</script>
<template>
</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>
--- /dev/null
+<script setup vapor>
+import { ref } from 'vue'
+import VdomComp from './components/VdomComp.vue'
+
+const items = ref(['a', 'b', 'c'])
+const enterClick = () => items.value.push('d', 'e')
+const leaveClick = () => (items.value = ['b'])
+const enterLeaveClick = () => (items.value = ['b', 'c', 'd'])
+const appear = ref(false)
+window.setAppear = () => (appear.value = true)
+const moveClick = () => (items.value = ['d', 'b', 'a'])
+
+const name = ref('invalid')
+const dynamicClick = () => (items.value = ['b', 'c', 'a'])
+const changeName = () => {
+ name.value = 'group'
+ items.value = ['a', 'b', 'c']
+}
+
+let calls = []
+window.getCalls = () => {
+ const ret = calls.slice()
+ calls = []
+ return ret
+}
+const eventsClick = () => (items.value = ['b', 'c', 'd'])
+
+const interopClick = () => (items.value = ['b', 'c', 'd'])
+</script>
+
+<template>
+ <div class="transition-group-container">
+ <div class="enter">
+ <button @click="enterClick">enter button</button>
+ <div>
+ <transition-group name="test">
+ <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="leave">
+ <button @click="leaveClick">leave button</button>
+ <div>
+ <transition-group name="test">
+ <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="enter-leave">
+ <button @click="enterLeaveClick">enter-leave button</button>
+ <div>
+ <transition-group name="test">
+ <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="appear">
+ <button @click="enterClick">appear button</button>
+ <div v-if="appear">
+ <transition-group
+ appear
+ appear-from-class="test-appear-from"
+ appear-to-class="test-appear-to"
+ appear-active-class="test-appear-active"
+ name="test"
+ >
+ <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="move">
+ <button @click="moveClick">move button</button>
+ <div>
+ <transition-group name="group">
+ <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="dynamic-name">
+ <button class="toggleBtn" @click="dynamicClick">dynamic button</button>
+ <button class="changeNameBtn" @click="changeName">change name</button>
+ <div>
+ <transition-group :name="name">
+ <div v-for="item in items" :key="item">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="events">
+ <button @click="eventsClick">events button</button>
+ <div v-if="appear">
+ <transition-group
+ name="test"
+ appear
+ appear-from-class="test-appear-from"
+ appear-to-class="test-appear-to"
+ appear-active-class="test-appear-active"
+ @beforeEnter="() => calls.push('beforeEnter')"
+ @enter="() => calls.push('onEnter')"
+ @afterEnter="() => calls.push('afterEnter')"
+ @beforeLeave="() => calls.push('beforeLeave')"
+ @leave="() => calls.push('onLeave')"
+ @afterLeave="() => calls.push('afterLeave')"
+ @beforeAppear="() => calls.push('beforeAppear')"
+ @appear="() => calls.push('onAppear')"
+ @afterAppear="() => calls.push('afterAppear')"
+ >
+ <div v-for="item in items" :key="item" class="test">{{ item }}</div>
+ </transition-group>
+ </div>
+ </div>
+ <div class="interop">
+ <button @click="interopClick">interop button</button>
+ <div>
+ <transition-group name="test">
+ <VdomComp v-for="item in items" :key="item">
+ <div>{{ item }}</div>
+ </VdomComp>
+ </transition-group>
+ </div>
+ </div>
+ </div>
+</template>
+<style>
+.transition-group-container > div {
+ padding: 15px;
+ border: 1px solid #f7f7f7;
+ margin-top: 15px;
+}
+
+.test-move,
+.test-enter-active,
+.test-leave-active {
+ transition: all 50ms cubic-bezier(0.55, 0, 0.1, 1);
+}
+
+.test-enter-from,
+.test-leave-to {
+ opacity: 0;
+ transform: scaleY(0.01) translate(30px, 0);
+}
+
+.test-leave-active {
+ position: absolute;
+}
+</style>
--- /dev/null
+<script vapor>
+const msg = 'vapor comp'
+</script>
+
+<template>
+ <div>
+ <slot />
+ </div>
+</template>
--- /dev/null
+<script setup>
+const msg = 'vdom comp'
+</script>
+
+<template>
+ <div>
+ <slot />
+ </div>
+</template>
--- /dev/null
+<script type="module" src="./main.ts"></script>
+<div id="app"></div>
--- /dev/null
+import { createVaporApp, vaporInteropPlugin } from 'vue'
+import App from './App.vue'
+import '../../../packages/vue/__tests__/e2e/style.css'
+
+createVaporApp(App).use(vaporInteropPlugin).mount('#app')
</script>
<template>
- <div class="vshow">
- <button @click="show = !show">Show</button>
- <Transition>
- <h1 v-show="show">vShow</h1>
- </Transition>
- </div>
- <div class="vif">
- <button @click="toggle = !toggle">Toggle</button>
- <Transition
- appear
- @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')"
- >
- <h1 v-if="toggle">vIf</h1>
- </Transition>
- </div>
- <div class="keyed">
- <button @click="count++">inc</button>
- <Transition>
- <h1 style="position: absolute" :key="count">{{ count }}</h1>
- </Transition>
- </div>
- <div class="out-in">
- <button @click="toggleComponent">toggle out-in</button>
- <div>
- <Transition name="fade" mode="out-in">
- <component :is="activeComponent"></component>
+ <div class="transition-container">
+ <div class="vshow">
+ <button @click="show = !show">Show</button>
+ <Transition>
+ <h1 v-show="show">vShow</h1>
</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>
+ <div class="vif">
+ <button @click="toggle = !toggle">Toggle</button>
+ <Transition
+ appear
+ @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')"
+ >
+ <h1 v-if="toggle">vIf</h1>
</Transition>
</div>
- </div>
- <div class="vdom">
- <button @click="toggleVdom = !toggleVdom">toggle vdom component</button>
- <div>
+ <div class="keyed">
+ <button @click="count++">inc</button>
<Transition>
- <VDomComp v-if="toggleVdom" />
+ <h1 style="position: absolute" :key="count">{{ count }}</h1>
</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 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>
- <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 class="in-out">
+ <button @click="toggleComponent">toggle in-out</button>
+ <div>
+ <Transition name="fade" mode="in-out">
+ <component :is="activeComponent"></component>
+ </Transition>
+ </div>
+ </div>
+ <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>
</div>
</template>
height: 100px;
}
</style>
+<style>
+.transition-container > div {
+ padding: 15px;
+ border: 1px solid #f7f7f7;
+ margin-top: 15px;
+}
+</style>
--- /dev/null
+<script setup vapor lang="ts">
+const msg = 'vapor'
+</script>
+<template>
+ <div>
+ <slot></slot>
+ </div>
+</template>
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')
.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;
+}
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',
+ ),
},
},
},
}
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)
}
context: TransformContext<ElementNode>,
): boolean {
const parentNode = context.parent && context.parent.node
- return !!(parentNode && isTransitionNode(parentNode as ElementNode))
+ return !!(
+ parentNode &&
+ (isTransitionNode(parentNode as ElementNode) ||
+ isTransitionGroupNode(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'
}
}
const positionMap = new WeakMap<VNode, DOMRect>()
const newPositionMap = new WeakMap<VNode, DOMRect>()
-const moveCbKey = Symbol('_moveCb')
+export const moveCbKey: symbol = Symbol('_moveCb')
const enterCbKey = Symbol('_enterCb')
export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
// we divide the work into three loops to avoid mixing DOM reads and writes
// in each iteration - which helps prevent layout thrashing.
- prevChildren.forEach(callPendingCbs)
+ prevChildren.forEach(vnode => callPendingCbs(vnode.el))
prevChildren.forEach(recordPosition)
const movedChildren = prevChildren.filter(applyTranslation)
movedChildren.forEach(c => {
const el = c.el as ElementWithTransition
- const style = el.style
- addTransitionClass(el, moveClass)
- style.transform = style.webkitTransform = style.transitionDuration = ''
- const cb = ((el as any)[moveCbKey] = (e: TransitionEvent) => {
- if (e && e.target !== el) {
- return
- }
- if (!e || /transform$/.test(e.propertyName)) {
- el.removeEventListener('transitionend', cb)
- ;(el as any)[moveCbKey] = null
- removeTransitionClass(el, moveClass)
- }
- })
- el.addEventListener('transitionend', cb)
+ handleMovedChildren(el, moveClass)
})
})
}
}
-function callPendingCbs(c: VNode) {
- const el = c.el as any
+export function callPendingCbs(el: any): void {
if (el[moveCbKey]) {
el[moveCbKey]()
}
}
function applyTranslation(c: VNode): VNode | undefined {
- const oldPos = positionMap.get(c)!
- const newPos = newPositionMap.get(c)!
+ if (
+ baseApplyTranslation(
+ positionMap.get(c)!,
+ newPositionMap.get(c)!,
+ c.el as ElementWithTransition,
+ )
+ ) {
+ return c
+ }
+}
+
+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(
+export function hasCSSTransform(
el: ElementWithTransition,
root: Node,
moveClass: string,
container.removeChild(clone)
return hasTransform
}
+
+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)
+}
export { useCssVars } from './helpers/useCssVars'
// DOM-only components
-export { Transition, type TransitionProps } from './components/Transition'
+export {
+ Transition,
+ type TransitionProps,
+ forceReflow,
+ addTransitionClass,
+ removeTransitionClass,
+} from './components/Transition'
+export type { ElementWithTransition } from './components/Transition'
export {
TransitionGroup,
type TransitionGroupProps,
+ hasCSSTransform,
+ callPendingCbs,
+ moveCbKey,
+ handleMovedChildren,
+ baseApplyTranslation,
} from './components/TransitionGroup'
// **Internal** DOM-only runtime directive helpers
import type { DynamicSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
import { VaporVForFlags } from '../../shared/src/vaporFlags'
+import { applyTransitionEnterHooks } from './components/Transition'
class ForBlock extends VaporFragment {
scope: EffectScope | undefined
getKey && getKey(item, key, index),
))
+ if (frag.$transition) {
+ applyTransitionEnterHooks(block.nodes, frag.$transition)
+ }
+
if (parent) insert(block.nodes, parent, anchor)
return block
}
}
-function normalizeAnchor(node: Block): Node {
- if (node instanceof Node) {
+function normalizeAnchor(node: Block): Node | undefined {
+ if (node && node instanceof Node) {
return node
} else if (isArray(node)) {
return normalizeAnchor(node[0])
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
+}
export interface VaporTransitionHooks extends TransitionHooks {
state: TransitionState
props: TransitionProps
+ disabledOnMoving?: boolean
}
export type TransitionBlock =
if (block instanceof Node) {
if (!isHydrating) {
// don't apply transition on text or comment nodes
- if ((block as TransitionBlock).$transition && block instanceof Element) {
+ if (
+ block instanceof Element &&
+ (block as TransitionBlock).$transition &&
+ !(block as TransitionBlock).$transition!.disabledOnMoving
+ ) {
performTransitionEnter(
block,
(block as TransitionBlock).$transition as TransitionHooks,
isFragment,
} from '../block'
import { type VaporComponentInstance, isVaporComponent } from '../component'
+import { isArray } from '@vue/shared'
const decorate = (t: typeof VaporTransition) => {
t.displayName = 'VaporTransition'
return context
}
-function resolveTransitionHooks(
+export function resolveTransitionHooks(
block: TransitionBlock,
props: TransitionProps,
state: TransitionState,
return hooks
}
-function setTransitionHooks(
+export function setTransitionHooks(
block: TransitionBlock,
hooks: VaporTransitionHooks,
-) {
+): void {
block.$transition = hooks
}
setTransitionHooks(child, enterHooks)
if (isFragment(block)) {
// also set transition hooks on fragment for reusing during it's updating
- setTransitionHooks(block, enterHooks)
+ setTransitionHooksToFragment(block, enterHooks)
}
return enterHooks
}
} else if (isVaporComponent(block)) {
child = findTransitionBlock(block.block)
if (child && child.$key === undefined) child.$key = block.type.__name
- } else if (Array.isArray(block)) {
+ } else if (isArray(block)) {
child = block[0] as TransitionBlock
let hasFound = false
for (const c of block) {
setTransitionHooks(child, hooks)
}
+
+export function setTransitionHooksToFragment(
+ block: Block,
+ hooks: VaporTransitionHooks,
+): void {
+ if (isFragment(block)) {
+ setTransitionHooks(block, hooks)
+ } else if (isArray(block)) {
+ for (let i = 0; i < block.length; i++) {
+ setTransitionHooksToFragment(block[i], hooks)
+ }
+ }
+}
--- /dev/null
+import {
+ type ElementWithTransition,
+ type TransitionGroupProps,
+ TransitionPropsValidators,
+ baseApplyTranslation,
+ callPendingCbs,
+ currentInstance,
+ forceReflow,
+ handleMovedChildren,
+ hasCSSTransform,
+ onBeforeUpdate,
+ onUpdated,
+ resolveTransitionProps,
+ useTransitionState,
+ warn,
+} from '@vue/runtime-dom'
+import { extend, isArray } from '@vue/shared'
+import {
+ type Block,
+ DynamicFragment,
+ type TransitionBlock,
+ type VaporTransitionHooks,
+ insert,
+ isFragment,
+} from '../block'
+import {
+ resolveTransitionHooks,
+ setTransitionHooks,
+ setTransitionHooksToFragment,
+} from './Transition'
+import { type ObjectVaporComponent, isVaporComponent } from '../component'
+import { isForBlock } from '../apiCreateFor'
+import { renderEffect, setDynamicProps } from '@vue/runtime-vapor'
+
+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 }: any) {
+ const instance = currentInstance
+ 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)
+ const hook = (child as TransitionBlock).$transition!
+ // disabled transition during moving, so the children will be
+ // inserted into the correct position immediately. this prevents
+ // `recordPosition` from getting incorrect positions in `onUpdated`
+ hook.disabledOnMoving = true
+ positionMap.set(child, getEl(child).getBoundingClientRect())
+ }
+ }
+ }
+ })
+
+ onUpdated(() => {
+ if (!prevChildren.length) {
+ return
+ }
+
+ const moveClass = props.moveClass || `${props.name || 'v'}-move`
+
+ const firstChild = findFirstChild(prevChildren)
+ if (
+ !firstChild ||
+ !hasCSSTransform(
+ firstChild as ElementWithTransition,
+ firstChild.parentNode as Node,
+ moveClass,
+ )
+ ) {
+ return
+ }
+
+ prevChildren.forEach(callPendingCbs)
+ prevChildren.forEach(child => {
+ delete child.$transition!.disabledOnMoving
+ recordPosition(child)
+ })
+ const movedChildren = prevChildren.filter(applyTranslation)
+
+ // force reflow to put everything in position
+ forceReflow()
+
+ movedChildren.forEach(c =>
+ handleMovedChildren(getEl(c) as ElementWithTransition, moveClass),
+ )
+ })
+
+ slottedBlock = slots.default && slots.default()
+
+ // store props and state on fragment for reusing during insert new items
+ setTransitionHooksToFragment(slottedBlock, {
+ props: cssTransitionProps,
+ state,
+ } as VaporTransitionHooks)
+
+ children = getTransitionBlocks(slottedBlock)
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ if (isValidTransitionBlock(child)) {
+ if ((child as TransitionBlock).$key != null) {
+ setTransitionHooks(
+ child,
+ resolveTransitionHooks(child, cssTransitionProps, state, instance!),
+ )
+ } else if (__DEV__ && (child as TransitionBlock).$key == null) {
+ warn(`<transition-group> children must be keyed`)
+ }
+ }
+ }
+
+ const tag = props.tag
+ if (tag) {
+ const el = document.createElement(tag)
+ insert(slottedBlock, el)
+ // fallthrough attrs
+ renderEffect(() => setDynamicProps(el, [instance!.attrs]))
+ return [el]
+ } else {
+ const frag = __DEV__
+ ? new DynamicFragment('transitionGroup')
+ : 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 getEl(c: TransitionBlock): Element {
+ return (isFragment(c) ? c.nodes : c) as Element
+}
+
+function recordPosition(c: TransitionBlock) {
+ newPositionMap.set(c, getEl(c).getBoundingClientRect())
+}
+
+function applyTranslation(c: TransitionBlock): TransitionBlock | undefined {
+ if (
+ baseApplyTranslation(
+ positionMap.get(c)!,
+ newPositionMap.get(c)!,
+ getEl(c) as ElementWithTransition,
+ )
+ ) {
+ return c
+ }
+}
+
+function findFirstChild(children: TransitionBlock[]): Element | undefined {
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ const el = getEl(child)
+ if (el.isConnected) return el
+ }
+}
} from './directives/vModel'
export { withVaporDirectives } from './directives/custom'
export { VaporTransition } from './components/Transition'
+export { VaporTransitionGroup } from './components/TransitionGroup'
insert,
remove,
} from './block'
-import { EMPTY_OBJ, extend, isFunction } from '@vue/shared'
+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'
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)
// @ts-expect-error
parentInstance as any,
)
}
+ frag.nodes = vnode.el as Node
simpleSetCurrentInstance(prev)
}
--- /dev/null
+.test {
+ -webkit-transition: opacity 50ms ease;
+ transition: opacity 50ms ease;
+}
+.group-move {
+ -webkit-transition: -webkit-transform 50ms ease;
+ transition: transform 50ms ease;
+}
+.v-appear,
+.v-enter,
+.v-leave-active,
+.test-appear,
+.test-enter,
+.test-leave-active,
+.test-reflow-enter,
+.test-reflow-leave-to,
+.hello,
+.bye.active,
+.changed-enter {
+ opacity: 0;
+}
+.test-reflow-leave-active,
+.test-reflow-enter-active {
+ -webkit-transition: opacity 50ms ease;
+ transition: opacity 50ms ease;
+}
+.test-reflow-leave-from {
+ opacity: 0.9;
+}
+.test-anim-enter-active {
+ animation: test-enter 50ms;
+ -webkit-animation: test-enter 50ms;
+}
+.test-anim-leave-active {
+ animation: test-leave 50ms;
+ -webkit-animation: test-leave 50ms;
+}
+.test-anim-long-enter-active {
+ animation: test-enter 100ms;
+ -webkit-animation: test-enter 100ms;
+}
+.test-anim-long-leave-active {
+ animation: test-leave 100ms;
+ -webkit-animation: test-leave 100ms;
+}
+@keyframes test-enter {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@-webkit-keyframes test-enter {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@keyframes test-leave {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+@-webkit-keyframes test-leave {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
<script src="../../dist/vue.global.js"></script>
<div id="app"></div>
-<style>
- .test {
- -webkit-transition: opacity 50ms ease;
- transition: opacity 50ms ease;
- }
- .group-move {
- -webkit-transition: -webkit-transform 50ms ease;
- transition: transform 50ms ease;
- }
- .v-appear,
- .v-enter,
- .v-leave-active,
- .test-appear,
- .test-enter,
- .test-leave-active,
- .test-reflow-enter,
- .test-reflow-leave-to,
- .hello,
- .bye.active,
- .changed-enter {
- opacity: 0;
- }
- .test-reflow-leave-active,
- .test-reflow-enter-active {
- -webkit-transition: opacity 50ms ease;
- transition: opacity 50ms ease;
- }
- .test-reflow-leave-from {
- opacity: 0.9;
- }
- .test-anim-enter-active {
- animation: test-enter 50ms;
- -webkit-animation: test-enter 50ms;
- }
- .test-anim-leave-active {
- animation: test-leave 50ms;
- -webkit-animation: test-leave 50ms;
- }
- .test-anim-long-enter-active {
- animation: test-enter 100ms;
- -webkit-animation: test-enter 100ms;
- }
- .test-anim-long-leave-active {
- animation: test-leave 100ms;
- -webkit-animation: test-leave 100ms;
- }
- @keyframes test-enter {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
- }
- @-webkit-keyframes test-enter {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
- }
- @keyframes test-leave {
- from {
- opacity: 1;
- }
- to {
- opacity: 0;
- }
- }
- @-webkit-keyframes test-leave {
- from {
- opacity: 1;
- }
- to {
- opacity: 0;
- }
- }
-</style>
+<link rel="stylesheet" href="style.css" />