fix(transition): fix higher order transition components with merged listeners

fix #3227
This commit is contained in:
Evan You 2021-05-28 15:42:08 -04:00
parent d6607c9864
commit 071986a2c6
3 changed files with 97 additions and 30 deletions

View File

@ -69,8 +69,8 @@ export interface TransitionHooks<
delayedLeave?(): void delayedLeave?(): void
} }
type TransitionHookCaller = ( export type TransitionHookCaller = (
hook: ((el: any) => void) | undefined, hook: ((el: any) => void) | Array<(el: any) => void> | undefined,
args?: any[] args?: any[]
) => void ) => void

View File

@ -7,7 +7,7 @@ import {
compatUtils, compatUtils,
DeprecationTypes DeprecationTypes
} from '@vue/runtime-core' } from '@vue/runtime-core'
import { isObject, toNumber, extend } from '@vue/shared' import { isObject, toNumber, extend, isArray } from '@vue/shared'
const TRANSITION = 'transition' const TRANSITION = 'transition'
const ANIMATION = 'animation' const ANIMATION = 'animation'
@ -75,6 +75,35 @@ export const TransitionPropsValidators = (Transition.props = /*#__PURE__*/ exten
DOMTransitionPropsValidators 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( export function resolveTransitionProps(
rawProps: TransitionProps rawProps: TransitionProps
): BaseTransitionProps<Element> { ): BaseTransitionProps<Element> {
@ -154,7 +183,7 @@ export function resolveTransitionProps(
return (el: Element, done: () => void) => { return (el: Element, done: () => void) => {
const hook = isAppear ? onAppear : onEnter const hook = isAppear ? onAppear : onEnter
const resolve = () => finishEnter(el, isAppear, done) const resolve = () => finishEnter(el, isAppear, done)
hook && hook(el, resolve) callHook(hook, [el, resolve])
nextFrame(() => { nextFrame(() => {
removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass) removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
if (__COMPAT__ && legacyClassEnabled) { if (__COMPAT__ && legacyClassEnabled) {
@ -164,7 +193,7 @@ export function resolveTransitionProps(
) )
} }
addTransitionClass(el, isAppear ? appearToClass : enterToClass) addTransitionClass(el, isAppear ? appearToClass : enterToClass)
if (!(hook && hook.length > 1)) { if (!hasExplicitCallback(hook)) {
whenTransitionEnds(el, type, enterDuration, resolve) whenTransitionEnds(el, type, enterDuration, resolve)
} }
}) })
@ -173,7 +202,7 @@ export function resolveTransitionProps(
return extend(baseProps, { return extend(baseProps, {
onBeforeEnter(el) { onBeforeEnter(el) {
onBeforeEnter && onBeforeEnter(el) callHook(onBeforeEnter, [el])
addTransitionClass(el, enterFromClass) addTransitionClass(el, enterFromClass)
if (__COMPAT__ && legacyClassEnabled) { if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyEnterFromClass) addTransitionClass(el, legacyEnterFromClass)
@ -181,7 +210,7 @@ export function resolveTransitionProps(
addTransitionClass(el, enterActiveClass) addTransitionClass(el, enterActiveClass)
}, },
onBeforeAppear(el) { onBeforeAppear(el) {
onBeforeAppear && onBeforeAppear(el) callHook(onBeforeAppear, [el])
addTransitionClass(el, appearFromClass) addTransitionClass(el, appearFromClass)
if (__COMPAT__ && legacyClassEnabled) { if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyAppearFromClass) addTransitionClass(el, legacyAppearFromClass)
@ -205,23 +234,23 @@ export function resolveTransitionProps(
removeTransitionClass(el, legacyLeaveFromClass) removeTransitionClass(el, legacyLeaveFromClass)
} }
addTransitionClass(el, leaveToClass) addTransitionClass(el, leaveToClass)
if (!(onLeave && onLeave.length > 1)) { if (!hasExplicitCallback(onLeave)) {
whenTransitionEnds(el, type, leaveDuration, resolve) whenTransitionEnds(el, type, leaveDuration, resolve)
} }
}) })
onLeave && onLeave(el, resolve) callHook(onLeave, [el, resolve])
}, },
onEnterCancelled(el) { onEnterCancelled(el) {
finishEnter(el, false) finishEnter(el, false)
onEnterCancelled && onEnterCancelled(el) callHook(onEnterCancelled, [el])
}, },
onAppearCancelled(el) { onAppearCancelled(el) {
finishEnter(el, true) finishEnter(el, true)
onAppearCancelled && onAppearCancelled(el) callHook(onAppearCancelled, [el])
}, },
onLeaveCancelled(el) { onLeaveCancelled(el) {
finishLeave(el) finishLeave(el)
onLeaveCancelled && onLeaveCancelled(el) callHook(onLeaveCancelled, [el])
} }
} as BaseTransitionProps<Element>) } as BaseTransitionProps<Element>)
} }

View File

@ -1,6 +1,6 @@
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils' import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
import path from 'path' import path from 'path'
import { h, createApp, Transition } from 'vue' import { h, createApp, Transition, ref, nextTick } from 'vue'
describe('e2e: Transition', () => { describe('e2e: Transition', () => {
const { const {
@ -1634,23 +1634,6 @@ describe('e2e: Transition', () => {
) )
}) })
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', () => { describe('explicit durations', () => {
test( test(
'single value', 'single value',
@ -1916,4 +1899,59 @@ describe('e2e: Transition', () => {
E2E_TIMEOUT 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(`<!---->`)
})
}) })