Skip to content

Commit

Permalink
Allow the root namespace to be configured
Browse files Browse the repository at this point in the history
This allows us to insert the correct wrappers when streaming into an
existing non-HTML tree.
  • Loading branch information
sebmarkbage committed Mar 26, 2021
1 parent 49b206f commit 96eb192
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 16 deletions.
59 changes: 58 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ describe('ReactDOMFizzServer', () => {
if (
node.tagName !== 'SCRIPT' &&
node.tagName !== 'TEMPLATE' &&
!node.hasAttribute('hidden')
!node.hasAttribute('hidden') &&
!node.hasAttribute('aria-hidden')
) {
const props = {};
const attributes = node.attributes;
Expand Down Expand Up @@ -596,4 +597,60 @@ describe('ReactDOMFizzServer', () => {
</table>,
);
});

// @gate experimental
it('can stream into an SVG container', async () => {
function AsyncPath({id}) {
return <path id={readText(id)}>{[]}</path>;
}

function App() {
return (
<g>
<Suspense fallback={<text>Loading...</text>}>
<AsyncPath id="my-path" />
</Suspense>
</g>
);
}

await act(async () => {
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
<App />,
writable,
{
namespaceURI: 'http://www.w3.org/2000/svg',
onReadyToStream() {
writable.write('<svg>');
startWriting();
writable.write('</svg>');
},
},
);
});

expect(getVisibleChildren(container)).toEqual(
<svg>
<g>
<text>Loading...</text>
</g>
</svg>,
);

await act(async () => {
resolveText('my-path');
});

expect(getVisibleChildren(container)).toEqual(
<svg>
<g>
<path id="my-path" />
</g>
</svg>,
);

expect(container.querySelector('#my-path').namespaceURI).toBe(
'http://www.w3.org/2000/svg',
);
});
});
3 changes: 2 additions & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {

type Options = {
identifierPrefix?: string,
namespaceURI?: string,
progressiveChunkSize?: number,
signal?: AbortSignal,
onReadyToStream?: () => void,
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function createDrainHandler(destination, request) {

type Options = {
identifierPrefix?: string,
namespaceURI?: string,
progressiveChunkSize?: number,
onReadyToStream?: () => void,
onCompleteAll?: () => void,
Expand All @@ -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,
Expand Down
28 changes: 15 additions & 13 deletions packages/react-dom/src/server/ReactDOMServerFormatConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,9 @@ export function createResponseState(
// 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 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.
const HTML_MODE = 0;
const SVG_MODE = 1;
const MATHML_MODE = 2;
const HTML_MODE = 3; // If we reenter HTML from SVG we know for sure it's HTML.
const HTML_TABLE_MODE = 4;
const HTML_TABLE_BODY_MODE = 5;
const HTML_TABLE_ROW_MODE = 6;
Expand All @@ -80,8 +79,14 @@ function createFormatContext(
};
}

export function createRootFormatContext(): FormatContext {
return createFormatContext(ROOT_MODE, null);
export function createRootFormatContext(namespaceURI?: string): FormatContext {
const insertionMode =
namespaceURI === 'http://www.w3.org/2000/svg'
? SVG_MODE
: namespaceURI === 'http://www.w3.org/1998/Math/MathML'
? MATHML_MODE
: HTML_MODE;
return createFormatContext(insertionMode, null);
}

export function getChildFormatContext(
Expand Down Expand Up @@ -306,10 +311,9 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
return writeChunk(destination, endSuspenseBoundary);
}

// TODO: div won't work if the Root is SVG or MathML.
const startSegmentRoot = stringToPrecomputedChunk('<div hidden id="');
const startSegmentRoot2 = stringToPrecomputedChunk('">');
const endSegmentRoot = stringToPrecomputedChunk('</div>');
const startSegmentHTML = stringToPrecomputedChunk('<div hidden id="');
const startSegmentHTML2 = stringToPrecomputedChunk('">');
const endSegmentHTML = stringToPrecomputedChunk('</div>');

const startSegmentSVG = stringToPrecomputedChunk(
'<svg aria-hidden="true" style="display:none" id="',
Expand Down Expand Up @@ -350,12 +354,11 @@ export function writeStartSegment(
id: number,
): boolean {
switch (formatContext.insertionMode) {
case ROOT_MODE:
case HTML_MODE: {
writeChunk(destination, startSegmentRoot);
writeChunk(destination, startSegmentHTML);
writeChunk(destination, responseState.segmentPrefix);
writeChunk(destination, stringToChunk(id.toString(16)));
return writeChunk(destination, startSegmentRoot2);
return writeChunk(destination, startSegmentHTML2);
}
case SVG_MODE: {
writeChunk(destination, startSegmentSVG);
Expand Down Expand Up @@ -407,9 +410,8 @@ export function writeEndSegment(
formatContext: FormatContext,
): boolean {
switch (formatContext.insertionMode) {
case ROOT_MODE:
case HTML_MODE: {
return writeChunk(destination, endSegmentRoot);
return writeChunk(destination, endSegmentHTML);
}
case SVG_MODE: {
return writeChunk(destination, endSegmentSVG);
Expand Down

0 comments on commit 96eb192

Please sign in to comment.