diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index 9021c23c6ce74..31b1f437c9871 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -97,10 +97,22 @@ describe('ReactDOMFizzServer', () => {
let node = element.firstChild;
while (node) {
if (node.nodeType === 1) {
- if (node.tagName !== 'SCRIPT' && !node.hasAttribute('hidden')) {
+ if (
+ node.tagName !== 'SCRIPT' &&
+ node.tagName !== 'TEMPLATE' &&
+ !node.hasAttribute('hidden') &&
+ !node.hasAttribute('aria-hidden')
+ ) {
const props = {};
const attributes = node.attributes;
for (let i = 0; i < attributes.length; i++) {
+ if (
+ attributes[i].name === 'id' &&
+ attributes[i].value.includes(':')
+ ) {
+ // We assume this is a React added ID that's a non-visual implementation detail.
+ continue;
+ }
props[attributes[i].name] = attributes[i].value;
}
props.children = getVisibleChildren(node);
@@ -112,7 +124,7 @@ describe('ReactDOMFizzServer', () => {
node = node.nextSibling;
}
return children.length === 0
- ? null
+ ? undefined
: children.length === 1
? children[0]
: children;
@@ -408,4 +420,237 @@ describe('ReactDOMFizzServer', () => {
,
]);
});
+
+ // @gate experimental
+ it('can resolve async content in esoteric parents', async () => {
+ function AsyncOption({text}) {
+ return ;
+ }
+
+ function AsyncCol({className}) {
+ return
{[]};
+ }
+
+ function AsyncPath({id}) {
+ return {[]};
+ }
+
+ function AsyncMi({id}) {
+ return {[]};
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
+ await act(async () => {
+ const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
+ ,
+ writable,
+ );
+ startWriting();
+ });
+
+ expect(getVisibleChildren(container)).toEqual(
+
+ Loading...
+
,
+ );
+
+ await act(async () => {
+ resolveText('Hello');
+ });
+
+ await act(async () => {
+ resolveText('World');
+ });
+
+ await act(async () => {
+ resolveText('my-path');
+ resolveText('my-mi');
+ });
+
+ expect(getVisibleChildren(container)).toEqual(
+
+
+
+
+
+
,
+ );
+
+ expect(container.querySelector('#my-path').namespaceURI).toBe(
+ 'http://www.w3.org/2000/svg',
+ );
+ expect(container.querySelector('#my-mi').namespaceURI).toBe(
+ 'http://www.w3.org/1998/Math/MathML',
+ );
+ });
+
+ // @gate experimental
+ it('can resolve async content in table parents', async () => {
+ function AsyncTableBody({className, children}) {
+ return {children};
+ }
+
+ function AsyncTableRow({className, children}) {
+ return {children}
;
+ }
+
+ function AsyncTableCell({text}) {
+ return {readText(text)} | ;
+ }
+
+ function App() {
+ return (
+
+
+
+ Loading... |
+
+
+ }>
+
+
+
+
+
+
+
+ );
+ }
+
+ await act(async () => {
+ const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
+ ,
+ writable,
+ );
+ startWriting();
+ });
+
+ expect(getVisibleChildren(container)).toEqual(
+ ,
+ );
+
+ await act(async () => {
+ resolveText('A');
+ });
+
+ await act(async () => {
+ resolveText('B');
+ });
+
+ await act(async () => {
+ resolveText('C');
+ });
+
+ expect(getVisibleChildren(container)).toEqual(
+ ,
+ );
+ });
+
+ // @gate experimental
+ it('can stream into an SVG container', async () => {
+ function AsyncPath({id}) {
+ return {[]};
+ }
+
+ function App() {
+ return (
+
+ Loading...}>
+
+
+
+ );
+ }
+
+ await act(async () => {
+ const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
+ ,
+ writable,
+ {
+ namespaceURI: 'http://www.w3.org/2000/svg',
+ onReadyToStream() {
+ writable.write('');
+ },
+ },
+ );
+ });
+
+ expect(getVisibleChildren(container)).toEqual(
+ ,
+ );
+
+ await act(async () => {
+ resolveText('my-path');
+ });
+
+ expect(getVisibleChildren(container)).toEqual(
+ ,
+ );
+
+ expect(container.querySelector('#my-path').namespaceURI).toBe(
+ 'http://www.w3.org/2000/svg',
+ );
+ });
});
diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
index 254d2380d5ab2..8d5c59a2d6862 100644
--- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
@@ -23,6 +23,7 @@ import {
type Options = {
identifierPrefix?: string,
+ namespaceURI?: string,
progressiveChunkSize?: number,
signal?: AbortSignal,
onReadyToStream?: () => void,
@@ -49,7 +50,7 @@ function renderToReadableStream(
children,
controller,
createResponseState(options ? options.identifierPrefix : undefined),
- createRootFormatContext(), // We call this here in case we need options to initialize it.
+ createRootFormatContext(options ? options.namespaceURI : undefined),
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
options ? options.onCompleteAll : undefined,
diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js
index c9d0cbc303148..1d53e6fda472e 100644
--- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js
+++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js
@@ -28,6 +28,7 @@ function createDrainHandler(destination, request) {
type Options = {
identifierPrefix?: string,
+ namespaceURI?: string,
progressiveChunkSize?: number,
onReadyToStream?: () => void,
onCompleteAll?: () => void,
@@ -49,7 +50,7 @@ function pipeToNodeWritable(
children,
destination,
createResponseState(options ? options.identifierPrefix : undefined),
- createRootFormatContext(), // We call this here in case we need options to initialize it.
+ createRootFormatContext(options ? options.namespaceURI : undefined),
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
options ? options.onCompleteAll : undefined,
diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
index f7ed7ccf93971..d42e92aa1e2b4 100644
--- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
@@ -50,33 +50,45 @@ export function createResponseState(
};
}
-// Constants for the namespace we use. We don't actually provide the namespace but conditionally
-// use different segment parents based on namespace. Therefore we use constants instead of the string.
-const ROOT_NAMESPACE = 0; // At the root we don't need to know which namespace it is. We just need to know that it's already the right one.
-const HTML_NAMESPACE = 1;
-const SVG_NAMESPACE = 2;
-const MATHML_NAMESPACE = 3;
-
-type NamespaceFlag = 0 | 1 | 2 | 3;
+// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
+// modes. We only include the variants as they matter for the sake of our purposes.
+// We don't actually provide the namespace therefore we use constants instead of the string.
+const HTML_MODE = 0;
+const SVG_MODE = 1;
+const MATHML_MODE = 2;
+const HTML_TABLE_MODE = 4;
+const HTML_TABLE_BODY_MODE = 5;
+const HTML_TABLE_ROW_MODE = 6;
+const HTML_COLGROUP_MODE = 7;
+// We have a greater than HTML_TABLE_MODE check elsewhere. If you add more cases here, make sure it
+// still makes sense
+
+type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
// Lets us keep track of contextual state and pick it back up after suspending.
export type FormatContext = {
- namespace: NamespaceFlag, // root/svg/html/mathml
+ insertionMode: InsertionMode, // root/svg/html/mathml/table
selectedValue: null | string, // the selected value(s) inside a