Skip to content

Commit 3224a9e

Browse files
authored
Fix incorrect Transition boundary for Dialog component (#3331)
* always wrap `Dialog` in a `Transition` Initially we didn't do this, because it's a bit silly to do that if you already had a `Transition` component on the outside. E.g.: ```tsx <Transition show={open}> <Dialog onClose={() => setOpen(false)}> {/* ... */} </Dialog> </Transition> ``` Because this means that we technically have this: ```tsx <Transition show={open}> <Dialog onClose={() => setOpen(false)}> <Transition> <InternalDialog> {/* ... */} </InternalDialog> </Dialog> </Transition> </Transition> ``` The good part is that the inner `Transition` is rendering a `Fragment` and forwards all the props to the underlying element (the internal dialog). This way we have a guaranteed transition boundary. * use public `transition` API instead of private internal API This also mimics better what we are actually trying to do. * update changelog
1 parent 275c205 commit 3224a9e

File tree

3 files changed

+39
-14
lines changed

3 files changed

+39
-14
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Fix issues spreading omitted props onto components ([#3313](https://github.com/tailwindlabs/headlessui/pull/3313))
1313
- Fix initial `anchor="selection"` positioning ([#3324](https://github.com/tailwindlabs/headlessui/pull/3324))
1414
- Fix render prop in `ComboboxOptions` to use `any` instead of `unknown` ([#3327](https://github.com/tailwindlabs/headlessui/pull/3327))
15+
- Fix incorrect `Transition` boundary for `Dialog` component ([#3331](https://github.com/tailwindlabs/headlessui/pull/3331))
1516

1617
## [2.1.0] - 2024-06-21
1718

packages/@headlessui-react/src/components/dialog/dialog.test.tsx

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
import { render } from '@testing-library/react'
2-
import React, { createElement, Fragment, useCallback, useEffect, useRef, useState } from 'react'
2+
import React, { Fragment, createElement, useCallback, useEffect, useRef, useState } from 'react'
33
import { createPortal } from 'react-dom'
4-
import { OpenClosedProvider, State } from '../../internal/open-closed'
54
import {
5+
DialogState,
6+
PopoverState,
67
assertActiveElement,
78
assertDialog,
89
assertDialogDescription,
910
assertDialogTitle,
1011
assertPopoverPanel,
11-
DialogState,
1212
getByText,
1313
getDialog,
1414
getDialogs,
1515
getPopoverButton,
16-
PopoverState,
1716
} from '../../test-utils/accessibility-assertions'
18-
import { click, focus, Keys, mouseDrag, press, shift } from '../../test-utils/interactions'
17+
import { Keys, click, focus, mouseDrag, press, shift } from '../../test-utils/interactions'
1918
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
2019
import type { PropsOf } from '../../types'
2120
import { Popover } from '../popover/popover'
@@ -498,25 +497,51 @@ describe('Rendering', () => {
498497
it(
499498
'should remove the scroll lock when the open closed state is `Closing`',
500499
suppressConsoleLogs(async () => {
501-
function Example({ value = State.Open }) {
500+
function Example({ open = true }) {
501+
return (
502+
<Dialog transition autoFocus={false} open={open} onClose={() => {}}>
503+
<input id="a" type="text" />
504+
<input id="b" type="text" />
505+
<input id="c" type="text" />
506+
</Dialog>
507+
)
508+
}
509+
510+
let { rerender } = render(<Example open={true} />)
511+
512+
// The overflow should be there
513+
expect(document.documentElement.style.overflow).toBe('hidden')
514+
515+
// Re-render but with an exit transition
516+
rerender(<Example open={false} />)
517+
518+
// The moment the dialog is closing, the overflow should be gone
519+
expect(document.documentElement.style.overflow).toBe('')
520+
})
521+
)
522+
523+
it(
524+
'should remove the scroll lock when the open closed state is `Closing` (using Transition wrapper)',
525+
suppressConsoleLogs(async () => {
526+
function Example({ open = true }) {
502527
return (
503-
<OpenClosedProvider value={value}>
504-
<Dialog autoFocus={false} open={true} onClose={() => {}}>
528+
<Transition show={open}>
529+
<Dialog autoFocus={false} onClose={() => {}}>
505530
<input id="a" type="text" />
506531
<input id="b" type="text" />
507532
<input id="c" type="text" />
508533
</Dialog>
509-
</OpenClosedProvider>
534+
</Transition>
510535
)
511536
}
512537

513-
let { rerender } = render(<Example value={State.Open} />)
538+
let { rerender } = render(<Example open={true} />)
514539

515540
// The overflow should be there
516541
expect(document.documentElement.style.overflow).toBe('hidden')
517542

518-
// Re-render but with the `Closing` state
519-
rerender(<Example value={State.Open | State.Closing} />)
543+
// Re-render but with an exit transition
544+
rerender(<Example open={false} />)
520545

521546
// The moment the dialog is closing, the overflow should be gone
522547
expect(document.documentElement.style.overflow).toBe('')

packages/@headlessui-react/src/components/dialog/dialog.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,8 +390,7 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
390390
)
391391
}
392392

393-
let inTransitionComponent = usesOpenClosedState !== null
394-
if (!inTransitionComponent && open !== undefined && !rest.static) {
393+
if ((open !== undefined || transition) && !rest.static) {
395394
return (
396395
<Transition show={open} transition={transition} unmount={rest.unmount}>
397396
<InternalDialog ref={ref} {...rest} />

0 commit comments

Comments
 (0)