Skip to content

Commit 96eb192

Browse files
committed
Allow the root namespace to be configured
This allows us to insert the correct wrappers when streaming into an existing non-HTML tree.
1 parent 49b206f commit 96eb192

File tree

4 files changed

+77
-16
lines changed

4 files changed

+77
-16
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ describe('ReactDOMFizzServer', () => {
100100
if (
101101
node.tagName !== 'SCRIPT' &&
102102
node.tagName !== 'TEMPLATE' &&
103-
!node.hasAttribute('hidden')
103+
!node.hasAttribute('hidden') &&
104+
!node.hasAttribute('aria-hidden')
104105
) {
105106
const props = {};
106107
const attributes = node.attributes;
@@ -596,4 +597,60 @@ describe('ReactDOMFizzServer', () => {
596597
</table>,
597598
);
598599
});
600+
601+
// @gate experimental
602+
it('can stream into an SVG container', async () => {
603+
function AsyncPath({id}) {
604+
return <path id={readText(id)}>{[]}</path>;
605+
}
606+
607+
function App() {
608+
return (
609+
<g>
610+
<Suspense fallback={<text>Loading...</text>}>
611+
<AsyncPath id="my-path" />
612+
</Suspense>
613+
</g>
614+
);
615+
}
616+
617+
await act(async () => {
618+
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
619+
<App />,
620+
writable,
621+
{
622+
namespaceURI: 'http://www.w3.org/2000/svg',
623+
onReadyToStream() {
624+
writable.write('<svg>');
625+
startWriting();
626+
writable.write('</svg>');
627+
},
628+
},
629+
);
630+
});
631+
632+
expect(getVisibleChildren(container)).toEqual(
633+
<svg>
634+
<g>
635+
<text>Loading...</text>
636+
</g>
637+
</svg>,
638+
);
639+
640+
await act(async () => {
641+
resolveText('my-path');
642+
});
643+
644+
expect(getVisibleChildren(container)).toEqual(
645+
<svg>
646+
<g>
647+
<path id="my-path" />
648+
</g>
649+
</svg>,
650+
);
651+
652+
expect(container.querySelector('#my-path').namespaceURI).toBe(
653+
'http://www.w3.org/2000/svg',
654+
);
655+
});
599656
});

packages/react-dom/src/server/ReactDOMFizzServerBrowser.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323

2424
type Options = {
2525
identifierPrefix?: string,
26+
namespaceURI?: string,
2627
progressiveChunkSize?: number,
2728
signal?: AbortSignal,
2829
onReadyToStream?: () => void,
@@ -49,7 +50,7 @@ function renderToReadableStream(
4950
children,
5051
controller,
5152
createResponseState(options ? options.identifierPrefix : undefined),
52-
createRootFormatContext(), // We call this here in case we need options to initialize it.
53+
createRootFormatContext(options ? options.namespaceURI : undefined),
5354
options ? options.progressiveChunkSize : undefined,
5455
options ? options.onError : undefined,
5556
options ? options.onCompleteAll : undefined,

packages/react-dom/src/server/ReactDOMFizzServerNode.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ function createDrainHandler(destination, request) {
2828

2929
type Options = {
3030
identifierPrefix?: string,
31+
namespaceURI?: string,
3132
progressiveChunkSize?: number,
3233
onReadyToStream?: () => void,
3334
onCompleteAll?: () => void,
@@ -49,7 +50,7 @@ function pipeToNodeWritable(
4950
children,
5051
destination,
5152
createResponseState(options ? options.identifierPrefix : undefined),
52-
createRootFormatContext(), // We call this here in case we need options to initialize it.
53+
createRootFormatContext(options ? options.namespaceURI : undefined),
5354
options ? options.progressiveChunkSize : undefined,
5455
options ? options.onError : undefined,
5556
options ? options.onCompleteAll : undefined,

packages/react-dom/src/server/ReactDOMServerFormatConfig.js

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,9 @@ export function createResponseState(
5353
// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
5454
// modes. We only include the variants as they matter for the sake of our purposes.
5555
// We don't actually provide the namespace therefore we use constants instead of the string.
56-
const ROOT_MODE = 0; // At the root we don't need to know which mode it is. We just need to know that it's already the right one.
56+
const HTML_MODE = 0;
5757
const SVG_MODE = 1;
5858
const MATHML_MODE = 2;
59-
const HTML_MODE = 3; // If we reenter HTML from SVG we know for sure it's HTML.
6059
const HTML_TABLE_MODE = 4;
6160
const HTML_TABLE_BODY_MODE = 5;
6261
const HTML_TABLE_ROW_MODE = 6;
@@ -80,8 +79,14 @@ function createFormatContext(
8079
};
8180
}
8281

83-
export function createRootFormatContext(): FormatContext {
84-
return createFormatContext(ROOT_MODE, null);
82+
export function createRootFormatContext(namespaceURI?: string): FormatContext {
83+
const insertionMode =
84+
namespaceURI === 'http://www.w3.org/2000/svg'
85+
? SVG_MODE
86+
: namespaceURI === 'http://www.w3.org/1998/Math/MathML'
87+
? MATHML_MODE
88+
: HTML_MODE;
89+
return createFormatContext(insertionMode, null);
8590
}
8691

8792
export function getChildFormatContext(
@@ -306,10 +311,9 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
306311
return writeChunk(destination, endSuspenseBoundary);
307312
}
308313

309-
// TODO: div won't work if the Root is SVG or MathML.
310-
const startSegmentRoot = stringToPrecomputedChunk('<div hidden id="');
311-
const startSegmentRoot2 = stringToPrecomputedChunk('">');
312-
const endSegmentRoot = stringToPrecomputedChunk('</div>');
314+
const startSegmentHTML = stringToPrecomputedChunk('<div hidden id="');
315+
const startSegmentHTML2 = stringToPrecomputedChunk('">');
316+
const endSegmentHTML = stringToPrecomputedChunk('</div>');
313317

314318
const startSegmentSVG = stringToPrecomputedChunk(
315319
'<svg aria-hidden="true" style="display:none" id="',
@@ -350,12 +354,11 @@ export function writeStartSegment(
350354
id: number,
351355
): boolean {
352356
switch (formatContext.insertionMode) {
353-
case ROOT_MODE:
354357
case HTML_MODE: {
355-
writeChunk(destination, startSegmentRoot);
358+
writeChunk(destination, startSegmentHTML);
356359
writeChunk(destination, responseState.segmentPrefix);
357360
writeChunk(destination, stringToChunk(id.toString(16)));
358-
return writeChunk(destination, startSegmentRoot2);
361+
return writeChunk(destination, startSegmentHTML2);
359362
}
360363
case SVG_MODE: {
361364
writeChunk(destination, startSegmentSVG);
@@ -407,9 +410,8 @@ export function writeEndSegment(
407410
formatContext: FormatContext,
408411
): boolean {
409412
switch (formatContext.insertionMode) {
410-
case ROOT_MODE:
411413
case HTML_MODE: {
412-
return writeChunk(destination, endSegmentRoot);
414+
return writeChunk(destination, endSegmentHTML);
413415
}
414416
case SVG_MODE: {
415417
return writeChunk(destination, endSegmentSVG);

0 commit comments

Comments
 (0)