compatUtils,
DeprecationTypes
} from '@vue/runtime-core'
-import { isObject, toNumber, extend } from '@vue/shared'
+import { isObject, toNumber, extend, isArray } from '@vue/shared'
const TRANSITION = 'transition'
const ANIMATION = 'animation'
DOMTransitionPropsValidators
))
+/**
+ * #3227 Incoming hooks may be merged into arrays when wrapping Transition
+ * with custom HOCs.
+ */
+const callHook = (
+ hook: Function | Function[] | undefined,
+ args: any[] = []
+) => {
+ if (isArray(hook)) {
+ hook.forEach(h => h(...args))
+ } else if (hook) {
+ hook(...args)
+ }
+}
+
+/**
+ * Check if a hook expects a callback (2nd arg), which means the user
+ * intends to explicitly control the end of the transition.
+ */
+const hasExplicitCallback = (
+ hook: Function | Function[] | undefined
+): boolean => {
+ return hook
+ ? isArray(hook)
+ ? hook.some(h => h.length > 1)
+ : hook.length > 1
+ : false
+}
+
export function resolveTransitionProps(
rawProps: TransitionProps
): BaseTransitionProps<Element> {
return (el: Element, done: () => void) => {
const hook = isAppear ? onAppear : onEnter
const resolve = () => finishEnter(el, isAppear, done)
- hook && hook(el, resolve)
+ callHook(hook, [el, resolve])
nextFrame(() => {
removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
if (__COMPAT__ && legacyClassEnabled) {
)
}
addTransitionClass(el, isAppear ? appearToClass : enterToClass)
- if (!(hook && hook.length > 1)) {
+ if (!hasExplicitCallback(hook)) {
whenTransitionEnds(el, type, enterDuration, resolve)
}
})
return extend(baseProps, {
onBeforeEnter(el) {
- onBeforeEnter && onBeforeEnter(el)
+ callHook(onBeforeEnter, [el])
addTransitionClass(el, enterFromClass)
if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyEnterFromClass)
addTransitionClass(el, enterActiveClass)
},
onBeforeAppear(el) {
- onBeforeAppear && onBeforeAppear(el)
+ callHook(onBeforeAppear, [el])
addTransitionClass(el, appearFromClass)
if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyAppearFromClass)
removeTransitionClass(el, legacyLeaveFromClass)
}
addTransitionClass(el, leaveToClass)
- if (!(onLeave && onLeave.length > 1)) {
+ if (!hasExplicitCallback(onLeave)) {
whenTransitionEnds(el, type, leaveDuration, resolve)
}
})
- onLeave && onLeave(el, resolve)
+ callHook(onLeave, [el, resolve])
},
onEnterCancelled(el) {
finishEnter(el, false)
- onEnterCancelled && onEnterCancelled(el)
+ callHook(onEnterCancelled, [el])
},
onAppearCancelled(el) {
finishEnter(el, true)
- onAppearCancelled && onAppearCancelled(el)
+ callHook(onAppearCancelled, [el])
},
onLeaveCancelled(el) {
finishLeave(el)
- onLeaveCancelled && onLeaveCancelled(el)
+ callHook(onLeaveCancelled, [el])
}
} as BaseTransitionProps<Element>)
}
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
import path from 'path'
-import { h, createApp, Transition } from 'vue'
+import { h, createApp, Transition, ref, nextTick } from 'vue'
describe('e2e: Transition', () => {
const {
)
})
- test(
- 'warn when used on multiple elements',
- async () => {
- createApp({
- render() {
- return h(Transition, null, {
- default: () => [h('div'), h('div')]
- })
- }
- }).mount(document.createElement('div'))
- expect(
- '<transition> can only be used on a single element or component'
- ).toHaveBeenWarned()
- },
- E2E_TIMEOUT
- )
-
describe('explicit durations', () => {
test(
'single value',
E2E_TIMEOUT
)
})
+
+ test('warn when used on multiple elements', async () => {
+ createApp({
+ render() {
+ return h(Transition, null, {
+ default: () => [h('div'), h('div')]
+ })
+ }
+ }).mount(document.createElement('div'))
+ expect(
+ '<transition> can only be used on a single element or component'
+ ).toHaveBeenWarned()
+ })
+
+ // #3227
+ test(`HOC w/ merged hooks`, async () => {
+ const innerSpy = jest.fn()
+ const outerSpy = jest.fn()
+
+ const MyTransition = {
+ render(this: any) {
+ return h(
+ Transition,
+ {
+ onLeave(el, end) {
+ innerSpy()
+ end()
+ }
+ },
+ this.$slots.default
+ )
+ }
+ }
+
+ const toggle = ref(true)
+
+ const root = document.createElement('div')
+ createApp({
+ render() {
+ return h(
+ MyTransition,
+ { onLeave: () => outerSpy() },
+ () => (toggle.value ? h('div') : null)
+ )
+ }
+ }).mount(root)
+
+ expect(root.innerHTML).toBe(`<div></div>`)
+
+ toggle.value = false
+ await nextTick()
+ expect(innerSpy).toHaveBeenCalledTimes(1)
+ expect(outerSpy).toHaveBeenCalledTimes(1)
+ expect(root.innerHTML).toBe(`<!---->`)
+ })
})