Repository: tailwindlabs/headlessui
Pull request #3331 (2024-06-26 11:48:03)

Fix incorrect `Transition` boundary for `Dialog` component
Ensure that each `Dialog` is always a new `Transition` boundary such that nested `TransitionChild` components are correctly handled.

If you happen to nest `Dialog` components in places where you shouldn't (e.g.: inside a `Disclosure` instead of a `DisclosurePanel`) then it could lead to a crash that has been fixed by this change.

Fixes: #3316
Fixes: #3322



vercel[bot]: [vc]: #Cto/hzAPEJ0JNOn9i7UDDwbslGCVMY6cXgvtegzBD8k=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJoZWFkbGVzc3VpLXJlYWN0Iiwicm9vdERpcmVjdG9yeSI6InBsYXlncm91bmRzL3JlYWN0IiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3RhaWx3aW5kbGFicy9oZWFkbGVzc3VpLXJlYWN0Lzc0U01Ub0pQV1A5eDhkMVhGblg5Szh0WDNKSDIiLCJwcmV2aWV3VXJsIjoiaGVhZGxlc3N1aS1yZWFjdC1naXQtZml4LWlzc3VlLTMzMjItdGFpbHdpbmRsYWJzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiUEVORElORyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJoZWFkbGVzc3VpLXJlYWN0LWdpdC1maXgtaXNzdWUtMzMyMi10YWlsd2luZGxhYnMudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJoZWFkbGVzc3VpLXZ1ZSIsInJvb3REaXJlY3RvcnkiOiJwbGF5Z3JvdW5kcy92dWUiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiaGVhZGxlc3N1aS12dWUtZ2l0LWZpeC1pc3N1ZS0zMzIyLXRhaWx3aW5kbGFicy52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS90YWlsd2luZGxhYnMvaGVhZGxlc3N1aS12dWUvRzJlYm53YlV1QWVEcUE4eHRlTlVjUjR6YnV5cCIsInByZXZpZXdVcmwiOiJoZWFkbGVzc3VpLXZ1ZS1naXQtZml4LWlzc3VlLTMzMjItdGFpbHdpbmRsYWJzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **headlessui-react** | 🔄 Building ([Inspect](https://vercel.com/tailwindlabs/headlessui-react/74SMToJPWP9x8d1XFnX9K8tX3JH2)) | [Visit Preview](https://vercel.live/open-feedback/headlessui-react-git-fix-issue-3322-tailwindlabs.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/headlessui-react-git-fix-issue-3322-tailwindlabs.vercel.app?via=pr-comment-feedback-link) | Jun 26, 2024 11:48am |
| **headlessui-vue** | ✅ Ready ([Inspect](https://vercel.com/tailwindlabs/headlessui-vue/G2ebnwbUuAeDqA8xteNUcR4zbuyp)) | [Visit Preview](https://vercel.live/open-feedback/headlessui-vue-git-fix-issue-3322-tailwindlabs.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/headlessui-vue-git-fix-issue-3322-tailwindlabs.vercel.app?via=pr-comment-feedback-link) | Jun 26, 2024 11:48am |




Patch:
diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index 757fa1b..f8705e3 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fix issues spreading omitted props onto components ([#3313](https://github.com/tailwindlabs/headlessui/pull/3313))
 - Fix initial `anchor="selection"` positioning ([#3324](https://github.com/tailwindlabs/headlessui/pull/3324))
 - Fix render prop in `ComboboxOptions` to use `any` instead of `unknown` ([#3327](https://github.com/tailwindlabs/headlessui/pull/3327))
+- Fix incorrect `Transition` boundary for `Dialog` component ([#3331](https://github.com/tailwindlabs/headlessui/pull/3331))
 
 ## [2.1.0] - 2024-06-21
 
diff --git a/packages/@headlessui-react/src/components/dialog/dialog.tsx b/packages/@headlessui-react/src/components/dialog/dialog.tsx
index 5db0b2f..1a9b7fe 100644
--- a/packages/@headlessui-react/src/components/dialog/dialog.tsx
+++ b/packages/@headlessui-react/src/components/dialog/dialog.tsx
@@ -390,8 +390,7 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
     )
   }
 
-  let inTransitionComponent = usesOpenClosedState !== null
-  if (!inTransitionComponent && open !== undefined && !rest.static) {
+  if ((open !== undefined || transition) && !rest.static) {
     return (
       <Transition show={open} transition={transition} unmount={rest.unmount}>
         <InternalDialog ref={ref} {...rest} />


Test patch:
diff --git a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx
index 2967dde..e9a71d4 100644
--- a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx
+++ b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx
@@ -1,21 +1,20 @@
 import { render } from '@testing-library/react'
-import React, { createElement, Fragment, useCallback, useEffect, useRef, useState } from 'react'
+import React, { Fragment, createElement, useCallback, useEffect, useRef, useState } from 'react'
 import { createPortal } from 'react-dom'
-import { OpenClosedProvider, State } from '../../internal/open-closed'
 import {
+  DialogState,
+  PopoverState,
   assertActiveElement,
   assertDialog,
   assertDialogDescription,
   assertDialogTitle,
   assertPopoverPanel,
-  DialogState,
   getByText,
   getDialog,
   getDialogs,
   getPopoverButton,
-  PopoverState,
 } from '../../test-utils/accessibility-assertions'
-import { click, focus, Keys, mouseDrag, press, shift } from '../../test-utils/interactions'
+import { Keys, click, focus, mouseDrag, press, shift } from '../../test-utils/interactions'
 import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
 import type { PropsOf } from '../../types'
 import { Popover } from '../popover/popover'
@@ -498,25 +497,51 @@ describe('Rendering', () => {
     it(
       'should remove the scroll lock when the open closed state is `Closing`',
       suppressConsoleLogs(async () => {
-        function Example({ value = State.Open }) {
+        function Example({ open = true }) {
+          return (
+            <Dialog transition autoFocus={false} open={open} onClose={() => {}}>
+              <input id="a" type="text" />
+              <input id="b" type="text" />
+              <input id="c" type="text" />
+            </Dialog>
+          )
+        }
+
+        let { rerender } = render(<Example open={true} />)
+
+        // The overflow should be there
+        expect(document.documentElement.style.overflow).toBe('hidden')
+
+        // Re-render but with an exit transition
+        rerender(<Example open={false} />)
+
+        // The moment the dialog is closing, the overflow should be gone
+        expect(document.documentElement.style.overflow).toBe('')
+      })
+    )
+
+    it(
+      'should remove the scroll lock when the open closed state is `Closing` (using Transition wrapper)',
+      suppressConsoleLogs(async () => {
+        function Example({ open = true }) {
           return (
-            <OpenClosedProvider value={value}>
-              <Dialog autoFocus={false} open={true} onClose={() => {}}>
+            <Transition show={open}>
+              <Dialog autoFocus={false} onClose={() => {}}>
                 <input id="a" type="text" />
                 <input id="b" type="text" />
                 <input id="c" type="text" />
               </Dialog>
-            </OpenClosedProvider>
+            </Transition>
           )
         }
 
-        let { rerender } = render(<Example value={State.Open} />)
+        let { rerender } = render(<Example open={true} />)
 
         // The overflow should be there
         expect(document.documentElement.style.overflow).toBe('hidden')
 
-        // Re-render but with the `Closing` state
-        rerender(<Example value={State.Open | State.Closing} />)
+        // Re-render but with an exit transition
+        rerender(<Example open={false} />)
 
         // The moment the dialog is closing, the overflow should be gone
         expect(document.documentElement.style.overflow).toBe('')