Skip to content
3 changes: 2 additions & 1 deletion src/platforms/web/runtime/components/transition.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export const transitionProps = {
leaveActiveClass: String,
appearClass: String,
appearActiveClass: String,
appearToClass: String
appearToClass: String,
duration: [Number, Object]
}

// in case the child is also an abstract component, e.g. <keep-alive>
Expand Down
60 changes: 51 additions & 9 deletions src/platforms/web/runtime/modules/transition.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/* @flow */

import { inBrowser, isIE9 } from 'core/util/index'
import { once } from 'shared/util'
import { once, isObject } from 'shared/util'
import { inBrowser, isIE9, warn } from 'core/util/index'
import { mergeVNodeHook } from 'core/vdom/helpers/index'
import { activeInstance } from 'core/instance/lifecycle'
import {
resolveTransition,
nextFrame,
resolveTransition,
whenTransitionEnds,
addTransitionClass,
removeTransitionClass,
whenTransitionEnds
removeTransitionClass
} from '../transition-util'

export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
Expand Down Expand Up @@ -47,7 +47,8 @@ export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
beforeAppear,
appear,
afterAppear,
appearCancelled
appearCancelled,
duration
} = data

// activeInstance will always be the <transition> component managing this
Expand All @@ -70,11 +71,17 @@ export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
const startClass = isAppear ? appearClass : enterClass
const activeClass = isAppear ? appearActiveClass : enterActiveClass
const toClass = isAppear ? appearToClass : enterToClass

const beforeEnterHook = isAppear ? (beforeAppear || beforeEnter) : beforeEnter
const enterHook = isAppear ? (typeof appear === 'function' ? appear : enter) : enter
const afterEnterHook = isAppear ? (afterAppear || afterEnter) : afterEnter
const enterCancelledHook = isAppear ? (appearCancelled || enterCancelled) : enterCancelled

const explicitEnterDuration = isObject(duration) ? duration.enter : duration
if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) {
checkDuration(explicitEnterDuration, 'enter', vnode)
}

const expectsCSS = css !== false && !isIE9
const userWantsControl =
enterHook &&
Expand Down Expand Up @@ -121,7 +128,11 @@ export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
addTransitionClass(el, toClass)
removeTransitionClass(el, startClass)
if (!cb.cancelled && !userWantsControl) {
whenTransitionEnds(el, type, cb)
if (isValidDuration(explicitEnterDuration)) {
setTimeout(cb, explicitEnterDuration)
} else {
whenTransitionEnds(el, type, cb)
}
}
})
}
Expand Down Expand Up @@ -165,7 +176,8 @@ export function leave (vnode: VNodeWithData, rm: Function) {
leave,
afterLeave,
leaveCancelled,
delayLeave
delayLeave,
duration
} = data

const expectsCSS = css !== false && !isIE9
Expand All @@ -175,6 +187,11 @@ export function leave (vnode: VNodeWithData, rm: Function) {
// the length of original fn as _length
(leave._length || leave.length) > 1

const explicitLeaveDuration = isObject(duration) ? duration.leave : duration
if (process.env.NODE_ENV !== 'production' && explicitLeaveDuration != null) {
checkDuration(explicitLeaveDuration, 'leave', vnode)
}

const cb = el._leaveCb = once(() => {
if (el.parentNode && el.parentNode._pending) {
el.parentNode._pending[vnode.key] = null
Expand Down Expand Up @@ -218,7 +235,11 @@ export function leave (vnode: VNodeWithData, rm: Function) {
addTransitionClass(el, leaveToClass)
removeTransitionClass(el, leaveClass)
if (!cb.cancelled && !userWantsControl) {
whenTransitionEnds(el, type, cb)
if (isValidDuration(explicitLeaveDuration)) {
setTimeout(cb, explicitLeaveDuration)
} else {
whenTransitionEnds(el, type, cb)
}
}
})
}
Expand All @@ -229,6 +250,27 @@ export function leave (vnode: VNodeWithData, rm: Function) {
}
}

// only used in dev mode
function checkDuration (val, name, vnode) {
if (typeof val !== 'number') {
warn(
`<transition> explicit ${name} duration is not a valid number - ` +
`got ${JSON.stringify(val)}.`,
vnode.context
)
} else if (isNaN(val)) {
warn(
`<transition> explicit ${name} duration is NaN - ` +
'the duration expression might be incorrect.',
vnode.context
)
}
}

function isValidDuration (val) {
return typeof val === 'number' && !isNaN(val)
}

function _enter (_: any, vnode: VNodeWithData) {
if (!vnode.data.show) {
enter(vnode)
Expand Down
2 changes: 1 addition & 1 deletion src/platforms/web/runtime/transition-util.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* @flow */

import { inBrowser, isIE9 } from 'core/util/index'
import { remove, extend, cached } from 'shared/util'
import { addClass, removeClass } from './class-util'
import { remove, extend, cached } from 'shared/util'

export function resolveTransition (def?: string | Object): ?Object {
if (!def) {
Expand Down
228 changes: 228 additions & 0 deletions test/unit/features/transition/transition.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { nextFrame } from 'web/runtime/transition-util'
if (!isIE9) {
describe('Transition basic', () => {
const { duration, buffer } = injectStyles()
const explicitDuration = 100

let el
beforeEach(() => {
Expand Down Expand Up @@ -875,5 +876,232 @@ if (!isIE9) {
}).$mount()
expect(`<transition> can only be used on a single element`).toHaveBeenWarned()
})

it('explicit transition total duration', done => {
const vm = new Vue({
template: `
<div>
<transition :duration="${explicitDuration}">
<div v-if="ok" class="test">foo</div>
</transition>
</div>
`,
data: { ok: true }
}).$mount(el)

vm.ok = false

waitForUpdate(() => {
expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
}).thenWaitFor(explicitDuration - buffer).then(() => {
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
}).thenWaitFor(buffer * 2).then(() => {
expect(vm.$el.children.length).toBe(0)
vm.ok = true
}).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
}).thenWaitFor(explicitDuration - buffer).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
}).thenWaitFor(buffer * 2).then(() => {
expect(vm.$el.children[0].className).toBe('test')
}).then(done)
})

it('explicit transition enter duration and auto leave duration', done => {
const vm = new Vue({
template: `
<div>
<transition :duration="{ enter: ${explicitDuration} }">
<div v-if="ok" class="test">foo</div>
</transition>
</div>
`,
data: { ok: true }
}).$mount(el)

vm.ok = false

waitForUpdate(() => {
expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
}).thenWaitFor(duration - buffer).then(() => {
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
}).thenWaitFor(buffer * 2).then(() => {
expect(vm.$el.children.length).toBe(0)
vm.ok = true
}).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
}).thenWaitFor(explicitDuration - buffer).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
}).thenWaitFor(buffer * 2).then(() => {
expect(vm.$el.children[0].className).toBe('test')
}).then(done)
})

it('explicit transition leave duration and auto enter duration', done => {
const vm = new Vue({
template: `
<div>
<transition :duration="{ leave: ${explicitDuration} }">
<div v-if="ok" class="test">foo</div>
</transition>
</div>
`,
data: { ok: true }
}).$mount(el)

vm.ok = false

waitForUpdate(() => {
expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
}).thenWaitFor(explicitDuration - buffer).then(() => {
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
}).thenWaitFor(buffer * 2).then(() => {
expect(vm.$el.children.length).toBe(0)
vm.ok = true
}).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
}).thenWaitFor(duration - buffer).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
}).thenWaitFor(buffer * 2).then(() => {
expect(vm.$el.children[0].className).toBe('test')
}).then(done)
})

it('explicit transition separate enter and leave duration', done => {
const enter = 100
const leave = 200

const vm = new Vue({
template: `
<div>
<transition :duration="{ enter: ${enter}, leave: ${leave} }">
<div v-if="ok" class="test">foo</div>
</transition>
</div>
`,
data: { ok: true }
}).$mount(el)

vm.ok = false

waitForUpdate(() => {
expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
}).thenWaitFor(leave - buffer).then(() => {
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
}).thenWaitFor(buffer * 2).then(() => {
expect(vm.$el.children.length).toBe(0)
vm.ok = true
}).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
}).thenWaitFor(enter - buffer).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
}).thenWaitFor(buffer * 2).then(() => {
expect(vm.$el.children[0].className).toBe('test')
}).then(done)
})

it('explicit transition enter and leave duration + duration change', done => {
const enter1 = 200
const enter2 = 100
const leave1 = 50
const leave2 = 300

const vm = new Vue({
template: `
<div>
<transition :duration="{ enter: enter, leave: leave }">
<div v-if="ok" class="test">foo</div>
</transition>
</div>
`,
data: {
ok: true,
enter: enter1,
leave: leave1
}
}).$mount(el)

vm.ok = false

waitForUpdate(() => {
expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
}).thenWaitFor(leave1 - buffer).then(() => {
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
}).thenWaitFor(buffer * 2).then(() => {
expect(vm.$el.children.length).toBe(0)
vm.ok = true
}).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
}).thenWaitFor(enter1 - buffer).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
}).thenWaitFor(buffer * 2).then(() => {
expect(vm.$el.children[0].className).toBe('test')
vm.enter = enter2
vm.leave = leave2
}).then(() => {
vm.ok = false
}).then(() => {
expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
}).thenWaitFor(leave2 - buffer).then(() => {
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
}).thenWaitFor(buffer * 2).then(() => {
expect(vm.$el.children.length).toBe(0)
vm.ok = true
}).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
}).thenWaitFor(enter2 - buffer).then(() => {
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
}).thenWaitFor(buffer * 2).then(() => {
expect(vm.$el.children[0].className).toBe('test')
}).then(done)
})

it('warn invalid explicit durations', done => {
const vm = new Vue({
template: `
<div>
<transition :duration="{ enter: NaN, leave: 'foo' }">
<div v-if="ok" class="test">foo</div>
</transition>
</div>
`,
data: {
ok: true
}
}).$mount(el)

vm.ok = false
waitForUpdate(() => {
expect(`<transition> explicit leave duration is not a valid number - got "foo"`).toHaveBeenWarned()
}).thenWaitFor(duration + buffer).then(() => {
vm.ok = true
}).then(() => {
expect(`<transition> explicit enter duration is NaN`).toHaveBeenWarned()
}).then(done)
})
})
}