Skip to content

Commit 0598ab0

Browse files
Guillaume Chauyyx990803
Guillaume Chau
authored andcommitted
#4371 - Explicit transition durations (#4857)
* Add transition explicit duration * Fix tests for explicit transition duration * Tweaks & default to milliseconds * Better tests * Better test for change value case * Fix transition duration tests * Better flow typing * Fix transition test * Revert "Fix transition test" This reverts commit db75b3801ed11182119c78ebae87f40a62803714. * Fix transition test Revert "Fix transition test" This reverts commit db75b3801ed11182119c78ebae87f40a62803714. Fix transition test * Better flow types * Warn message * Better prop handling * Better flow typings * adjustments
1 parent acec8db commit 0598ab0

File tree

4 files changed

+282
-11
lines changed

4 files changed

+282
-11
lines changed

src/platforms/web/runtime/components/transition.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export const transitionProps = {
2121
leaveActiveClass: String,
2222
appearClass: String,
2323
appearActiveClass: String,
24-
appearToClass: String
24+
appearToClass: String,
25+
duration: [Number, Object]
2526
}
2627

2728
// in case the child is also an abstract component, e.g. <keep-alive>

src/platforms/web/runtime/modules/transition.js

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
/* @flow */
22

3-
import { inBrowser, isIE9 } from 'core/util/index'
4-
import { once } from 'shared/util'
3+
import { once, isObject } from 'shared/util'
4+
import { inBrowser, isIE9, warn } from 'core/util/index'
55
import { mergeVNodeHook } from 'core/vdom/helpers/index'
66
import { activeInstance } from 'core/instance/lifecycle'
77
import {
8-
resolveTransition,
98
nextFrame,
9+
resolveTransition,
10+
whenTransitionEnds,
1011
addTransitionClass,
11-
removeTransitionClass,
12-
whenTransitionEnds
12+
removeTransitionClass
1313
} from '../transition-util'
1414

1515
export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
@@ -47,7 +47,8 @@ export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
4747
beforeAppear,
4848
appear,
4949
afterAppear,
50-
appearCancelled
50+
appearCancelled,
51+
duration
5152
} = data
5253

5354
// activeInstance will always be the <transition> component managing this
@@ -70,11 +71,17 @@ export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
7071
const startClass = isAppear ? appearClass : enterClass
7172
const activeClass = isAppear ? appearActiveClass : enterActiveClass
7273
const toClass = isAppear ? appearToClass : enterToClass
74+
7375
const beforeEnterHook = isAppear ? (beforeAppear || beforeEnter) : beforeEnter
7476
const enterHook = isAppear ? (typeof appear === 'function' ? appear : enter) : enter
7577
const afterEnterHook = isAppear ? (afterAppear || afterEnter) : afterEnter
7678
const enterCancelledHook = isAppear ? (appearCancelled || enterCancelled) : enterCancelled
7779

80+
const explicitEnterDuration = isObject(duration) ? duration.enter : duration
81+
if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) {
82+
checkDuration(explicitEnterDuration, 'enter', vnode)
83+
}
84+
7885
const expectsCSS = css !== false && !isIE9
7986
const userWantsControl =
8087
enterHook &&
@@ -121,7 +128,11 @@ export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
121128
addTransitionClass(el, toClass)
122129
removeTransitionClass(el, startClass)
123130
if (!cb.cancelled && !userWantsControl) {
124-
whenTransitionEnds(el, type, cb)
131+
if (isValidDuration(explicitEnterDuration)) {
132+
setTimeout(cb, explicitEnterDuration)
133+
} else {
134+
whenTransitionEnds(el, type, cb)
135+
}
125136
}
126137
})
127138
}
@@ -165,7 +176,8 @@ export function leave (vnode: VNodeWithData, rm: Function) {
165176
leave,
166177
afterLeave,
167178
leaveCancelled,
168-
delayLeave
179+
delayLeave,
180+
duration
169181
} = data
170182

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

190+
const explicitLeaveDuration = isObject(duration) ? duration.leave : duration
191+
if (process.env.NODE_ENV !== 'production' && explicitLeaveDuration != null) {
192+
checkDuration(explicitLeaveDuration, 'leave', vnode)
193+
}
194+
178195
const cb = el._leaveCb = once(() => {
179196
if (el.parentNode && el.parentNode._pending) {
180197
el.parentNode._pending[vnode.key] = null
@@ -218,7 +235,11 @@ export function leave (vnode: VNodeWithData, rm: Function) {
218235
addTransitionClass(el, leaveToClass)
219236
removeTransitionClass(el, leaveClass)
220237
if (!cb.cancelled && !userWantsControl) {
221-
whenTransitionEnds(el, type, cb)
238+
if (isValidDuration(explicitLeaveDuration)) {
239+
setTimeout(cb, explicitLeaveDuration)
240+
} else {
241+
whenTransitionEnds(el, type, cb)
242+
}
222243
}
223244
})
224245
}
@@ -229,6 +250,27 @@ export function leave (vnode: VNodeWithData, rm: Function) {
229250
}
230251
}
231252

253+
// only used in dev mode
254+
function checkDuration (val, name, vnode) {
255+
if (typeof val !== 'number') {
256+
warn(
257+
`<transition> explicit ${name} duration is not a valid number - ` +
258+
`got ${JSON.stringify(val)}.`,
259+
vnode.context
260+
)
261+
} else if (isNaN(val)) {
262+
warn(
263+
`<transition> explicit ${name} duration is NaN - ` +
264+
'the duration expression might be incorrect.',
265+
vnode.context
266+
)
267+
}
268+
}
269+
270+
function isValidDuration (val) {
271+
return typeof val === 'number' && !isNaN(val)
272+
}
273+
232274
function _enter (_: any, vnode: VNodeWithData) {
233275
if (!vnode.data.show) {
234276
enter(vnode)

src/platforms/web/runtime/transition-util.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/* @flow */
22

33
import { inBrowser, isIE9 } from 'core/util/index'
4-
import { remove, extend, cached } from 'shared/util'
54
import { addClass, removeClass } from './class-util'
5+
import { remove, extend, cached } from 'shared/util'
66

77
export function resolveTransition (def?: string | Object): ?Object {
88
if (!def) {

test/unit/features/transition/transition.spec.js

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { nextFrame } from 'web/runtime/transition-util'
66
if (!isIE9) {
77
describe('Transition basic', () => {
88
const { duration, buffer } = injectStyles()
9+
const explicitDuration = 100
910

1011
let el
1112
beforeEach(() => {
@@ -875,5 +876,232 @@ if (!isIE9) {
875876
}).$mount()
876877
expect(`<transition> can only be used on a single element`).toHaveBeenWarned()
877878
})
879+
880+
it('explicit transition total duration', done => {
881+
const vm = new Vue({
882+
template: `
883+
<div>
884+
<transition :duration="${explicitDuration}">
885+
<div v-if="ok" class="test">foo</div>
886+
</transition>
887+
</div>
888+
`,
889+
data: { ok: true }
890+
}).$mount(el)
891+
892+
vm.ok = false
893+
894+
waitForUpdate(() => {
895+
expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
896+
}).thenWaitFor(nextFrame).then(() => {
897+
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
898+
}).thenWaitFor(explicitDuration - buffer).then(() => {
899+
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
900+
}).thenWaitFor(buffer * 2).then(() => {
901+
expect(vm.$el.children.length).toBe(0)
902+
vm.ok = true
903+
}).then(() => {
904+
expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
905+
}).thenWaitFor(nextFrame).then(() => {
906+
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
907+
}).thenWaitFor(explicitDuration - buffer).then(() => {
908+
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
909+
}).thenWaitFor(buffer * 2).then(() => {
910+
expect(vm.$el.children[0].className).toBe('test')
911+
}).then(done)
912+
})
913+
914+
it('explicit transition enter duration and auto leave duration', done => {
915+
const vm = new Vue({
916+
template: `
917+
<div>
918+
<transition :duration="{ enter: ${explicitDuration} }">
919+
<div v-if="ok" class="test">foo</div>
920+
</transition>
921+
</div>
922+
`,
923+
data: { ok: true }
924+
}).$mount(el)
925+
926+
vm.ok = false
927+
928+
waitForUpdate(() => {
929+
expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
930+
}).thenWaitFor(nextFrame).then(() => {
931+
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
932+
}).thenWaitFor(duration - buffer).then(() => {
933+
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
934+
}).thenWaitFor(buffer * 2).then(() => {
935+
expect(vm.$el.children.length).toBe(0)
936+
vm.ok = true
937+
}).then(() => {
938+
expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
939+
}).thenWaitFor(nextFrame).then(() => {
940+
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
941+
}).thenWaitFor(explicitDuration - buffer).then(() => {
942+
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
943+
}).thenWaitFor(buffer * 2).then(() => {
944+
expect(vm.$el.children[0].className).toBe('test')
945+
}).then(done)
946+
})
947+
948+
it('explicit transition leave duration and auto enter duration', done => {
949+
const vm = new Vue({
950+
template: `
951+
<div>
952+
<transition :duration="{ leave: ${explicitDuration} }">
953+
<div v-if="ok" class="test">foo</div>
954+
</transition>
955+
</div>
956+
`,
957+
data: { ok: true }
958+
}).$mount(el)
959+
960+
vm.ok = false
961+
962+
waitForUpdate(() => {
963+
expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
964+
}).thenWaitFor(nextFrame).then(() => {
965+
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
966+
}).thenWaitFor(explicitDuration - buffer).then(() => {
967+
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
968+
}).thenWaitFor(buffer * 2).then(() => {
969+
expect(vm.$el.children.length).toBe(0)
970+
vm.ok = true
971+
}).then(() => {
972+
expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
973+
}).thenWaitFor(nextFrame).then(() => {
974+
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
975+
}).thenWaitFor(duration - buffer).then(() => {
976+
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
977+
}).thenWaitFor(buffer * 2).then(() => {
978+
expect(vm.$el.children[0].className).toBe('test')
979+
}).then(done)
980+
})
981+
982+
it('explicit transition separate enter and leave duration', done => {
983+
const enter = 100
984+
const leave = 200
985+
986+
const vm = new Vue({
987+
template: `
988+
<div>
989+
<transition :duration="{ enter: ${enter}, leave: ${leave} }">
990+
<div v-if="ok" class="test">foo</div>
991+
</transition>
992+
</div>
993+
`,
994+
data: { ok: true }
995+
}).$mount(el)
996+
997+
vm.ok = false
998+
999+
waitForUpdate(() => {
1000+
expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
1001+
}).thenWaitFor(nextFrame).then(() => {
1002+
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
1003+
}).thenWaitFor(leave - buffer).then(() => {
1004+
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
1005+
}).thenWaitFor(buffer * 2).then(() => {
1006+
expect(vm.$el.children.length).toBe(0)
1007+
vm.ok = true
1008+
}).then(() => {
1009+
expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
1010+
}).thenWaitFor(nextFrame).then(() => {
1011+
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
1012+
}).thenWaitFor(enter - buffer).then(() => {
1013+
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
1014+
}).thenWaitFor(buffer * 2).then(() => {
1015+
expect(vm.$el.children[0].className).toBe('test')
1016+
}).then(done)
1017+
})
1018+
1019+
it('explicit transition enter and leave duration + duration change', done => {
1020+
const enter1 = 200
1021+
const enter2 = 100
1022+
const leave1 = 50
1023+
const leave2 = 300
1024+
1025+
const vm = new Vue({
1026+
template: `
1027+
<div>
1028+
<transition :duration="{ enter: enter, leave: leave }">
1029+
<div v-if="ok" class="test">foo</div>
1030+
</transition>
1031+
</div>
1032+
`,
1033+
data: {
1034+
ok: true,
1035+
enter: enter1,
1036+
leave: leave1
1037+
}
1038+
}).$mount(el)
1039+
1040+
vm.ok = false
1041+
1042+
waitForUpdate(() => {
1043+
expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
1044+
}).thenWaitFor(nextFrame).then(() => {
1045+
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
1046+
}).thenWaitFor(leave1 - buffer).then(() => {
1047+
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
1048+
}).thenWaitFor(buffer * 2).then(() => {
1049+
expect(vm.$el.children.length).toBe(0)
1050+
vm.ok = true
1051+
}).then(() => {
1052+
expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
1053+
}).thenWaitFor(nextFrame).then(() => {
1054+
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
1055+
}).thenWaitFor(enter1 - buffer).then(() => {
1056+
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
1057+
}).thenWaitFor(buffer * 2).then(() => {
1058+
expect(vm.$el.children[0].className).toBe('test')
1059+
vm.enter = enter2
1060+
vm.leave = leave2
1061+
}).then(() => {
1062+
vm.ok = false
1063+
}).then(() => {
1064+
expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
1065+
}).thenWaitFor(nextFrame).then(() => {
1066+
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
1067+
}).thenWaitFor(leave2 - buffer).then(() => {
1068+
expect(vm.$el.children[0].className).toBe('test v-leave-active v-leave-to')
1069+
}).thenWaitFor(buffer * 2).then(() => {
1070+
expect(vm.$el.children.length).toBe(0)
1071+
vm.ok = true
1072+
}).then(() => {
1073+
expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
1074+
}).thenWaitFor(nextFrame).then(() => {
1075+
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
1076+
}).thenWaitFor(enter2 - buffer).then(() => {
1077+
expect(vm.$el.children[0].className).toBe('test v-enter-active v-enter-to')
1078+
}).thenWaitFor(buffer * 2).then(() => {
1079+
expect(vm.$el.children[0].className).toBe('test')
1080+
}).then(done)
1081+
})
1082+
1083+
it('warn invalid explicit durations', done => {
1084+
const vm = new Vue({
1085+
template: `
1086+
<div>
1087+
<transition :duration="{ enter: NaN, leave: 'foo' }">
1088+
<div v-if="ok" class="test">foo</div>
1089+
</transition>
1090+
</div>
1091+
`,
1092+
data: {
1093+
ok: true
1094+
}
1095+
}).$mount(el)
1096+
1097+
vm.ok = false
1098+
waitForUpdate(() => {
1099+
expect(`<transition> explicit leave duration is not a valid number - got "foo"`).toHaveBeenWarned()
1100+
}).thenWaitFor(duration + buffer).then(() => {
1101+
vm.ok = true
1102+
}).then(() => {
1103+
expect(`<transition> explicit enter duration is NaN`).toHaveBeenWarned()
1104+
}).then(done)
1105+
})
8781106
})
8791107
}

0 commit comments

Comments
 (0)