diff --git a/packages/@headlessui-react/src/components/portal/portal.test.tsx b/packages/@headlessui-react/src/components/portal/portal.test.tsx index e504bd0..db4c9c6 100644 --- a/packages/@headlessui-react/src/components/portal/portal.test.tsx +++ b/packages/@headlessui-react/src/components/portal/portal.test.tsx @@ -6,7 +6,7 @@ import { Portal } from './portal' import { click } from '../../test-utils/interactions' function getPortalRoot() { - return document.getElementById('headlessui-portal-root') + return document.getElementById('headlessui-portal-root')! } beforeEach(() => { @@ -140,3 +140,136 @@ it('should cleanup the Portal root when the last Portal is unmounted', async () expect(getPortalRoot()).not.toBe(null) expect(getPortalRoot().childNodes).toHaveLength(1) }) + +it('should be possible to render multiple portals at the same time', async () => { + expect(getPortalRoot()).toBe(null) + + function Example() { + let [renderA, setRenderA] = useState(true) + let [renderB, setRenderB] = useState(true) + let [renderC, setRenderC] = useState(true) + + return ( +
+ + + + + + + {renderA && ( + +

Contents 1 ...

+
+ )} + + {renderB && ( + +

Contents 2 ...

+
+ )} + + {renderC && ( + +

Contents 3 ...

+
+ )} +
+ ) + } + + render() + + expect(getPortalRoot()).not.toBe(null) + expect(getPortalRoot().childNodes).toHaveLength(3) + + // Remove Portal 1 + await click(document.getElementById('a')) + expect(getPortalRoot().childNodes).toHaveLength(2) + + // Remove Portal 2 + await click(document.getElementById('b')) + expect(getPortalRoot().childNodes).toHaveLength(1) + + // Re-add Portal 1 + await click(document.getElementById('a')) + expect(getPortalRoot().childNodes).toHaveLength(2) + + // Remove Portal 3 + await click(document.getElementById('c')) + expect(getPortalRoot().childNodes).toHaveLength(1) + + // Remove Portal 1 + await click(document.getElementById('a')) + expect(getPortalRoot()).toBe(null) + + // Render A and B at the same time! + await click(document.getElementById('double')) + expect(getPortalRoot().childNodes).toHaveLength(2) +}) + +it('should be possible to tamper with the modal root and restore correctly', async () => { + expect(getPortalRoot()).toBe(null) + + function Example() { + let [renderA, setRenderA] = useState(true) + let [renderB, setRenderB] = useState(true) + + return ( +
+ + + + {renderA && ( + +

Contents 1 ...

+
+ )} + + {renderB && ( + +

Contents 2 ...

+
+ )} +
+ ) + } + + render() + + expect(getPortalRoot()).not.toBe(null) + + // Tamper tamper + document.body.removeChild(document.getElementById('headlessui-portal-root')!) + + // Hide Portal 1 and 2 + await click(document.getElementById('a')) + await click(document.getElementById('b')) + + expect(getPortalRoot()).toBe(null) + + // Re-show Portal 1 and 2 + await click(document.getElementById('a')) + await click(document.getElementById('b')) + + expect(getPortalRoot()).not.toBe(null) + expect(getPortalRoot().childNodes).toHaveLength(2) +}) diff --git a/packages/@headlessui-react/src/components/portal/portal.tsx b/packages/@headlessui-react/src/components/portal/portal.tsx index ede249a..8b4aa96 100644 --- a/packages/@headlessui-react/src/components/portal/portal.tsx +++ b/packages/@headlessui-react/src/components/portal/portal.tsx @@ -13,6 +13,16 @@ import { render } from '../../utils/render' import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect' import { StackProvider, useElemenStack } from '../../internal/stack-context' +function resolvePortalRoot() { + if (typeof window === 'undefined') return null + let existingRoot = document.getElementById('headlessui-portal-root') + if (existingRoot) return existingRoot + + let root = document.createElement('div') + root.setAttribute('id', 'headlessui-portal-root') + return document.body.appendChild(root) +} + // --- let DEFAULT_PORTAL_TAG = Fragment @@ -21,15 +31,7 @@ interface PortalRenderPropArg {} export function Portal( props: Props ) { - let [target] = useState(() => { - if (typeof window === 'undefined') return null - let existingRoot = document.getElementById('headlessui-portal-root') - if (existingRoot) return existingRoot - - let root = document.createElement('div') - root.setAttribute('id', 'headlessui-portal-root') - return document.body.appendChild(root) - }) + let [target, setTarget] = useState(resolvePortalRoot) let [element] = useState(() => typeof window === 'undefined' ? null : document.createElement('div') ) @@ -37,7 +39,7 @@ export function Portal( useElemenStack(element) useIsoMorphicEffect(() => { - if (!target) return + if (!target) return setTarget(resolvePortalRoot()) if (!element) return target.appendChild(element) @@ -47,7 +49,7 @@ export function Portal( if (!element) return target.removeChild(element) - if (target.childNodes.length <= 0) document.body.removeChild(target) + if (target.childNodes.length <= 0) target.parentElement?.removeChild(target) } }, [target, element])