Skip to content

Commit 8ca73eb

Browse files
Don’t throw when SSR rendering internal portals in Vue (#1459)
* Don’t throw when SSR rendering portals * Update changelog
1 parent 6fd5f58 commit 8ca73eb

File tree

3 files changed

+81
-1
lines changed

3 files changed

+81
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Simplify `Popover` Tab logic by using sentinel nodes instead of keydown event interception ([#1440](https://github.com/tailwindlabs/headlessui/pull/1440))
1616
- Ensure the `PopoverPanel` is clickable without closing the `Popover` ([#1443](https://github.com/tailwindlabs/headlessui/pull/1443))
1717
- Improve "Scroll lock" scrollbar width for `Dialog` component ([#1457](https://github.com/tailwindlabs/headlessui/pull/1457))
18+
- Don’t throw when SSR rendering internal portals in Vue ([#1459](https://github.com/tailwindlabs/headlessui/pull/1459))
1819

1920
## [Unreleased - @headlessui/react]
2021

packages/@headlessui-vue/src/components/portal/portal.test.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { defineComponent, ref, nextTick, ComponentOptionsWithoutProps } from 'vue'
1+
import { h, defineComponent, ref, nextTick, ComponentOptionsWithoutProps, createSSRApp } from 'vue'
22

33
import { render } from '../../test-utils/vue-testing-library'
4+
import { renderToString } from 'vue/server-renderer'
45
import { Portal, PortalGroup } from './portal'
56
import { click } from '../../test-utils/interactions'
67
import { html } from '../../test-utils/html'
@@ -38,6 +39,80 @@ function renderTemplate(input: string | ComponentOptionsWithoutProps) {
3839
)
3940
}
4041

42+
async function ssrRenderTemplate(input: string | ComponentOptionsWithoutProps) {
43+
let defaultComponents = { Portal, PortalGroup }
44+
45+
if (typeof input === 'string') {
46+
let app = createSSRApp({
47+
render: () => h(defineComponent({ template: input, components: defaultComponents })),
48+
})
49+
50+
return await renderToString(app)
51+
}
52+
53+
let app = createSSRApp({
54+
render: () =>
55+
h(
56+
defineComponent(
57+
Object.assign({}, input, {
58+
components: { ...defaultComponents, ...input.components },
59+
}) as Parameters<typeof defineComponent>[0]
60+
)
61+
),
62+
})
63+
64+
return await renderToString(app)
65+
}
66+
67+
async function withoutBrowserGlobals<T>(fn: () => Promise<T>) {
68+
let oldWindow = globalThis.window
69+
let oldDocument = globalThis.document
70+
71+
Object.defineProperty(globalThis, '_document', {
72+
value: undefined,
73+
configurable: true,
74+
})
75+
76+
Object.defineProperty(globalThis, '_globalProxy', {
77+
value: undefined,
78+
configurable: true,
79+
})
80+
81+
try {
82+
return await fn()
83+
} finally {
84+
Object.defineProperty(globalThis, '_globalProxy', {
85+
value: oldWindow,
86+
configurable: true,
87+
})
88+
89+
Object.defineProperty(globalThis, '_document', {
90+
value: oldDocument,
91+
configurable: true,
92+
})
93+
}
94+
}
95+
96+
it('SSR-rendering a Portal should not error', async () => {
97+
expect(getPortalRoot()).toBe(null)
98+
99+
let result = await withoutBrowserGlobals(() =>
100+
ssrRenderTemplate(
101+
html`
102+
<main id="parent">
103+
<Portal>
104+
<p id="content">Contents...</p>
105+
</Portal>
106+
</main>
107+
`
108+
)
109+
)
110+
111+
expect(getPortalRoot()).toBe(null)
112+
113+
expect(result).toBe(html`<main id="parent"><!----></main>`)
114+
})
115+
41116
it('should be possible to use a Portal', () => {
42117
expect(getPortalRoot()).toBe(null)
43118

packages/@headlessui-vue/src/components/portal/portal.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import { getOwnerDocument } from '../../utils/owner'
2323
function getPortalRoot(contextElement?: Element | null) {
2424
let ownerDocument = getOwnerDocument(contextElement)
2525
if (!ownerDocument) {
26+
if (contextElement === null) {
27+
return null
28+
}
29+
2630
throw new Error(
2731
`[Headless UI]: Cannot find ownerDocument for contextElement: ${contextElement}`
2832
)

0 commit comments

Comments
 (0)