diff --git a/CHANGELOG.md b/CHANGELOG.md index ecb306b1bb..70a622e659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Simplify `Popover` Tab logic by using sentinel nodes instead of keydown event interception ([#1440](https://github.com/tailwindlabs/headlessui/pull/1440)) - Ensure the `PopoverPanel` is clickable without closing the `Popover` ([#1443](https://github.com/tailwindlabs/headlessui/pull/1443)) - Improve "Scroll lock" scrollbar width for `Dialog` component ([#1457](https://github.com/tailwindlabs/headlessui/pull/1457)) +- Don’t throw when SSR rendering internal portals in Vue ([#1459](https://github.com/tailwindlabs/headlessui/pull/1459)) ## [Unreleased - @headlessui/react] diff --git a/packages/@headlessui-vue/src/components/portal/portal.test.ts b/packages/@headlessui-vue/src/components/portal/portal.test.ts index 0c1055e783..e0baae06d0 100644 --- a/packages/@headlessui-vue/src/components/portal/portal.test.ts +++ b/packages/@headlessui-vue/src/components/portal/portal.test.ts @@ -1,6 +1,7 @@ -import { defineComponent, ref, nextTick, ComponentOptionsWithoutProps } from 'vue' +import { h, defineComponent, ref, nextTick, ComponentOptionsWithoutProps, createSSRApp } from 'vue' import { render } from '../../test-utils/vue-testing-library' +import { renderToString } from 'vue/server-renderer' import { Portal, PortalGroup } from './portal' import { click } from '../../test-utils/interactions' import { html } from '../../test-utils/html' @@ -38,6 +39,80 @@ function renderTemplate(input: string | ComponentOptionsWithoutProps) { ) } +async function ssrRenderTemplate(input: string | ComponentOptionsWithoutProps) { + let defaultComponents = { Portal, PortalGroup } + + if (typeof input === 'string') { + let app = createSSRApp({ + render: () => h(defineComponent({ template: input, components: defaultComponents })), + }) + + return await renderToString(app) + } + + let app = createSSRApp({ + render: () => + h( + defineComponent( + Object.assign({}, input, { + components: { ...defaultComponents, ...input.components }, + }) as Parameters[0] + ) + ), + }) + + return await renderToString(app) +} + +async function withoutBrowserGlobals(fn: () => Promise) { + let oldWindow = globalThis.window + let oldDocument = globalThis.document + + Object.defineProperty(globalThis, '_document', { + value: undefined, + configurable: true, + }) + + Object.defineProperty(globalThis, '_globalProxy', { + value: undefined, + configurable: true, + }) + + try { + return await fn() + } finally { + Object.defineProperty(globalThis, '_globalProxy', { + value: oldWindow, + configurable: true, + }) + + Object.defineProperty(globalThis, '_document', { + value: oldDocument, + configurable: true, + }) + } +} + +it('SSR-rendering a Portal should not error', async () => { + expect(getPortalRoot()).toBe(null) + + let result = await withoutBrowserGlobals(() => + ssrRenderTemplate( + html` +
+ +

Contents...

+
+
+ ` + ) + ) + + expect(getPortalRoot()).toBe(null) + + expect(result).toBe(html`
`) +}) + it('should be possible to use a Portal', () => { expect(getPortalRoot()).toBe(null) diff --git a/packages/@headlessui-vue/src/components/portal/portal.ts b/packages/@headlessui-vue/src/components/portal/portal.ts index 87c33e3580..3ee164d0bf 100644 --- a/packages/@headlessui-vue/src/components/portal/portal.ts +++ b/packages/@headlessui-vue/src/components/portal/portal.ts @@ -23,6 +23,10 @@ import { getOwnerDocument } from '../../utils/owner' function getPortalRoot(contextElement?: Element | null) { let ownerDocument = getOwnerDocument(contextElement) if (!ownerDocument) { + if (contextElement === null) { + return null + } + throw new Error( `[Headless UI]: Cannot find ownerDocument for contextElement: ${contextElement}` )