From: Evan You Date: Thu, 25 Jun 2020 20:02:28 +0000 (-0400) Subject: fix(transition): fix appear hooks handling X-Git-Tag: v3.0.0-beta.16~26 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=7ae70ea44cf66be134c6ec3b060d9872fa0774e0;p=thirdparty%2Fvuejs%2Fcore.git fix(transition): fix appear hooks handling --- diff --git a/packages/runtime-core/__tests__/components/BaseTransition.spec.ts b/packages/runtime-core/__tests__/components/BaseTransition.spec.ts index a1dfa4d61e..ccb1ae6172 100644 --- a/packages/runtime-core/__tests__/components/BaseTransition.spec.ts +++ b/packages/runtime-core/__tests__/components/BaseTransition.spec.ts @@ -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[`
`]() + 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[`
`]() - expect(props.onAfterEnter).toHaveBeenCalledTimes(1) + expect(props.onAfterAppear).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index b2d7c32e11..a05f6ff5c0 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -41,9 +41,16 @@ export interface BaseTransitionProps { 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 { +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, state: TransitionState, instance: ComponentInternalInstance @@ -275,8 +291,13 @@ export function resolveTransitionHooks( const hooks: TransitionHooks = { 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() } }, diff --git a/packages/runtime-dom/src/components/Transition.ts b/packages/runtime-dom/src/components/Transition.ts index 967a4ee568..edda77cde5 100644 --- a/packages/runtime-dom/src/components/Transition.ts +++ b/packages/runtime-dom/src/components/Transition.ts @@ -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) diff --git a/packages/vue/__tests__/Transition.spec.ts b/packages/vue/__tests__/Transition.spec.ts index 7242075526..976e34a07b 100644 --- a/packages/vue/__tests__/Transition.spec.ts +++ b/packages/vue/__tests__/Transition.spec.ts @@ -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('
content
') - 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('') 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('
content
') expect(afterEnterSpy).toBeCalled()