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:
Jordan Pittman
2023-02-10 09:36:09 -05:00
committed by GitHub
parent 5051fff04f
commit 0ff2326171
4 changed files with 183 additions and 3 deletions
@@ -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',
])
})
)
})
+3 -1
View File
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- Nothing yet!
### Fixed
- Dont 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')