]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(transition): fix higher order transition components with merged listeners
authorEvan You <yyx990803@gmail.com>
Fri, 28 May 2021 19:42:08 +0000 (15:42 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 28 May 2021 19:42:08 +0000 (15:42 -0400)
fix #3227

packages/runtime-core/src/components/BaseTransition.ts
packages/runtime-dom/src/components/Transition.ts
packages/vue/__tests__/Transition.spec.ts

index 1cb47c8195eca8aea9abfdba0c643b5cec01d541..0a6e302a1f579e8b15cb52a0cde30fdaeaadd489 100644 (file)
@@ -69,8 +69,8 @@ export interface TransitionHooks<
   delayedLeave?(): void
 }
 
-type TransitionHookCaller = (
-  hook: ((el: any) => void) | undefined,
+export type TransitionHookCaller = (
+  hook: ((el: any) => void) | Array<(el: any) => void> | undefined,
   args?: any[]
 ) => void
 
index a253f51b435646519c2fee754dc4c928797815c2..e369922fcfaa9ca963358618b628b673139684fd 100644 (file)
@@ -7,7 +7,7 @@ import {
   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'
@@ -75,6 +75,35 @@ export const TransitionPropsValidators = (Transition.props = /*#__PURE__*/ exten
   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> {
@@ -154,7 +183,7 @@ export function resolveTransitionProps(
     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) {
@@ -164,7 +193,7 @@ export function resolveTransitionProps(
           )
         }
         addTransitionClass(el, isAppear ? appearToClass : enterToClass)
-        if (!(hook && hook.length > 1)) {
+        if (!hasExplicitCallback(hook)) {
           whenTransitionEnds(el, type, enterDuration, resolve)
         }
       })
@@ -173,7 +202,7 @@ export function resolveTransitionProps(
 
   return extend(baseProps, {
     onBeforeEnter(el) {
-      onBeforeEnter && onBeforeEnter(el)
+      callHook(onBeforeEnter, [el])
       addTransitionClass(el, enterFromClass)
       if (__COMPAT__ && legacyClassEnabled) {
         addTransitionClass(el, legacyEnterFromClass)
@@ -181,7 +210,7 @@ export function resolveTransitionProps(
       addTransitionClass(el, enterActiveClass)
     },
     onBeforeAppear(el) {
-      onBeforeAppear && onBeforeAppear(el)
+      callHook(onBeforeAppear, [el])
       addTransitionClass(el, appearFromClass)
       if (__COMPAT__ && legacyClassEnabled) {
         addTransitionClass(el, legacyAppearFromClass)
@@ -205,23 +234,23 @@ export function resolveTransitionProps(
           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>)
 }
index 5f9a30fda5221e9f00ce85889c0aa0127cbb38dd..eb56c4fd39b09a7cc1d844186fc6e391a7261c81 100644 (file)
@@ -1,6 +1,6 @@
 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 {
@@ -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', () => {
     test(
       'single value',
@@ -1916,4 +1899,59 @@ describe('e2e: Transition', () => {
       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(`<!---->`)
+  })
 })