diff --git a/packages/@headlessui-react/src/components/portal/portal.test.tsx b/packages/@headlessui-react/src/components/portal/portal.test.tsx
index 53bdd5c1f..41c874ed4 100644
--- a/packages/@headlessui-react/src/components/portal/portal.test.tsx
+++ b/packages/@headlessui-react/src/components/portal/portal.test.tsx
@@ -272,6 +272,42 @@ it('should be possible to tamper with the modal root and restore correctly', asy
expect(getPortalRoot().childNodes).toHaveLength(2)
})
+it('should restore the portal root to the end of the body after external reordering', async () => {
+ expect(getPortalRoot()).toBe(null)
+
+ function Example() {
+ let [tick, setTick] = useState(0)
+
+ return (
+
+
+
+
+
Contents...
+
+
+ )
+ }
+
+ let { container } = render()
+
+ let rerender = document.getElementById('rerender')
+
+ expect(document.body.lastElementChild).toBe(getPortalRoot())
+
+ document.body.insertBefore(getPortalRoot(), container)
+
+ expect(document.body.firstElementChild).toBe(getPortalRoot())
+ expect(document.body.lastElementChild).toBe(container)
+
+ await click(rerender)
+
+ expect(document.body.firstElementChild).toBe(container)
+ expect(document.body.lastElementChild).toBe(getPortalRoot())
+})
+
it('should be possible to force the Portal into a specific element using Portal.Group', async () => {
function Example() {
let container = useRef(null)
diff --git a/packages/@headlessui-react/src/components/portal/portal.tsx b/packages/@headlessui-react/src/components/portal/portal.tsx
index 4bf04b0b8..afe40654e 100644
--- a/packages/@headlessui-react/src/components/portal/portal.tsx
+++ b/packages/@headlessui-react/src/components/portal/portal.tsx
@@ -16,6 +16,7 @@ import React, {
import { createPortal } from 'react-dom'
import { useDisposables } from '../../hooks/use-disposables'
import { useEvent } from '../../hooks/use-event'
+import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
import { useOnUnmount } from '../../hooks/use-on-unmount'
import { useOwnerDocument } from '../../hooks/use-owner'
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
@@ -45,14 +46,20 @@ function usePortalTarget(ownerDocument: Document | null): HTMLElement | null {
return ownerDocument.body.appendChild(root)
})
- // Ensure the portal root is always in the DOM
- useEffect(() => {
+ useIsoMorphicEffect(() => {
if (target === null) return
-
- if (!ownerDocument?.body.contains(target)) {
- ownerDocument?.body.appendChild(target)
+ if (ownerDocument === null) return
+ if (!forceInRoot && groupTarget !== null) return
+
+ // Keep the shared portal root as the last element in the body so external
+ // DOM updates can't leave future dialogs behind newly rendered page content.
+ if (
+ target.parentElement !== ownerDocument.body ||
+ target !== ownerDocument.body.lastElementChild
+ ) {
+ ownerDocument.body.appendChild(target)
}
- }, [target, ownerDocument])
+ })
useEffect(() => {
if (forceInRoot) return