diff --git a/packages/@headlessui-react/src/components/transitions/transition.test.tsx b/packages/@headlessui-react/src/components/transitions/transition.test.tsx index 391c9ab2c..a58b82163 100644 --- a/packages/@headlessui-react/src/components/transitions/transition.test.tsx +++ b/packages/@headlessui-react/src/components/transitions/transition.test.tsx @@ -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 ( + <> + + 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" + > + + + + + + + + ) + } + + render() + + 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', + ]) + }) + ) }) diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index 0ae9bd8f2..790e92c64 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -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 diff --git a/packages/@headlessui-vue/src/components/transitions/transition.test.ts b/packages/@headlessui-vue/src/components/transitions/transition.test.ts index e07a58f5b..fe6b32a50 100644 --- a/packages/@headlessui-vue/src/components/transitions/transition.test.ts +++ b/packages/@headlessui-vue/src/components/transitions/transition.test.ts @@ -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` + + + + + + + + + `, + 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', + ]) + }) + ) }) diff --git a/packages/@headlessui-vue/src/components/transitions/transition.ts b/packages/@headlessui-vue/src/components/transitions/transition.ts index 527c25070..875825200 100644 --- a/packages/@headlessui-vue/src/components/transitions/transition.ts +++ b/packages/@headlessui-vue/src/components/transitions/transition.ts @@ -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')