test(transition): in-out, appear & persisted
This commit is contained in:
parent
7aac3418c0
commit
32602ccee1
@ -7,7 +7,8 @@ import {
|
|||||||
ref,
|
ref,
|
||||||
nextTick,
|
nextTick,
|
||||||
serializeInner,
|
serializeInner,
|
||||||
serialize
|
serialize,
|
||||||
|
VNodeProps
|
||||||
} from '@vue/runtime-test'
|
} from '@vue/runtime-test'
|
||||||
|
|
||||||
function mount(props: BaseTransitionProps, slot: () => any) {
|
function mount(props: BaseTransitionProps, slot: () => any) {
|
||||||
@ -26,7 +27,9 @@ function mockProps(extra: BaseTransitionProps = {}) {
|
|||||||
}
|
}
|
||||||
const props: BaseTransitionProps = {
|
const props: BaseTransitionProps = {
|
||||||
onBeforeEnter: jest.fn(el => {
|
onBeforeEnter: jest.fn(el => {
|
||||||
|
if (!extra.persisted) {
|
||||||
expect(el.parentNode).toBeNull()
|
expect(el.parentNode).toBeNull()
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
onEnter: jest.fn((el, done) => {
|
onEnter: jest.fn((el, done) => {
|
||||||
cbs.doneEnter[serialize(el)] = done
|
cbs.doneEnter[serialize(el)] = done
|
||||||
@ -67,8 +70,8 @@ interface ToggleOptions {
|
|||||||
falseSerialized: string
|
falseSerialized: string
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runTestWithElements(tester: (o: ToggleOptions) => void) {
|
function runTestWithElements(tester: (o: ToggleOptions) => void) {
|
||||||
await tester({
|
return tester({
|
||||||
trueBranch: () => h('div'),
|
trueBranch: () => h('div'),
|
||||||
falseBranch: () => h('span'),
|
falseBranch: () => h('span'),
|
||||||
trueSerialized: `<div></div>`,
|
trueSerialized: `<div></div>`,
|
||||||
@ -76,12 +79,12 @@ async function runTestWithElements(tester: (o: ToggleOptions) => void) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runTestWithComponents(tester: (o: ToggleOptions) => void) {
|
function runTestWithComponents(tester: (o: ToggleOptions) => void) {
|
||||||
const CompA = ({ msg }: { msg: string }) => h('div', msg)
|
const CompA = ({ msg }: { msg: string }) => h('div', msg)
|
||||||
// test HOC
|
// test HOC
|
||||||
const CompB = ({ msg }: { msg: string }) => h(CompC, { msg })
|
const CompB = ({ msg }: { msg: string }) => h(CompC, { msg })
|
||||||
const CompC = ({ msg }: { msg: string }) => h('span', msg)
|
const CompC = ({ msg }: { msg: string }) => h('span', msg)
|
||||||
await tester({
|
return tester({
|
||||||
trueBranch: () => h(CompA, { msg: 'foo' }),
|
trueBranch: () => h(CompA, { msg: 'foo' }),
|
||||||
falseBranch: () => h(CompB, { msg: 'bar' }),
|
falseBranch: () => h(CompB, { msg: 'bar' }),
|
||||||
trueSerialized: `<div>foo</div>`,
|
trueSerialized: `<div>foo</div>`,
|
||||||
@ -90,6 +93,89 @@ async function runTestWithComponents(tester: (o: ToggleOptions) => void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('BaseTransition', () => {
|
describe('BaseTransition', () => {
|
||||||
|
test('appear: true', () => {
|
||||||
|
const { props, cbs } = mockProps({ appear: true })
|
||||||
|
mount(props, () => h('div'))
|
||||||
|
expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
|
||||||
|
expect(props.onEnter).toHaveBeenCalledTimes(1)
|
||||||
|
expect(props.onAfterEnter).not.toHaveBeenCalled()
|
||||||
|
cbs.doneEnter[`<div></div>`]()
|
||||||
|
expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('persisted: true', () => {
|
||||||
|
// this is pretty much how v-show is implemented
|
||||||
|
// (but using the directive API instead)
|
||||||
|
function mockPersistedHooks() {
|
||||||
|
const state = { show: true }
|
||||||
|
const toggle = ref(true)
|
||||||
|
const hooks: VNodeProps = {
|
||||||
|
onVnodeBeforeMount(vnode) {
|
||||||
|
vnode.transition!.beforeEnter(vnode.el)
|
||||||
|
},
|
||||||
|
onVnodeMounted(vnode) {
|
||||||
|
vnode.transition!.enter(vnode.el)
|
||||||
|
},
|
||||||
|
onVnodeUpdated(vnode, oldVnode) {
|
||||||
|
if (oldVnode.props!.id !== vnode.props!.id) {
|
||||||
|
if (vnode.props!.id) {
|
||||||
|
vnode.transition!.beforeEnter(vnode.el)
|
||||||
|
state.show = true
|
||||||
|
vnode.transition!.enter(vnode.el)
|
||||||
|
} else {
|
||||||
|
vnode.transition!.leave(vnode.el, () => {
|
||||||
|
state.show = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { state, toggle, hooks }
|
||||||
|
}
|
||||||
|
|
||||||
|
test('w/ appear: false', async () => {
|
||||||
|
const { props, cbs } = mockProps({ persisted: true })
|
||||||
|
const { toggle, state, hooks } = mockPersistedHooks()
|
||||||
|
|
||||||
|
mount(props, () => h('div', { id: toggle.value, ...hooks }))
|
||||||
|
// without appear: true, enter hooks should not be called on mount
|
||||||
|
expect(props.onBeforeEnter).not.toHaveBeenCalled()
|
||||||
|
expect(props.onEnter).not.toHaveBeenCalled()
|
||||||
|
expect(props.onAfterEnter).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
toggle.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(props.onBeforeLeave).toHaveBeenCalledTimes(1)
|
||||||
|
expect(props.onLeave).toHaveBeenCalledTimes(1)
|
||||||
|
expect(props.onAfterLeave).not.toHaveBeenCalled()
|
||||||
|
expect(state.show).toBe(true) // should still be shown
|
||||||
|
cbs.doneLeave[`<div id=false></div>`]()
|
||||||
|
expect(state.show).toBe(false) // should be hidden now
|
||||||
|
expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
toggle.value = true
|
||||||
|
await nextTick()
|
||||||
|
expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
|
||||||
|
expect(props.onEnter).toHaveBeenCalledTimes(1)
|
||||||
|
expect(props.onAfterEnter).not.toHaveBeenCalled()
|
||||||
|
expect(state.show).toBe(true) // should be shown now
|
||||||
|
cbs.doneEnter[`<div id=true></div>`]()
|
||||||
|
expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('w/ appear: true', () => {
|
||||||
|
const { props, cbs } = mockProps({ persisted: true, appear: true })
|
||||||
|
const { hooks } = mockPersistedHooks()
|
||||||
|
mount(props, () => h('div', hooks))
|
||||||
|
|
||||||
|
expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
|
||||||
|
expect(props.onEnter).toHaveBeenCalledTimes(1)
|
||||||
|
expect(props.onAfterEnter).not.toHaveBeenCalled()
|
||||||
|
cbs.doneEnter[`<div></div>`]()
|
||||||
|
expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('toggle on-off', () => {
|
describe('toggle on-off', () => {
|
||||||
async function testToggleOnOff({
|
async function testToggleOnOff({
|
||||||
trueBranch,
|
trueBranch,
|
||||||
@ -694,15 +780,181 @@ describe('BaseTransition', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('mode: "in-out"', () => {})
|
describe('mode: "in-out"', () => {
|
||||||
|
async function testInOut({
|
||||||
|
trueBranch,
|
||||||
|
falseBranch,
|
||||||
|
trueSerialized,
|
||||||
|
falseSerialized
|
||||||
|
}: ToggleOptions) {
|
||||||
|
const toggle = ref(true)
|
||||||
|
const { props, cbs } = mockProps({ mode: 'in-out' })
|
||||||
|
const root = mount(
|
||||||
|
props,
|
||||||
|
() => (toggle.value ? trueBranch() : falseBranch())
|
||||||
|
)
|
||||||
|
|
||||||
describe('mode: "in-out" toggle before finish', () => {})
|
toggle.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe(`${trueSerialized}${falseSerialized}`)
|
||||||
|
// enter should start
|
||||||
|
expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
|
||||||
|
assertCalledWithEl(props.onBeforeEnter, falseSerialized)
|
||||||
|
expect(props.onEnter).toHaveBeenCalledTimes(1)
|
||||||
|
assertCalledWithEl(props.onEnter, falseSerialized)
|
||||||
|
expect(props.onAfterEnter).not.toHaveBeenCalled()
|
||||||
|
// leave should not start
|
||||||
|
expect(props.onBeforeLeave).not.toHaveBeenCalled()
|
||||||
|
expect(props.onLeave).not.toHaveBeenCalled()
|
||||||
|
expect(props.onAfterLeave).not.toHaveBeenCalled()
|
||||||
|
|
||||||
test('persisted: true', () => {
|
// finish enter
|
||||||
// test onLeaveCancelled
|
cbs.doneEnter[falseSerialized]()
|
||||||
|
expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
|
||||||
|
assertCalledWithEl(props.onAfterEnter, falseSerialized)
|
||||||
|
|
||||||
|
// leave should start now
|
||||||
|
expect(props.onBeforeLeave).toHaveBeenCalledTimes(1)
|
||||||
|
assertCalledWithEl(props.onBeforeLeave, trueSerialized)
|
||||||
|
expect(props.onLeave).toHaveBeenCalledTimes(1)
|
||||||
|
assertCalledWithEl(props.onLeave, trueSerialized)
|
||||||
|
expect(props.onAfterLeave).not.toHaveBeenCalled()
|
||||||
|
// finish leave
|
||||||
|
cbs.doneLeave[trueSerialized]()
|
||||||
|
expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
|
||||||
|
assertCalledWithEl(props.onAfterLeave, trueSerialized)
|
||||||
|
|
||||||
|
// toggle again
|
||||||
|
toggle.value = true
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe(`${falseSerialized}${trueSerialized}`)
|
||||||
|
// enter should start
|
||||||
|
expect(props.onBeforeEnter).toHaveBeenCalledTimes(2)
|
||||||
|
assertCalledWithEl(props.onBeforeEnter, trueSerialized, 1)
|
||||||
|
expect(props.onEnter).toHaveBeenCalledTimes(2)
|
||||||
|
assertCalledWithEl(props.onEnter, trueSerialized, 1)
|
||||||
|
expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
|
||||||
|
// leave should not start
|
||||||
|
expect(props.onBeforeLeave).toHaveBeenCalledTimes(1)
|
||||||
|
expect(props.onLeave).toHaveBeenCalledTimes(1)
|
||||||
|
expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
// finish enter
|
||||||
|
cbs.doneEnter[trueSerialized]()
|
||||||
|
expect(props.onAfterEnter).toHaveBeenCalledTimes(2)
|
||||||
|
assertCalledWithEl(props.onAfterEnter, trueSerialized, 1)
|
||||||
|
|
||||||
|
// leave should start now
|
||||||
|
expect(props.onBeforeLeave).toHaveBeenCalledTimes(2)
|
||||||
|
assertCalledWithEl(props.onBeforeLeave, falseSerialized, 1)
|
||||||
|
expect(props.onLeave).toHaveBeenCalledTimes(2)
|
||||||
|
assertCalledWithEl(props.onLeave, falseSerialized, 1)
|
||||||
|
expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
|
||||||
|
// finish leave
|
||||||
|
cbs.doneLeave[falseSerialized]()
|
||||||
|
expect(props.onAfterLeave).toHaveBeenCalledTimes(2)
|
||||||
|
assertCalledWithEl(props.onAfterLeave, falseSerialized, 1)
|
||||||
|
|
||||||
|
assertCalls(props, {
|
||||||
|
onBeforeEnter: 2,
|
||||||
|
onEnter: 2,
|
||||||
|
onAfterEnter: 2,
|
||||||
|
onEnterCancelled: 0,
|
||||||
|
onBeforeLeave: 2,
|
||||||
|
onLeave: 2,
|
||||||
|
onAfterLeave: 2,
|
||||||
|
onLeaveCancelled: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test('w/ elements', async () => {
|
||||||
|
await runTestWithElements(testInOut)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('appear: true', () => {})
|
test('w/ components', async () => {
|
||||||
|
await runTestWithComponents(testInOut)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mode: "in-out" toggle before finish', () => {
|
||||||
|
async function testInOutBeforeFinish({
|
||||||
|
trueBranch,
|
||||||
|
falseBranch,
|
||||||
|
trueSerialized,
|
||||||
|
falseSerialized
|
||||||
|
}: ToggleOptions) {
|
||||||
|
const toggle = ref(true)
|
||||||
|
const { props, cbs } = mockProps({ mode: 'in-out' })
|
||||||
|
const root = mount(
|
||||||
|
props,
|
||||||
|
() => (toggle.value ? trueBranch() : falseBranch())
|
||||||
|
)
|
||||||
|
|
||||||
|
toggle.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe(`${trueSerialized}${falseSerialized}`)
|
||||||
|
|
||||||
|
// toggle back before enter finishes
|
||||||
|
toggle.value = true
|
||||||
|
await nextTick()
|
||||||
|
// should force remove stale true branch
|
||||||
|
expect(serializeInner(root)).toBe(`${falseSerialized}${trueSerialized}`)
|
||||||
|
expect(props.onBeforeEnter).toHaveBeenCalledTimes(2)
|
||||||
|
assertCalledWithEl(props.onBeforeEnter, falseSerialized)
|
||||||
|
assertCalledWithEl(props.onBeforeEnter, trueSerialized, 1)
|
||||||
|
expect(props.onEnter).toHaveBeenCalledTimes(2)
|
||||||
|
assertCalledWithEl(props.onEnter, falseSerialized)
|
||||||
|
assertCalledWithEl(props.onEnter, trueSerialized, 1)
|
||||||
|
expect(props.onAfterEnter).not.toHaveBeenCalled()
|
||||||
|
expect(props.onEnterCancelled).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
// calling the enter done for false branch does fire the afterEnter
|
||||||
|
// hook, but should have no other effects since stale branch has already
|
||||||
|
// left
|
||||||
|
cbs.doneEnter[falseSerialized]()
|
||||||
|
expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
|
||||||
|
assertCalledWithEl(props.onAfterEnter, falseSerialized)
|
||||||
|
|
||||||
|
// leave should not start for either branch
|
||||||
|
expect(props.onBeforeLeave).not.toHaveBeenCalled()
|
||||||
|
expect(props.onLeave).not.toHaveBeenCalled()
|
||||||
|
expect(props.onAfterLeave).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
cbs.doneEnter[trueSerialized]()
|
||||||
|
expect(props.onAfterEnter).toHaveBeenCalledTimes(2)
|
||||||
|
assertCalledWithEl(props.onAfterEnter, trueSerialized, 1)
|
||||||
|
// should start leave for false branch
|
||||||
|
expect(props.onBeforeLeave).toHaveBeenCalledTimes(1)
|
||||||
|
assertCalledWithEl(props.onBeforeLeave, falseSerialized)
|
||||||
|
expect(props.onLeave).toHaveBeenCalledTimes(1)
|
||||||
|
assertCalledWithEl(props.onLeave, falseSerialized)
|
||||||
|
expect(props.onAfterLeave).not.toHaveBeenCalled()
|
||||||
|
// finish leave
|
||||||
|
cbs.doneLeave[falseSerialized]()
|
||||||
|
expect(serializeInner(root)).toBe(trueSerialized)
|
||||||
|
expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
|
||||||
|
assertCalledWithEl(props.onAfterLeave, falseSerialized)
|
||||||
|
|
||||||
|
assertCalls(props, {
|
||||||
|
onBeforeEnter: 2,
|
||||||
|
onEnter: 2,
|
||||||
|
onAfterEnter: 2,
|
||||||
|
onEnterCancelled: 0,
|
||||||
|
onBeforeLeave: 1,
|
||||||
|
onLeave: 1,
|
||||||
|
onAfterLeave: 1,
|
||||||
|
onLeaveCancelled: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test('w/ elements', async () => {
|
||||||
|
await runTestWithElements(testInOutBeforeFinish)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('w/ components', async () => {
|
||||||
|
await runTestWithComponents(testInOutBeforeFinish)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('with KeepAlive', () => {
|
describe('with KeepAlive', () => {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -48,7 +48,11 @@ export interface TransitionHooks {
|
|||||||
enter(el: object): void
|
enter(el: object): void
|
||||||
leave(el: object, remove: () => void): void
|
leave(el: object, remove: () => void): void
|
||||||
afterLeave?(): void
|
afterLeave?(): void
|
||||||
delayLeave?(delayedLeave: () => void): void
|
delayLeave?(
|
||||||
|
el: object,
|
||||||
|
earlyRemove: () => void,
|
||||||
|
delayedLeave: () => void
|
||||||
|
): void
|
||||||
delayedLeave?(): void
|
delayedLeave?(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,7 +178,22 @@ const BaseTransitionImpl = {
|
|||||||
return emptyPlaceholder(child)
|
return emptyPlaceholder(child)
|
||||||
} else if (mode === 'in-out') {
|
} else if (mode === 'in-out') {
|
||||||
delete prevHooks.delayedLeave
|
delete prevHooks.delayedLeave
|
||||||
leavingHooks.delayLeave = delayedLeave => {
|
leavingHooks.delayLeave = (
|
||||||
|
el: TransitionElement,
|
||||||
|
earlyRemove,
|
||||||
|
delayedLeave
|
||||||
|
) => {
|
||||||
|
const leavingVNodesCache = getLeavingNodesForType(
|
||||||
|
state,
|
||||||
|
oldInnerChild
|
||||||
|
)
|
||||||
|
leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild
|
||||||
|
// early removal callback
|
||||||
|
el._leaveCb = () => {
|
||||||
|
earlyRemove()
|
||||||
|
el._leaveCb = undefined
|
||||||
|
delete enterHooks.delayedLeave
|
||||||
|
}
|
||||||
enterHooks.delayedLeave = delayedLeave
|
enterHooks.delayedLeave = delayedLeave
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,6 +230,19 @@ export const BaseTransition = (BaseTransitionImpl as any) as {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLeavingNodesForType(
|
||||||
|
state: TransitionState,
|
||||||
|
vnode: VNode
|
||||||
|
): Record<string, VNode> {
|
||||||
|
const { leavingVNodes } = state
|
||||||
|
let leavingVNodesCache = leavingVNodes.get(vnode.type)!
|
||||||
|
if (!leavingVNodesCache) {
|
||||||
|
leavingVNodesCache = Object.create(null)
|
||||||
|
leavingVNodes.set(vnode.type, leavingVNodesCache)
|
||||||
|
}
|
||||||
|
return leavingVNodesCache
|
||||||
|
}
|
||||||
|
|
||||||
// The transition hooks are attached to the vnode as vnode.transition
|
// The transition hooks are attached to the vnode as vnode.transition
|
||||||
// and will be called at appropriate timing in the renderer.
|
// and will be called at appropriate timing in the renderer.
|
||||||
function resolveTransitionHooks(
|
function resolveTransitionHooks(
|
||||||
@ -231,12 +263,7 @@ function resolveTransitionHooks(
|
|||||||
callHook: TransitionHookCaller
|
callHook: TransitionHookCaller
|
||||||
): TransitionHooks {
|
): TransitionHooks {
|
||||||
const key = String(vnode.key)
|
const key = String(vnode.key)
|
||||||
const { leavingVNodes } = state
|
const leavingVNodesCache = getLeavingNodesForType(state, vnode)
|
||||||
let leavingVNodesCache = leavingVNodes.get(vnode.type)!
|
|
||||||
if (!leavingVNodesCache) {
|
|
||||||
leavingVNodesCache = Object.create(null)
|
|
||||||
leavingVNodes.set(vnode.type, leavingVNodesCache)
|
|
||||||
}
|
|
||||||
|
|
||||||
const hooks: TransitionHooks = {
|
const hooks: TransitionHooks = {
|
||||||
persisted,
|
persisted,
|
||||||
|
@ -1430,14 +1430,15 @@ export function createRenderer<
|
|||||||
queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
|
queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
|
||||||
} else {
|
} else {
|
||||||
const { leave, delayLeave, afterLeave } = transition!
|
const { leave, delayLeave, afterLeave } = transition!
|
||||||
|
const remove = () => hostInsert(el!, container, anchor)
|
||||||
const performLeave = () => {
|
const performLeave = () => {
|
||||||
leave(el!, () => {
|
leave(el!, () => {
|
||||||
hostInsert(el!, container, anchor)
|
remove()
|
||||||
afterLeave && afterLeave()
|
afterLeave && afterLeave()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (delayLeave) {
|
if (delayLeave) {
|
||||||
delayLeave(performLeave)
|
delayLeave(el!, remove, performLeave)
|
||||||
} else {
|
} else {
|
||||||
performLeave()
|
performLeave()
|
||||||
}
|
}
|
||||||
@ -1526,7 +1527,7 @@ export function createRenderer<
|
|||||||
const { leave, delayLeave } = transition
|
const { leave, delayLeave } = transition
|
||||||
const performLeave = () => leave(el!, remove)
|
const performLeave = () => leave(el!, remove)
|
||||||
if (delayLeave) {
|
if (delayLeave) {
|
||||||
delayLeave(performLeave)
|
delayLeave(vnode.el!, remove, performLeave)
|
||||||
} else {
|
} else {
|
||||||
performLeave()
|
performLeave()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user