Fix Portal SSR hydration mismatches (#2700)

* Register portal based on element presence in the DOM

This always coincides with `onMounted` currently but that’s about to change

* Mount element lazily for portals

This prevent’s SSR hydration issues and matches the behavior of React’s `<Portal>` element

* Fix portal tests

* Update comment

* Update changelog
This commit is contained in:
Jordan Pittman
2023-08-23 14:38:53 -04:00
committed by GitHub
parent 5a3d556f29
commit 6444e01bc1
3 changed files with 25 additions and 5 deletions
+1
View File
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Don't call `<Dialog>`'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
@@ -99,7 +99,7 @@ it('SSR-rendering a Portal should not error', async () => {
expect(result).toBe(html`<main id="parent"><!----></main>`)
})
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)
@@ -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 = {