diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt
index f277e317d9a..cb71a3431df 100644
--- a/scripts/fiber/tests-passing.txt
+++ b/scripts/fiber/tests-passing.txt
@@ -514,6 +514,12 @@ src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
* should render one portal
* should render many portals
* should render nested portals
+* should keep track of namespace across portals (simple)
+* should keep track of namespace across portals (medium)
+* should keep track of namespace across portals (complex)
+* should unwind namespaces on uncaught errors
+* should unwind namespaces on caught errors
+* should unwind namespaces on caught errors in a portal
* should pass portal context when rendering subtree elsewhere
* should update portal context if it changes due to setState
* should update portal context if it changes due to re-render
@@ -669,6 +675,8 @@ src/renderers/dom/shared/__tests__/ReactDOMInvalidARIAHook-test.js
src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js
* creates initial namespaced markup
+* creates elements with SVG namespace inside SVG tag during mount
+* creates elements with SVG namespace inside SVG tag during update
src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js
* updates a mounted text component in place
diff --git a/src/renderers/art/ReactARTFiber.js b/src/renderers/art/ReactARTFiber.js
index 467f9764634..8655b8b8e25 100644
--- a/src/renderers/art/ReactARTFiber.js
+++ b/src/renderers/art/ReactARTFiber.js
@@ -485,6 +485,10 @@ const ARTRenderer = ReactFiberReconciler({
// Noop
},
+ getChildHostContext() {
+ return null;
+ },
+
scheduleAnimationCallback: window.requestAnimationFrame,
scheduleDeferredCallback: window.requestIdleCallback,
diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js
index c4c851d8e33..5b579517c91 100644
--- a/src/renderers/dom/fiber/ReactDOMFiber.js
+++ b/src/renderers/dom/fiber/ReactDOMFiber.js
@@ -33,6 +33,7 @@ var warning = require('warning');
var {
createElement,
+ getChildNamespace,
setInitialProperties,
updateProperties,
} = ReactDOMFiberComponent;
@@ -60,6 +61,11 @@ let selectionInformation : ?mixed = null;
var DOMRenderer = ReactFiberReconciler({
+ getChildHostContext(parentHostContext : string | null, type : string) {
+ const parentNamespace = parentHostContext;
+ return getChildNamespace(parentNamespace, type);
+ },
+
prepareForCommit() : void {
eventsEnabled = ReactBrowserEventEmitter.isEnabled();
ReactBrowserEventEmitter.setEnabled(false);
@@ -76,11 +82,11 @@ var DOMRenderer = ReactFiberReconciler({
createInstance(
type : string,
props : Props,
- internalInstanceHandle : Object
+ rootContainerInstance : Container,
+ hostContext : string | null,
+ internalInstanceHandle : Object,
) : Instance {
- const root = document.documentElement; // HACK
-
- const domElement : Instance = createElement(type, props, root);
+ const domElement : Instance = createElement(type, props, rootContainerInstance, hostContext);
precacheFiberNode(internalInstanceHandle, domElement);
return domElement;
},
@@ -89,10 +95,18 @@ var DOMRenderer = ReactFiberReconciler({
parentInstance.appendChild(child);
},
- finalizeInitialChildren(domElement : Instance, type : string, props : Props) : void {
- const root = document.documentElement; // HACK
-
- setInitialProperties(domElement, type, props, root);
+ finalizeInitialChildren(
+ domElement : Instance,
+ props : Props,
+ rootContainerInstance : Container,
+ ) : void {
+ // TODO: we normalize here because DOM renderer expects tag to be lowercase.
+ // We can change DOM renderer to compare special case against upper case,
+ // and use tagName (which is upper case for HTML DOM elements). Or we could
+ // let the renderer "normalize" the fiber type so we don't have to read
+ // the type from DOM. However we need to remember SVG is case-sensitive.
+ var tag = domElement.tagName.toLowerCase();
+ setInitialProperties(domElement, tag, props, rootContainerInstance);
},
prepareUpdate(
@@ -107,14 +121,19 @@ var DOMRenderer = ReactFiberReconciler({
domElement : Instance,
oldProps : Props,
newProps : Props,
- internalInstanceHandle : Object
+ rootContainerInstance : Container,
+ internalInstanceHandle : Object,
) : void {
- var type = domElement.tagName.toLowerCase(); // HACK
- var root = document.documentElement; // HACK
+ // TODO: we normalize here because DOM renderer expects tag to be lowercase.
+ // We can change DOM renderer to compare special case against upper case,
+ // and use tagName (which is upper case for HTML DOM elements). Or we could
+ // let the renderer "normalize" the fiber type so we don't have to read
+ // the type from DOM. However we need to remember SVG is case-sensitive.
+ var tag = domElement.tagName.toLowerCase();
// Update the internal instance handle so that we know which props are
// the current ones.
precacheFiberNode(internalInstanceHandle, domElement);
- updateProperties(domElement, type, oldProps, newProps, root);
+ updateProperties(domElement, tag, oldProps, newProps, rootContainerInstance);
},
shouldSetTextContent(props : Props) : boolean {
diff --git a/src/renderers/dom/fiber/ReactDOMFiberComponent.js b/src/renderers/dom/fiber/ReactDOMFiberComponent.js
index 7c6ebd9b3c6..826dcc5b6a0 100644
--- a/src/renderers/dom/fiber/ReactDOMFiberComponent.js
+++ b/src/renderers/dom/fiber/ReactDOMFiberComponent.js
@@ -46,6 +46,11 @@ var CHILDREN = 'children';
var STYLE = 'style';
var HTML = '__html';
+var {
+ svg: SVG_NAMESPACE,
+ mathml: MATH_NAMESPACE,
+} = DOMNamespaces;
+
// Node type for document fragments (Node.DOCUMENT_FRAGMENT_NODE).
var DOC_FRAGMENT_TYPE = 11;
@@ -451,45 +456,49 @@ function updateDOMProperties(
}
}
-var ReactDOMFiberComponent = {
+// Assumes there is no parent namespace.
+function getIntrinsicNamespace(type : string) : string | null {
+ switch (type) {
+ case 'svg':
+ return SVG_NAMESPACE;
+ case 'math':
+ return MATH_NAMESPACE;
+ default:
+ return null;
+ }
+}
- // TODO: Use this to keep track of changes to the host context and use this
- // to determine whether we switch to svg and back.
- // TODO: Does this need to check the current namespace? In case these tags
- // happen to be valid in some other namespace.
- isNewHostContainer(tag : string) {
- return tag === 'svg' || tag === 'foreignobject';
+var ReactDOMFiberComponent = {
+ getChildNamespace(parentNamespace : string | null, type : string) : string | null {
+ if (parentNamespace == null) {
+ // No parent namespace: potential entry point.
+ return getIntrinsicNamespace(type);
+ }
+ if (parentNamespace === SVG_NAMESPACE && type === 'foreignObject') {
+ // We're leaving SVG.
+ return null;
+ }
+ // By default, pass namespace below.
+ return parentNamespace;
},
createElement(
- tag : string,
+ type : string,
props : Object,
- rootContainerElement : Element
+ rootContainerElement : Element,
+ parentNamespace : string | null
) : Element {
- validateDangerousTag(tag);
+ validateDangerousTag(type);
// TODO:
- // tag.toLowerCase(); Do we need to apply lower case only on non-custom elements?
+ // const tag = type.toLowerCase(); Do we need to apply lower case only on non-custom elements?
// We create tags in the namespace of their parent container, except HTML
// tags get no namespace.
- var namespaceURI = rootContainerElement.namespaceURI;
- if (namespaceURI == null ||
- namespaceURI === DOMNamespaces.svg &&
- rootContainerElement.tagName === 'foreignObject') {
- namespaceURI = DOMNamespaces.html;
- }
- if (namespaceURI === DOMNamespaces.html) {
- if (tag === 'svg') {
- namespaceURI = DOMNamespaces.svg;
- } else if (tag === 'math') {
- namespaceURI = DOMNamespaces.mathml;
- }
- // TODO: Make this a new root container element.
- }
-
var ownerDocument = rootContainerElement.ownerDocument;
var domElement : Element;
- if (namespaceURI === DOMNamespaces.html) {
+ var namespaceURI = parentNamespace || getIntrinsicNamespace(type);
+ if (namespaceURI == null) {
+ const tag = type.toLowerCase();
if (tag === 'script') {
// Create the script via .innerHTML so its "parser-inserted" flag is
// set to true and it does not execute
@@ -499,17 +508,17 @@ var ReactDOMFiberComponent = {
var firstChild = ((div.firstChild : any) : HTMLScriptElement);
domElement = div.removeChild(firstChild);
} else if (props.is) {
- domElement = ownerDocument.createElement(tag, props.is);
+ domElement = ownerDocument.createElement(type, props.is);
} else {
// Separate else branch instead of using `props.is || undefined` above becuase of a Firefox bug.
// See discussion in https://github.com/facebook/react/pull/6896
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
- domElement = ownerDocument.createElement(tag);
+ domElement = ownerDocument.createElement(type);
}
} else {
domElement = ownerDocument.createElementNS(
namespaceURI,
- tag
+ type
);
}
diff --git a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
index fe8fc2d5ed3..775d1b328cc 100644
--- a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
+++ b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
@@ -188,6 +188,39 @@ describe('ReactDOMFiber', () => {
}
if (ReactDOMFeatureFlags.useFiber) {
+ var svgEls, htmlEls, mathEls;
+ var expectSVG = {ref: el => svgEls.push(el)};
+ var expectHTML = {ref: el => htmlEls.push(el)};
+ var expectMath = {ref: el => mathEls.push(el)};
+
+ var portal = function(tree) {
+ return ReactDOM.unstable_createPortal(
+ tree,
+ document.createElement('div')
+ );
+ };
+
+ var assertNamespacesMatch = function(tree) {
+ container = document.createElement('div');
+ svgEls = [];
+ htmlEls = [];
+ mathEls = [];
+
+ ReactDOM.render(tree, container);
+ svgEls.forEach(el => {
+ expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg');
+ });
+ htmlEls.forEach(el => {
+ expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml');
+ });
+ mathEls.forEach(el => {
+ expect(el.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML');
+ });
+
+ ReactDOM.unmountComponentAtNode(container);
+ expect(container.innerHTML).toBe('');
+ };
+
it('should render one portal', () => {
var portalContainer = document.createElement('div');
@@ -333,6 +366,265 @@ describe('ReactDOMFiber', () => {
expect(container.innerHTML).toBe('');
});
+ it('should keep track of namespace across portals (simple)', () => {
+ assertNamespacesMatch(
+
+ );
+ assertNamespacesMatch(
+
+ );
+ assertNamespacesMatch(
+
+
+ {portal(
+
+ )}
+
+
+ );
+ });
+
+ it('should keep track of namespace across portals (medium)', () => {
+ assertNamespacesMatch(
+