Skip to content

Commit 7ae70ea

Browse files
committed
fix(transition): fix appear hooks handling
1 parent acd3156 commit 7ae70ea

File tree

4 files changed

+136
-64
lines changed

4 files changed

+136
-64
lines changed

packages/runtime-core/__tests__/components/BaseTransition.spec.ts

+37-6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ function mockProps(extra: BaseTransitionProps = {}, withKeepAlive = false) {
5353
}),
5454
onAfterLeave: jest.fn(),
5555
onLeaveCancelled: jest.fn(),
56+
onBeforeAppear: jest.fn(),
57+
onAppear: jest.fn((el, done) => {
58+
cbs.doneEnter[serialize(el as TestElement)] = done
59+
}),
60+
onAfterAppear: jest.fn(),
61+
onAppearCancelled: jest.fn(),
5662
...extra
5763
}
5864
return {
@@ -132,8 +138,33 @@ function runTestWithKeepAlive(tester: TestFn) {
132138
}
133139

134140
describe('BaseTransition', () => {
135-
test('appear: true', () => {
136-
const { props, cbs } = mockProps({ appear: true })
141+
test('appear: true w/ appear hooks', () => {
142+
const { props, cbs } = mockProps({
143+
appear: true
144+
})
145+
mount(props, () => h('div'))
146+
expect(props.onBeforeAppear).toHaveBeenCalledTimes(1)
147+
expect(props.onAppear).toHaveBeenCalledTimes(1)
148+
expect(props.onAfterAppear).not.toHaveBeenCalled()
149+
150+
// enter should not be called
151+
expect(props.onBeforeEnter).not.toHaveBeenCalled()
152+
expect(props.onEnter).not.toHaveBeenCalled()
153+
expect(props.onAfterEnter).not.toHaveBeenCalled()
154+
155+
cbs.doneEnter[`<div></div>`]()
156+
expect(props.onAfterAppear).toHaveBeenCalledTimes(1)
157+
expect(props.onAfterEnter).not.toHaveBeenCalled()
158+
})
159+
160+
test('appear: true w/ fallback to enter hooks', () => {
161+
const { props, cbs } = mockProps({
162+
appear: true,
163+
onBeforeAppear: undefined,
164+
onAppear: undefined,
165+
onAfterAppear: undefined,
166+
onAppearCancelled: undefined
167+
})
137168
mount(props, () => h('div'))
138169
expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
139170
expect(props.onEnter).toHaveBeenCalledTimes(1)
@@ -207,11 +238,11 @@ describe('BaseTransition', () => {
207238
const { hooks } = mockPersistedHooks()
208239
mount(props, () => h('div', hooks))
209240

210-
expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
211-
expect(props.onEnter).toHaveBeenCalledTimes(1)
212-
expect(props.onAfterEnter).not.toHaveBeenCalled()
241+
expect(props.onBeforeAppear).toHaveBeenCalledTimes(1)
242+
expect(props.onAppear).toHaveBeenCalledTimes(1)
243+
expect(props.onAfterAppear).not.toHaveBeenCalled()
213244
cbs.doneEnter[`<div></div>`]()
214-
expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
245+
expect(props.onAfterAppear).toHaveBeenCalledTimes(1)
215246
})
216247
})
217248

packages/runtime-core/src/components/BaseTransition.ts

+44-14
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,16 @@ export interface BaseTransitionProps<HostElement = RendererElement> {
4141
onLeave?: (el: HostElement, done: () => void) => void
4242
onAfterLeave?: (el: HostElement) => void
4343
onLeaveCancelled?: (el: HostElement) => void // only fired in persisted mode
44+
// appear
45+
onBeforeAppear?: (el: HostElement) => void
46+
onAppear?: (el: HostElement, done: () => void) => void
47+
onAfterAppear?: (el: HostElement) => void
48+
onAppearCancelled?: (el: HostElement) => void
4449
}
4550

46-
export interface TransitionHooks<HostElement extends RendererElement = RendererElement> {
51+
export interface TransitionHooks<
52+
HostElement extends RendererElement = RendererElement
53+
> {
4754
persisted: boolean
4855
beforeEnter(el: HostElement): void
4956
enter(el: HostElement): void
@@ -115,7 +122,12 @@ const BaseTransitionImpl = {
115122
onBeforeLeave: Function,
116123
onLeave: Function,
117124
onAfterLeave: Function,
118-
onLeaveCancelled: Function
125+
onLeaveCancelled: Function,
126+
// appear
127+
onBeforeAppear: Function,
128+
onAppear: Function,
129+
onAfterAppear: Function,
130+
onAppearCancelled: Function
119131
},
120132

121133
setup(props: BaseTransitionProps, { slots }: SetupContext) {
@@ -254,7 +266,11 @@ export function resolveTransitionHooks(
254266
onBeforeLeave,
255267
onLeave,
256268
onAfterLeave,
257-
onLeaveCancelled
269+
onLeaveCancelled,
270+
onBeforeAppear,
271+
onAppear,
272+
onAfterAppear,
273+
onAppearCancelled
258274
}: BaseTransitionProps<any>,
259275
state: TransitionState,
260276
instance: ComponentInternalInstance
@@ -275,8 +291,13 @@ export function resolveTransitionHooks(
275291
const hooks: TransitionHooks<TransitionElement> = {
276292
persisted,
277293
beforeEnter(el) {
278-
if (!appear && !state.isMounted) {
279-
return
294+
let hook = onBeforeEnter
295+
if (!state.isMounted) {
296+
if (appear) {
297+
hook = onBeforeAppear || onBeforeEnter
298+
} else {
299+
return
300+
}
280301
}
281302
// for same element (v-show)
282303
if (el._leaveCb) {
@@ -292,31 +313,40 @@ export function resolveTransitionHooks(
292313
// force early removal (not cancelled)
293314
leavingVNode.el!._leaveCb()
294315
}
295-
callHook(onBeforeEnter, [el])
316+
callHook(hook, [el])
296317
},
297318

298319
enter(el) {
299-
if (!appear && !state.isMounted) {
300-
return
320+
let hook = onEnter
321+
let afterHook = onAfterEnter
322+
let cancelHook = onEnterCancelled
323+
if (!state.isMounted) {
324+
if (appear) {
325+
hook = onAppear || onEnter
326+
afterHook = onAfterAppear || onAfterEnter
327+
cancelHook = onAppearCancelled || onEnterCancelled
328+
} else {
329+
return
330+
}
301331
}
302332
let called = false
303-
const afterEnter = (el._enterCb = (cancelled?) => {
333+
const done = (el._enterCb = (cancelled?) => {
304334
if (called) return
305335
called = true
306336
if (cancelled) {
307-
callHook(onEnterCancelled, [el])
337+
callHook(cancelHook, [el])
308338
} else {
309-
callHook(onAfterEnter, [el])
339+
callHook(afterHook, [el])
310340
}
311341
if (hooks.delayedLeave) {
312342
hooks.delayedLeave()
313343
}
314344
el._enterCb = undefined
315345
})
316-
if (onEnter) {
317-
onEnter(el, afterEnter)
346+
if (hook) {
347+
hook(el, done)
318348
} else {
319-
afterEnter()
349+
done()
320350
}
321351
},
322352

packages/runtime-dom/src/components/Transition.ts

+38-34
Original file line numberDiff line numberDiff line change
@@ -94,39 +94,28 @@ export function resolveTransitionProps(
9494
return baseProps
9595
}
9696

97-
const originEnterClass = [enterFromClass, enterActiveClass, enterToClass]
9897
const instance = getCurrentInstance()!
9998
const durations = normalizeDuration(duration)
10099
const enterDuration = durations && durations[0]
101100
const leaveDuration = durations && durations[1]
102101
const {
103-
appear,
104102
onBeforeEnter,
105103
onEnter,
106-
onLeave,
107104
onEnterCancelled,
108-
onLeaveCancelled
105+
onLeave,
106+
onLeaveCancelled,
107+
onBeforeAppear,
108+
onAppear,
109+
onAppearCancelled
109110
} = baseProps
110111

111-
// is appearing
112-
if (appear && !instance.isMounted) {
113-
enterFromClass = appearFromClass
114-
enterActiveClass = appearActiveClass
115-
enterToClass = appearToClass
116-
}
117-
118-
type Hook =
119-
| ((el: Element, done: () => void) => void)
120-
| ((el: Element) => void)
112+
type HookWithDone = (el: Element, done: () => void) => void
113+
type Hook = HookWithDone | ((el: Element) => void)
121114

122-
const finishEnter = (el: Element, done?: () => void) => {
123-
removeTransitionClass(el, enterToClass)
124-
removeTransitionClass(el, enterActiveClass)
115+
const finishEnter = (el: Element, isAppear: boolean, done?: () => void) => {
116+
removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
117+
removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
125118
done && done()
126-
// reset enter class
127-
if (appear) {
128-
;[enterFromClass, enterActiveClass, enterToClass] = originEnterClass
129-
}
130119
}
131120

132121
const finishLeave = (el: Element, done?: () => void) => {
@@ -147,27 +136,38 @@ export function resolveTransitionProps(
147136
)
148137
}
149138

150-
return extend(baseProps, {
151-
onBeforeEnter(el) {
152-
onBeforeEnter && onBeforeEnter(el)
153-
addTransitionClass(el, enterActiveClass)
154-
addTransitionClass(el, enterFromClass)
155-
},
156-
onEnter(el, done) {
139+
const makeEnterHook = (isAppear: boolean): HookWithDone => {
140+
const hook = isAppear ? onAppear : onEnter
141+
return (el, done) => {
157142
nextFrame(() => {
158-
const resolve = () => finishEnter(el, done)
159-
callHook(onEnter, [el, resolve])
160-
removeTransitionClass(el, enterFromClass)
161-
addTransitionClass(el, enterToClass)
162-
if (!(onEnter && onEnter.length > 1)) {
143+
const resolve = () => finishEnter(el, isAppear, done)
144+
callHook(hook, [el, resolve])
145+
removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
146+
addTransitionClass(el, isAppear ? appearToClass : enterToClass)
147+
if (!(hook && hook.length > 1)) {
163148
if (enterDuration) {
164149
setTimeout(resolve, enterDuration)
165150
} else {
166151
whenTransitionEnds(el, type, resolve)
167152
}
168153
}
169154
})
155+
}
156+
}
157+
158+
return extend(baseProps, {
159+
onBeforeEnter(el) {
160+
onBeforeEnter && onBeforeEnter(el)
161+
addTransitionClass(el, enterActiveClass)
162+
addTransitionClass(el, enterFromClass)
170163
},
164+
onBeforeAppear(el) {
165+
onBeforeAppear && onBeforeAppear(el)
166+
addTransitionClass(el, appearActiveClass)
167+
addTransitionClass(el, appearFromClass)
168+
},
169+
onEnter: makeEnterHook(false),
170+
onAppear: makeEnterHook(true),
171171
onLeave(el, done) {
172172
addTransitionClass(el, leaveActiveClass)
173173
addTransitionClass(el, leaveFromClass)
@@ -186,9 +186,13 @@ export function resolveTransitionProps(
186186
})
187187
},
188188
onEnterCancelled(el) {
189-
finishEnter(el)
189+
finishEnter(el, false)
190190
onEnterCancelled && onEnterCancelled(el)
191191
},
192+
onAppearCancelled(el) {
193+
finishEnter(el, true)
194+
onAppearCancelled && onAppearCancelled(el)
195+
},
192196
onLeaveCancelled(el) {
193197
finishLeave(el)
194198
onLeaveCancelled && onLeaveCancelled(el)

packages/vue/__tests__/Transition.spec.ts

+17-10
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ describe('e2e: Transition', () => {
447447
test(
448448
'transition on appear',
449449
async () => {
450-
await page().evaluate(async () => {
450+
const appearClass = await page().evaluate(async () => {
451451
const { createApp, ref } = (window as any).Vue
452452
createApp({
453453
template: `
@@ -468,9 +468,12 @@ describe('e2e: Transition', () => {
468468
return { toggle, click }
469469
}
470470
}).mount('#app')
471+
return Promise.resolve().then(() => {
472+
return document.querySelector('.test')!.className.split(/\s+/g)
473+
})
471474
})
472475
// appear
473-
expect(await classList('.test')).toStrictEqual([
476+
expect(appearClass).toStrictEqual([
474477
'test',
475478
'test-appear-active',
476479
'test-appear-from'
@@ -598,13 +601,13 @@ describe('e2e: Transition', () => {
598601
return document.querySelector('.test')!.className.split(/\s+/g)
599602
})
600603
})
601-
// appear fixme spy called
604+
// appear
602605
expect(appearClass).toStrictEqual([
603606
'test',
604607
'test-appear-active',
605608
'test-appear-from'
606609
])
607-
expect(beforeAppearSpy).not.toBeCalled()
610+
expect(beforeAppearSpy).toBeCalled()
608611
expect(onAppearSpy).not.toBeCalled()
609612
expect(afterAppearSpy).not.toBeCalled()
610613
await nextFrame()
@@ -613,11 +616,15 @@ describe('e2e: Transition', () => {
613616
'test-appear-active',
614617
'test-appear-to'
615618
])
616-
expect(onAppearSpy).not.toBeCalled()
619+
expect(onAppearSpy).toBeCalled()
617620
expect(afterAppearSpy).not.toBeCalled()
618621
await transitionFinish()
619622
expect(await html('#container')).toBe('<div class="test">content</div>')
620-
expect(afterAppearSpy).not.toBeCalled()
623+
expect(afterAppearSpy).toBeCalled()
624+
625+
expect(beforeEnterSpy).not.toBeCalled()
626+
expect(onEnterSpy).not.toBeCalled()
627+
expect(afterEnterSpy).not.toBeCalled()
621628

622629
// leave
623630
expect(await classWhenTransitionStart()).toStrictEqual([
@@ -640,23 +647,23 @@ describe('e2e: Transition', () => {
640647
expect(await html('#container')).toBe('<!--v-if-->')
641648
expect(afterLeaveSpy).toBeCalled()
642649

643-
// enter fixme spy called
650+
// enter
644651
expect(await classWhenTransitionStart()).toStrictEqual([
645652
'test',
646653
'test-enter-active',
647654
'test-enter-from'
648655
])
649656
expect(beforeEnterSpy).toBeCalled()
650-
expect(onEnterSpy).toBeCalled()
651-
expect(afterEnterSpy).toBeCalled()
657+
expect(onEnterSpy).not.toBeCalled()
658+
expect(afterEnterSpy).not.toBeCalled()
652659
await nextFrame()
653660
expect(await classList('.test')).toStrictEqual([
654661
'test',
655662
'test-enter-active',
656663
'test-enter-to'
657664
])
658665
expect(onEnterSpy).toBeCalled()
659-
expect(afterEnterSpy).toBeCalled()
666+
expect(afterEnterSpy).not.toBeCalled()
660667
await transitionFinish()
661668
expect(await html('#container')).toBe('<div class="test">content</div>')
662669
expect(afterEnterSpy).toBeCalled()

0 commit comments

Comments
 (0)