fix(transition): fix appear hooks handling

This commit is contained in:
Evan You 2020-06-25 16:02:28 -04:00
parent acd3156d2c
commit 7ae70ea44c
4 changed files with 136 additions and 64 deletions

View File

@ -53,6 +53,12 @@ function mockProps(extra: BaseTransitionProps = {}, withKeepAlive = false) {
}), }),
onAfterLeave: jest.fn(), onAfterLeave: jest.fn(),
onLeaveCancelled: 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 ...extra
} }
return { return {
@ -132,8 +138,33 @@ function runTestWithKeepAlive(tester: TestFn) {
} }
describe('BaseTransition', () => { describe('BaseTransition', () => {
test('appear: true', () => { test('appear: true w/ appear hooks', () => {
const { props, cbs } = mockProps({ appear: true }) 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')) mount(props, () => h('div'))
expect(props.onBeforeEnter).toHaveBeenCalledTimes(1) expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
expect(props.onEnter).toHaveBeenCalledTimes(1) expect(props.onEnter).toHaveBeenCalledTimes(1)
@ -207,11 +238,11 @@ describe('BaseTransition', () => {
const { hooks } = mockPersistedHooks() const { hooks } = mockPersistedHooks()
mount(props, () => h('div', hooks)) mount(props, () => h('div', hooks))
expect(props.onBeforeEnter).toHaveBeenCalledTimes(1) expect(props.onBeforeAppear).toHaveBeenCalledTimes(1)
expect(props.onEnter).toHaveBeenCalledTimes(1) expect(props.onAppear).toHaveBeenCalledTimes(1)
expect(props.onAfterEnter).not.toHaveBeenCalled() expect(props.onAfterAppear).not.toHaveBeenCalled()
cbs.doneEnter[`<div></div>`]() cbs.doneEnter[`<div></div>`]()
expect(props.onAfterEnter).toHaveBeenCalledTimes(1) expect(props.onAfterAppear).toHaveBeenCalledTimes(1)
}) })
}) })

View File

@ -41,9 +41,16 @@ export interface BaseTransitionProps<HostElement = RendererElement> {
onLeave?: (el: HostElement, done: () => void) => void onLeave?: (el: HostElement, done: () => void) => void
onAfterLeave?: (el: HostElement) => void onAfterLeave?: (el: HostElement) => void
onLeaveCancelled?: (el: HostElement) => void // only fired in persisted mode 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 persisted: boolean
beforeEnter(el: HostElement): void beforeEnter(el: HostElement): void
enter(el: HostElement): void enter(el: HostElement): void
@ -115,7 +122,12 @@ const BaseTransitionImpl = {
onBeforeLeave: Function, onBeforeLeave: Function,
onLeave: Function, onLeave: Function,
onAfterLeave: Function, onAfterLeave: Function,
onLeaveCancelled: Function onLeaveCancelled: Function,
// appear
onBeforeAppear: Function,
onAppear: Function,
onAfterAppear: Function,
onAppearCancelled: Function
}, },
setup(props: BaseTransitionProps, { slots }: SetupContext) { setup(props: BaseTransitionProps, { slots }: SetupContext) {
@ -254,7 +266,11 @@ export function resolveTransitionHooks(
onBeforeLeave, onBeforeLeave,
onLeave, onLeave,
onAfterLeave, onAfterLeave,
onLeaveCancelled onLeaveCancelled,
onBeforeAppear,
onAppear,
onAfterAppear,
onAppearCancelled
}: BaseTransitionProps<any>, }: BaseTransitionProps<any>,
state: TransitionState, state: TransitionState,
instance: ComponentInternalInstance instance: ComponentInternalInstance
@ -275,9 +291,14 @@ export function resolveTransitionHooks(
const hooks: TransitionHooks<TransitionElement> = { const hooks: TransitionHooks<TransitionElement> = {
persisted, persisted,
beforeEnter(el) { beforeEnter(el) {
if (!appear && !state.isMounted) { let hook = onBeforeEnter
if (!state.isMounted) {
if (appear) {
hook = onBeforeAppear || onBeforeEnter
} else {
return return
} }
}
// for same element (v-show) // for same element (v-show)
if (el._leaveCb) { if (el._leaveCb) {
el._leaveCb(true /* cancelled */) el._leaveCb(true /* cancelled */)
@ -292,31 +313,40 @@ export function resolveTransitionHooks(
// force early removal (not cancelled) // force early removal (not cancelled)
leavingVNode.el!._leaveCb() leavingVNode.el!._leaveCb()
} }
callHook(onBeforeEnter, [el]) callHook(hook, [el])
}, },
enter(el) { enter(el) {
if (!appear && !state.isMounted) { 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 return
} }
}
let called = false let called = false
const afterEnter = (el._enterCb = (cancelled?) => { const done = (el._enterCb = (cancelled?) => {
if (called) return if (called) return
called = true called = true
if (cancelled) { if (cancelled) {
callHook(onEnterCancelled, [el]) callHook(cancelHook, [el])
} else { } else {
callHook(onAfterEnter, [el]) callHook(afterHook, [el])
} }
if (hooks.delayedLeave) { if (hooks.delayedLeave) {
hooks.delayedLeave() hooks.delayedLeave()
} }
el._enterCb = undefined el._enterCb = undefined
}) })
if (onEnter) { if (hook) {
onEnter(el, afterEnter) hook(el, done)
} else { } else {
afterEnter() done()
} }
}, },

View File

@ -94,39 +94,28 @@ export function resolveTransitionProps(
return baseProps return baseProps
} }
const originEnterClass = [enterFromClass, enterActiveClass, enterToClass]
const instance = getCurrentInstance()! const instance = getCurrentInstance()!
const durations = normalizeDuration(duration) const durations = normalizeDuration(duration)
const enterDuration = durations && durations[0] const enterDuration = durations && durations[0]
const leaveDuration = durations && durations[1] const leaveDuration = durations && durations[1]
const { const {
appear,
onBeforeEnter, onBeforeEnter,
onEnter, onEnter,
onLeave,
onEnterCancelled, onEnterCancelled,
onLeaveCancelled onLeave,
onLeaveCancelled,
onBeforeAppear,
onAppear,
onAppearCancelled
} = baseProps } = baseProps
// is appearing type HookWithDone = (el: Element, done: () => void) => void
if (appear && !instance.isMounted) { type Hook = HookWithDone | ((el: Element) => void)
enterFromClass = appearFromClass
enterActiveClass = appearActiveClass
enterToClass = appearToClass
}
type Hook = const finishEnter = (el: Element, isAppear: boolean, done?: () => void) => {
| ((el: Element, done: () => void) => void) removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
| ((el: Element) => void) removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
const finishEnter = (el: Element, done?: () => void) => {
removeTransitionClass(el, enterToClass)
removeTransitionClass(el, enterActiveClass)
done && done() done && done()
// reset enter class
if (appear) {
;[enterFromClass, enterActiveClass, enterToClass] = originEnterClass
}
} }
const finishLeave = (el: Element, done?: () => void) => { const finishLeave = (el: Element, done?: () => void) => {
@ -147,19 +136,15 @@ export function resolveTransitionProps(
) )
} }
return extend(baseProps, { const makeEnterHook = (isAppear: boolean): HookWithDone => {
onBeforeEnter(el) { const hook = isAppear ? onAppear : onEnter
onBeforeEnter && onBeforeEnter(el) return (el, done) => {
addTransitionClass(el, enterActiveClass)
addTransitionClass(el, enterFromClass)
},
onEnter(el, done) {
nextFrame(() => { nextFrame(() => {
const resolve = () => finishEnter(el, done) const resolve = () => finishEnter(el, isAppear, done)
callHook(onEnter, [el, resolve]) callHook(hook, [el, resolve])
removeTransitionClass(el, enterFromClass) removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
addTransitionClass(el, enterToClass) addTransitionClass(el, isAppear ? appearToClass : enterToClass)
if (!(onEnter && onEnter.length > 1)) { if (!(hook && hook.length > 1)) {
if (enterDuration) { if (enterDuration) {
setTimeout(resolve, enterDuration) setTimeout(resolve, enterDuration)
} else { } 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) { onLeave(el, done) {
addTransitionClass(el, leaveActiveClass) addTransitionClass(el, leaveActiveClass)
addTransitionClass(el, leaveFromClass) addTransitionClass(el, leaveFromClass)
@ -186,9 +186,13 @@ export function resolveTransitionProps(
}) })
}, },
onEnterCancelled(el) { onEnterCancelled(el) {
finishEnter(el) finishEnter(el, false)
onEnterCancelled && onEnterCancelled(el) onEnterCancelled && onEnterCancelled(el)
}, },
onAppearCancelled(el) {
finishEnter(el, true)
onAppearCancelled && onAppearCancelled(el)
},
onLeaveCancelled(el) { onLeaveCancelled(el) {
finishLeave(el) finishLeave(el)
onLeaveCancelled && onLeaveCancelled(el) onLeaveCancelled && onLeaveCancelled(el)

View File

@ -447,7 +447,7 @@ describe('e2e: Transition', () => {
test( test(
'transition on appear', 'transition on appear',
async () => { async () => {
await page().evaluate(async () => { const appearClass = await page().evaluate(async () => {
const { createApp, ref } = (window as any).Vue const { createApp, ref } = (window as any).Vue
createApp({ createApp({
template: ` template: `
@ -468,9 +468,12 @@ describe('e2e: Transition', () => {
return { toggle, click } return { toggle, click }
} }
}).mount('#app') }).mount('#app')
return Promise.resolve().then(() => {
return document.querySelector('.test')!.className.split(/\s+/g)
})
}) })
// appear // appear
expect(await classList('.test')).toStrictEqual([ expect(appearClass).toStrictEqual([
'test', 'test',
'test-appear-active', 'test-appear-active',
'test-appear-from' 'test-appear-from'
@ -598,13 +601,13 @@ describe('e2e: Transition', () => {
return document.querySelector('.test')!.className.split(/\s+/g) return document.querySelector('.test')!.className.split(/\s+/g)
}) })
}) })
// appear fixme spy called // appear
expect(appearClass).toStrictEqual([ expect(appearClass).toStrictEqual([
'test', 'test',
'test-appear-active', 'test-appear-active',
'test-appear-from' 'test-appear-from'
]) ])
expect(beforeAppearSpy).not.toBeCalled() expect(beforeAppearSpy).toBeCalled()
expect(onAppearSpy).not.toBeCalled() expect(onAppearSpy).not.toBeCalled()
expect(afterAppearSpy).not.toBeCalled() expect(afterAppearSpy).not.toBeCalled()
await nextFrame() await nextFrame()
@ -613,11 +616,15 @@ describe('e2e: Transition', () => {
'test-appear-active', 'test-appear-active',
'test-appear-to' 'test-appear-to'
]) ])
expect(onAppearSpy).not.toBeCalled() expect(onAppearSpy).toBeCalled()
expect(afterAppearSpy).not.toBeCalled() expect(afterAppearSpy).not.toBeCalled()
await transitionFinish() await transitionFinish()
expect(await html('#container')).toBe('<div class="test">content</div>') 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 // leave
expect(await classWhenTransitionStart()).toStrictEqual([ expect(await classWhenTransitionStart()).toStrictEqual([
@ -640,15 +647,15 @@ describe('e2e: Transition', () => {
expect(await html('#container')).toBe('<!--v-if-->') expect(await html('#container')).toBe('<!--v-if-->')
expect(afterLeaveSpy).toBeCalled() expect(afterLeaveSpy).toBeCalled()
// enter fixme spy called // enter
expect(await classWhenTransitionStart()).toStrictEqual([ expect(await classWhenTransitionStart()).toStrictEqual([
'test', 'test',
'test-enter-active', 'test-enter-active',
'test-enter-from' 'test-enter-from'
]) ])
expect(beforeEnterSpy).toBeCalled() expect(beforeEnterSpy).toBeCalled()
expect(onEnterSpy).toBeCalled() expect(onEnterSpy).not.toBeCalled()
expect(afterEnterSpy).toBeCalled() expect(afterEnterSpy).not.toBeCalled()
await nextFrame() await nextFrame()
expect(await classList('.test')).toStrictEqual([ expect(await classList('.test')).toStrictEqual([
'test', 'test',
@ -656,7 +663,7 @@ describe('e2e: Transition', () => {
'test-enter-to' 'test-enter-to'
]) ])
expect(onEnterSpy).toBeCalled() expect(onEnterSpy).toBeCalled()
expect(afterEnterSpy).toBeCalled() expect(afterEnterSpy).not.toBeCalled()
await transitionFinish() await transitionFinish()
expect(await html('#container')).toBe('<div class="test">content</div>') expect(await html('#container')).toBe('<div class="test">content</div>')
expect(afterEnterSpy).toBeCalled() expect(afterEnterSpy).toBeCalled()