Don’t fire afterLeave event more than once for a given transition (#2267)
* Don’t fire afterLeave event more than once for a given component * Port test to React * Fix CS * Remove focus on test * Update changelog
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { Fragment, useState, useRef, useLayoutEffect } from 'react'
|
||||
import React, { Fragment, useState, useRef, useLayoutEffect, useEffect } from 'react'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
|
||||
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
|
||||
@@ -1397,4 +1397,93 @@ describe('Events', () => {
|
||||
])
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should fire only one event for a given component change',
|
||||
suppressConsoleLogs(async () => {
|
||||
let eventHandler = jest.fn()
|
||||
let enterDuration = 50
|
||||
let leaveDuration = 75
|
||||
|
||||
function Example() {
|
||||
let [show, setShow] = useState(false)
|
||||
let [start, setStart] = useState(Date.now())
|
||||
|
||||
useEffect(() => setStart(Date.now()), [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
.enter-1 { transition-duration: ${enterDuration * 1}ms; }
|
||||
.enter-2 { transition-duration: ${enterDuration * 2}ms; }
|
||||
.enter-from { opacity: 0%; }
|
||||
.enter-to { opacity: 100%; }
|
||||
|
||||
.leave-1 { transition-duration: ${leaveDuration * 1}ms; }
|
||||
.leave-2 { transition-duration: ${leaveDuration * 2}ms; }
|
||||
.leave-from { opacity: 100%; }
|
||||
.leave-to { opacity: 0%; }
|
||||
`}</style>
|
||||
<Transition.Root
|
||||
show={show}
|
||||
as="div"
|
||||
beforeEnter={() => eventHandler('beforeEnter', Date.now() - start)}
|
||||
afterEnter={() => eventHandler('afterEnter', Date.now() - start)}
|
||||
beforeLeave={() => eventHandler('beforeLeave', Date.now() - start)}
|
||||
afterLeave={() => eventHandler('afterLeave', Date.now() - start)}
|
||||
enter="enter-2"
|
||||
enterFrom="enter-from"
|
||||
enterTo="enter-to"
|
||||
leave="leave-2"
|
||||
leaveFrom="leave-from"
|
||||
leaveTo="leave-to"
|
||||
>
|
||||
<Transition.Child
|
||||
enter="enter-1"
|
||||
enterFrom="enter-from"
|
||||
enterTo="enter-to"
|
||||
leave="leave-1"
|
||||
leaveFrom="leave-from"
|
||||
leaveTo="leave-to"
|
||||
/>
|
||||
<Transition.Child
|
||||
enter="enter-1"
|
||||
enterFrom="enter-from"
|
||||
enterTo="enter-to"
|
||||
leave="leave-1"
|
||||
leaveFrom="leave-from"
|
||||
leaveTo="leave-to"
|
||||
>
|
||||
<button data-testid="hide" onClick={() => setShow(false)}>
|
||||
Hide
|
||||
</button>
|
||||
</Transition.Child>
|
||||
</Transition.Root>
|
||||
<button data-testid="show" onClick={() => setShow(true)}>
|
||||
Show
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
render(<Example />)
|
||||
|
||||
fireEvent.click(document.querySelector('[data-testid=show]')!)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
fireEvent.click(document.querySelector('[data-testid=hide]')!)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
expect(eventHandler).toHaveBeenCalledTimes(4)
|
||||
expect(eventHandler.mock.calls.map(([name]) => name)).toEqual([
|
||||
// Order is important here
|
||||
'beforeEnter',
|
||||
'afterEnter',
|
||||
'beforeLeave',
|
||||
'afterLeave',
|
||||
])
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Nothing yet!
|
||||
### Fixed
|
||||
|
||||
- Don’t fire `afterLeave` event more than once for a given transition ([#2267](https://github.com/tailwindlabs/headlessui/pull/2267))
|
||||
|
||||
## [1.7.9] - 2023-02-03
|
||||
|
||||
|
||||
@@ -1258,4 +1258,93 @@ describe('Events', () => {
|
||||
expect(leaveHookDiff).toBeLessThanOrEqual(leaveDuration * 3)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should fire only one event for a given component change',
|
||||
suppressConsoleLogs(async () => {
|
||||
let eventHandler = jest.fn()
|
||||
let enterDuration = 50
|
||||
let leaveDuration = 75
|
||||
|
||||
withStyles(`
|
||||
.enter-1 { transition-duration: ${enterDuration * 1}ms; }
|
||||
.enter-2 { transition-duration: ${enterDuration * 2}ms; }
|
||||
.enter-from { opacity: 0%; }
|
||||
.enter-to { opacity: 100%; }
|
||||
|
||||
.leave-1 { transition-duration: ${leaveDuration * 1}ms; }
|
||||
.leave-2 { transition-duration: ${leaveDuration * 2}ms; }
|
||||
.leave-from { opacity: 100%; }
|
||||
.leave-to { opacity: 0%; }
|
||||
`)
|
||||
|
||||
let Example = defineComponent({
|
||||
components: { TransitionRoot, TransitionChild },
|
||||
template: html`
|
||||
<TransitionRoot
|
||||
:show="show"
|
||||
as="div"
|
||||
@beforeEnter="eventHandler('beforeEnter', Date.now() - start)"
|
||||
@afterEnter="eventHandler('afterEnter', Date.now() - start)"
|
||||
@beforeLeave="eventHandler('beforeLeave', Date.now() - start)"
|
||||
@afterLeave="eventHandler('afterLeave', Date.now() - start)"
|
||||
enter="enter-2"
|
||||
enterFrom="enter-from"
|
||||
enterTo="enter-to"
|
||||
leave="leave-2"
|
||||
leaveFrom="leave-from"
|
||||
leaveTo="leave-to"
|
||||
>
|
||||
<TransitionChild
|
||||
enter="enter-1"
|
||||
enterFrom="enter-from"
|
||||
enterTo="enter-to"
|
||||
leave="leave-1"
|
||||
leaveFrom="leave-from"
|
||||
leaveTo="leave-to"
|
||||
/>
|
||||
<TransitionChild
|
||||
enter="enter-1"
|
||||
enterFrom="enter-from"
|
||||
enterTo="enter-to"
|
||||
leave="leave-1"
|
||||
leaveFrom="leave-from"
|
||||
leaveTo="leave-to"
|
||||
>
|
||||
<button data-testid="hide" @click="show = false" @click="hide">Hide</button>
|
||||
</TransitionChild>
|
||||
</TransitionRoot>
|
||||
|
||||
<button data-testid="show" @click="show = true">Show</button>
|
||||
`,
|
||||
setup() {
|
||||
let show = ref(false)
|
||||
let start = ref(Date.now())
|
||||
|
||||
onMounted(() => (start.value = Date.now()))
|
||||
|
||||
return { show, start, eventHandler }
|
||||
},
|
||||
})
|
||||
|
||||
renderTemplate(Example)
|
||||
|
||||
fireEvent.click(getByTestId('show'))
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
fireEvent.click(getByTestId('hide'))
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
expect(eventHandler).toHaveBeenCalledTimes(4)
|
||||
expect(eventHandler.mock.calls.map(([name]) => name)).toEqual([
|
||||
// Order is important here
|
||||
'beforeEnter',
|
||||
'afterEnter',
|
||||
'beforeLeave',
|
||||
'afterLeave',
|
||||
])
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -188,7 +188,7 @@ export let TransitionChild = defineComponent({
|
||||
let nesting = useNesting(() => {
|
||||
// When all children have been unmounted we can only hide ourselves if and only if we are not
|
||||
// transitioning ourselves. Otherwise we would unmount before the transitions are finished.
|
||||
if (!isTransitioning.value) {
|
||||
if (!isTransitioning.value && state.value !== TreeStates.Hidden) {
|
||||
state.value = TreeStates.Hidden
|
||||
unregister(id)
|
||||
emit('afterLeave')
|
||||
|
||||
Reference in New Issue
Block a user