fix(transition): fix appear hooks handling
This commit is contained in:
parent
acd3156d2c
commit
7ae70ea44c
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user