Skip to content

Commit 0ff2326

Browse files
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
1 parent 5051fff commit 0ff2326

File tree

4 files changed

+183
-3
lines changed

4 files changed

+183
-3
lines changed

packages/@headlessui-react/src/components/transitions/transition.test.tsx

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { Fragment, useState, useRef, useLayoutEffect } from 'react'
1+
import React, { Fragment, useState, useRef, useLayoutEffect, useEffect } from 'react'
22
import { render, fireEvent } from '@testing-library/react'
33

44
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
@@ -1397,4 +1397,93 @@ describe('Events', () => {
13971397
])
13981398
})
13991399
)
1400+
1401+
it(
1402+
'should fire only one event for a given component change',
1403+
suppressConsoleLogs(async () => {
1404+
let eventHandler = jest.fn()
1405+
let enterDuration = 50
1406+
let leaveDuration = 75
1407+
1408+
function Example() {
1409+
let [show, setShow] = useState(false)
1410+
let [start, setStart] = useState(Date.now())
1411+
1412+
useEffect(() => setStart(Date.now()), [])
1413+
1414+
return (
1415+
<>
1416+
<style>{`
1417+
.enter-1 { transition-duration: ${enterDuration * 1}ms; }
1418+
.enter-2 { transition-duration: ${enterDuration * 2}ms; }
1419+
.enter-from { opacity: 0%; }
1420+
.enter-to { opacity: 100%; }
1421+
1422+
.leave-1 { transition-duration: ${leaveDuration * 1}ms; }
1423+
.leave-2 { transition-duration: ${leaveDuration * 2}ms; }
1424+
.leave-from { opacity: 100%; }
1425+
.leave-to { opacity: 0%; }
1426+
`}</style>
1427+
<Transition.Root
1428+
show={show}
1429+
as="div"
1430+
beforeEnter={() => eventHandler('beforeEnter', Date.now() - start)}
1431+
afterEnter={() => eventHandler('afterEnter', Date.now() - start)}
1432+
beforeLeave={() => eventHandler('beforeLeave', Date.now() - start)}
1433+
afterLeave={() => eventHandler('afterLeave', Date.now() - start)}
1434+
enter="enter-2"
1435+
enterFrom="enter-from"
1436+
enterTo="enter-to"
1437+
leave="leave-2"
1438+
leaveFrom="leave-from"
1439+
leaveTo="leave-to"
1440+
>
1441+
<Transition.Child
1442+
enter="enter-1"
1443+
enterFrom="enter-from"
1444+
enterTo="enter-to"
1445+
leave="leave-1"
1446+
leaveFrom="leave-from"
1447+
leaveTo="leave-to"
1448+
/>
1449+
<Transition.Child
1450+
enter="enter-1"
1451+
enterFrom="enter-from"
1452+
enterTo="enter-to"
1453+
leave="leave-1"
1454+
leaveFrom="leave-from"
1455+
leaveTo="leave-to"
1456+
>
1457+
<button data-testid="hide" onClick={() => setShow(false)}>
1458+
Hide
1459+
</button>
1460+
</Transition.Child>
1461+
</Transition.Root>
1462+
<button data-testid="show" onClick={() => setShow(true)}>
1463+
Show
1464+
</button>
1465+
</>
1466+
)
1467+
}
1468+
1469+
render(<Example />)
1470+
1471+
fireEvent.click(document.querySelector('[data-testid=show]')!)
1472+
1473+
await new Promise((resolve) => setTimeout(resolve, 1000))
1474+
1475+
fireEvent.click(document.querySelector('[data-testid=hide]')!)
1476+
1477+
await new Promise((resolve) => setTimeout(resolve, 1000))
1478+
1479+
expect(eventHandler).toHaveBeenCalledTimes(4)
1480+
expect(eventHandler.mock.calls.map(([name]) => name)).toEqual([
1481+
// Order is important here
1482+
'beforeEnter',
1483+
'afterEnter',
1484+
'beforeLeave',
1485+
'afterLeave',
1486+
])
1487+
})
1488+
)
14001489
})

packages/@headlessui-vue/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Don’t fire `afterLeave` event more than once for a given transition ([#2267](https://github.com/tailwindlabs/headlessui/pull/2267))
1113

1214
## [1.7.9] - 2023-02-03
1315

packages/@headlessui-vue/src/components/transitions/transition.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,4 +1258,93 @@ describe('Events', () => {
12581258
expect(leaveHookDiff).toBeLessThanOrEqual(leaveDuration * 3)
12591259
})
12601260
)
1261+
1262+
it(
1263+
'should fire only one event for a given component change',
1264+
suppressConsoleLogs(async () => {
1265+
let eventHandler = jest.fn()
1266+
let enterDuration = 50
1267+
let leaveDuration = 75
1268+
1269+
withStyles(`
1270+
.enter-1 { transition-duration: ${enterDuration * 1}ms; }
1271+
.enter-2 { transition-duration: ${enterDuration * 2}ms; }
1272+
.enter-from { opacity: 0%; }
1273+
.enter-to { opacity: 100%; }
1274+
1275+
.leave-1 { transition-duration: ${leaveDuration * 1}ms; }
1276+
.leave-2 { transition-duration: ${leaveDuration * 2}ms; }
1277+
.leave-from { opacity: 100%; }
1278+
.leave-to { opacity: 0%; }
1279+
`)
1280+
1281+
let Example = defineComponent({
1282+
components: { TransitionRoot, TransitionChild },
1283+
template: html`
1284+
<TransitionRoot
1285+
:show="show"
1286+
as="div"
1287+
@beforeEnter="eventHandler('beforeEnter', Date.now() - start)"
1288+
@afterEnter="eventHandler('afterEnter', Date.now() - start)"
1289+
@beforeLeave="eventHandler('beforeLeave', Date.now() - start)"
1290+
@afterLeave="eventHandler('afterLeave', Date.now() - start)"
1291+
enter="enter-2"
1292+
enterFrom="enter-from"
1293+
enterTo="enter-to"
1294+
leave="leave-2"
1295+
leaveFrom="leave-from"
1296+
leaveTo="leave-to"
1297+
>
1298+
<TransitionChild
1299+
enter="enter-1"
1300+
enterFrom="enter-from"
1301+
enterTo="enter-to"
1302+
leave="leave-1"
1303+
leaveFrom="leave-from"
1304+
leaveTo="leave-to"
1305+
/>
1306+
<TransitionChild
1307+
enter="enter-1"
1308+
enterFrom="enter-from"
1309+
enterTo="enter-to"
1310+
leave="leave-1"
1311+
leaveFrom="leave-from"
1312+
leaveTo="leave-to"
1313+
>
1314+
<button data-testid="hide" @click="show = false" @click="hide">Hide</button>
1315+
</TransitionChild>
1316+
</TransitionRoot>
1317+
1318+
<button data-testid="show" @click="show = true">Show</button>
1319+
`,
1320+
setup() {
1321+
let show = ref(false)
1322+
let start = ref(Date.now())
1323+
1324+
onMounted(() => (start.value = Date.now()))
1325+
1326+
return { show, start, eventHandler }
1327+
},
1328+
})
1329+
1330+
renderTemplate(Example)
1331+
1332+
fireEvent.click(getByTestId('show'))
1333+
1334+
await new Promise((resolve) => setTimeout(resolve, 1000))
1335+
1336+
fireEvent.click(getByTestId('hide'))
1337+
1338+
await new Promise((resolve) => setTimeout(resolve, 1000))
1339+
1340+
expect(eventHandler).toHaveBeenCalledTimes(4)
1341+
expect(eventHandler.mock.calls.map(([name]) => name)).toEqual([
1342+
// Order is important here
1343+
'beforeEnter',
1344+
'afterEnter',
1345+
'beforeLeave',
1346+
'afterLeave',
1347+
])
1348+
})
1349+
)
12611350
})

packages/@headlessui-vue/src/components/transitions/transition.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ export let TransitionChild = defineComponent({
188188
let nesting = useNesting(() => {
189189
// When all children have been unmounted we can only hide ourselves if and only if we are not
190190
// transitioning ourselves. Otherwise we would unmount before the transitions are finished.
191-
if (!isTransitioning.value) {
191+
if (!isTransitioning.value && state.value !== TreeStates.Hidden) {
192192
state.value = TreeStates.Hidden
193193
unregister(id)
194194
emit('afterLeave')

0 commit comments

Comments
 (0)