From 98e5a013f9ab3eba78ce9dc48c20b75e41292e5d Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 1 Nov 2022 14:57:45 -0700 Subject: [PATCH] [Float] handle resource Resource creation inside svg context (#25599) `title` is a valid element descendent of `svg`. this PR adds a prohibition on turning titles in svg into Resources. This PR also adds additional warnings if you render something that is almost a Resource inside an svg. --- .../src/client/ReactDOMFloatClient.js | 100 +++++- .../src/client/ReactDOMHostConfig.js | 33 +- .../src/server/ReactDOMServerFormatConfig.js | 4 + .../src/__tests__/ReactDOMFloat-test.js | 333 +++++++++++++++++- 4 files changed, 449 insertions(+), 21 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js b/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js index c9e8607eaaf7d..55a37d240351b 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js +++ b/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js @@ -34,6 +34,8 @@ import { getHostContext, } from 'react-reconciler/src/ReactFiberHostContext'; import {getResourceFormOnly} from './validateDOMNesting'; +import {getNamespace} from './ReactDOMHostConfig'; +import {SVG_NAMESPACE} from '../shared/DOMNamespaces'; // The resource types we support. currently they match the form for the as argument. // In the future this may need to change, especially when modules / scripts are supported @@ -201,6 +203,28 @@ function getCurrentResourceRoot(): null | FloatRoot { return currentContainer ? currentContainer.getRootNode() : null; } +// This resource type constraint can be loosened. It really is everything except PreloadResource +// because that is the only one that does not have an optional instance type. Expand as needed. +function resetInstance(resource: ScriptResource | HeadResource) { + resource.instance = undefined; +} + +export function clearRootResources(rootContainer: Container): void { + const rootNode: FloatRoot = (rootContainer.getRootNode(): any); + const resources = getResourcesFromRoot(rootNode); + + // We can't actually delete the resource cache because this function is called + // during commit after we have rendered. Instead we detatch any instances from + // the Resource object if they are going to be cleared + + // Styles stay put + // Scripts get reset + resources.scripts.forEach(resetInstance); + // Head Resources get reset + resources.head.forEach(resetInstance); + // lastStructuredMeta stays put +} + // Preloads are somewhat special. Even if we don't have the Document // used by the root that is rendering a component trying to insert a preload // we can still seed the file cache by doing the preload on any document we have @@ -1077,7 +1101,14 @@ function acquireHeadResource(resource: HeadResource): Instance { props, root, ); - insertResourceInstanceBefore(root, instance, titles.item(0)); + const firstTitle = titles[0]; + insertResourceInstanceBefore( + root, + instance, + firstTitle && firstTitle.namespaceURI !== SVG_NAMESPACE + ? firstTitle + : null, + ); break; } case 'meta': { @@ -1397,16 +1428,21 @@ function insertResourceInstanceBefore( export function isHostResourceType(type: string, props: Props): boolean { let resourceFormOnly: boolean; + let namespace: string; if (__DEV__) { const hostContext = getHostContext(); resourceFormOnly = getResourceFormOnly(hostContext); + namespace = getNamespace(hostContext); } switch (type) { case 'base': - case 'meta': - case 'title': { + case 'meta': { return true; } + case 'title': { + const hostContext = getHostContext(); + return getNamespace(hostContext) !== SVG_NAMESPACE; + } case 'link': { const {onLoad, onError} = props; if (onLoad || onError) { @@ -1417,6 +1453,11 @@ export function isHostResourceType(type: string, props: Props): boolean { ' Try removing onLoad={...} and onError={...} or moving it into the root tag or' + ' somewhere in the .', ); + } else if (namespace === SVG_NAMESPACE) { + console.error( + 'Cannot render a with onLoad or onError listeners as a descendent of .' + + ' Try removing onLoad={...} and onError={...} or moving it above the ancestor.', + ); } } return false; @@ -1426,11 +1467,18 @@ export function isHostResourceType(type: string, props: Props): boolean { const {href, precedence, disabled} = props; if (__DEV__) { validateLinkPropsForStyleResource(props); - if (typeof precedence !== 'string' && resourceFormOnly) { - console.error( - 'Cannot render a outside the main document without knowing its precedence.' + - ' Consider adding precedence="default" or moving it into the root tag.', - ); + if (typeof precedence !== 'string') { + if (resourceFormOnly) { + console.error( + 'Cannot render a outside the main document without knowing its precedence.' + + ' Consider adding precedence="default" or moving it into the root tag.', + ); + } else if (namespace === SVG_NAMESPACE) { + console.error( + 'Cannot render a as a descendent of an element without knowing its precedence.' + + ' Consider adding precedence="default" or moving it above the ancestor.', + ); + } } } return ( @@ -1450,17 +1498,31 @@ export function isHostResourceType(type: string, props: Props): boolean { // precedence with these for style resources const {src, async, onLoad, onError} = props; if (__DEV__) { - if (async !== true && resourceFormOnly) { - console.error( - 'Cannot render a sync or defer tag.', - ); - } else if ((onLoad || onError) && resourceFormOnly) { - console.error( - 'Cannot render a