diff --git a/src/internal/portal/__tests__/portal.ssr.test.tsx b/src/internal/portal/__tests__/portal.ssr.test.tsx new file mode 100644 index 0000000..0e9fe18 --- /dev/null +++ b/src/internal/portal/__tests__/portal.ssr.test.tsx @@ -0,0 +1,31 @@ +/** + * @jest-environment node + */ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; + +import Portal from '../index'; + +// The probe span must be part of the server markup, otherwise it appears as an +// added element during hydration and React discards the whole subtree. +// See https://github.com/cloudscape-design/component-toolkit/pull/214 +test('renders the probe span so that server and client markup match', () => { + const content = renderToStaticMarkup( + +

Hello!

+
+ ); + expect(content).toBe(''); +}); + +test('does not render portal children on the server', () => { + const content = renderToStaticMarkup( + +

Hello!

+
+ ); + expect(content).not.toContain('Hello!'); +}); diff --git a/src/internal/portal/index.tsx b/src/internal/portal/index.tsx index 8c30f8e..55c0753 100644 --- a/src/internal/portal/index.tsx +++ b/src/internal/portal/index.tsx @@ -88,8 +88,9 @@ export default function Portal({ // On the first render, activeContainer is null because the layout effect hasn't // created it yet. We render a hidden probe span so the effect can read // ref.current.ownerDocument to discover the correct document (e.g. inside iframes). - // In SSR there's no document, so we return null to match the previous behavior. - if (!activeContainer && typeof document !== 'undefined') { + // The span is rendered during SSR too, so that the server markup matches the first + // client render and hydration does not fail. + if (!activeContainer) { return ; }