diff --git a/package.json b/package.json index eb5fe1324ba..2091f552769 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "@mattiasbuelens/web-streams-polyfill": "0.1.0" }, "devEngines": { - "node": "8.x || 9.x || 10.x" + "node": "8.x || 9.x || 10.x || 11.x" }, "jest": { "testRegex": "/scripts/jest/dont-run-jest-directly\\.js$" @@ -104,6 +104,8 @@ "test-fire": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-fire.js", "test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js", "test-fire-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source-fire.js", + "debug-test-fire": "cross-env NODE_ENV=development node --inspect-brk node_modules/.bin/jest --config ./scripts/jest/config.source-fire.js --runInBand", + "debug-test-fire-prod": "cross-env NODE_ENV=production node --inspect-brk node_modules/.bin/jest --config ./scripts/jest/config.source-fire.js --runInBand", "test-prod-build": "yarn test-build-prod", "test-build": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build.js", "test-build-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.build.js", diff --git a/packages/react-dom/src/__tests__/EventPluginHub-test.js b/packages/react-dom/src/__tests__/EventPluginHub-test.js index aaf06ebdb69..95c813373c5 100644 --- a/packages/react-dom/src/__tests__/EventPluginHub-test.js +++ b/packages/react-dom/src/__tests__/EventPluginHub-test.js @@ -9,36 +9,43 @@ 'use strict'; +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); + jest.mock('../events/isEventSupported'); -describe('EventPluginHub', () => { - let React; - let ReactTestUtils; +// Much of this logic was removed with Fire +if (ReactFeatureFlags.enableReactDOMFire) { + it('Empty test', () => {}); +} else { + describe('EventPluginHub', () => { + let React; + let ReactTestUtils; - beforeEach(() => { - jest.resetModules(); - React = require('react'); - ReactTestUtils = require('react-dom/test-utils'); - }); + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactTestUtils = require('react-dom/test-utils'); + }); - it('should prevent non-function listeners, at dispatch', () => { - let node; - expect(() => { - node = ReactTestUtils.renderIntoDocument( -
, + it('should prevent non-function listeners, at dispatch', () => { + let node; + expect(() => { + node = ReactTestUtils.renderIntoDocument( +
, + ); + }).toWarnDev( + 'Expected `onClick` listener to be a function, instead got a value of `string` type.', ); - }).toWarnDev( - 'Expected `onClick` listener to be a function, instead got a value of `string` type.', - ); - expect(() => ReactTestUtils.SimulateNative.click(node)).toThrowError( - 'Expected `onClick` listener to be a function, instead got a value of `string` type.', - ); - }); + expect(() => ReactTestUtils.SimulateNative.click(node)).toThrowError( + 'Expected `onClick` listener to be a function, instead got a value of `string` type.', + ); + }); - it('should not prevent null listeners, at dispatch', () => { - const node = ReactTestUtils.renderIntoDocument(
); - expect(function() { - ReactTestUtils.SimulateNative.click(node); - }).not.toThrow(); + it('should not prevent null listeners, at dispatch', () => { + const node = ReactTestUtils.renderIntoDocument(
); + expect(function() { + ReactTestUtils.SimulateNative.click(node); + }).not.toThrow(); + }); }); -}); +} diff --git a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js index 2a6b526218c..5bff7d7e4df 100644 --- a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js @@ -16,6 +16,7 @@ let ReactDOM; let ReactDOMComponentTree; let ReactBrowserEventEmitter; let ReactTestUtils; +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); let idCallOrder; const recordID = function(id) { @@ -51,318 +52,331 @@ function registerSimpleTestHandler() { return getListener(CHILD, ON_CLICK_KEY); } -describe('ReactBrowserEventEmitter', () => { - beforeEach(() => { - jest.resetModules(); - LISTENER.mockClear(); - - // TODO: can we express this test with only public API? - EventPluginHub = require('events/EventPluginHub'); - EventPluginRegistry = require('events/EventPluginRegistry'); - React = require('react'); - ReactDOM = require('react-dom'); - ReactDOMComponentTree = require('../client/ReactDOMComponentTree'); - ReactBrowserEventEmitter = require('../events/ReactBrowserEventEmitter'); - ReactTestUtils = require('react-dom/test-utils'); - - container = document.createElement('div'); - document.body.appendChild(container); - - let GRANDPARENT_PROPS = {}; - let PARENT_PROPS = {}; - let CHILD_PROPS = {}; - - function Child(props) { - return
(CHILD = c)} {...props} />; - } - - class ChildWrapper extends React.PureComponent { - render() { - return ; +// ReactBrowserEventEmitter was removed in Fire +if (ReactFeatureFlags.enableReactDOMFire) { + it('Empty test', () => {}); +} else { + describe('ReactBrowserEventEmitter', () => { + beforeEach(() => { + jest.resetModules(); + LISTENER.mockClear(); + + // TODO: can we express this test with only public API? + EventPluginHub = require('events/EventPluginHub'); + EventPluginRegistry = require('events/EventPluginRegistry'); + React = require('react'); + ReactDOM = require('react-dom'); + ReactDOMComponentTree = require('../client/ReactDOMComponentTree'); + ReactBrowserEventEmitter = require('../events/ReactBrowserEventEmitter'); + ReactTestUtils = require('react-dom/test-utils'); + + container = document.createElement('div'); + document.body.appendChild(container); + + let GRANDPARENT_PROPS = {}; + let PARENT_PROPS = {}; + let CHILD_PROPS = {}; + + function Child(props) { + return
(CHILD = c)} {...props} />; } - } - - function renderTree() { - ReactDOM.render( -
(GRANDPARENT = c)} {...GRANDPARENT_PROPS}> -
(PARENT = c)} {...PARENT_PROPS}> - -
-
, - container, - ); - } - - renderTree(); - - getListener = function(node, eventName) { - const inst = ReactDOMComponentTree.getInstanceFromNode(node); - return EventPluginHub.getListener(inst, eventName); - }; - putListener = function(node, eventName, listener) { - switch (node) { - case CHILD: - CHILD_PROPS[eventName] = listener; - break; - case PARENT: - PARENT_PROPS[eventName] = listener; - break; - case GRANDPARENT: - GRANDPARENT_PROPS[eventName] = listener; - break; - } - // Rerender with new event listeners - renderTree(); - }; - deleteAllListeners = function(node) { - switch (node) { - case CHILD: - CHILD_PROPS = {}; - break; - case PARENT: - PARENT_PROPS = {}; - break; - case GRANDPARENT: - GRANDPARENT_PROPS = {}; - break; - } - renderTree(); - }; - idCallOrder = []; - }); + class ChildWrapper extends React.PureComponent { + render() { + return ; + } + } - afterEach(() => { - document.body.removeChild(container); - container = null; - }); + function renderTree() { + ReactDOM.render( +
(GRANDPARENT = c)} {...GRANDPARENT_PROPS}> +
(PARENT = c)} {...PARENT_PROPS}> + +
+
, + container, + ); + } - it('should store a listener correctly', () => { - registerSimpleTestHandler(); - const listener = getListener(CHILD, ON_CLICK_KEY); - expect(listener).toBe(LISTENER); - }); + renderTree(); - it('should retrieve a listener correctly', () => { - registerSimpleTestHandler(); - const listener = getListener(CHILD, ON_CLICK_KEY); - expect(listener).toEqual(LISTENER); - }); + getListener = function(node, eventName) { + const inst = ReactDOMComponentTree.getInstanceFromNode(node); + return EventPluginHub.getListener(inst, eventName); + }; + putListener = function(node, eventName, listener) { + switch (node) { + case CHILD: + CHILD_PROPS[eventName] = listener; + break; + case PARENT: + PARENT_PROPS[eventName] = listener; + break; + case GRANDPARENT: + GRANDPARENT_PROPS[eventName] = listener; + break; + } + // Rerender with new event listeners + renderTree(); + }; + deleteAllListeners = function(node) { + switch (node) { + case CHILD: + CHILD_PROPS = {}; + break; + case PARENT: + PARENT_PROPS = {}; + break; + case GRANDPARENT: + GRANDPARENT_PROPS = {}; + break; + } + renderTree(); + }; + + idCallOrder = []; + }); - it('should clear all handlers when asked to', () => { - registerSimpleTestHandler(); - deleteAllListeners(CHILD); - const listener = getListener(CHILD, ON_CLICK_KEY); - expect(listener).toBe(undefined); - }); + afterEach(() => { + document.body.removeChild(container); + container = null; + }); - it('should invoke a simple handler registered on a node', () => { - registerSimpleTestHandler(); - CHILD.click(); - expect(LISTENER).toHaveBeenCalledTimes(1); - }); + it('should store a listener correctly', () => { + registerSimpleTestHandler(); + const listener = getListener(CHILD, ON_CLICK_KEY); + expect(listener).toBe(LISTENER); + }); - it('should not invoke handlers if ReactBrowserEventEmitter is disabled', () => { - registerSimpleTestHandler(); - ReactBrowserEventEmitter.setEnabled(false); - CHILD.click(); - expect(LISTENER).toHaveBeenCalledTimes(0); - ReactBrowserEventEmitter.setEnabled(true); - CHILD.click(); - expect(LISTENER).toHaveBeenCalledTimes(1); - }); + it('should retrieve a listener correctly', () => { + registerSimpleTestHandler(); + const listener = getListener(CHILD, ON_CLICK_KEY); + expect(listener).toEqual(LISTENER); + }); - it('should bubble simply', () => { - putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD)); - putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT)); - putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT)); - CHILD.click(); - expect(idCallOrder.length).toBe(3); - expect(idCallOrder[0]).toBe(CHILD); - expect(idCallOrder[1]).toBe(PARENT); - expect(idCallOrder[2]).toBe(GRANDPARENT); - }); + it('should clear all handlers when asked to', () => { + registerSimpleTestHandler(); + deleteAllListeners(CHILD); + const listener = getListener(CHILD, ON_CLICK_KEY); + expect(listener).toBe(undefined); + }); - it('should bubble to the right handler after an update', () => { - putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, 'GRANDPARENT')); - putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, 'PARENT')); - putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, 'CHILD')); - CHILD.click(); - expect(idCallOrder).toEqual(['CHILD', 'PARENT', 'GRANDPARENT']); + it('should invoke a simple handler registered on a node', () => { + registerSimpleTestHandler(); + CHILD.click(); + expect(LISTENER).toHaveBeenCalledTimes(1); + }); - idCallOrder = []; + it('should not invoke handlers if ReactBrowserEventEmitter is disabled', () => { + registerSimpleTestHandler(); + ReactBrowserEventEmitter.setEnabled(false); + CHILD.click(); + expect(LISTENER).toHaveBeenCalledTimes(0); + ReactBrowserEventEmitter.setEnabled(true); + CHILD.click(); + expect(LISTENER).toHaveBeenCalledTimes(1); + }); - // Update just the grand parent without updating the child. - putListener( - GRANDPARENT, - ON_CLICK_KEY, - recordID.bind(null, 'UPDATED_GRANDPARENT'), - ); + it('should bubble simply', () => { + putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD)); + putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT)); + putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT)); + CHILD.click(); + expect(idCallOrder.length).toBe(3); + expect(idCallOrder[0]).toBe(CHILD); + expect(idCallOrder[1]).toBe(PARENT); + expect(idCallOrder[2]).toBe(GRANDPARENT); + }); - CHILD.click(); - expect(idCallOrder).toEqual(['CHILD', 'PARENT', 'UPDATED_GRANDPARENT']); - }); + it('should bubble to the right handler after an update', () => { + putListener( + GRANDPARENT, + ON_CLICK_KEY, + recordID.bind(null, 'GRANDPARENT'), + ); + putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, 'PARENT')); + putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, 'CHILD')); + CHILD.click(); + expect(idCallOrder).toEqual(['CHILD', 'PARENT', 'GRANDPARENT']); + + idCallOrder = []; + + // Update just the grand parent without updating the child. + putListener( + GRANDPARENT, + ON_CLICK_KEY, + recordID.bind(null, 'UPDATED_GRANDPARENT'), + ); - it('should continue bubbling if an error is thrown', () => { - putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD)); - putListener(PARENT, ON_CLICK_KEY, function() { - recordID(PARENT); - throw new Error('Handler interrupted'); + CHILD.click(); + expect(idCallOrder).toEqual(['CHILD', 'PARENT', 'UPDATED_GRANDPARENT']); }); - putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT)); - expect(function() { - ReactTestUtils.Simulate.click(CHILD); - }).toThrow(); - expect(idCallOrder.length).toBe(3); - expect(idCallOrder[0]).toBe(CHILD); - expect(idCallOrder[1]).toBe(PARENT); - expect(idCallOrder[2]).toBe(GRANDPARENT); - }); - it('should set currentTarget', () => { - putListener(CHILD, ON_CLICK_KEY, function(event) { - recordID(CHILD); - expect(event.currentTarget).toBe(CHILD); - }); - putListener(PARENT, ON_CLICK_KEY, function(event) { - recordID(PARENT); - expect(event.currentTarget).toBe(PARENT); + it('should continue bubbling if an error is thrown', () => { + putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD)); + putListener(PARENT, ON_CLICK_KEY, function() { + recordID(PARENT); + throw new Error('Handler interrupted'); + }); + putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT)); + expect(function() { + ReactTestUtils.Simulate.click(CHILD); + }).toThrow(); + expect(idCallOrder.length).toBe(3); + expect(idCallOrder[0]).toBe(CHILD); + expect(idCallOrder[1]).toBe(PARENT); + expect(idCallOrder[2]).toBe(GRANDPARENT); }); - putListener(GRANDPARENT, ON_CLICK_KEY, function(event) { - recordID(GRANDPARENT); - expect(event.currentTarget).toBe(GRANDPARENT); - }); - CHILD.click(); - expect(idCallOrder.length).toBe(3); - expect(idCallOrder[0]).toBe(CHILD); - expect(idCallOrder[1]).toBe(PARENT); - expect(idCallOrder[2]).toBe(GRANDPARENT); - }); - it('should support stopPropagation()', () => { - putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD)); - putListener( - PARENT, - ON_CLICK_KEY, - recordIDAndStopPropagation.bind(null, PARENT), - ); - putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT)); - CHILD.click(); - expect(idCallOrder.length).toBe(2); - expect(idCallOrder[0]).toBe(CHILD); - expect(idCallOrder[1]).toBe(PARENT); - }); + it('should set currentTarget', () => { + putListener(CHILD, ON_CLICK_KEY, function(event) { + recordID(CHILD); + expect(event.currentTarget).toBe(CHILD); + }); + putListener(PARENT, ON_CLICK_KEY, function(event) { + recordID(PARENT); + expect(event.currentTarget).toBe(PARENT); + }); + putListener(GRANDPARENT, ON_CLICK_KEY, function(event) { + recordID(GRANDPARENT); + expect(event.currentTarget).toBe(GRANDPARENT); + }); + CHILD.click(); + expect(idCallOrder.length).toBe(3); + expect(idCallOrder[0]).toBe(CHILD); + expect(idCallOrder[1]).toBe(PARENT); + expect(idCallOrder[2]).toBe(GRANDPARENT); + }); - it('should support overriding .isPropagationStopped()', () => { - // Ew. See D4504876. - putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD)); - putListener(PARENT, ON_CLICK_KEY, function(e) { - recordID(PARENT, e); - // This stops React bubbling but avoids touching the native event - e.isPropagationStopped = () => true; + it('should support stopPropagation()', () => { + putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD)); + putListener( + PARENT, + ON_CLICK_KEY, + recordIDAndStopPropagation.bind(null, PARENT), + ); + putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT)); + CHILD.click(); + expect(idCallOrder.length).toBe(2); + expect(idCallOrder[0]).toBe(CHILD); + expect(idCallOrder[1]).toBe(PARENT); }); - putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT)); - CHILD.click(); - expect(idCallOrder.length).toBe(2); - expect(idCallOrder[0]).toBe(CHILD); - expect(idCallOrder[1]).toBe(PARENT); - }); - it('should stop after first dispatch if stopPropagation', () => { - putListener( - CHILD, - ON_CLICK_KEY, - recordIDAndStopPropagation.bind(null, CHILD), - ); - putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT)); - putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT)); - CHILD.click(); - expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(CHILD); - }); + it('should support overriding .isPropagationStopped()', () => { + // Ew. See D4504876. + putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD)); + putListener(PARENT, ON_CLICK_KEY, function(e) { + recordID(PARENT, e); + // This stops React bubbling but avoids touching the native event + e.isPropagationStopped = () => true; + }); + putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT)); + CHILD.click(); + expect(idCallOrder.length).toBe(2); + expect(idCallOrder[0]).toBe(CHILD); + expect(idCallOrder[1]).toBe(PARENT); + }); - it('should not stopPropagation if false is returned', () => { - putListener(CHILD, ON_CLICK_KEY, recordIDAndReturnFalse.bind(null, CHILD)); - putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT)); - putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT)); - CHILD.click(); - expect(idCallOrder.length).toBe(3); - expect(idCallOrder[0]).toBe(CHILD); - expect(idCallOrder[1]).toBe(PARENT); - expect(idCallOrder[2]).toBe(GRANDPARENT); - }); + it('should stop after first dispatch if stopPropagation', () => { + putListener( + CHILD, + ON_CLICK_KEY, + recordIDAndStopPropagation.bind(null, CHILD), + ); + putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT)); + putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT)); + CHILD.click(); + expect(idCallOrder.length).toBe(1); + expect(idCallOrder[0]).toBe(CHILD); + }); - /** - * The entire event registration state of the world should be "locked-in" at - * the time the event occurs. This is to resolve many edge cases that come - * about from a listener on a lower-in-DOM node causing structural changes at - * places higher in the DOM. If this lower-in-DOM node causes new content to - * be rendered at a place higher-in-DOM, we need to be careful not to invoke - * these new listeners. - */ - - it('should invoke handlers that were removed while bubbling', () => { - const handleParentClick = jest.fn(); - const handleChildClick = function(event) { - deleteAllListeners(PARENT); - }; - putListener(CHILD, ON_CLICK_KEY, handleChildClick); - putListener(PARENT, ON_CLICK_KEY, handleParentClick); - CHILD.click(); - expect(handleParentClick).toHaveBeenCalledTimes(1); - }); + it('should not stopPropagation if false is returned', () => { + putListener( + CHILD, + ON_CLICK_KEY, + recordIDAndReturnFalse.bind(null, CHILD), + ); + putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT)); + putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT)); + CHILD.click(); + expect(idCallOrder.length).toBe(3); + expect(idCallOrder[0]).toBe(CHILD); + expect(idCallOrder[1]).toBe(PARENT); + expect(idCallOrder[2]).toBe(GRANDPARENT); + }); - it('should not invoke newly inserted handlers while bubbling', () => { - const handleParentClick = jest.fn(); - const handleChildClick = function(event) { + /** + * The entire event registration state of the world should be "locked-in" at + * the time the event occurs. This is to resolve many edge cases that come + * about from a listener on a lower-in-DOM node causing structural changes at + * places higher in the DOM. If this lower-in-DOM node causes new content to + * be rendered at a place higher-in-DOM, we need to be careful not to invoke + * these new listeners. + */ + + it('should invoke handlers that were removed while bubbling', () => { + const handleParentClick = jest.fn(); + const handleChildClick = function(event) { + deleteAllListeners(PARENT); + }; + putListener(CHILD, ON_CLICK_KEY, handleChildClick); putListener(PARENT, ON_CLICK_KEY, handleParentClick); - }; - putListener(CHILD, ON_CLICK_KEY, handleChildClick); - CHILD.click(); - expect(handleParentClick).toHaveBeenCalledTimes(0); - }); + CHILD.click(); + expect(handleParentClick).toHaveBeenCalledTimes(1); + }); - it('should have mouse enter simulated by test utils', () => { - putListener(CHILD, ON_MOUSE_ENTER_KEY, recordID.bind(null, CHILD)); - ReactTestUtils.Simulate.mouseEnter(CHILD); - expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(CHILD); - }); + it('should not invoke newly inserted handlers while bubbling', () => { + const handleParentClick = jest.fn(); + const handleChildClick = function(event) { + putListener(PARENT, ON_CLICK_KEY, handleParentClick); + }; + putListener(CHILD, ON_CLICK_KEY, handleChildClick); + CHILD.click(); + expect(handleParentClick).toHaveBeenCalledTimes(0); + }); - it('should listen to events only once', () => { - spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); - ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); - ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); - expect(EventTarget.prototype.addEventListener).toHaveBeenCalledTimes(1); - }); + it('should have mouse enter simulated by test utils', () => { + putListener(CHILD, ON_MOUSE_ENTER_KEY, recordID.bind(null, CHILD)); + ReactTestUtils.Simulate.mouseEnter(CHILD); + expect(idCallOrder.length).toBe(1); + expect(idCallOrder[0]).toBe(CHILD); + }); - it('should work with event plugins without dependencies', () => { - spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); + it('should listen to events only once', () => { + spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); + ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); + ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); + expect(EventTarget.prototype.addEventListener).toHaveBeenCalledTimes(1); + }); - ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); + it('should work with event plugins without dependencies', () => { + spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); - expect(EventTarget.prototype.addEventListener.calls.argsFor(0)[0]).toBe( - 'click', - ); - }); + ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); - it('should work with event plugins with dependencies', () => { - spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); + expect(EventTarget.prototype.addEventListener.calls.argsFor(0)[0]).toBe( + 'click', + ); + }); - ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document); + it('should work with event plugins with dependencies', () => { + spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); - const setEventListeners = []; - const listenCalls = EventTarget.prototype.addEventListener.calls.allArgs(); - for (let i = 0; i < listenCalls.length; i++) { - setEventListeners.push(listenCalls[i][1]); - } + ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document); + + const setEventListeners = []; + const listenCalls = EventTarget.prototype.addEventListener.calls.allArgs(); + for (let i = 0; i < listenCalls.length; i++) { + setEventListeners.push(listenCalls[i][1]); + } - const module = EventPluginRegistry.registrationNameModules[ON_CHANGE_KEY]; - const dependencies = module.eventTypes.change.dependencies; - expect(setEventListeners.length).toEqual(dependencies.length); + const module = EventPluginRegistry.registrationNameModules[ON_CHANGE_KEY]; + const dependencies = module.eventTypes.change.dependencies; + expect(setEventListeners.length).toEqual(dependencies.length); - for (let i = 0; i < setEventListeners.length; i++) { - expect(dependencies.indexOf(setEventListeners[i])).toBeTruthy(); - } + for (let i = 0; i < setEventListeners.length; i++) { + expect(dependencies.indexOf(setEventListeners[i])).toBeTruthy(); + } + }); }); -}); +} diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index e695a845ff5..0c81570454d 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -14,6 +14,7 @@ describe('ReactDOMComponent', () => { let ReactTestUtils; let ReactDOM; let ReactDOMServer; + let ReactFeatureFlags = require('shared/ReactFeatureFlags'); function normalizeCodeLocInfo(str) { return str && str.replace(/\(at .+?:\d+\)/g, '(at **)'); @@ -195,6 +196,7 @@ describe('ReactDOMComponent', () => { ); expect(container.firstChild.hasAttribute('onunknown')).toBe(false); expect(container.firstChild.onunknown).toBe(undefined); + expect(() => ReactDOM.render(
, @@ -207,30 +209,32 @@ describe('ReactDOMComponent', () => { expect(container.firstChild['on-unknown']).toBe(undefined); }); - it('should warn for unknown function event handlers', () => { - const container = document.createElement('div'); - expect(() => - ReactDOM.render(
, container), - ).toWarnDev( - 'Warning: Unknown event handler property `onUnknown`. It will be ignored.\n in div (at **)', - ); - expect(container.firstChild.hasAttribute('onUnknown')).toBe(false); - expect(container.firstChild.onUnknown).toBe(undefined); - expect(() => - ReactDOM.render(
, container), - ).toWarnDev( - 'Warning: Unknown event handler property `onunknown`. It will be ignored.\n in div (at **)', - ); - expect(container.firstChild.hasAttribute('onunknown')).toBe(false); - expect(container.firstChild.onunknown).toBe(undefined); - expect(() => - ReactDOM.render(
, container), - ).toWarnDev( - 'Warning: Unknown event handler property `on-unknown`. It will be ignored.\n in div (at **)', - ); - expect(container.firstChild.hasAttribute('on-unknown')).toBe(false); - expect(container.firstChild['on-unknown']).toBe(undefined); - }); + if (!ReactFeatureFlags.enableReactDOMFire) { + it('should warn for unknown function event handlers', () => { + const container = document.createElement('div'); + expect(() => + ReactDOM.render(
, container), + ).toWarnDev( + 'Warning: Unknown event handler property `onUnknown`. It will be ignored.\n in div (at **)', + ); + expect(container.firstChild.hasAttribute('onUnknown')).toBe(false); + expect(container.firstChild.onUnknown).toBe(undefined); + expect(() => + ReactDOM.render(
, container), + ).toWarnDev( + 'Warning: Unknown event handler property `onunknown`. It will be ignored.\n in div (at **)', + ); + expect(container.firstChild.hasAttribute('onunknown')).toBe(false); + expect(container.firstChild.onunknown).toBe(undefined); + expect(() => + ReactDOM.render(
, container), + ).toWarnDev( + 'Warning: Unknown event handler property `on-unknown`. It will be ignored.\n in div (at **)', + ); + expect(container.firstChild.hasAttribute('on-unknown')).toBe(false); + expect(container.firstChild['on-unknown']).toBe(undefined); + }); + } it('should warn for badly cased React attributes', () => { const container = document.createElement('div'); @@ -1900,12 +1904,22 @@ describe('ReactDOMComponent', () => { ReactTestUtils.renderIntoDocument( React.createElement('input', {type: 'text', oninput: '1'}), ); - }).toWarnDev('onInput'); + }).toWarnDev( + ReactFeatureFlags.enableReactDOMFire + ? 'Warning: Invalid event handler property `oninput`. React events use the camelCase naming convention, ' + + 'for example `onClick`.\n in input' + : 'onInput', + ); expect(() => { ReactTestUtils.renderIntoDocument( React.createElement('input', {type: 'text', onKeydown: '1'}), ); - }).toWarnDev('onKeyDown'); + }).toWarnDev( + ReactFeatureFlags.enableReactDOMFire + ? 'Warning: Expected `onKeydown` listener to be a function, instead got a value of `string` type.' + + '\n in input' + : 'onKeyDown', + ); }); it('should warn about class', () => { diff --git a/packages/react-dom/src/events/__tests__/BeforeInputEventPlugin-test.js b/packages/react-dom/src/events/__tests__/BeforeInputEventPlugin-test.js index ad4ecd7b81f..00ccd01b8a6 100644 --- a/packages/react-dom/src/events/__tests__/BeforeInputEventPlugin-test.js +++ b/packages/react-dom/src/events/__tests__/BeforeInputEventPlugin-test.js @@ -12,6 +12,7 @@ let React; let ReactDOM; +// Fire has a polyfill for this plugin describe('BeforeInputEventPlugin', () => { let container; diff --git a/packages/react-dom/src/events/__tests__/EnterLeaveEventPlugin-test.js b/packages/react-dom/src/events/__tests__/EnterLeaveEventPlugin-test.js index 33dd3e964ee..318e72755b5 100644 --- a/packages/react-dom/src/events/__tests__/EnterLeaveEventPlugin-test.js +++ b/packages/react-dom/src/events/__tests__/EnterLeaveEventPlugin-test.js @@ -45,7 +45,9 @@ describe('EnterLeaveEventPlugin', () => {
{ e.persist(); - leaveEvents.push(e); + // We do this for React Fire because the properties + // are dynamic getters and can change after creation + leaveEvents.push({target: e.target, relatedTarget: e.relatedTarget}); }} />, iframeDocument.body.getElementsByTagName('div')[0], diff --git a/packages/react-dom/src/events/__tests__/SelectEventPlugin-test.js b/packages/react-dom/src/events/__tests__/SelectEventPlugin-test.js index 8ac5b3a1314..c3de6a2e203 100644 --- a/packages/react-dom/src/events/__tests__/SelectEventPlugin-test.js +++ b/packages/react-dom/src/events/__tests__/SelectEventPlugin-test.js @@ -12,6 +12,7 @@ let React; let ReactDOM; +// Fire has a polyfill for this plugin describe('SelectEventPlugin', () => { let container; diff --git a/packages/react-dom/src/fire/ReactDOMFB.js b/packages/react-dom/src/fire/ReactDOMFB.js new file mode 100644 index 00000000000..7e560d76011 --- /dev/null +++ b/packages/react-dom/src/fire/ReactDOMFB.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {findCurrentFiberUsingSlowPath} from 'react-reconciler/reflection'; +import {get as getInstance} from 'shared/ReactInstanceMap'; +import {addUserTimingListener} from 'shared/ReactFeatureFlags'; + +import ReactDOM from './ReactFire'; +import {isEventsEnabled} from './events/ReactFireEvents'; +import {getClosestFiberFromDOMNode} from './ReactFireInternal'; + +Object.assign( + (ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: any), + { + // These are real internal dependencies that are trickier to remove: + ReactBrowserEventEmitter: { + isEnabled: isEventsEnabled, + }, + ReactFiberTreeReflection: { + findCurrentFiberUsingSlowPath, + }, + ReactDOMComponentTree: { + getClosestInstanceFromNode: getClosestFiberFromDOMNode, + }, + ReactInstanceMap: { + get: getInstance, + }, + // Perf experiment + addUserTimingListener, + }, +); + +export default ReactDOM; diff --git a/packages/react-dom/src/fire/ReactFire.js b/packages/react-dom/src/fire/ReactFire.js index b1a7855ab24..1e47a73fdb2 100644 --- a/packages/react-dom/src/fire/ReactFire.js +++ b/packages/react-dom/src/fire/ReactFire.js @@ -7,87 +7,68 @@ * @flow */ -// This file is copy paste from ReactDOM with adjusted paths -// and a different host config import (react-reconciler/inline.fire). -// TODO: real implementation. -// console.log('Hello from Fire entry point.'); - -import type {ReactNodeList} from 'shared/ReactTypes'; -// TODO: This type is shared between the reconciler and ReactDOM, but will -// eventually be lifted out to the renderer. -import type { - FiberRoot, - Batch as FiberRootBatch, -} from 'react-reconciler/src/ReactFiberRoot'; -import type {Container} from '../client/ReactDOMHostConfig'; - -import '../shared/checkReact'; -import '../client/ReactDOMClientInjection'; - +import {roots, ReactRoot, type Root} from './ReactFireRoots'; +import { + enqueueStateRestore, + restoreStateIfNeeded, + setRestoreImplementation, +} from './controlled/ReactFireControlledState'; +import {setBatchingImplementation} from './ReactFireBatching'; import { - computeUniqueAsyncExpiration, - findHostInstanceWithNoPortals, - updateContainerAtExpirationTime, - flushRoot, - createContainer, - updateContainer, batchedUpdates, - unbatchedUpdates, - interactiveUpdates, - flushInteractiveUpdates, - flushSync, - flushControlled, - injectIntoDevTools, - getPublicRootInstance, + findHostInstanceWithNoPortals, findHostInstance, findHostInstanceWithWarning, + flushControlled, + flushInteractiveUpdates, + flushSync, + interactiveUpdates, + unbatchedUpdates, } from 'react-reconciler/inline.fire'; -import {createPortal as createPortalImpl} from 'shared/ReactPortal'; -import {canUseDOM} from 'shared/ExecutionEnvironment'; -import {setBatchingImplementation} from 'events/ReactGenericBatching'; +import {restoreHostComponentInputControlledState} from './controlled/ReactFireInput'; +import {restoreHostComponentTextareaControlledState} from './controlled/ReactFireTextarea'; +import {restoreHostComponentSelectControlledState} from './controlled/ReactFireSelect'; +import {setupDevTools} from './ReactFireDevTools'; import { - setRestoreImplementation, - enqueueStateRestore, - restoreStateIfNeeded, -} from 'events/ReactControlledComponent'; -import { - injection as EventPluginHubInjection, - runEventsInBatch, -} from 'events/EventPluginHub'; -import {eventNameDispatchConfigs} from 'events/EventPluginRegistry'; + COMMENT_NODE, + DOCUMENT_NODE, + ELEMENT_NODE, + ROOT_ATTRIBUTE_NAME, +} from './ReactFireDOMConfig'; +import {getPublicRootInstance, isValidContainer} from './ReactFireUtils'; import { - accumulateTwoPhaseDispatches, - accumulateDirectDispatches, -} from 'events/EventPropagators'; -import {has as hasInstance} from 'shared/ReactInstanceMap'; -import ReactVersion from 'shared/ReactVersion'; -import ReactSharedInternals from 'shared/ReactSharedInternals'; -import getComponentName from 'shared/getComponentName'; + getFiberFromDomNode, + getFiberPropsFromDomNodeInstance, +} from './ReactFireInternal'; +import {proxyListener} from './events/ReactFireEvents'; + +import type {ReactNodeList} from 'shared/ReactTypes'; import invariant from 'shared/invariant'; -import lowPriorityWarning from 'shared/lowPriorityWarning'; -import warningWithoutStack from 'shared/warningWithoutStack'; +import {createPortal as createPortalImpl} from 'shared/ReactPortal'; +import getComponentName from 'shared/getComponentName'; import {enableStableConcurrentModeAPIs} from 'shared/ReactFeatureFlags'; - -import { - getInstanceFromNode, - getNodeFromInstance, - getFiberCurrentPropsFromNode, - getClosestInstanceFromNode, -} from '../client/ReactDOMComponentTree'; -import {restoreControlledState} from '../client/ReactDOMComponent'; -import {dispatchEvent} from '../events/ReactDOMEventListener'; -import { - ELEMENT_NODE, - COMMENT_NODE, - DOCUMENT_NODE, - DOCUMENT_FRAGMENT_NODE, -} from '../shared/HTMLNodeType'; -import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty'; +import warningWithoutStack from 'shared/warningWithoutStack'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import {HostComponent, HostText} from 'shared/ReactWorkTags'; +import lowPriorityWarning from 'shared/lowPriorityWarning'; +import {has as hasInstance} from 'shared/ReactInstanceMap'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; +type RootOptions = { + hydrate?: boolean, +}; + +export type DOMContainer = + | (Element & { + _reactRootContainer: ?Root, + }) + | (Document & { + _reactRootContainer: ?Root, + }); + let topLevelUpdateWarnings; -let warnOnInvalidCallback; +let warnedAboutHydrateAPI = false; let didWarnAboutUnstableCreatePortal = false; if (__DEV__) { @@ -110,10 +91,11 @@ if (__DEV__) { } topLevelUpdateWarnings = (container: DOMContainer) => { - if (container._reactRootContainer && container.nodeType !== COMMENT_NODE) { - const hostInstance = findHostInstanceWithNoPortals( - container._reactRootContainer._internalRoot.current, - ); + if (roots.has(container) && container.nodeType !== COMMENT_NODE) { + const root = roots.get(container); + const hostInstance = + root !== undefined && + findHostInstanceWithNoPortals(root._internalRoot.current); if (hostInstance) { warningWithoutStack( hostInstance.parentNode === container, @@ -125,9 +107,9 @@ if (__DEV__) { } } - const isRootRenderedBySomeReact = !!container._reactRootContainer; + const isRootRenderedBySomeReact = roots.has(container); const rootEl = getReactRootElementInContainer(container); - const hasNonRootReactChild = !!(rootEl && getInstanceFromNode(rootEl)); + const hasNonRootReactChild = !!(rootEl && getFiberFromDomNode(rootEl)); warningWithoutStack( !hasNonRootReactChild || isRootRenderedBySomeReact, @@ -148,334 +130,103 @@ if (__DEV__) { 'for your app.', ); }; - - warnOnInvalidCallback = function(callback: mixed, callerName: string) { - warningWithoutStack( - callback === null || typeof callback === 'function', - '%s(...): Expected the last optional `callback` argument to be a ' + - 'function. Instead received: %s.', - callerName, - callback, - ); - }; } -setRestoreImplementation(restoreControlledState); - -export type DOMContainer = - | (Element & { - _reactRootContainer: ?Root, - }) - | (Document & { - _reactRootContainer: ?Root, - }); - -type Batch = FiberRootBatch & { - render(children: ReactNodeList): Work, - then(onComplete: () => mixed): void, - commit(): void, - - // The ReactRoot constructor is hoisted but the prototype methods are not. If - // we move ReactRoot to be above ReactBatch, the inverse error occurs. - // $FlowFixMe Hoisting issue. - _root: Root, - _hasChildren: boolean, - _children: ReactNodeList, - - _callbacks: Array<() => mixed> | null, - _didComplete: boolean, -}; +setRestoreImplementation((domNode: Element, tag: string, props: Object) => { + switch (tag) { + case 'input': + restoreHostComponentInputControlledState(domNode, props); + return; + case 'textarea': + restoreHostComponentTextareaControlledState(domNode, props); + return; + case 'select': + restoreHostComponentSelectControlledState(domNode, props); + return; + } +}); -type Root = { - render(children: ReactNodeList, callback: ?() => mixed): Work, - unmount(callback: ?() => mixed): Work, - legacy_renderSubtreeIntoContainer( - parentComponent: ?React$Component, - children: ReactNodeList, - callback: ?() => mixed, - ): Work, - createBatch(): Batch, - - _internalRoot: FiberRoot, -}; +setBatchingImplementation( + batchedUpdates, + interactiveUpdates, + flushInteractiveUpdates, +); -function ReactBatch(root: ReactRoot) { - const expirationTime = computeUniqueAsyncExpiration(); - this._expirationTime = expirationTime; - this._root = root; - this._next = null; - this._callbacks = null; - this._didComplete = false; - this._hasChildren = false; - this._children = null; - this._defer = true; -} -ReactBatch.prototype.render = function(children: ReactNodeList) { +function createPortal( + children: ReactNodeList, + container: DOMContainer, + key: ?string = null, +) { invariant( - this._defer, - 'batch.render: Cannot render a batch that already committed.', - ); - this._hasChildren = true; - this._children = children; - const internalRoot = this._root._internalRoot; - const expirationTime = this._expirationTime; - const work = new ReactWork(); - updateContainerAtExpirationTime( - children, - internalRoot, - null, - expirationTime, - work._onCommit, + isValidContainer(container), + 'Target container is not a DOM element.', ); - return work; -}; -ReactBatch.prototype.then = function(onComplete: () => mixed) { - if (this._didComplete) { - onComplete(); - return; - } - let callbacks = this._callbacks; - if (callbacks === null) { - callbacks = this._callbacks = []; - } - callbacks.push(onComplete); -}; -ReactBatch.prototype.commit = function() { - const internalRoot = this._root._internalRoot; - let firstBatch = internalRoot.firstBatch; + // TODO: pass ReactDOM portal implementation as third argument + return createPortalImpl(children, container, null, key); +} + +function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot { + const functionName = enableStableConcurrentModeAPIs + ? 'createRoot' + : 'unstable_createRoot'; invariant( - this._defer && firstBatch !== null, - 'batch.commit: Cannot commit a batch multiple times.', + isValidContainer(container), + '%s(...): Target container is not a DOM element.', + functionName, ); - - if (!this._hasChildren) { - // This batch is empty. Return. - this._next = null; - this._defer = false; - return; - } - - let expirationTime = this._expirationTime; - - // Ensure this is the first batch in the list. - if (firstBatch !== this) { - // This batch is not the earliest batch. We need to move it to the front. - // Update its expiration time to be the expiration time of the earliest - // batch, so that we can flush it without flushing the other batches. - if (this._hasChildren) { - expirationTime = this._expirationTime = firstBatch._expirationTime; - // Rendering this batch again ensures its children will be the final state - // when we flush (updates are processed in insertion order: last - // update wins). - // TODO: This forces a restart. Should we print a warning? - this.render(this._children); - } - - // Remove the batch from the list. - let previous = null; - let batch = firstBatch; - while (batch !== this) { - previous = batch; - batch = batch._next; - } - invariant( - previous !== null, - 'batch.commit: Cannot commit a batch multiple times.', - ); - previous._next = batch._next; - - // Add it to the front. - this._next = firstBatch; - firstBatch = internalRoot.firstBatch = this; - } - - // Synchronously flush all the work up to this batch's expiration time. - this._defer = false; - flushRoot(internalRoot, expirationTime); - - // Pop the batch from the list. - const next = this._next; - this._next = null; - firstBatch = internalRoot.firstBatch = next; - - // Append the next earliest batch's children to the update queue. - if (firstBatch !== null && firstBatch._hasChildren) { - firstBatch.render(firstBatch._children); - } -}; -ReactBatch.prototype._onComplete = function() { - if (this._didComplete) { - return; - } - this._didComplete = true; - const callbacks = this._callbacks; - if (callbacks === null) { - return; - } - // TODO: Error handling. - for (let i = 0; i < callbacks.length; i++) { - const callback = callbacks[i]; - callback(); - } -}; - -type Work = { - then(onCommit: () => mixed): void, - _onCommit: () => void, - _callbacks: Array<() => mixed> | null, - _didCommit: boolean, -}; - -function ReactWork() { - this._callbacks = null; - this._didCommit = false; - // TODO: Avoid need to bind by replacing callbacks in the update queue with - // list of Work objects. - this._onCommit = this._onCommit.bind(this); + const shouldHydrate = options != null && options.hydrate === true; + return new ReactRoot(container, true, shouldHydrate); } -ReactWork.prototype.then = function(onCommit: () => mixed): void { - if (this._didCommit) { - onCommit(); - return; - } - let callbacks = this._callbacks; - if (callbacks === null) { - callbacks = this._callbacks = []; - } - callbacks.push(onCommit); -}; -ReactWork.prototype._onCommit = function(): void { - if (this._didCommit) { - return; - } - this._didCommit = true; - const callbacks = this._callbacks; - if (callbacks === null) { - return; - } - // TODO: Error handling. - for (let i = 0; i < callbacks.length; i++) { - const callback = callbacks[i]; - invariant( - typeof callback === 'function', - 'Invalid argument passed as callback. Expected a function. Instead ' + - 'received: %s', - callback, - ); - callback(); - } -}; -function ReactRoot( - container: Container, - isConcurrent: boolean, - hydrate: boolean, -) { - const root = createContainer(container, isConcurrent, hydrate); - this._internalRoot = root; -} -ReactRoot.prototype.render = function( - children: ReactNodeList, - callback: ?() => mixed, -): Work { - const root = this._internalRoot; - const work = new ReactWork(); - callback = callback === undefined ? null : callback; +function findDOMNode(componentOrElement: Element | ?React$Component) { if (__DEV__) { - warnOnInvalidCallback(callback, 'render'); - } - if (callback !== null) { - work.then(callback); + let owner = (ReactCurrentOwner.current: any); + if (owner !== null && owner.stateNode !== null) { + const warnedAboutRefsInRender = owner.stateNode._warnedAboutRefsInRender; + warningWithoutStack( + warnedAboutRefsInRender, + '%s is accessing findDOMNode inside its render(). ' + + 'render() should be a pure function of props and state. It should ' + + 'never access something that requires stale data from the previous ' + + 'render, such as refs. Move this logic to componentDidMount and ' + + 'componentDidUpdate instead.', + getComponentName(owner.type) || 'A component', + ); + owner.stateNode._warnedAboutRefsInRender = true; + } } - updateContainer(children, root, null, work._onCommit); - return work; -}; -ReactRoot.prototype.unmount = function(callback: ?() => mixed): Work { - const root = this._internalRoot; - const work = new ReactWork(); - callback = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(callback, 'render'); + if (componentOrElement == null) { + return null; } - if (callback !== null) { - work.then(callback); + if ((componentOrElement: any).nodeType === ELEMENT_NODE) { + return (componentOrElement: any); } - updateContainer(null, root, null, work._onCommit); - return work; -}; -ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function( - parentComponent: ?React$Component, - children: ReactNodeList, - callback: ?() => mixed, -): Work { - const root = this._internalRoot; - const work = new ReactWork(); - callback = callback === undefined ? null : callback; if (__DEV__) { - warnOnInvalidCallback(callback, 'render'); - } - if (callback !== null) { - work.then(callback); - } - updateContainer(children, root, parentComponent, work._onCommit); - return work; -}; -ReactRoot.prototype.createBatch = function(): Batch { - const batch = new ReactBatch(this); - const expirationTime = batch._expirationTime; - - const internalRoot = this._internalRoot; - const firstBatch = internalRoot.firstBatch; - if (firstBatch === null) { - internalRoot.firstBatch = batch; - batch._next = null; - } else { - // Insert sorted by expiration time then insertion order - let insertAfter = null; - let insertBefore = firstBatch; - while ( - insertBefore !== null && - insertBefore._expirationTime >= expirationTime - ) { - insertAfter = insertBefore; - insertBefore = insertBefore._next; - } - batch._next = insertBefore; - if (insertAfter !== null) { - insertAfter._next = batch; - } + return findHostInstanceWithWarning(componentOrElement, 'findDOMNode'); } - - return batch; -}; - -/** - * True if the supplied DOM node is a valid node element. - * - * @param {?DOMElement} node The candidate DOM node. - * @return {boolean} True if the DOM is a valid DOM node. - * @internal - */ -function isValidContainer(node) { - return !!( - node && - (node.nodeType === ELEMENT_NODE || - node.nodeType === DOCUMENT_NODE || - node.nodeType === DOCUMENT_FRAGMENT_NODE || - (node.nodeType === COMMENT_NODE && - node.nodeValue === ' react-mount-point-unstable ')) - ); + return findHostInstance(componentOrElement); } -function getReactRootElementInContainer(container: any) { - if (!container) { - return null; - } - - if (container.nodeType === DOCUMENT_NODE) { - return container.documentElement; - } else { - return container.firstChild; +function cleanRootDOMContainer(container: DOMContainer) { + let warned = false; + let rootSibling; + while ((rootSibling = container.lastChild)) { + if (__DEV__) { + if ( + !warned && + rootSibling.nodeType === ELEMENT_NODE && + (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME) + ) { + warned = true; + warningWithoutStack( + false, + 'render(): Target node has markup rendered by React, but there ' + + 'are unrelated nodes as well. This is most commonly caused by ' + + 'white-space inserted around server-rendered markup.', + ); + } + } + container.removeChild(rootSibling); } } @@ -488,271 +239,220 @@ function shouldHydrateDueToLegacyHeuristic(container) { ); } -setBatchingImplementation( - batchedUpdates, - interactiveUpdates, - flushInteractiveUpdates, -); - -let warnedAboutHydrateAPI = false; - -function legacyCreateRootFromDOMContainer( - container: DOMContainer, - forceHydrate: boolean, -): Root { - const shouldHydrate = - forceHydrate || shouldHydrateDueToLegacyHeuristic(container); - // First clear any existing content. - if (!shouldHydrate) { - let warned = false; - let rootSibling; - while ((rootSibling = container.lastChild)) { - if (__DEV__) { - if ( - !warned && - rootSibling.nodeType === ELEMENT_NODE && - (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME) - ) { - warned = true; - warningWithoutStack( - false, - 'render(): Target node has markup rendered by React, but there ' + - 'are unrelated nodes as well. This is most commonly caused by ' + - 'white-space inserted around server-rendered markup.', - ); - } - } - container.removeChild(rootSibling); - } - } - if (__DEV__) { - if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) { - warnedAboutHydrateAPI = true; - lowPriorityWarning( - false, - 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' + - 'will stop working in React v17. Replace the ReactDOM.render() call ' + - 'with ReactDOM.hydrate() if you want React to attach to the server HTML.', - ); - } - } - // Legacy roots are not async by default. - const isConcurrent = false; - return new ReactRoot(container, isConcurrent, shouldHydrate); -} - function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component, children: ReactNodeList, - container: DOMContainer, + domContainer: DOMContainer, forceHydrate: boolean, callback: ?Function, ) { - // TODO: Ensure all entry points contain this check + let root = roots.get(domContainer); + invariant( - isValidContainer(container), - 'Target container is not a DOM element.', + isValidContainer(domContainer), + 'unmountComponentAtNode(...): Target container is not a DOM element.', ); if (__DEV__) { - topLevelUpdateWarnings(container); + (domContainer: any)._reactRootDev = true; + topLevelUpdateWarnings(domContainer); } - // TODO: Without `any` type, Flow says "Property cannot be accessed on any - // member of intersection type." Whyyyyyy. - let root: Root = (container._reactRootContainer: any); - if (!root) { + if (root === undefined) { + const shouldHydrate = + forceHydrate || shouldHydrateDueToLegacyHeuristic(domContainer); // Initial mount - root = container._reactRootContainer = legacyCreateRootFromDOMContainer( - container, - forceHydrate, - ); + if (!shouldHydrate) { + cleanRootDOMContainer(domContainer); + } + if (__DEV__) { + if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) { + warnedAboutHydrateAPI = true; + lowPriorityWarning( + false, + 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' + + 'will stop working in React v17. Replace the ReactDOM.render() call ' + + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.', + ); + } + } + root = new ReactRoot(domContainer, false, shouldHydrate); + roots.set(domContainer, ((root: any): Root)); if (typeof callback === 'function') { const originalCallback = callback; callback = function() { - const instance = getPublicRootInstance(root._internalRoot); + const instance = getPublicRootInstance( + ((root: any): Root)._internalRoot, + ); originalCallback.call(instance); }; } // Initial mount should not be batched. unbatchedUpdates(() => { if (parentComponent != null) { - root.legacy_renderSubtreeIntoContainer( + ((root: any): Root).legacy_renderSubtreeIntoContainer( parentComponent, children, callback, ); } else { - root.render(children, callback); + ((root: any): Root).render(children, callback); } }); } else { if (typeof callback === 'function') { const originalCallback = callback; callback = function() { - const instance = getPublicRootInstance(root._internalRoot); + const instance = getPublicRootInstance( + ((root: any): Root)._internalRoot, + ); originalCallback.call(instance); }; } // Update if (parentComponent != null) { - root.legacy_renderSubtreeIntoContainer( + ((root: any): Root).legacy_renderSubtreeIntoContainer( parentComponent, children, callback, ); } else { - root.render(children, callback); + ((root: any): Root).render(children, callback); } } - return getPublicRootInstance(root._internalRoot); + return getPublicRootInstance(((root: any): Root)._internalRoot); } -function createPortal( +function render( children: ReactNodeList, - container: DOMContainer, - key: ?string = null, + domContainer: DOMContainer, + callback: ?Function, ) { - invariant( - isValidContainer(container), - 'Target container is not a DOM element.', + return legacyRenderSubtreeIntoContainer( + null, + children, + domContainer, + false, + callback, ); - // TODO: pass ReactDOM portal implementation as third argument - return createPortalImpl(children, container, null, key); } -const ReactDOM: Object = { - createPortal, - - findDOMNode( - componentOrElement: Element | ?React$Component, - ): null | Element | Text { - if (__DEV__) { - let owner = (ReactCurrentOwner.current: any); - if (owner !== null && owner.stateNode !== null) { - const warnedAboutRefsInRender = - owner.stateNode._warnedAboutRefsInRender; - warningWithoutStack( - warnedAboutRefsInRender, - '%s is accessing findDOMNode inside its render(). ' + - 'render() should be a pure function of props and state. It should ' + - 'never access something that requires stale data from the previous ' + - 'render, such as refs. Move this logic to componentDidMount and ' + - 'componentDidUpdate instead.', - getComponentName(owner.type) || 'A component', - ); - owner.stateNode._warnedAboutRefsInRender = true; - } - } - if (componentOrElement == null) { - return null; - } - if ((componentOrElement: any).nodeType === ELEMENT_NODE) { - return (componentOrElement: any); - } - if (__DEV__) { - return findHostInstanceWithWarning(componentOrElement, 'findDOMNode'); - } - return findHostInstance(componentOrElement); - }, +function getReactRootElementInContainer(container: any) { + if (!container) { + return null; + } - hydrate(element: React$Node, container: DOMContainer, callback: ?Function) { - // TODO: throw or warn if we couldn't hydrate? - return legacyRenderSubtreeIntoContainer( - null, - element, - container, - true, - callback, - ); - }, + if (container.nodeType === DOCUMENT_NODE) { + return container.documentElement; + } else { + return container.firstChild; + } +} - render( - element: React$Element, - container: DOMContainer, - callback: ?Function, - ) { - return legacyRenderSubtreeIntoContainer( - null, - element, - container, - false, - callback, - ); - }, +function unmountComponentAtNode(domContainer: DOMContainer) { + invariant( + isValidContainer(domContainer), + 'unmountComponentAtNode(...): Target container is not a DOM element.', + ); + if (roots.has(domContainer)) { + unbatchedUpdates(() => { + legacyRenderSubtreeIntoContainer(null, null, domContainer, false, () => { + roots.delete(domContainer); + }); + }); + return true; + } else { + if (__DEV__ && (domContainer: any)._reactRootDev === true) { + warningWithoutStack( + false, + "unmountComponentAtNode(): The node you're attempting to unmount " + + 'was rendered by another copy of React.', + ); + } + } + if (__DEV__) { + const rootEl = getReactRootElementInContainer(domContainer); + const hasNonRootReactChild = !!(rootEl && getFiberFromDomNode(rootEl)); - unstable_renderSubtreeIntoContainer( - parentComponent: React$Component, - element: React$Element, - containerNode: DOMContainer, - callback: ?Function, - ) { - invariant( - parentComponent != null && hasInstance(parentComponent), - 'parentComponent must be a valid React Component', - ); - return legacyRenderSubtreeIntoContainer( - parentComponent, - element, - containerNode, - false, - callback, - ); - }, + // Check if the container itself is a React root node. + const isContainerReactRoot = + domContainer.nodeType === ELEMENT_NODE && + isValidContainer(domContainer.parentNode) && + roots.has(domContainer.parentNode); - unmountComponentAtNode(container: DOMContainer) { - invariant( - isValidContainer(container), - 'unmountComponentAtNode(...): Target container is not a DOM element.', + warningWithoutStack( + !hasNonRootReactChild, + "unmountComponentAtNode(): The node you're attempting to unmount " + + 'was rendered by React and is not a top-level container. %s', + isContainerReactRoot + ? 'You may have accidentally passed in a React root node instead ' + + 'of its container.' + : 'Instead, have the parent component update its state and ' + + 'rerender in order to remove this component.', ); + } + return false; +} - if (container._reactRootContainer) { - if (__DEV__) { - const rootEl = getReactRootElementInContainer(container); - const renderedByDifferentReact = rootEl && !getInstanceFromNode(rootEl); - warningWithoutStack( - !renderedByDifferentReact, - "unmountComponentAtNode(): The node you're attempting to unmount " + - 'was rendered by another copy of React.', - ); - } +function hydrate( + element: React$Node, + container: DOMContainer, + callback: ?Function, +) { + return legacyRenderSubtreeIntoContainer( + null, + element, + container, + true, + callback, + ); +} - // Unmount should not be batched. - unbatchedUpdates(() => { - legacyRenderSubtreeIntoContainer(null, null, container, false, () => { - container._reactRootContainer = null; - }); - }); - // If you call unmountComponentAtNode twice in quick succession, you'll - // get `true` twice. That's probably fine? - return true; - } else { - if (__DEV__) { - const rootEl = getReactRootElementInContainer(container); - const hasNonRootReactChild = !!(rootEl && getInstanceFromNode(rootEl)); +/** + * Given a ReactDOMComponent or ReactDOMTextComponent, return the corresponding + * DOM node. + */ +function getDomNodeFromFiber(inst: Object) { + if (inst.tag === HostComponent || inst.tag === HostText) { + // In Fiber this, is just the state node right now. We assume it will be + // a host component or host text. + return inst.stateNode; + } - // Check if the container itself is a React root node. - const isContainerReactRoot = - container.nodeType === ELEMENT_NODE && - isValidContainer(container.parentNode) && - !!container.parentNode._reactRootContainer; + // Without this first invariant, passing a non-DOM-component triggers the next + // invariant for a missing parent, which is super confusing. + invariant(false, 'getNodeFromInstance: Invalid argument.'); +} - warningWithoutStack( - !hasNonRootReactChild, - "unmountComponentAtNode(): The node you're attempting to unmount " + - 'was rendered by React and is not a top-level container. %s', - isContainerReactRoot - ? 'You may have accidentally passed in a React root node instead ' + - 'of its container.' - : 'Instead, have the parent component update its state and ' + - 'rerender in order to remove this component.', - ); - } +// Ideally we should aim to remove this from React Fire +function unstable_renderSubtreeIntoContainer( + parentComponent: React$Component, + element: React$Element, + containerNode: DOMContainer, + callback: ?Function, +) { + invariant( + parentComponent != null && hasInstance(parentComponent), + 'parentComponent must be a valid React Component', + ); + return legacyRenderSubtreeIntoContainer( + parentComponent, + element, + containerNode, + false, + callback, + ); +} - return false; - } - }, +const noOp = () => {}; +const ReactDOM: Object = { + createPortal, + findDOMNode, + flushSync, + hydrate, + render, + unmountComponentAtNode, // Temporary alias since we already shipped React 16 RC with it. // TODO: remove in React 17. unstable_createPortal(...args) { @@ -768,88 +468,36 @@ const ReactDOM: Object = { } return createPortal(...args); }, - + unstable_createRoot: undefined, unstable_batchedUpdates: batchedUpdates, - - unstable_interactiveUpdates: interactiveUpdates, - - flushSync: flushSync, - - unstable_createRoot: createRoot, unstable_flushControlled: flushControlled, - + unstable_interactiveUpdates: interactiveUpdates, + unstable_renderSubtreeIntoContainer, __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { // Keep in sync with ReactDOMUnstableNativeDependencies.js // and ReactTestUtils.js. This is an array for better minification. Events: [ - getInstanceFromNode, - getNodeFromInstance, - getFiberCurrentPropsFromNode, - EventPluginHubInjection.injectEventPluginsByName, - eventNameDispatchConfigs, - accumulateTwoPhaseDispatches, - accumulateDirectDispatches, + getFiberFromDomNode, + getDomNodeFromFiber, + getFiberPropsFromDomNodeInstance, + noOp, + null, + noOp, + noOp, enqueueStateRestore, restoreStateIfNeeded, - dispatchEvent, - runEventsInBatch, + proxyListener, + noOp, ], }, }; -type RootOptions = { - hydrate?: boolean, -}; - -function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot { - const functionName = enableStableConcurrentModeAPIs - ? 'createRoot' - : 'unstable_createRoot'; - invariant( - isValidContainer(container), - '%s(...): Target container is not a DOM element.', - functionName, - ); - const hydrate = options != null && options.hydrate === true; - return new ReactRoot(container, true, hydrate); -} - if (enableStableConcurrentModeAPIs) { ReactDOM.createRoot = createRoot; - ReactDOM.unstable_createRoot = undefined; +} else { + ReactDOM.unstable_createRoot = createRoot; } -const foundDevTools = injectIntoDevTools({ - findFiberByHostInstance: getClosestInstanceFromNode, - bundleType: __DEV__ ? 1 : 0, - version: ReactVersion, - rendererPackageName: 'react-dom', -}); - -if (__DEV__) { - if (!foundDevTools && canUseDOM && window.top === window.self) { - // If we're in Chrome or Firefox, provide a download link if not installed. - if ( - (navigator.userAgent.indexOf('Chrome') > -1 && - navigator.userAgent.indexOf('Edge') === -1) || - navigator.userAgent.indexOf('Firefox') > -1 - ) { - const protocol = window.location.protocol; - // Don't warn in exotic cases like chrome-extension://. - if (/^(https?|file):$/.test(protocol)) { - console.info( - '%cDownload the React DevTools ' + - 'for a better development experience: ' + - 'https://fb.me/react-devtools' + - (protocol === 'file:' - ? '\nYou might need to use a local HTTP server (instead of file://): ' + - 'https://fb.me/react-devtools-faq' - : ''), - 'font-weight:bold', - ); - } - } - } -} +setupDevTools(); export default ReactDOM; diff --git a/packages/react-dom/src/fire/ReactFireBatching.js b/packages/react-dom/src/fire/ReactFireBatching.js new file mode 100644 index 00000000000..8914ca4e0da --- /dev/null +++ b/packages/react-dom/src/fire/ReactFireBatching.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { + needsStateRestore, + restoreStateIfNeeded, +} from './controlled/ReactFireControlledState'; + +// Used as a way to call batchedUpdates when we don't have a reference to +// the renderer. Such as when we're dispatching events or if third party +// libraries need to call batchedUpdates. Eventually, this API will go away when +// everything is batched by default. We'll then have a similar API to opt-out of +// scheduled work and instead do synchronous work. + +// Defaults +let batchedUpdatesImpl = (fn, bookkeeping) => fn(bookkeeping); +let interactiveUpdatesImpl = (fn, a, b, c) => fn(a, b, c); +let flushInteractiveUpdatesImpl = () => {}; + +let isBatching = false; +export function batchedUpdates(fn, bookkeeping) { + if (isBatching) { + // If we are currently inside another batch, we need to wait until it + // fully completes before restoring state. + return fn(bookkeeping); + } + isBatching = true; + try { + return batchedUpdatesImpl(fn, bookkeeping); + } finally { + // Here we wait until all updates have propagated, which is important + // when using controlled components within layers: + // https://github.com/facebook/react/issues/1698 + // Then we restore state of any controlled component. + isBatching = false; + const controlledComponentsHavePendingUpdates = needsStateRestore(); + if (controlledComponentsHavePendingUpdates) { + // If a controlled event was fired, we may need to restore the state of + // the DOM node back to the controlled value. This is necessary when React + // bails out of the update without touching the DOM. + flushInteractiveUpdatesImpl(); + restoreStateIfNeeded(); + } + } +} + +export function interactiveUpdates(fn, a, b, c) { + return interactiveUpdatesImpl(fn, a, b, c); +} + +export function flushInteractiveUpdates() { + return flushInteractiveUpdatesImpl(); +} + +export function setBatchingImplementation( + _batchedUpdatesImpl, + _interactiveUpdatesImpl, + _flushInteractiveUpdatesImpl, +) { + batchedUpdatesImpl = _batchedUpdatesImpl; + interactiveUpdatesImpl = _interactiveUpdatesImpl; + flushInteractiveUpdatesImpl = _flushInteractiveUpdatesImpl; +} diff --git a/packages/react-dom/src/fire/ReactFireComponent.js b/packages/react-dom/src/fire/ReactFireComponent.js new file mode 100644 index 00000000000..38b273880a0 --- /dev/null +++ b/packages/react-dom/src/fire/ReactFireComponent.js @@ -0,0 +1,482 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {createElement, isCustomComponent} from './ReactFireUtils'; +import {mediaEventTypesArr} from './events/ReactFireEventTypes'; +import { + applyHostComponentInputMountWrapper, + updateWrapper as applyHostComponentInputUpdateWrapper, + getHostComponentInputProps, + initHostComponentInputWrapperState, + updateChecked, +} from './controlled/ReactFireInput'; +import { + applyHostComponentOptionMountWrapper, + getHostComponentOptionProps, + validateHostComponentOptionProps, +} from './controlled/ReactFireOption'; +import { + applyHostComponentSelectMountWrapper, + applyHostComponentSelectUpdateWrapper, + getHostComponentSelectProps, + initHostComponentSelectWrapperState, +} from './controlled/ReactFireSelect'; +import { + applyHostComponentTextareaMountWrapper, + updateWrapper as applyHostComponentTextareaUpdateWrapper, + getHostComponentTextareaProps, + initHostComponentTextareaWrapperState, +} from './controlled/ReactFireTextarea'; +import { + ensureListeningTo, + trapBubbledEvent, + trapClickOnNonInteractiveElement, +} from './events/ReactFireEvents'; +import {track} from './controlled/ReactFireValueTracking'; +import { + validateARIAProperties, + validateInputProperties, + validateUnknownProperties, +} from './ReactFireValidation'; +import { + diffDOMElementProperties, + diffHydratedDOMElementProperties, + setDOMElementProperties, + updateDOMElementProperties, +} from './ReactFireComponentProperties'; +import type { + HostContext, + HostContextDev, + HostContextProd, +} from './ReactFireHostConfig'; +import { + ERROR, + INVALID, + LOAD, + RESET, + SUBMIT, + TOGGLE, +} from './events/ReactFireEventTypes'; + +import warning from 'shared/warning'; +import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber'; + +let didWarnShadyDOM = false; +let validatePropertiesInDevelopment; + +if (__DEV__) { + validatePropertiesInDevelopment = function(type, props) { + validateARIAProperties(type, props); + validateInputProperties(type, props); + validateUnknownProperties(type, props, /* canUseEventSystem */ true); + }; +} + +const specialHostComponentTypes = { + audio: typeIsVideoOrAudio, + details: typeIsDetails, + form: typeIsForm, + iframe: typeIsIframeOrObject, + image: typeIsImageOrLink, + img: typeIsImageOrLink, + input: typeIsInput, + link: typeIsImageOrLink, + object: typeIsIframeOrObject, + option: typeIsOption, + select: typeIsSelect, + source: typeIsSource, + textarea: typeIsTextarea, + video: typeIsVideoOrAudio, +}; + +function typeIsTextarea( + props: Object, + domNode: Element, + rootContainerElement: Element | Document, +) { + initHostComponentTextareaWrapperState(domNode, props); + props = getHostComponentTextareaProps(domNode, props); + trapBubbledEvent('invalid', domNode); + // For controlled components we always need to ensure we're listening + // to onChange. Even if there is no listener. + ensureListeningTo(rootContainerElement, 'onChange'); + return props; +} + +function typeIsSelect( + props: Object, + domNode: Element, + rootContainerElement: Element | Document, +) { + initHostComponentSelectWrapperState(domNode, props); + props = getHostComponentSelectProps(domNode, props); + trapBubbledEvent('invalid', domNode); + // For controlled components we always need to ensure we're listening + // to onChange. Even if there is no listener. + ensureListeningTo(rootContainerElement, 'onChange'); + return props; +} + +function typeIsOption( + props: Object, + domNode: Element, + rootContainerElement: Element | Document, +) { + validateHostComponentOptionProps(domNode, props); + return getHostComponentOptionProps(domNode, props); +} + +function typeIsInput( + props: Object, + domNode: Element, + rootContainerElement: Element | Document, +) { + initHostComponentInputWrapperState(domNode, props); + props = getHostComponentInputProps(domNode, props); + trapBubbledEvent(INVALID, domNode); + // For controlled components we always need to ensure we're listening + // to onChange. Even if there is no listener. + ensureListeningTo(rootContainerElement, 'onChange'); + return props; +} + +function typeIsDetails( + props: Object, + domNode: Element, + rootContainerElement: Element | Document, +) { + trapBubbledEvent(TOGGLE, domNode); + return props; +} + +function typeIsForm( + props: Object, + domNode: Element, + rootContainerElement: Element | Document, +) { + trapBubbledEvent(RESET, domNode); + trapBubbledEvent(SUBMIT, domNode); + return props; +} + +function typeIsImageOrLink( + props: Object, + domNode: Element, + rootContainerElement: Element | Document, +) { + trapBubbledEvent(ERROR, domNode); + trapBubbledEvent(LOAD, domNode); + return props; +} + +function typeIsIframeOrObject( + props: Object, + domNode: Element, + rootContainerElement: Element | Document, +) { + trapBubbledEvent(LOAD, domNode); + return props; +} + +function typeIsVideoOrAudio( + props: Object, + domNode: Element, + rootContainerElement: Element | Document, +) { + // Create listener for each media event + for (let i = 0; i < mediaEventTypesArr.length; i++) { + // TODO should this be bubbled still? I think it should be captured instead... + trapBubbledEvent(mediaEventTypesArr[i], domNode); + } + return props; +} + +function typeIsSource( + props: Object, + domNode: Element, + rootContainerElement: Element | Document, +) { + trapBubbledEvent(ERROR, domNode); + return props; +} + +export function createHostComponent( + type: string, + props: Object, + rootContainerInstance: Element | Document, + hostContext: HostContext, +) { + const parentNamespace = __DEV__ + ? ((hostContext: any): HostContextDev).namespace + : ((hostContext: any): HostContextProd); + const domElement = createElement( + type, + props, + rootContainerInstance, + parentNamespace, + ); + return domElement; +} + +export function setHostComponentInitialProps( + type: string, + rawProps: Object, + domNode: Element, + rootContainerElement: Element | Document, + hostContext: HostContext, +) { + const isCustomComponentTag = isCustomComponent(type, rawProps); + if (__DEV__) { + validatePropertiesInDevelopment(type, rawProps); + if (isCustomComponentTag && !didWarnShadyDOM && (domNode: any).shadyRoot) { + warning( + false, + '%s is using shady DOM. Using shady DOM with React can ' + + 'cause things to break subtly.', + getCurrentFiberOwnerNameInDevOrNull() || 'A component', + ); + didWarnShadyDOM = true; + } + } + const specicalHostComponentTypeFunc = specialHostComponentTypes.hasOwnProperty( + type, + ) + ? specialHostComponentTypes[type] + : null; + let props = rawProps; + + if (specicalHostComponentTypeFunc !== null) { + props = specicalHostComponentTypeFunc( + rawProps, + domNode, + rootContainerElement, + ); + } + + setDOMElementProperties( + type, + props, + domNode, + rootContainerElement, + isCustomComponentTag, + ); + + if (specicalHostComponentTypeFunc !== null) { + switch (type) { + case 'input': + // TODO: Make sure we check if this is still unmounted or do any clean + // up necessary since we never stop tracking anymore. + track((domNode: any)); + applyHostComponentInputMountWrapper(domNode, rawProps, false); + break; + case 'textarea': + // TODO: Make sure we check if this is still unmounted or do any clean + // up necessary since we never stop tracking anymore. + track((domNode: any)); + applyHostComponentTextareaMountWrapper(domNode, rawProps); + break; + case 'option': + applyHostComponentOptionMountWrapper(domNode, rawProps); + break; + case 'select': + applyHostComponentSelectMountWrapper(domNode, rawProps); + break; + default: + } + } else { + if (typeof props.onClick === 'function') { + // TODO: This cast may not be sound for SVG, MathML or custom elements. + trapClickOnNonInteractiveElement(((domNode: any): HTMLElement)); + } + } +} + +// Calculate the diff between the two objects. +export function diffHostComponentProperties( + domNode: Element, + type: string, + lastRawProps: Object, + nextRawProps: Object, + rootContainerElement: Element | Document, +): null | Array { + if (__DEV__) { + validatePropertiesInDevelopment(type, nextRawProps); + } + + let updatePayload: null | Array = null; + let lastProps: Object; + let nextProps: Object; + switch (type) { + case 'input': + lastProps = getHostComponentInputProps(domNode, lastRawProps); + nextProps = getHostComponentInputProps(domNode, nextRawProps); + updatePayload = []; + break; + case 'option': + lastProps = getHostComponentOptionProps(domNode, lastRawProps); + nextProps = getHostComponentOptionProps(domNode, nextRawProps); + updatePayload = []; + break; + case 'select': + lastProps = getHostComponentSelectProps(domNode, lastRawProps); + nextProps = getHostComponentSelectProps(domNode, nextRawProps); + updatePayload = []; + break; + case 'textarea': + lastProps = getHostComponentTextareaProps(domNode, lastRawProps); + nextProps = getHostComponentTextareaProps(domNode, nextRawProps); + updatePayload = []; + break; + default: + lastProps = lastRawProps; + nextProps = nextRawProps; + if ( + typeof lastProps.onClick !== 'function' && + typeof nextProps.onClick === 'function' + ) { + // TODO: This cast may not be sound for SVG, MathML or custom elements. + trapClickOnNonInteractiveElement(((domNode: any): HTMLElement)); + } + break; + } + + return diffDOMElementProperties( + domNode, + type, + updatePayload, + lastProps, + nextProps, + rootContainerElement, + ); +} + +export function updateHostComponentProperties( + domNode: Element, + updatePayload: Array, + type: string, + lastRawProps: Object, + nextRawProps: Object, +) { + // Update checked *before* name. + // In the middle of an update, it is possible to have multiple checked. + // When a checked radio tries to change name, browser makes another radio's checked false. + if ( + type === 'input' && + nextRawProps.type === 'radio' && + nextRawProps.name != null + ) { + updateChecked(domNode, nextRawProps); + } + + const wasCustomComponentTag = isCustomComponent(type, lastRawProps); + const isCustomComponentTag = isCustomComponent(type, nextRawProps); + + // Apply the diff. + updateDOMElementProperties( + domNode, + lastRawProps, + updatePayload, + wasCustomComponentTag, + isCustomComponentTag, + ); + + // TODO: Ensure that an update gets scheduled if any of the special props + // changed. + switch (type) { + case 'input': + // Update the wrapper around inputs *after* updating props. This has to + // happen after `updateDOMProperties`. Otherwise HTML5 input validations + // raise warnings and prevent the new value from being assigned. + applyHostComponentInputUpdateWrapper(domNode, nextRawProps); + break; + case 'textarea': + applyHostComponentTextareaUpdateWrapper(domNode, nextRawProps); + break; + case 'select': + //