Skip to content

Commit 071986a

Browse files
committed
fix(transition): fix higher order transition components with merged listeners
fix #3227
1 parent d6607c9 commit 071986a

File tree

3 files changed

+97
-30
lines changed

3 files changed

+97
-30
lines changed

packages/runtime-core/src/components/BaseTransition.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ export interface TransitionHooks<
6969
delayedLeave?(): void
7070
}
7171

72-
type TransitionHookCaller = (
73-
hook: ((el: any) => void) | undefined,
72+
export type TransitionHookCaller = (
73+
hook: ((el: any) => void) | Array<(el: any) => void> | undefined,
7474
args?: any[]
7575
) => void
7676

packages/runtime-dom/src/components/Transition.ts

+39-10
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
compatUtils,
88
DeprecationTypes
99
} from '@vue/runtime-core'
10-
import { isObject, toNumber, extend } from '@vue/shared'
10+
import { isObject, toNumber, extend, isArray } from '@vue/shared'
1111

1212
const TRANSITION = 'transition'
1313
const ANIMATION = 'animation'
@@ -75,6 +75,35 @@ export const TransitionPropsValidators = (Transition.props = /*#__PURE__*/ exten
7575
DOMTransitionPropsValidators
7676
))
7777

78+
/**
79+
* #3227 Incoming hooks may be merged into arrays when wrapping Transition
80+
* with custom HOCs.
81+
*/
82+
const callHook = (
83+
hook: Function | Function[] | undefined,
84+
args: any[] = []
85+
) => {
86+
if (isArray(hook)) {
87+
hook.forEach(h => h(...args))
88+
} else if (hook) {
89+
hook(...args)
90+
}
91+
}
92+
93+
/**
94+
* Check if a hook expects a callback (2nd arg), which means the user
95+
* intends to explicitly control the end of the transition.
96+
*/
97+
const hasExplicitCallback = (
98+
hook: Function | Function[] | undefined
99+
): boolean => {
100+
return hook
101+
? isArray(hook)
102+
? hook.some(h => h.length > 1)
103+
: hook.length > 1
104+
: false
105+
}
106+
78107
export function resolveTransitionProps(
79108
rawProps: TransitionProps
80109
): BaseTransitionProps<Element> {
@@ -154,7 +183,7 @@ export function resolveTransitionProps(
154183
return (el: Element, done: () => void) => {
155184
const hook = isAppear ? onAppear : onEnter
156185
const resolve = () => finishEnter(el, isAppear, done)
157-
hook && hook(el, resolve)
186+
callHook(hook, [el, resolve])
158187
nextFrame(() => {
159188
removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
160189
if (__COMPAT__ && legacyClassEnabled) {
@@ -164,7 +193,7 @@ export function resolveTransitionProps(
164193
)
165194
}
166195
addTransitionClass(el, isAppear ? appearToClass : enterToClass)
167-
if (!(hook && hook.length > 1)) {
196+
if (!hasExplicitCallback(hook)) {
168197
whenTransitionEnds(el, type, enterDuration, resolve)
169198
}
170199
})
@@ -173,15 +202,15 @@ export function resolveTransitionProps(
173202

174203
return extend(baseProps, {
175204
onBeforeEnter(el) {
176-
onBeforeEnter && onBeforeEnter(el)
205+
callHook(onBeforeEnter, [el])
177206
addTransitionClass(el, enterFromClass)
178207
if (__COMPAT__ && legacyClassEnabled) {
179208
addTransitionClass(el, legacyEnterFromClass)
180209
}
181210
addTransitionClass(el, enterActiveClass)
182211
},
183212
onBeforeAppear(el) {
184-
onBeforeAppear && onBeforeAppear(el)
213+
callHook(onBeforeAppear, [el])
185214
addTransitionClass(el, appearFromClass)
186215
if (__COMPAT__ && legacyClassEnabled) {
187216
addTransitionClass(el, legacyAppearFromClass)
@@ -205,23 +234,23 @@ export function resolveTransitionProps(
205234
removeTransitionClass(el, legacyLeaveFromClass)
206235
}
207236
addTransitionClass(el, leaveToClass)
208-
if (!(onLeave && onLeave.length > 1)) {
237+
if (!hasExplicitCallback(onLeave)) {
209238
whenTransitionEnds(el, type, leaveDuration, resolve)
210239
}
211240
})
212-
onLeave && onLeave(el, resolve)
241+
callHook(onLeave, [el, resolve])
213242
},
214243
onEnterCancelled(el) {
215244
finishEnter(el, false)
216-
onEnterCancelled && onEnterCancelled(el)
245+
callHook(onEnterCancelled, [el])
217246
},
218247
onAppearCancelled(el) {
219248
finishEnter(el, true)
220-
onAppearCancelled && onAppearCancelled(el)
249+
callHook(onAppearCancelled, [el])
221250
},
222251
onLeaveCancelled(el) {
223252
finishLeave(el)
224-
onLeaveCancelled && onLeaveCancelled(el)
253+
callHook(onLeaveCancelled, [el])
225254
}
226255
} as BaseTransitionProps<Element>)
227256
}

packages/vue/__tests__/Transition.spec.ts

+56-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
22
import path from 'path'
3-
import { h, createApp, Transition } from 'vue'
3+
import { h, createApp, Transition, ref, nextTick } from 'vue'
44

55
describe('e2e: Transition', () => {
66
const {
@@ -1634,23 +1634,6 @@ describe('e2e: Transition', () => {
16341634
)
16351635
})
16361636

1637-
test(
1638-
'warn when used on multiple elements',
1639-
async () => {
1640-
createApp({
1641-
render() {
1642-
return h(Transition, null, {
1643-
default: () => [h('div'), h('div')]
1644-
})
1645-
}
1646-
}).mount(document.createElement('div'))
1647-
expect(
1648-
'<transition> can only be used on a single element or component'
1649-
).toHaveBeenWarned()
1650-
},
1651-
E2E_TIMEOUT
1652-
)
1653-
16541637
describe('explicit durations', () => {
16551638
test(
16561639
'single value',
@@ -1916,4 +1899,59 @@ describe('e2e: Transition', () => {
19161899
E2E_TIMEOUT
19171900
)
19181901
})
1902+
1903+
test('warn when used on multiple elements', async () => {
1904+
createApp({
1905+
render() {
1906+
return h(Transition, null, {
1907+
default: () => [h('div'), h('div')]
1908+
})
1909+
}
1910+
}).mount(document.createElement('div'))
1911+
expect(
1912+
'<transition> can only be used on a single element or component'
1913+
).toHaveBeenWarned()
1914+
})
1915+
1916+
// #3227
1917+
test(`HOC w/ merged hooks`, async () => {
1918+
const innerSpy = jest.fn()
1919+
const outerSpy = jest.fn()
1920+
1921+
const MyTransition = {
1922+
render(this: any) {
1923+
return h(
1924+
Transition,
1925+
{
1926+
onLeave(el, end) {
1927+
innerSpy()
1928+
end()
1929+
}
1930+
},
1931+
this.$slots.default
1932+
)
1933+
}
1934+
}
1935+
1936+
const toggle = ref(true)
1937+
1938+
const root = document.createElement('div')
1939+
createApp({
1940+
render() {
1941+
return h(
1942+
MyTransition,
1943+
{ onLeave: () => outerSpy() },
1944+
() => (toggle.value ? h('div') : null)
1945+
)
1946+
}
1947+
}).mount(root)
1948+
1949+
expect(root.innerHTML).toBe(`<div></div>`)
1950+
1951+
toggle.value = false
1952+
await nextTick()
1953+
expect(innerSpy).toHaveBeenCalledTimes(1)
1954+
expect(outerSpy).toHaveBeenCalledTimes(1)
1955+
expect(root.innerHTML).toBe(`<!---->`)
1956+
})
19191957
})

0 commit comments

Comments
 (0)