diff --git a/src/platforms/web/runtime/components/transition.js b/src/platforms/web/runtime/components/transition.js index 5165456ac51..f23e8f86815 100644 --- a/src/platforms/web/runtime/components/transition.js +++ b/src/platforms/web/runtime/components/transition.js @@ -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. diff --git a/src/platforms/web/runtime/modules/transition.js b/src/platforms/web/runtime/modules/transition.js index 7f547ad8f4e..a542ca5e5b7 100644 --- a/src/platforms/web/runtime/modules/transition.js +++ b/src/platforms/web/runtime/modules/transition.js @@ -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) { @@ -47,7 +47,8 @@ export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) { beforeAppear, appear, afterAppear, - appearCancelled + appearCancelled, + duration } = data // activeInstance will always be the component managing this @@ -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 && @@ -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) + } } }) } @@ -165,7 +176,8 @@ export function leave (vnode: VNodeWithData, rm: Function) { leave, afterLeave, leaveCancelled, - delayLeave + delayLeave, + duration } = data const expectsCSS = css !== false && !isIE9 @@ -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 @@ -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) + } } }) } @@ -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( + ` explicit ${name} duration is not a valid number - ` + + `got ${JSON.stringify(val)}.`, + vnode.context + ) + } else if (isNaN(val)) { + warn( + ` 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) diff --git a/src/platforms/web/runtime/transition-util.js b/src/platforms/web/runtime/transition-util.js index 765f67856df..ad65dc22c28 100644 --- a/src/platforms/web/runtime/transition-util.js +++ b/src/platforms/web/runtime/transition-util.js @@ -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) { diff --git a/test/unit/features/transition/transition.spec.js b/test/unit/features/transition/transition.spec.js index 7d65eeacefa..d80226ac719 100644 --- a/test/unit/features/transition/transition.spec.js +++ b/test/unit/features/transition/transition.spec.js @@ -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(() => { @@ -875,5 +876,232 @@ if (!isIE9) { }).$mount() expect(` can only be used on a single element`).toHaveBeenWarned() }) + + it('explicit transition total duration', done => { + const vm = new Vue({ + template: ` +
+ +
foo
+
+
+ `, + 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: ` +
+ +
foo
+
+
+ `, + 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: ` +
+ +
foo
+
+
+ `, + 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: ` +
+ +
foo
+
+
+ `, + 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: ` +
+ +
foo
+
+
+ `, + 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: ` +
+ +
foo
+
+
+ `, + data: { + ok: true + } + }).$mount(el) + + vm.ok = false + waitForUpdate(() => { + expect(` explicit leave duration is not a valid number - got "foo"`).toHaveBeenWarned() + }).thenWaitFor(duration + buffer).then(() => { + vm.ok = true + }).then(() => { + expect(` explicit enter duration is NaN`).toHaveBeenWarned() + }).then(done) + }) }) }