Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#4371 - Explicit transition durations #4857

Merged
merged 15 commits into from
Feb 15, 2017
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)
})
})
}