diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index b54b01f4c7..ffe2b1460a 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Don't call ``'s `onClose` twice on mobile devices ([#2690](https://github.com/tailwindlabs/headlessui/pull/2690)) +- Fix Portal SSR hydration mismatches ([#2700](https://github.com/tailwindlabs/headlessui/pull/2700)) ## [1.7.16] - 2023-08-17 diff --git a/packages/@headlessui-vue/src/components/portal/portal.test.ts b/packages/@headlessui-vue/src/components/portal/portal.test.ts index 620cf386c3..ccc49534dd 100644 --- a/packages/@headlessui-vue/src/components/portal/portal.test.ts +++ b/packages/@headlessui-vue/src/components/portal/portal.test.ts @@ -99,7 +99,7 @@ it('SSR-rendering a Portal should not error', async () => { expect(result).toBe(html`
`) }) -it('should be possible to use a Portal', () => { +it('should be possible to use a Portal', async () => { expect(getPortalRoot()).toBe(null) renderTemplate( @@ -112,6 +112,8 @@ it('should be possible to use a Portal', () => { ` ) + await nextTick() + let parent = document.getElementById('parent') let content = document.getElementById('content') @@ -125,7 +127,7 @@ it('should be possible to use a Portal', () => { expect(content).toHaveTextContent('Contents...') }) -it('should be possible to use multiple Portal elements', () => { +it('should be possible to use multiple Portal elements', async () => { expect(getPortalRoot()).toBe(null) renderTemplate( @@ -142,6 +144,8 @@ it('should be possible to use multiple Portal elements', () => { ` ) + await nextTick() + let parent = document.getElementById('parent') let content1 = document.getElementById('content1') let content2 = document.getElementById('content2') @@ -284,6 +288,8 @@ it('should be possible to render multiple portals at the same time', async () => }, }) + await nextTick() + expect(getPortalRoot()).not.toBe(null) expect(getPortalRoot().children).toHaveLength(3) diff --git a/packages/@headlessui-vue/src/components/portal/portal.ts b/packages/@headlessui-vue/src/components/portal/portal.ts index f82eef33b1..1c667b9cc9 100644 --- a/packages/@headlessui-vue/src/components/portal/portal.ts +++ b/packages/@headlessui-vue/src/components/portal/portal.ts @@ -15,6 +15,7 @@ import { InjectionKey, PropType, Ref, + watch, } from 'vue' import { render } from '../../utils/render' import { usePortalRoot } from '../../internal/portal-force-root' @@ -63,6 +64,11 @@ export let Portal = defineComponent({ : groupContext.resolveTarget() ) + let ready = ref(false) + onMounted(() => { + ready.value = true + }) + watchEffect(() => { if (forcePortalRoot) return if (groupContext == null) return @@ -70,12 +76,18 @@ export let Portal = defineComponent({ }) let parent = inject(PortalParentContext, null) - onMounted(() => { + + // Since the element is mounted lazily (because of SSR hydration) + // We use `watch` on `element` + a local var rather than + // `onMounted` to ensure registration only happens once + let didRegister = false + watch(element, () => { + if (didRegister) return + if (!parent) return let domElement = dom(element) if (!domElement) return - if (!parent) return - onUnmounted(parent.register(domElement)) + didRegister = true }) onUnmounted(() => { @@ -89,6 +101,7 @@ export let Portal = defineComponent({ }) return () => { + if (!ready.value) return null if (myTarget.value === null) return null let ourProps = {