]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(transition): fix appear hooks handling
authorEvan You <yyx990803@gmail.com>
Thu, 25 Jun 2020 20:02:28 +0000 (16:02 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 25 Jun 2020 20:02:28 +0000 (16:02 -0400)
packages/runtime-core/__tests__/components/BaseTransition.spec.ts
packages/runtime-core/src/components/BaseTransition.ts
packages/runtime-dom/src/components/Transition.ts
packages/vue/__tests__/Transition.spec.ts

index a1dfa4d61ea3e28413caeaef75e86ed5b12c69d3..ccb1ae61720460d45a0be8b822c9e15e3490e569 100644 (file)
@@ -53,6 +53,12 @@ function mockProps(extra: BaseTransitionProps = {}, withKeepAlive = false) {
     }),
     onAfterLeave: jest.fn(),
     onLeaveCancelled: jest.fn(),
+    onBeforeAppear: jest.fn(),
+    onAppear: jest.fn((el, done) => {
+      cbs.doneEnter[serialize(el as TestElement)] = done
+    }),
+    onAfterAppear: jest.fn(),
+    onAppearCancelled: jest.fn(),
     ...extra
   }
   return {
@@ -132,8 +138,33 @@ function runTestWithKeepAlive(tester: TestFn) {
 }
 
 describe('BaseTransition', () => {
-  test('appear: true', () => {
-    const { props, cbs } = mockProps({ appear: true })
+  test('appear: true w/ appear hooks', () => {
+    const { props, cbs } = mockProps({
+      appear: true
+    })
+    mount(props, () => h('div'))
+    expect(props.onBeforeAppear).toHaveBeenCalledTimes(1)
+    expect(props.onAppear).toHaveBeenCalledTimes(1)
+    expect(props.onAfterAppear).not.toHaveBeenCalled()
+
+    // enter should not be called
+    expect(props.onBeforeEnter).not.toHaveBeenCalled()
+    expect(props.onEnter).not.toHaveBeenCalled()
+    expect(props.onAfterEnter).not.toHaveBeenCalled()
+
+    cbs.doneEnter[`<div></div>`]()
+    expect(props.onAfterAppear).toHaveBeenCalledTimes(1)
+    expect(props.onAfterEnter).not.toHaveBeenCalled()
+  })
+
+  test('appear: true w/ fallback to enter hooks', () => {
+    const { props, cbs } = mockProps({
+      appear: true,
+      onBeforeAppear: undefined,
+      onAppear: undefined,
+      onAfterAppear: undefined,
+      onAppearCancelled: undefined
+    })
     mount(props, () => h('div'))
     expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
     expect(props.onEnter).toHaveBeenCalledTimes(1)
@@ -207,11 +238,11 @@ describe('BaseTransition', () => {
       const { hooks } = mockPersistedHooks()
       mount(props, () => h('div', hooks))
 
-      expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
-      expect(props.onEnter).toHaveBeenCalledTimes(1)
-      expect(props.onAfterEnter).not.toHaveBeenCalled()
+      expect(props.onBeforeAppear).toHaveBeenCalledTimes(1)
+      expect(props.onAppear).toHaveBeenCalledTimes(1)
+      expect(props.onAfterAppear).not.toHaveBeenCalled()
       cbs.doneEnter[`<div></div>`]()
-      expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
+      expect(props.onAfterAppear).toHaveBeenCalledTimes(1)
     })
   })
 
index b2d7c32e11e1dd82e7f0a5419c1573d3360f5d0e..a05f6ff5c059e6df477eac236413095d91740f56 100644 (file)
@@ -41,9 +41,16 @@ export interface BaseTransitionProps<HostElement = RendererElement> {
   onLeave?: (el: HostElement, done: () => void) => void
   onAfterLeave?: (el: HostElement) => void
   onLeaveCancelled?: (el: HostElement) => void // only fired in persisted mode
+  // appear
+  onBeforeAppear?: (el: HostElement) => void
+  onAppear?: (el: HostElement, done: () => void) => void
+  onAfterAppear?: (el: HostElement) => void
+  onAppearCancelled?: (el: HostElement) => void
 }
 
-export interface TransitionHooks<HostElement extends RendererElement = RendererElement> {
+export interface TransitionHooks<
+  HostElement extends RendererElement = RendererElement
+> {
   persisted: boolean
   beforeEnter(el: HostElement): void
   enter(el: HostElement): void
@@ -115,7 +122,12 @@ const BaseTransitionImpl = {
     onBeforeLeave: Function,
     onLeave: Function,
     onAfterLeave: Function,
-    onLeaveCancelled: Function
+    onLeaveCancelled: Function,
+    // appear
+    onBeforeAppear: Function,
+    onAppear: Function,
+    onAfterAppear: Function,
+    onAppearCancelled: Function
   },
 
   setup(props: BaseTransitionProps, { slots }: SetupContext) {
@@ -254,7 +266,11 @@ export function resolveTransitionHooks(
     onBeforeLeave,
     onLeave,
     onAfterLeave,
-    onLeaveCancelled
+    onLeaveCancelled,
+    onBeforeAppear,
+    onAppear,
+    onAfterAppear,
+    onAppearCancelled
   }: BaseTransitionProps<any>,
   state: TransitionState,
   instance: ComponentInternalInstance
@@ -275,8 +291,13 @@ export function resolveTransitionHooks(
   const hooks: TransitionHooks<TransitionElement> = {
     persisted,
     beforeEnter(el) {
-      if (!appear && !state.isMounted) {
-        return
+      let hook = onBeforeEnter
+      if (!state.isMounted) {
+        if (appear) {
+          hook = onBeforeAppear || onBeforeEnter
+        } else {
+          return
+        }
       }
       // for same element (v-show)
       if (el._leaveCb) {
@@ -292,31 +313,40 @@ export function resolveTransitionHooks(
         // force early removal (not cancelled)
         leavingVNode.el!._leaveCb()
       }
-      callHook(onBeforeEnter, [el])
+      callHook(hook, [el])
     },
 
     enter(el) {
-      if (!appear && !state.isMounted) {
-        return
+      let hook = onEnter
+      let afterHook = onAfterEnter
+      let cancelHook = onEnterCancelled
+      if (!state.isMounted) {
+        if (appear) {
+          hook = onAppear || onEnter
+          afterHook = onAfterAppear || onAfterEnter
+          cancelHook = onAppearCancelled || onEnterCancelled
+        } else {
+          return
+        }
       }
       let called = false
-      const afterEnter = (el._enterCb = (cancelled?) => {
+      const done = (el._enterCb = (cancelled?) => {
         if (called) return
         called = true
         if (cancelled) {
-          callHook(onEnterCancelled, [el])
+          callHook(cancelHook, [el])
         } else {
-          callHook(onAfterEnter, [el])
+          callHook(afterHook, [el])
         }
         if (hooks.delayedLeave) {
           hooks.delayedLeave()
         }
         el._enterCb = undefined
       })
-      if (onEnter) {
-        onEnter(el, afterEnter)
+      if (hook) {
+        hook(el, done)
       } else {
-        afterEnter()
+        done()
       }
     },
 
index 967a4ee568e827dbab40e36ea08087edee6cd058..edda77cde54c8ede9722375b9a108ea54453da89 100644 (file)
@@ -94,39 +94,28 @@ export function resolveTransitionProps(
     return baseProps
   }
 
-  const originEnterClass = [enterFromClass, enterActiveClass, enterToClass]
   const instance = getCurrentInstance()!
   const durations = normalizeDuration(duration)
   const enterDuration = durations && durations[0]
   const leaveDuration = durations && durations[1]
   const {
-    appear,
     onBeforeEnter,
     onEnter,
-    onLeave,
     onEnterCancelled,
-    onLeaveCancelled
+    onLeave,
+    onLeaveCancelled,
+    onBeforeAppear,
+    onAppear,
+    onAppearCancelled
   } = baseProps
 
-  // is appearing
-  if (appear && !instance.isMounted) {
-    enterFromClass = appearFromClass
-    enterActiveClass = appearActiveClass
-    enterToClass = appearToClass
-  }
-
-  type Hook =
-    | ((el: Element, done: () => void) => void)
-    | ((el: Element) => void)
+  type HookWithDone = (el: Element, done: () => void) => void
+  type Hook = HookWithDone | ((el: Element) => void)
 
-  const finishEnter = (el: Element, done?: () => void) => {
-    removeTransitionClass(el, enterToClass)
-    removeTransitionClass(el, enterActiveClass)
+  const finishEnter = (el: Element, isAppear: boolean, done?: () => void) => {
+    removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
+    removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
     done && done()
-    // reset enter class
-    if (appear) {
-      ;[enterFromClass, enterActiveClass, enterToClass] = originEnterClass
-    }
   }
 
   const finishLeave = (el: Element, done?: () => void) => {
@@ -147,19 +136,15 @@ export function resolveTransitionProps(
       )
   }
 
-  return extend(baseProps, {
-    onBeforeEnter(el) {
-      onBeforeEnter && onBeforeEnter(el)
-      addTransitionClass(el, enterActiveClass)
-      addTransitionClass(el, enterFromClass)
-    },
-    onEnter(el, done) {
+  const makeEnterHook = (isAppear: boolean): HookWithDone => {
+    const hook = isAppear ? onAppear : onEnter
+    return (el, done) => {
       nextFrame(() => {
-        const resolve = () => finishEnter(el, done)
-        callHook(onEnter, [el, resolve])
-        removeTransitionClass(el, enterFromClass)
-        addTransitionClass(el, enterToClass)
-        if (!(onEnter && onEnter.length > 1)) {
+        const resolve = () => finishEnter(el, isAppear, done)
+        callHook(hook, [el, resolve])
+        removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
+        addTransitionClass(el, isAppear ? appearToClass : enterToClass)
+        if (!(hook && hook.length > 1)) {
           if (enterDuration) {
             setTimeout(resolve, enterDuration)
           } else {
@@ -167,7 +152,22 @@ export function resolveTransitionProps(
           }
         }
       })
+    }
+  }
+
+  return extend(baseProps, {
+    onBeforeEnter(el) {
+      onBeforeEnter && onBeforeEnter(el)
+      addTransitionClass(el, enterActiveClass)
+      addTransitionClass(el, enterFromClass)
     },
+    onBeforeAppear(el) {
+      onBeforeAppear && onBeforeAppear(el)
+      addTransitionClass(el, appearActiveClass)
+      addTransitionClass(el, appearFromClass)
+    },
+    onEnter: makeEnterHook(false),
+    onAppear: makeEnterHook(true),
     onLeave(el, done) {
       addTransitionClass(el, leaveActiveClass)
       addTransitionClass(el, leaveFromClass)
@@ -186,9 +186,13 @@ export function resolveTransitionProps(
       })
     },
     onEnterCancelled(el) {
-      finishEnter(el)
+      finishEnter(el, false)
       onEnterCancelled && onEnterCancelled(el)
     },
+    onAppearCancelled(el) {
+      finishEnter(el, true)
+      onAppearCancelled && onAppearCancelled(el)
+    },
     onLeaveCancelled(el) {
       finishLeave(el)
       onLeaveCancelled && onLeaveCancelled(el)
index 7242075526e7e8d665831305c39248e5fe6a50a1..976e34a07bd9a37b6d2ae5b702102f0d993c1efa 100644 (file)
@@ -447,7 +447,7 @@ describe('e2e: Transition', () => {
     test(
       'transition on appear',
       async () => {
-        await page().evaluate(async () => {
+        const appearClass = await page().evaluate(async () => {
           const { createApp, ref } = (window as any).Vue
           createApp({
             template: `
@@ -468,9 +468,12 @@ describe('e2e: Transition', () => {
               return { toggle, click }
             }
           }).mount('#app')
+          return Promise.resolve().then(() => {
+            return document.querySelector('.test')!.className.split(/\s+/g)
+          })
         })
         // appear
-        expect(await classList('.test')).toStrictEqual([
+        expect(appearClass).toStrictEqual([
           'test',
           'test-appear-active',
           'test-appear-from'
@@ -598,13 +601,13 @@ describe('e2e: Transition', () => {
             return document.querySelector('.test')!.className.split(/\s+/g)
           })
         })
-        // appear fixme spy called
+        // appear
         expect(appearClass).toStrictEqual([
           'test',
           'test-appear-active',
           'test-appear-from'
         ])
-        expect(beforeAppearSpy).not.toBeCalled()
+        expect(beforeAppearSpy).toBeCalled()
         expect(onAppearSpy).not.toBeCalled()
         expect(afterAppearSpy).not.toBeCalled()
         await nextFrame()
@@ -613,11 +616,15 @@ describe('e2e: Transition', () => {
           'test-appear-active',
           'test-appear-to'
         ])
-        expect(onAppearSpy).not.toBeCalled()
+        expect(onAppearSpy).toBeCalled()
         expect(afterAppearSpy).not.toBeCalled()
         await transitionFinish()
         expect(await html('#container')).toBe('<div class="test">content</div>')
-        expect(afterAppearSpy).not.toBeCalled()
+        expect(afterAppearSpy).toBeCalled()
+
+        expect(beforeEnterSpy).not.toBeCalled()
+        expect(onEnterSpy).not.toBeCalled()
+        expect(afterEnterSpy).not.toBeCalled()
 
         // leave
         expect(await classWhenTransitionStart()).toStrictEqual([
@@ -640,15 +647,15 @@ describe('e2e: Transition', () => {
         expect(await html('#container')).toBe('<!--v-if-->')
         expect(afterLeaveSpy).toBeCalled()
 
-        // enter fixme spy called
+        // enter
         expect(await classWhenTransitionStart()).toStrictEqual([
           'test',
           'test-enter-active',
           'test-enter-from'
         ])
         expect(beforeEnterSpy).toBeCalled()
-        expect(onEnterSpy).toBeCalled()
-        expect(afterEnterSpy).toBeCalled()
+        expect(onEnterSpy).not.toBeCalled()
+        expect(afterEnterSpy).not.toBeCalled()
         await nextFrame()
         expect(await classList('.test')).toStrictEqual([
           'test',
@@ -656,7 +663,7 @@ describe('e2e: Transition', () => {
           'test-enter-to'
         ])
         expect(onEnterSpy).toBeCalled()
-        expect(afterEnterSpy).toBeCalled()
+        expect(afterEnterSpy).not.toBeCalled()
         await transitionFinish()
         expect(await html('#container')).toBe('<div class="test">content</div>')
         expect(afterEnterSpy).toBeCalled()