diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index ee3849f47efe7..8ac1c726f5b11 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -504,6 +504,7 @@ src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js * should render one portal * should render many portals * should render nested portals +* should not apply SVG mode across portals * 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 @@ -654,6 +655,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/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index c5da108c5a828..45dc4665ae06b 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); }, createTextInstance(text : string, internalInstanceHandle : Object) : TextInstance { diff --git a/src/renderers/dom/fiber/ReactDOMFiberComponent.js b/src/renderers/dom/fiber/ReactDOMFiberComponent.js index 7c6ebd9b3c65f..fdb3877196298 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, + math: 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 13f6a41499b3e..04c290495e4e6 100644 --- a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js +++ b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js @@ -333,6 +333,62 @@ describe('ReactDOMFiber', () => { expect(container.innerHTML).toBe(''); }); + it('should not apply SVG mode across portals', () => { + var portalContainer = document.createElement('div'); + + ReactDOM.render( + + + {ReactDOM.unstable_createPortal( +
portal
, + portalContainer + )} + + , + container + ); + + const div = portalContainer.childNodes[0]; + const image1 = container.firstChild.childNodes[0]; + const image2 = container.firstChild.childNodes[1]; + expect(div.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + expect(div.tagName).toBe('DIV'); + expect(image1.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(image1.tagName).toBe('image'); + expect( + image1.getAttributeNS('http://www.w3.org/1999/xlink', 'href') + ).toBe('http://i.imgur.com/w7GCRPb.png'); + expect(image2.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(image2.tagName).toBe('image'); + expect( + image2.getAttributeNS('http://www.w3.org/1999/xlink', 'href') + ).toBe('http://i.imgur.com/w7GCRPb.png'); + + ReactDOM.render( + + + {ReactDOM.unstable_createPortal( + portal, + portalContainer + )} + + , + container + ); + + const span = portalContainer.childNodes[0]; + expect(span.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + expect(span.tagName).toBe('SPAN'); + expect(container.firstChild.childNodes[0]).toBe(image1); + const g = container.firstChild.childNodes[1]; + expect(g.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(g.tagName).toBe('g'); + + ReactDOM.unmountComponentAtNode(container); + expect(portalContainer.innerHTML).toBe(''); + expect(container.innerHTML).toBe(''); + }); + it('should pass portal context when rendering subtree elsewhere', () => { var portalContainer = document.createElement('div'); diff --git a/src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js b/src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js index 30760d3ae88db..37818dc3fa949 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js @@ -12,12 +12,14 @@ 'use strict'; var React; +var ReactDOM; var ReactDOMServer; describe('ReactDOMSVG', () => { beforeEach(() => { React = require('React'); + ReactDOM = require('ReactDOM'); ReactDOMServer = require('ReactDOMServer'); }); @@ -30,4 +32,100 @@ describe('ReactDOMSVG', () => { expect(markup).toContain('xlink:href="http://i.imgur.com/w7GCRPb.png"'); }); + it('creates elements with SVG namespace inside SVG tag during mount', () => { + var node = document.createElement('div'); + var div, foreignDiv, foreignObject, g, image, image2, p, svg; + ReactDOM.render( +
+ svg = el}> + g = el} strokeWidth="5"> + image = el} xlinkHref="http://i.imgur.com/w7GCRPb.png" /> + foreignObject = el}> +
foreignDiv = el} /> + + + +

p = el}> + + image2 = el} xlinkHref="http://i.imgur.com/w7GCRPb.png" /> + +

+
div = el} /> +
, + node + ); + // SVG tagName is case sensitive. + expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(svg.tagName).toBe('svg'); + expect(g.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(g.tagName).toBe('g'); + expect(g.getAttribute('stroke-width')).toBe('5'); + expect(image.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(image.tagName).toBe('image'); + expect( + image.getAttributeNS('http://www.w3.org/1999/xlink', 'href') + ).toBe('http://i.imgur.com/w7GCRPb.png'); + expect(foreignObject.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(foreignObject.tagName).toBe('foreignObject'); + expect(image2.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(image2.tagName).toBe('image'); + expect( + image2.getAttributeNS('http://www.w3.org/1999/xlink', 'href') + ).toBe('http://i.imgur.com/w7GCRPb.png'); + // DOM tagName is capitalized by browsers. + expect(p.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + expect(p.tagName).toBe('P'); + expect(div.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + expect(div.tagName).toBe('DIV'); + expect(foreignDiv.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + expect(foreignDiv.tagName).toBe('DIV'); + }); + + it('creates elements with SVG namespace inside SVG tag during update', () => { + var inst, foreignObject, foreignDiv, g, image, svg; + + class App extends React.Component { + state = {step: 0}; + render() { + inst = this; + const {step} = this.state; + if (step === 0) { + return null; + } + return ( + g = el} strokeWidth="5"> + image = el} xlinkHref="http://i.imgur.com/w7GCRPb.png" /> + foreignObject = el}> +
foreignDiv = el} /> + + + ); + } + } + + var node = document.createElement('div'); + ReactDOM.render( + svg = el}> + + , + node + ); + inst.setState({step: 1}); + + expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(svg.tagName).toBe('svg'); + expect(g.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(g.tagName).toBe('g'); + expect(g.getAttribute('stroke-width')).toBe('5'); + expect(image.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(image.tagName).toBe('image'); + expect( + image.getAttributeNS('http://www.w3.org/1999/xlink', 'href') + ).toBe('http://i.imgur.com/w7GCRPb.png'); + expect(foreignObject.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(foreignObject.tagName).toBe('foreignObject'); + expect(foreignDiv.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + expect(foreignDiv.tagName).toBe('DIV'); + }); + }); diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 9f614a465ea32..8b3942888035a 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -40,6 +40,10 @@ var instanceCounter = 0; var NoopRenderer = ReactFiberReconciler({ + getChildHostContext() { + return null; + }, + createInstance(type : string, props : Props) : Instance { const inst = { id: instanceCounter++, @@ -56,7 +60,7 @@ var NoopRenderer = ReactFiberReconciler({ parentInstance.children.push(child); }, - finalizeInitialChildren(domElement : Instance, type : string, props : Props) : void { + finalizeInitialChildren(domElement : Instance, props : Props) : void { // Noop }, diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 9c3604a5f861e..b59318f5fa0b6 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -343,6 +343,7 @@ exports.createFiberFromPortal = function(portal : ReactPortal, priorityLevel : P fiber.stateNode = { containerInfo: portal.containerInfo, implementation: portal.implementation, + savedHostContext: null, }; return fiber; }; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 887d142691cea..81548f9fc7744 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -14,6 +14,7 @@ import type { ReactCoroutine } from 'ReactCoroutine'; import type { Fiber } from 'ReactFiber'; +import type { HostContext } from 'ReactFiberHostContext'; import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; import type { PriorityLevel } from 'ReactPriorityLevel'; @@ -57,11 +58,18 @@ var { var ReactCurrentOwner = require('ReactCurrentOwner'); var ReactFiberClassComponent = require('ReactFiberClassComponent'); -module.exports = function( - config : HostConfig, +module.exports = function( + config : HostConfig, + hostContext : HostContext, scheduleUpdate : (fiber: Fiber) => void ) { + const { + setRootHostContainer, + maybePushHostContext, + saveHostContextToPortal, + } = hostContext; + const { adoptClassInstance, constructClassInstance, @@ -254,6 +262,7 @@ module.exports = function( // Abort and don't process children yet. return null; } else { + maybePushHostContext(workInProgress); reconcileChildren(current, workInProgress, nextChildren); return workInProgress.child; } @@ -341,8 +350,9 @@ module.exports = function( function bailoutOnAlreadyFinishedWork(current, workInProgress : Fiber) : ?Fiber { const priorityLevel = workInProgress.pendingWorkPriority; + const isHostComponent = workInProgress.tag === HostComponent; - if (workInProgress.tag === HostComponent && + if (isHostComponent && workInProgress.memoizedProps.hidden && workInProgress.pendingWorkPriority !== OffscreenPriority) { // This subtree still has work, but it should be deprioritized so we need @@ -384,10 +394,27 @@ module.exports = function( cloneChildFibers(current, workInProgress); markChildAsProgressed(current, workInProgress, priorityLevel); + // Put context on the stack because we will work on children - if (isContextProvider(workInProgress)) { - pushContextProvider(workInProgress, false); + if (isHostComponent) { + maybePushHostContext(workInProgress); + } else { + switch (workInProgress.tag) { + case ClassComponent: + if (isContextProvider(workInProgress)) { + pushContextProvider(workInProgress, false); + } + break; + case HostContainer: + setRootHostContainer(workInProgress.stateNode.containerInfo); + break; + case Portal: + saveHostContextToPortal(workInProgress); + break; + } } + // TODO: this is annoyingly duplicating non-jump codepaths. + return workInProgress.child; } @@ -445,6 +472,7 @@ module.exports = function( } else { pushTopLevelContextObject(root.context, false); } + setRootHostContainer(workInProgress.stateNode.containerInfo); reconcileChildren(current, workInProgress, workInProgress.pendingProps); // A yield component is just a placeholder, we can just run through the // next one immediately. @@ -470,8 +498,8 @@ module.exports = function( // next one immediately. return null; case Portal: + saveHostContextToPortal(workInProgress); updatePortalComponent(current, workInProgress); - // TODO: is this right? return workInProgress.child; case Fragment: updateFragment(current, workInProgress); diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 5a2c07c2f4221..3e1d3e5cac0ab 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -13,6 +13,7 @@ 'use strict'; import type { Fiber } from 'ReactFiber'; +import type { HostContext } from 'ReactFiberHostContext'; import type { HostConfig } from 'ReactFiberReconciler'; var ReactTypeOfWork = require('ReactTypeOfWork'); @@ -32,17 +33,23 @@ var { Callback, } = require('ReactTypeOfSideEffect'); -module.exports = function( - config : HostConfig, +module.exports = function( + config : HostConfig, + hostContext : HostContext, trapError : (failedFiber : Fiber, error: Error, isUnmounting : boolean) => void ) { - const commitUpdate = config.commitUpdate; - const commitTextUpdate = config.commitTextUpdate; + const { + commitUpdate, + commitTextUpdate, + appendChild, + insertBefore, + removeChild, + } = config; - const appendChild = config.appendChild; - const insertBefore = config.insertBefore; - const removeChild = config.removeChild; + const { + getRootHostContainer, + } = hostContext; function detachRef(current : Fiber) { const ref = current.ref; @@ -303,7 +310,8 @@ module.exports = function( // Commit the work prepared earlier. const newProps = finishedWork.memoizedProps; const oldProps = current.memoizedProps; - commitUpdate(instance, oldProps, newProps, finishedWork); + const rootContainerInstance = getRootHostContainer(); + commitUpdate(instance, oldProps, newProps, rootContainerInstance, finishedWork); } detachRefIfNeeded(current, finishedWork); return; diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index a188cf5813e9b..9c8b61a66a0e3 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -14,6 +14,7 @@ import type { ReactCoroutine } from 'ReactCoroutine'; import type { Fiber } from 'ReactFiber'; +import type { HostContext } from 'ReactFiberHostContext'; import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; import type { ReifiedYield } from 'ReactReifiedYield'; @@ -43,13 +44,24 @@ var { Callback, } = ReactTypeOfSideEffect; -module.exports = function(config : HostConfig) { +module.exports = function( + config : HostConfig, + hostContext : HostContext, +) { + const { + createInstance, + createTextInstance, + appendInitialChild, + finalizeInitialChildren, + prepareUpdate, + } = config; - const createInstance = config.createInstance; - const appendInitialChild = config.appendInitialChild; - const finalizeInitialChildren = config.finalizeInitialChildren; - const createTextInstance = config.createTextInstance; - const prepareUpdate = config.prepareUpdate; + const { + getRootHostContainer, + maybePopHostContext, + getHostContext, + restoreHostContextFromPortal, + } = hostContext; function markUpdate(workInProgress : Fiber) { // Tag the fiber with an update effect. This turns a Placement into @@ -202,6 +214,7 @@ module.exports = function(config : HostConfig) { return null; } case HostComponent: + maybePopHostContext(workInProgress); let newProps = workInProgress.pendingProps; if (current && workInProgress.stateNode != null) { // If we have an alternate, that means this is an update and we need to @@ -229,14 +242,21 @@ module.exports = function(config : HostConfig) { } } + const rootContainerInstance = getRootHostContainer(); + const currentHostContext = getHostContext(); // TODO: Move createInstance to beginWork and keep it on a context // "stack" as the parent. Then append children as we go in beginWork // or completeWork depending on we want to add then top->down or // bottom->up. Top->down is faster in IE11. - // Finally, finalizeInitialChildren here in completeWork. - const instance = createInstance(workInProgress.type, newProps, workInProgress); + const instance = createInstance( + workInProgress.type, + newProps, + rootContainerInstance, + currentHostContext, + workInProgress + ); appendAllChildren(instance, workInProgress); - finalizeInitialChildren(instance, workInProgress.type, newProps); + finalizeInitialChildren(instance, newProps, rootContainerInstance); workInProgress.stateNode = instance; if (workInProgress.ref) { @@ -294,6 +314,7 @@ module.exports = function(config : HostConfig) { // TODO: Only mark this as an update if we have any pending callbacks. markUpdate(workInProgress); workInProgress.memoizedProps = workInProgress.pendingProps; + restoreHostContextFromPortal(workInProgress); return null; // Error cases diff --git a/src/renderers/shared/fiber/ReactFiberHostContext.js b/src/renderers/shared/fiber/ReactFiberHostContext.js new file mode 100644 index 0000000000000..58ce8f8cdf478 --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberHostContext.js @@ -0,0 +1,139 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactFiberHostContext + * @flow + */ + +'use strict'; + +import type { Fiber } from 'ReactFiber'; +import type { HostConfig } from 'ReactFiberReconciler'; + +export type HostContext = { + getRootHostContainer() : C, + setRootHostContainer(container : C) : void, + + getHostContext() : CX | null, + maybePushHostContext(fiber : Fiber) : void, + maybePopHostContext(fiber : Fiber) : void, + + resetHostContext() : void, + saveHostContextToPortal(portal : Fiber): void, + restoreHostContextFromPortal(portal : Fiber): void, +}; + +module.exports = function( + config : HostConfig +) : HostContext { + const { + getChildHostContext, + } = config; + + let rootHostContainer : C | null = null; + let hostContextFiberStack : Array | null = null; + let hostContextValueStack : Array | null = null; + let hostContextIndex = -1; + + function getRootHostContainer() : C { + if (rootHostContainer === null) { + throw new Error('Expected to find a root container instance.'); + } + return rootHostContainer; + } + + function setRootHostContainer(instance : C) : void { + rootHostContainer = instance; + } + + function getHostContext() : CX | null { + if (hostContextIndex === -1) { + return null; + } + if (hostContextValueStack == null) { + throw new Error('Expected host context stacks to exist when index is more than -1.'); + } + return hostContextValueStack[hostContextIndex]; + } + + function maybePushHostContext(fiber : Fiber) : void { + const parentHostContext = getHostContext(); + const currentHostContext = getChildHostContext(parentHostContext, fiber.type); + if (parentHostContext === currentHostContext) { + return; + } + hostContextIndex++; + hostContextFiberStack = hostContextFiberStack || []; + hostContextFiberStack[hostContextIndex] = fiber; + hostContextValueStack = hostContextValueStack || []; + hostContextValueStack[hostContextIndex] = currentHostContext; + } + + function maybePopHostContext(fiber : Fiber) : void { + if (hostContextIndex === -1) { + return; + } + if (hostContextFiberStack == null || hostContextValueStack == null) { + throw new Error('Expected host context stacks to exist when index is more than -1.'); + } + if (fiber !== hostContextFiberStack[hostContextIndex]) { + return; + } + hostContextFiberStack[hostContextIndex] = null; + hostContextValueStack[hostContextIndex] = null; + hostContextIndex--; + } + + function resetHostContext() { + rootHostContainer = null; + hostContextIndex = -1; + } + + function saveHostContextToPortal(portal : Fiber) { + // TODO: add tests for error boundaries inside portals when both are stable. + const stateNode = portal.stateNode; + if (!stateNode.savedHostContext) { + // We assume host context never changes between passes so store it once lazily. + stateNode.savedHostContext = { + rootHostContainer, + hostContextFiberStack, + hostContextValueStack, + hostContextIndex, + }; + } + rootHostContainer = stateNode.containerInfo; + hostContextFiberStack = null; + hostContextValueStack = null; + hostContextIndex = -1; + } + + function restoreHostContextFromPortal(portal : Fiber) { + const stateNode = portal.stateNode; + const savedHostContext = stateNode.savedHostContext; + if (savedHostContext == null) { + throw new Error('A portal has no host context saved on it.'); + } + rootHostContainer = savedHostContext.rootHostContainer; + hostContextFiberStack = savedHostContext.hostContextFiberStack; + hostContextValueStack = savedHostContext.hostContextValueStack; + hostContextIndex = savedHostContext.hostContextIndex; + } + + return { + getRootHostContainer, + setRootHostContainer, + + maybePushHostContext, + maybePopHostContext, + getHostContext, + + resetHostContext, + saveHostContextToPortal, + restoreHostContextFromPortal, + }; +}; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index ca2fbb7660b06..cd0dce848a68d 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -40,14 +40,16 @@ export type Deadline = { type OpaqueNode = Fiber; -export type HostConfig = { +export type HostConfig = { - createInstance(type : T, props : P, internalInstanceHandle : OpaqueNode) : I, - appendInitialChild(parentInstance : I, child : I) : void, - finalizeInitialChildren(parentInstance : I, type : T, props : P) : void, + getChildHostContext(parentHostContext : CX | null, type : T) : CX, + + createInstance(type : T, props : P, rootContainerInstance : C, hostContext : CX | null, internalInstanceHandle : OpaqueNode) : I, + appendInitialChild(parentInstance : I, child : I | TI) : void, + finalizeInitialChildren(parentInstance : I, props : P, rootContainerInstance : C) : void, prepareUpdate(instance : I, oldProps : P, newProps : P) : boolean, - commitUpdate(instance : I, oldProps : P, newProps : P, internalInstanceHandle : OpaqueNode) : void, + commitUpdate(instance : I, oldProps : P, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void, createTextInstance(text : string, internalInstanceHandle : OpaqueNode) : TI, commitTextUpdate(textInstance : TI, oldText : string, newText : string) : void, @@ -90,7 +92,7 @@ getContextForSubtree._injectFiber(function(fiber : Fiber) { parentContext; }); -module.exports = function(config : HostConfig) : Reconciler { +module.exports = function(config : HostConfig) : Reconciler { var { scheduleWork, diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 58fc5445fc49d..c8628f56f18b6 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -20,6 +20,7 @@ import type { PriorityLevel } from 'ReactPriorityLevel'; var ReactFiberBeginWork = require('ReactFiberBeginWork'); var ReactFiberCompleteWork = require('ReactFiberCompleteWork'); var ReactFiberCommitWork = require('ReactFiberCommitWork'); +var ReactFiberHostContext = require('ReactFiberHostContext'); var ReactCurrentOwner = require('ReactCurrentOwner'); var { cloneFiber } = require('ReactFiber'); @@ -64,18 +65,24 @@ type TrappedError = { error: any, }; -module.exports = function(config : HostConfig) { - const { beginWork } = ReactFiberBeginWork(config, scheduleUpdate); - const { completeWork } = ReactFiberCompleteWork(config); - const { commitInsertion, commitDeletion, commitWork, commitLifeCycles } = - ReactFiberCommitWork(config, trapError); - - const hostScheduleAnimationCallback = config.scheduleAnimationCallback; - const hostScheduleDeferredCallback = config.scheduleDeferredCallback; - const useSyncScheduling = config.useSyncScheduling; - - const prepareForCommit = config.prepareForCommit; - const resetAfterCommit = config.resetAfterCommit; +module.exports = function(config : HostConfig) { + const hostContext = ReactFiberHostContext(config); + const { resetHostContext } = hostContext; + const { beginWork } = ReactFiberBeginWork(config, hostContext, scheduleUpdate); + const { completeWork } = ReactFiberCompleteWork(config, hostContext); + const { + commitInsertion, + commitDeletion, + commitWork, + commitLifeCycles, + } = ReactFiberCommitWork(config, hostContext, trapError); + const { + scheduleAnimationCallback: hostScheduleAnimationCallback, + scheduleDeferredCallback: hostScheduleDeferredCallback, + useSyncScheduling, + prepareForCommit, + resetAfterCommit, + } = config; // The priority level to use when scheduling an update. let priorityContext : PriorityLevel = useSyncScheduling ? @@ -215,6 +222,7 @@ module.exports = function(config : HostConfig) { } resetAfterCommit(); + resetHostContext(); // Next, we'll perform all life-cycles and ref callbacks. Life-cycles // happens as a separate pass so that all effects in the entire tree have