From beee248f9f85726830c27adf09bc0b4bb8c9a050 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 3 Dec 2018 14:40:43 +0000 Subject: [PATCH 01/26] Add base implementation of React Fire Add eventData undefined guard switch polyfill array to a map ReactTestUtils test fixes Adds onSelect polyfill Redesign the traversal system for events Adds polyfill for onMouseEnter/leave Various fixes to tests for enter/leave polyfill Fixes many tests Fixes many tests All prod tests now pass --- package.json | 3 +- .../src/__tests__/EventPluginHub-test.js | 59 +- .../ReactBrowserEventEmitter-test.internal.js | 572 ++++---- .../src/__tests__/ReactDOMFiber-test.js | 47 +- .../src/__tests__/ReactRenderDocument-test.js | 368 ++--- .../ReactServerRenderingHydration-test.js | 178 +-- .../src/__tests__/ReactTestUtils-test.js | 5 +- .../renderSubtreeIntoContainer-test.js | 548 +++---- packages/react-dom/src/client/ReactDOM.js | 4 +- .../__tests__/BeforeInputEventPlugin-test.js | 1 + .../ChangeEventPlugin-test.internal.js | 10 +- .../__tests__/EnterLeaveEventPlugin-test.js | 4 +- .../__tests__/SelectEventPlugin-test.js | 1 + .../__tests__/SyntheticClipboardEvent-test.js | 276 ++-- .../events/__tests__/SyntheticEvent-test.js | 606 ++++---- .../__tests__/SyntheticKeyboardEvent-test.js | 848 +++++------ .../__tests__/SyntheticMouseEvent-test.js | 128 +- .../__tests__/SyntheticWheelEvent-test.js | 222 +-- .../src/events/__tests__/getEventKey-test.js | 210 +-- packages/react-dom/src/fire/ReactFire.js | 974 +++---------- .../react-dom/src/fire/ReactFireBatching.js | 69 + .../react-dom/src/fire/ReactFireComponent.js | 635 ++++++++ .../src/fire/ReactFireComponentProperties.js | 618 ++++++++ .../react-dom/src/fire/ReactFireDOMConfig.js | 161 ++ .../react-dom/src/fire/ReactFireDevOnly.js | 1296 +++++++++++++++++ .../react-dom/src/fire/ReactFireDevTools.js | 48 + .../src/fire/ReactFireEventTraversal.js | 234 +++ .../react-dom/src/fire/ReactFireEventTypes.js | 66 + .../react-dom/src/fire/ReactFireEvents.js | 323 ++++ .../react-dom/src/fire/ReactFireHostConfig.js | 610 +++++++- .../react-dom/src/fire/ReactFireHydration.js | 144 ++ .../react-dom/src/fire/ReactFireInternal.js | 93 ++ packages/react-dom/src/fire/ReactFireRoots.js | 277 ++++ .../react-dom/src/fire/ReactFireSelection.js | 385 +++++ .../react-dom/src/fire/ReactFireStyling.js | 187 +++ packages/react-dom/src/fire/ReactFireUtils.js | 477 ++++++ .../react-dom/src/fire/ReactFireValidation.js | 537 +++++++ .../controlled/ReactFireControlledState.js | 76 + .../src/fire/controlled/ReactFireInput.js | 371 +++++ .../src/fire/controlled/ReactFireOption.js | 99 ++ .../src/fire/controlled/ReactFirePropTypes.js | 79 + .../src/fire/controlled/ReactFireSelect.js | 220 +++ .../src/fire/controlled/ReactFireTextarea.js | 153 ++ .../fire/controlled/ReactFireValueTracking.js | 141 ++ .../src/fire/polyfills/ReactFireEnterLeave.js | 121 ++ .../fire/polyfills/ReactFireOnBeforeInput.js | 489 +++++++ .../src/fire/polyfills/ReactFireOnChange.js | 76 + .../src/fire/polyfills/ReactFireOnSelect.js | 169 +++ .../polyfills/ReactFirePolyfilledEvents.js | 84 ++ .../src/test-utils/ReactTestUtils.js | 168 ++- .../src/ReactFiberScheduler.js | 11 +- packages/shared/ReactFeatureFlags.js | 2 + scripts/jest/setupFire.js | 1 + scripts/rollup/results.json | 7 + yarn.lock | 827 +++++++++++ 55 files changed, 11519 insertions(+), 2799 deletions(-) create mode 100644 packages/react-dom/src/fire/ReactFireBatching.js create mode 100644 packages/react-dom/src/fire/ReactFireComponent.js create mode 100644 packages/react-dom/src/fire/ReactFireComponentProperties.js create mode 100644 packages/react-dom/src/fire/ReactFireDOMConfig.js create mode 100644 packages/react-dom/src/fire/ReactFireDevOnly.js create mode 100644 packages/react-dom/src/fire/ReactFireDevTools.js create mode 100644 packages/react-dom/src/fire/ReactFireEventTraversal.js create mode 100644 packages/react-dom/src/fire/ReactFireEventTypes.js create mode 100644 packages/react-dom/src/fire/ReactFireEvents.js create mode 100644 packages/react-dom/src/fire/ReactFireHydration.js create mode 100644 packages/react-dom/src/fire/ReactFireInternal.js create mode 100644 packages/react-dom/src/fire/ReactFireRoots.js create mode 100644 packages/react-dom/src/fire/ReactFireSelection.js create mode 100644 packages/react-dom/src/fire/ReactFireStyling.js create mode 100644 packages/react-dom/src/fire/ReactFireUtils.js create mode 100644 packages/react-dom/src/fire/ReactFireValidation.js create mode 100644 packages/react-dom/src/fire/controlled/ReactFireControlledState.js create mode 100644 packages/react-dom/src/fire/controlled/ReactFireInput.js create mode 100644 packages/react-dom/src/fire/controlled/ReactFireOption.js create mode 100644 packages/react-dom/src/fire/controlled/ReactFirePropTypes.js create mode 100644 packages/react-dom/src/fire/controlled/ReactFireSelect.js create mode 100644 packages/react-dom/src/fire/controlled/ReactFireTextarea.js create mode 100644 packages/react-dom/src/fire/controlled/ReactFireValueTracking.js create mode 100644 packages/react-dom/src/fire/polyfills/ReactFireEnterLeave.js create mode 100644 packages/react-dom/src/fire/polyfills/ReactFireOnBeforeInput.js create mode 100644 packages/react-dom/src/fire/polyfills/ReactFireOnChange.js create mode 100644 packages/react-dom/src/fire/polyfills/ReactFireOnSelect.js create mode 100644 packages/react-dom/src/fire/polyfills/ReactFirePolyfilledEvents.js diff --git a/package.json b/package.json index eb5fe1324ba..d18fbe3ae2b 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,7 @@ "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-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__/ReactDOMFiber-test.js b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js index 864caa0f335..4394f628dfc 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js @@ -12,6 +12,7 @@ const React = require('react'); const ReactDOM = require('react-dom'); const PropTypes = require('prop-types'); +const ReactFeatureFlags = require('shared/ReactFeatureFlags'); describe('ReactDOMFiber', () => { let container; @@ -247,30 +248,32 @@ describe('ReactDOMFiber', () => { }); // TODO: remove in React 17 - it('should support unstable_createPortal alias', () => { - const portalContainer = document.createElement('div'); + if (!ReactFeatureFlags.enableReactDOMFire) { + it('should support unstable_createPortal alias', () => { + const portalContainer = document.createElement('div'); - expect(() => - ReactDOM.render( -
- {ReactDOM.unstable_createPortal(
portal
, portalContainer)} -
, - container, - ), - ).toLowPriorityWarnDev( - 'The ReactDOM.unstable_createPortal() alias has been deprecated, ' + - 'and will be removed in React 17+. Update your code to use ' + - 'ReactDOM.createPortal() instead. It has the exact same API, ' + - 'but without the "unstable_" prefix.', - {withoutStack: true}, - ); - expect(portalContainer.innerHTML).toBe('
portal
'); - expect(container.innerHTML).toBe('
'); + expect(() => + ReactDOM.render( +
+ {ReactDOM.unstable_createPortal(
portal
, portalContainer)} +
, + container, + ), + ).toLowPriorityWarnDev( + 'The ReactDOM.unstable_createPortal() alias has been deprecated, ' + + 'and will be removed in React 17+. Update your code to use ' + + 'ReactDOM.createPortal() instead. It has the exact same API, ' + + 'but without the "unstable_" prefix.', + {withoutStack: true}, + ); + expect(portalContainer.innerHTML).toBe('
portal
'); + expect(container.innerHTML).toBe('
'); - ReactDOM.unmountComponentAtNode(container); - expect(portalContainer.innerHTML).toBe(''); - expect(container.innerHTML).toBe(''); - }); + ReactDOM.unmountComponentAtNode(container); + expect(portalContainer.innerHTML).toBe(''); + expect(container.innerHTML).toBe(''); + }); + } it('should render many portals', () => { const portalContainer1 = document.createElement('div'); diff --git a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js index 32903540eba..dbd1c581f69 100644 --- a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js +++ b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js @@ -12,6 +12,7 @@ let React; let ReactDOM; let ReactDOMServer; +const ReactFeatureFlags = require('shared/ReactFeatureFlags'); function getTestDocument(markup) { const doc = document.implementation.createHTMLDocument(''); @@ -33,224 +34,227 @@ describe('rendering React components at document', () => { ReactDOMServer = require('react-dom/server'); }); - describe('with old implicit hydration API', () => { - function expectDeprecationWarningWithFiber(callback) { - expect(callback).toLowPriorityWarnDev( - '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.', - {withoutStack: true}, - ); - } + // React Fire does not support the implicit API + if (!ReactFeatureFlags.enableReactDOMFire) { + describe('with old implicit hydration API', () => { + function expectDeprecationWarningWithFiber(callback) { + expect(callback).toLowPriorityWarnDev( + '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.', + {withoutStack: true}, + ); + } - it('should be able to adopt server markup', () => { - class Root extends React.Component { - render() { - return ( - - - Hello World - - {'Hello ' + this.props.hello} - - ); + it('should be able to adopt server markup', () => { + class Root extends React.Component { + render() { + return ( + + + Hello World + + {'Hello ' + this.props.hello} + + ); + } } - } - const markup = ReactDOMServer.renderToString(); - const testDocument = getTestDocument(markup); - const body = testDocument.body; + const markup = ReactDOMServer.renderToString(); + const testDocument = getTestDocument(markup); + const body = testDocument.body; - expectDeprecationWarningWithFiber(() => - ReactDOM.render(, testDocument), - ); - expect(testDocument.body.innerHTML).toBe('Hello world'); + expectDeprecationWarningWithFiber(() => + ReactDOM.render(, testDocument), + ); + expect(testDocument.body.innerHTML).toBe('Hello world'); - ReactDOM.render(, testDocument); - expect(testDocument.body.innerHTML).toBe('Hello moon'); + ReactDOM.render(, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello moon'); - expect(body === testDocument.body).toBe(true); - }); + expect(body === testDocument.body).toBe(true); + }); - it('should not be able to unmount component from document node', () => { - class Root extends React.Component { - render() { - return ( - - - Hello World - - Hello world - - ); + it('should not be able to unmount component from document node', () => { + class Root extends React.Component { + render() { + return ( + + + Hello World + + Hello world + + ); + } } - } - const markup = ReactDOMServer.renderToString(); - const testDocument = getTestDocument(markup); - expectDeprecationWarningWithFiber(() => - ReactDOM.render(, testDocument), - ); - expect(testDocument.body.innerHTML).toBe('Hello world'); + const markup = ReactDOMServer.renderToString(); + const testDocument = getTestDocument(markup); + expectDeprecationWarningWithFiber(() => + ReactDOM.render(, testDocument), + ); + expect(testDocument.body.innerHTML).toBe('Hello world'); - // In Fiber this actually works. It might not be a good idea though. - ReactDOM.unmountComponentAtNode(testDocument); - expect(testDocument.firstChild).toBe(null); - }); + // In Fiber this actually works. It might not be a good idea though. + ReactDOM.unmountComponentAtNode(testDocument); + expect(testDocument.firstChild).toBe(null); + }); - it('should not be able to switch root constructors', () => { - class Component extends React.Component { - render() { - return ( - - - Hello World - - Hello world - - ); + it('should not be able to switch root constructors', () => { + class Component extends React.Component { + render() { + return ( + + + Hello World + + Hello world + + ); + } } - } - class Component2 extends React.Component { - render() { - return ( - - - Hello World - - Goodbye world - - ); + class Component2 extends React.Component { + render() { + return ( + + + Hello World + + Goodbye world + + ); + } } - } - const markup = ReactDOMServer.renderToString(); - const testDocument = getTestDocument(markup); + const markup = ReactDOMServer.renderToString(); + const testDocument = getTestDocument(markup); - expectDeprecationWarningWithFiber(() => - ReactDOM.render(, testDocument), - ); - expect(testDocument.body.innerHTML).toBe('Hello world'); + expectDeprecationWarningWithFiber(() => + ReactDOM.render(, testDocument), + ); + expect(testDocument.body.innerHTML).toBe('Hello world'); - // This works but is probably a bad idea. - ReactDOM.render(, testDocument); + // This works but is probably a bad idea. + ReactDOM.render(, testDocument); - expect(testDocument.body.innerHTML).toBe('Goodbye world'); - }); + expect(testDocument.body.innerHTML).toBe('Goodbye world'); + }); - it('should be able to mount into document', () => { - class Component extends React.Component { - render() { - return ( - - - Hello World - - {this.props.text} - - ); + it('should be able to mount into document', () => { + class Component extends React.Component { + render() { + return ( + + + Hello World + + {this.props.text} + + ); + } } - } - const markup = ReactDOMServer.renderToString( - , - ); - const testDocument = getTestDocument(markup); + const markup = ReactDOMServer.renderToString( + , + ); + const testDocument = getTestDocument(markup); - expectDeprecationWarningWithFiber(() => - ReactDOM.render(, testDocument), - ); + expectDeprecationWarningWithFiber(() => + ReactDOM.render(, testDocument), + ); - expect(testDocument.body.innerHTML).toBe('Hello world'); - }); + expect(testDocument.body.innerHTML).toBe('Hello world'); + }); - it('renders over an existing text child without throwing', () => { - const container = document.createElement('div'); - container.textContent = 'potato'; - ReactDOM.render(
parsnip
, container); - expect(container.textContent).toBe('parsnip'); - // We don't expect a warning about new hydration API here because - // we aren't sure if the user meant to hydrate or replace a stub node. - // We would see a warning if the container had React-rendered HTML in it. - }); + it('renders over an existing text child without throwing', () => { + const container = document.createElement('div'); + container.textContent = 'potato'; + ReactDOM.render(
parsnip
, container); + expect(container.textContent).toBe('parsnip'); + // We don't expect a warning about new hydration API here because + // we aren't sure if the user meant to hydrate or replace a stub node. + // We would see a warning if the container had React-rendered HTML in it. + }); - it('should give helpful errors on state desync', () => { - class Component extends React.Component { - render() { - return ( - - - Hello World - - {this.props.text} - - ); + it('should give helpful errors on state desync', () => { + class Component extends React.Component { + render() { + return ( + + + Hello World + + {this.props.text} + + ); + } } - } - - const markup = ReactDOMServer.renderToString( - , - ); - const testDocument = getTestDocument(markup); - expect(() => { - expect(() => - ReactDOM.render(, testDocument), - ).toLowPriorityWarnDev( - '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.', - {withoutStack: true}, + const markup = ReactDOMServer.renderToString( + , ); - }).toWarnDev('Warning: Text content did not match.', { - withoutStack: true, + const testDocument = getTestDocument(markup); + + expect(() => { + expect(() => + ReactDOM.render(, testDocument), + ).toLowPriorityWarnDev( + '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.', + {withoutStack: true}, + ); + }).toWarnDev('Warning: Text content did not match.', { + withoutStack: true, + }); }); - }); - it('should throw on full document render w/ no markup', () => { - const testDocument = getTestDocument(); - - class Component extends React.Component { - render() { - return ( - - - Hello World - - {this.props.text} - - ); + it('should throw on full document render w/ no markup', () => { + const testDocument = getTestDocument(); + + class Component extends React.Component { + render() { + return ( + + + Hello World + + {this.props.text} + + ); + } } - } - ReactDOM.render(, testDocument); - expect(testDocument.body.innerHTML).toBe('Hello world'); - // We don't expect a warning about new hydration API here because - // we aren't sure if the user meant to hydrate or replace the document. - // We would see a warning if the document had React-rendered HTML in it. - }); + ReactDOM.render(, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello world'); + // We don't expect a warning about new hydration API here because + // we aren't sure if the user meant to hydrate or replace the document. + // We would see a warning if the document had React-rendered HTML in it. + }); - it('supports findDOMNode on full-page components', () => { - const tree = ( - - - Hello World - - Hello world - - ); + it('supports findDOMNode on full-page components', () => { + const tree = ( + + + Hello World + + Hello world + + ); - const markup = ReactDOMServer.renderToString(tree); - const testDocument = getTestDocument(markup); - let component; - expectDeprecationWarningWithFiber(() => { - component = ReactDOM.render(tree, testDocument); + const markup = ReactDOMServer.renderToString(tree); + const testDocument = getTestDocument(markup); + let component; + expectDeprecationWarningWithFiber(() => { + component = ReactDOM.render(tree, testDocument); + }); + expect(testDocument.body.innerHTML).toBe('Hello world'); + expect(ReactDOM.findDOMNode(component).tagName).toBe('HTML'); }); - expect(testDocument.body.innerHTML).toBe('Hello world'); - expect(ReactDOM.findDOMNode(component).tagName).toBe('HTML'); }); - }); + } describe('with new explicit hydration API', () => { it('should be able to adopt server markup', () => { diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index 2080488a244..f34ad6bb835 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -12,6 +12,7 @@ let React; let ReactDOM; let ReactDOMServer; +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); // These tests rely both on ReactDOMServer and ReactDOM. // If a test only needs ReactDOMServer, put it in ReactServerRendering-test instead. @@ -23,100 +24,103 @@ describe('ReactDOMServerHydration', () => { ReactDOMServer = require('react-dom/server'); }); - it('should have the correct mounting behavior (old hydrate API)', () => { - let mountCount = 0; - let numClicks = 0; - - class TestComponent extends React.Component { - componentDidMount() { - mountCount++; + // The old hydrate API was removed in React Fire + if (!ReactFeatureFlags.enableReactDOMFire) { + it('should have the correct mounting behavior (old hydrate API)', () => { + let mountCount = 0; + let numClicks = 0; + + class TestComponent extends React.Component { + componentDidMount() { + mountCount++; + } + + click = () => { + numClicks++; + }; + + render() { + return ( + + Name: {this.props.name} + + ); + } } - click = () => { - numClicks++; - }; - - render() { - return ( - - Name: {this.props.name} - + const element = document.createElement('div'); + document.body.appendChild(element); + try { + ReactDOM.render(, element); + + let lastMarkup = element.innerHTML; + + // Exercise the update path. Markup should not change, + // but some lifecycle methods should be run again. + ReactDOM.render(, element); + expect(mountCount).toEqual(1); + + // Unmount and remount. We should get another mount event and + // we should get different markup, as the IDs are unique each time. + ReactDOM.unmountComponentAtNode(element); + expect(element.innerHTML).toEqual(''); + ReactDOM.render(, element); + expect(mountCount).toEqual(2); + expect(element.innerHTML).not.toEqual(lastMarkup); + + // Now kill the node and render it on top of server-rendered markup, as if + // we used server rendering. We should mount again, but the markup should + // be unchanged. We will append a sentinel at the end of innerHTML to be + // sure that innerHTML was not changed. + ReactDOM.unmountComponentAtNode(element); + expect(element.innerHTML).toEqual(''); + + lastMarkup = ReactDOMServer.renderToString(); + element.innerHTML = lastMarkup; + + let instance; + + expect(() => { + instance = ReactDOM.render(, element); + }).toLowPriorityWarnDev( + '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.', + {withoutStack: true}, ); - } - } - - const element = document.createElement('div'); - document.body.appendChild(element); - try { - ReactDOM.render(, element); - - let lastMarkup = element.innerHTML; - - // Exercise the update path. Markup should not change, - // but some lifecycle methods should be run again. - ReactDOM.render(, element); - expect(mountCount).toEqual(1); - - // Unmount and remount. We should get another mount event and - // we should get different markup, as the IDs are unique each time. - ReactDOM.unmountComponentAtNode(element); - expect(element.innerHTML).toEqual(''); - ReactDOM.render(, element); - expect(mountCount).toEqual(2); - expect(element.innerHTML).not.toEqual(lastMarkup); - - // Now kill the node and render it on top of server-rendered markup, as if - // we used server rendering. We should mount again, but the markup should - // be unchanged. We will append a sentinel at the end of innerHTML to be - // sure that innerHTML was not changed. - ReactDOM.unmountComponentAtNode(element); - expect(element.innerHTML).toEqual(''); + expect(mountCount).toEqual(3); + expect(element.innerHTML).toBe(lastMarkup); - lastMarkup = ReactDOMServer.renderToString(); - element.innerHTML = lastMarkup; + // Ensure the events system works after mount into server markup + expect(numClicks).toEqual(0); - let instance; + instance.refs.span.click(); + expect(numClicks).toEqual(1); - expect(() => { - instance = ReactDOM.render(, element); - }).toLowPriorityWarnDev( - '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.', - {withoutStack: true}, - ); - expect(mountCount).toEqual(3); - expect(element.innerHTML).toBe(lastMarkup); + ReactDOM.unmountComponentAtNode(element); + expect(element.innerHTML).toEqual(''); - // Ensure the events system works after mount into server markup - expect(numClicks).toEqual(0); - - instance.refs.span.click(); - expect(numClicks).toEqual(1); - - ReactDOM.unmountComponentAtNode(element); - expect(element.innerHTML).toEqual(''); - - // Now simulate a situation where the app is not idempotent. React should - // warn but do the right thing. - element.innerHTML = lastMarkup; - expect(() => { - instance = ReactDOM.render(, element); - }).toWarnDev('Text content did not match. Server: "x" Client: "y"', { - withoutStack: true, - }); - expect(mountCount).toEqual(4); - expect(element.innerHTML.length > 0).toBe(true); - expect(element.innerHTML).not.toEqual(lastMarkup); - - // Ensure the events system works after markup mismatch. - expect(numClicks).toEqual(1); - instance.refs.span.click(); - expect(numClicks).toEqual(2); - } finally { - document.body.removeChild(element); - } - }); + // Now simulate a situation where the app is not idempotent. React should + // warn but do the right thing. + element.innerHTML = lastMarkup; + expect(() => { + instance = ReactDOM.render(, element); + }).toWarnDev('Text content did not match. Server: "x" Client: "y"', { + withoutStack: true, + }); + expect(mountCount).toEqual(4); + expect(element.innerHTML.length > 0).toBe(true); + expect(element.innerHTML).not.toEqual(lastMarkup); + + // Ensure the events system works after markup mismatch. + expect(numClicks).toEqual(1); + instance.refs.span.click(); + expect(numClicks).toEqual(2); + } finally { + document.body.removeChild(element); + } + }); + } it('should have the correct mounting behavior (new hydrate API)', () => { let mountCount = 0; diff --git a/packages/react-dom/src/__tests__/ReactTestUtils-test.js b/packages/react-dom/src/__tests__/ReactTestUtils-test.js index 687dbd1aaec..d7222acbec1 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtils-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtils-test.js @@ -14,6 +14,7 @@ let React; let ReactDOM; let ReactDOMServer; let ReactTestUtils; +const ReactFeatureFlags = require('shared/ReactFeatureFlags'); function getTestDocument(markup) { const doc = document.implementation.createHTMLDocument(''); @@ -471,7 +472,9 @@ describe('ReactTestUtils', () => { ReactTestUtils.Simulate.keyDown(node); expect(event.type).toBe('keydown'); - expect(event.nativeEvent.type).toBe('keydown'); + if (!ReactFeatureFlags.enableReactDOMFire) { + expect(event.nativeEvent.type).toBe('keydown'); + } }); it('should work with renderIntoDocument', () => { diff --git a/packages/react-dom/src/__tests__/renderSubtreeIntoContainer-test.js b/packages/react-dom/src/__tests__/renderSubtreeIntoContainer-test.js index 881c7b15d79..0c709e1683e 100644 --- a/packages/react-dom/src/__tests__/renderSubtreeIntoContainer-test.js +++ b/packages/react-dom/src/__tests__/renderSubtreeIntoContainer-test.js @@ -15,309 +15,315 @@ const ReactDOM = require('react-dom'); const ReactTestUtils = require('react-dom/test-utils'); const renderSubtreeIntoContainer = require('react-dom') .unstable_renderSubtreeIntoContainer; +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); + +// renderSubtreeIntoContainer was removed in React Fire +if (ReactFeatureFlags.enableReactDOMFire) { + it('Empty test', () => {}); +} else { + describe('renderSubtreeIntoContainer', () => { + it('should pass context when rendering subtree elsewhere', () => { + const portal = document.createElement('div'); + + class Component extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + }; -describe('renderSubtreeIntoContainer', () => { - it('should pass context when rendering subtree elsewhere', () => { - const portal = document.createElement('div'); - - class Component extends React.Component { - static contextTypes = { - foo: PropTypes.string.isRequired, - }; - - render() { - return
{this.context.foo}
; + render() { + return
{this.context.foo}
; + } } - } - - class Parent extends React.Component { - static childContextTypes = { - foo: PropTypes.string.isRequired, - }; - getChildContext() { - return { - foo: 'bar', + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, }; - } - render() { - return null; - } + getChildContext() { + return { + foo: 'bar', + }; + } - componentDidMount() { - expect( - function() { - renderSubtreeIntoContainer(this, , portal); - }.bind(this), - ).not.toThrow(); - } - } + render() { + return null; + } - ReactTestUtils.renderIntoDocument(); - expect(portal.firstChild.innerHTML).toBe('bar'); - }); + componentDidMount() { + expect( + function() { + renderSubtreeIntoContainer(this, , portal); + }.bind(this), + ).not.toThrow(); + } + } - it('should throw if parentComponent is invalid', () => { - const portal = document.createElement('div'); + ReactTestUtils.renderIntoDocument(); + expect(portal.firstChild.innerHTML).toBe('bar'); + }); - class Component extends React.Component { - static contextTypes = { - foo: PropTypes.string.isRequired, - }; + it('should throw if parentComponent is invalid', () => { + const portal = document.createElement('div'); - render() { - return
{this.context.foo}
; - } - } - - // ESLint is confused here and thinks Parent is unused, presumably because - // it is only used inside of the class body? - // eslint-disable-next-line no-unused-vars - class Parent extends React.Component { - static childContextTypes = { - foo: PropTypes.string.isRequired, - }; - - getChildContext() { - return { - foo: 'bar', + class Component extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, }; - } - render() { - return null; + render() { + return
{this.context.foo}
; + } } - componentDidMount() { - expect(function() { - renderSubtreeIntoContainer(, , portal); - }).toThrowError('parentComponentmust be a valid React Component'); - } - } - }); - - it('should update context if it changes due to setState', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const portal = document.createElement('div'); - - class Component extends React.Component { - static contextTypes = { - foo: PropTypes.string.isRequired, - getFoo: PropTypes.func.isRequired, - }; - - render() { - return
{this.context.foo + '-' + this.context.getFoo()}
; - } - } - - class Parent extends React.Component { - static childContextTypes = { - foo: PropTypes.string.isRequired, - getFoo: PropTypes.func.isRequired, - }; - - state = { - bar: 'initial', - }; - - getChildContext() { - return { - foo: this.state.bar, - getFoo: () => this.state.bar, + // ESLint is confused here and thinks Parent is unused, presumably because + // it is only used inside of the class body? + // eslint-disable-next-line no-unused-vars + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, }; - } - render() { - return null; - } - - componentDidMount() { - renderSubtreeIntoContainer(this, , portal); - } + getChildContext() { + return { + foo: 'bar', + }; + } + + render() { + return null; + } + + componentDidMount() { + expect(function() { + renderSubtreeIntoContainer(, , portal); + }).toThrowError('parentComponentmust be a valid React Component'); + } + } + }); + + it('should update context if it changes due to setState', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const portal = document.createElement('div'); + + class Component extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + getFoo: PropTypes.func.isRequired, + }; - componentDidUpdate() { - renderSubtreeIntoContainer(this, , portal); + render() { + return
{this.context.foo + '-' + this.context.getFoo()}
; + } } - } - const instance = ReactDOM.render(, container); - expect(portal.firstChild.innerHTML).toBe('initial-initial'); - instance.setState({bar: 'changed'}); - expect(portal.firstChild.innerHTML).toBe('changed-changed'); - }); - - it('should update context if it changes due to re-render', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const portal = document.createElement('div'); - - class Component extends React.Component { - static contextTypes = { - foo: PropTypes.string.isRequired, - getFoo: PropTypes.func.isRequired, - }; - - render() { - return
{this.context.foo + '-' + this.context.getFoo()}
; - } - } - - class Parent extends React.Component { - static childContextTypes = { - foo: PropTypes.string.isRequired, - getFoo: PropTypes.func.isRequired, - }; - - getChildContext() { - return { - foo: this.props.bar, - getFoo: () => this.props.bar, + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, + getFoo: PropTypes.func.isRequired, }; - } - render() { - return null; - } + state = { + bar: 'initial', + }; - componentDidMount() { - renderSubtreeIntoContainer(this, , portal); - } + getChildContext() { + return { + foo: this.state.bar, + getFoo: () => this.state.bar, + }; + } + + render() { + return null; + } + + componentDidMount() { + renderSubtreeIntoContainer(this, , portal); + } + + componentDidUpdate() { + renderSubtreeIntoContainer(this, , portal); + } + } + + const instance = ReactDOM.render(, container); + expect(portal.firstChild.innerHTML).toBe('initial-initial'); + instance.setState({bar: 'changed'}); + expect(portal.firstChild.innerHTML).toBe('changed-changed'); + }); + + it('should update context if it changes due to re-render', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const portal = document.createElement('div'); + + class Component extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + getFoo: PropTypes.func.isRequired, + }; - componentDidUpdate() { - renderSubtreeIntoContainer(this, , portal); + render() { + return
{this.context.foo + '-' + this.context.getFoo()}
; + } } - } - ReactDOM.render(, container); - expect(portal.firstChild.innerHTML).toBe('initial-initial'); - ReactDOM.render(, container); - expect(portal.firstChild.innerHTML).toBe('changed-changed'); - }); - - it('should render portal with non-context-provider parent', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const portal = document.createElement('div'); + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, + getFoo: PropTypes.func.isRequired, + }; - class Parent extends React.Component { - render() { - return null; + getChildContext() { + return { + foo: this.props.bar, + getFoo: () => this.props.bar, + }; + } + + render() { + return null; + } + + componentDidMount() { + renderSubtreeIntoContainer(this, , portal); + } + + componentDidUpdate() { + renderSubtreeIntoContainer(this, , portal); + } + } + + ReactDOM.render(, container); + expect(portal.firstChild.innerHTML).toBe('initial-initial'); + ReactDOM.render(, container); + expect(portal.firstChild.innerHTML).toBe('changed-changed'); + }); + + it('should render portal with non-context-provider parent', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const portal = document.createElement('div'); + + class Parent extends React.Component { + render() { + return null; + } + + componentDidMount() { + renderSubtreeIntoContainer(this,
hello
, portal); + } + } + + ReactDOM.render(, container); + expect(portal.firstChild.innerHTML).toBe('hello'); + }); + + it('should get context through non-context-provider parent', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const portal = document.createElement('div'); + + class Parent extends React.Component { + render() { + return ; + } + getChildContext() { + return {value: this.props.value}; + } + static childContextTypes = { + value: PropTypes.string.isRequired, + }; } - componentDidMount() { - renderSubtreeIntoContainer(this,
hello
, portal); + class Middle extends React.Component { + render() { + return null; + } + componentDidMount() { + renderSubtreeIntoContainer(this, , portal); + } } - } - ReactDOM.render(, container); - expect(portal.firstChild.innerHTML).toBe('hello'); - }); - - it('should get context through non-context-provider parent', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const portal = document.createElement('div'); - - class Parent extends React.Component { - render() { - return ; - } - getChildContext() { - return {value: this.props.value}; - } - static childContextTypes = { - value: PropTypes.string.isRequired, - }; - } - - class Middle extends React.Component { - render() { - return null; - } - componentDidMount() { - renderSubtreeIntoContainer(this, , portal); - } - } - - class Child extends React.Component { - static contextTypes = { - value: PropTypes.string.isRequired, - }; - render() { - return
{this.context.value}
; + class Child extends React.Component { + static contextTypes = { + value: PropTypes.string.isRequired, + }; + render() { + return
{this.context.value}
; + } + } + + ReactDOM.render(, container); + expect(portal.textContent).toBe('foo'); + }); + + it('should get context through middle non-context-provider layer', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const portal1 = document.createElement('div'); + const portal2 = document.createElement('div'); + + class Parent extends React.Component { + render() { + return null; + } + getChildContext() { + return {value: this.props.value}; + } + componentDidMount() { + renderSubtreeIntoContainer(this, , portal1); + } + static childContextTypes = { + value: PropTypes.string.isRequired, + }; } - } - - ReactDOM.render(, container); - expect(portal.textContent).toBe('foo'); - }); - it('should get context through middle non-context-provider layer', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const portal1 = document.createElement('div'); - const portal2 = document.createElement('div'); - - class Parent extends React.Component { - render() { - return null; - } - getChildContext() { - return {value: this.props.value}; - } - componentDidMount() { - renderSubtreeIntoContainer(this, , portal1); + class Middle extends React.Component { + render() { + return null; + } + componentDidMount() { + renderSubtreeIntoContainer(this, , portal2); + } } - static childContextTypes = { - value: PropTypes.string.isRequired, - }; - } - - class Middle extends React.Component { - render() { - return null; - } - componentDidMount() { - renderSubtreeIntoContainer(this, , portal2); - } - } - - class Child extends React.Component { - static contextTypes = { - value: PropTypes.string.isRequired, - }; - render() { - return
{this.context.value}
; - } - } - ReactDOM.render(, container); - expect(portal2.textContent).toBe('foo'); - }); - - it('fails gracefully when mixing React 15 and 16', () => { - class C extends React.Component { - render() { - return
; - } - } - const c = ReactDOM.render(, document.createElement('div')); - // React 15 calls this: - // https://github.com/facebook/react/blob/77b71fc3c4/src/renderers/dom/client/ReactMount.js#L478-L479 - expect(() => { - c._reactInternalInstance._processChildContext({}); - }).toThrow( - __DEV__ - ? '_processChildContext is not available in React 16+. This likely ' + - 'means you have multiple copies of React and are attempting to nest ' + - 'a React 15 tree inside a React 16 tree using ' + - "unstable_renderSubtreeIntoContainer, which isn't supported. Try to " + - 'make sure you have only one copy of React (and ideally, switch to ' + - 'ReactDOM.createPortal).' - : "Cannot read property '_processChildContext' of undefined", - ); + class Child extends React.Component { + static contextTypes = { + value: PropTypes.string.isRequired, + }; + render() { + return
{this.context.value}
; + } + } + + ReactDOM.render(, container); + expect(portal2.textContent).toBe('foo'); + }); + + it('fails gracefully when mixing React 15 and 16', () => { + class C extends React.Component { + render() { + return
; + } + } + const c = ReactDOM.render(, document.createElement('div')); + // React 15 calls this: + // https://github.com/facebook/react/blob/77b71fc3c4/src/renderers/dom/client/ReactMount.js#L478-L479 + expect(() => { + c._reactInternalInstance._processChildContext({}); + }).toThrow( + __DEV__ + ? '_processChildContext is not available in React 16+. This likely ' + + 'means you have multiple copies of React and are attempting to nest ' + + 'a React 15 tree inside a React 16 tree using ' + + "unstable_renderSubtreeIntoContainer, which isn't supported. Try to " + + 'make sure you have only one copy of React (and ideally, switch to ' + + 'ReactDOM.createPortal).' + : "Cannot read property '_processChildContext' of undefined", + ); + }); }); -}); +} diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index ded53f4e1c5..8934a67be37 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -770,7 +770,6 @@ const ReactDOM: Object = { flushSync: flushSync, - unstable_createRoot: createRoot, unstable_flushControlled: flushControlled, __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { @@ -811,7 +810,8 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot { if (enableStableConcurrentModeAPIs) { ReactDOM.createRoot = createRoot; - ReactDOM.unstable_createRoot = undefined; +} else { + ReactDOM.unstable_createRoot = createRoot; } const foundDevTools = injectIntoDevTools({ 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__/ChangeEventPlugin-test.internal.js b/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js index 03d2cddeaaf..13b24b19b07 100644 --- a/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js +++ b/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js @@ -11,7 +11,7 @@ const React = require('react'); let ReactDOM = require('react-dom'); -let ReactFeatureFlags; +const ReactFeatureFlags = require('shared/ReactFeatureFlags'); const setUntrackedChecked = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, @@ -28,11 +28,11 @@ const setUntrackedTextareaValue = Object.getOwnPropertyDescriptor( 'value', ).set; +// Fire has a polyfill for this plugin describe('ChangeEventPlugin', () => { let container; beforeEach(() => { - ReactFeatureFlags = require('shared/ReactFeatureFlags'); // TODO pull this into helper method, reduce repetition. // mock the browser APIs which are used in schedule: // - requestAnimationFrame should pass the DOMHighResTimeStamp argument @@ -481,9 +481,11 @@ describe('ChangeEventPlugin', () => { describe('async mode', () => { beforeEach(() => { jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; - ReactDOM = require('react-dom'); + if (!ReactFeatureFlags.enableReactDOMFire) { + // This breaks with the mocked ReactDOMFire + ReactDOM = require('react-dom'); + } }); it('text input', () => { const root = ReactDOM.unstable_createRoot(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/events/__tests__/SyntheticClipboardEvent-test.js b/packages/react-dom/src/events/__tests__/SyntheticClipboardEvent-test.js index 6bff9d7f40d..0060d3e517b 100644 --- a/packages/react-dom/src/events/__tests__/SyntheticClipboardEvent-test.js +++ b/packages/react-dom/src/events/__tests__/SyntheticClipboardEvent-test.js @@ -11,147 +11,153 @@ let React; let ReactDOM; +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); + +// ClipboardEvent is no longer polyfilled synthetically in React Fire +if (ReactFeatureFlags.enableReactDOMFire) { + it('Empty test', () => {}); +} else { + describe('SyntheticClipboardEvent', () => { + let container; + + beforeEach(() => { + React = require('react'); + ReactDOM = require('react-dom'); + + // The container has to be attached for events to fire. + container = document.createElement('div'); + document.body.appendChild(container); + }); -describe('SyntheticClipboardEvent', () => { - let container; - - beforeEach(() => { - React = require('react'); - ReactDOM = require('react-dom'); - - // The container has to be attached for events to fire. - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - document.body.removeChild(container); - container = null; - }); + afterEach(() => { + document.body.removeChild(container); + container = null; + }); - describe('ClipboardEvent interface', () => { - describe('clipboardData', () => { - describe('when event has clipboardData', () => { - it("returns event's clipboardData", () => { - let expectedCount = 0; - - // Mock clipboardData since jsdom implementation doesn't have a constructor - const clipboardData = { - dropEffect: null, - effectAllowed: null, - files: null, - items: null, - types: null, - }; - const eventHandler = event => { - expect(event.clipboardData).toBe(clipboardData); - expectedCount++; - }; - const div = ReactDOM.render( -
, - container, - ); - - let event; - event = document.createEvent('Event'); - event.initEvent('copy', true, true); - event.clipboardData = clipboardData; - div.dispatchEvent(event); - - event = document.createEvent('Event'); - event.initEvent('cut', true, true); - event.clipboardData = clipboardData; - div.dispatchEvent(event); - - event = document.createEvent('Event'); - event.initEvent('paste', true, true); - event.clipboardData = clipboardData; - div.dispatchEvent(event); - - expect(expectedCount).toBe(3); + describe('ClipboardEvent interface', () => { + describe('clipboardData', () => { + describe('when event has clipboardData', () => { + it("returns event's clipboardData", () => { + let expectedCount = 0; + + // Mock clipboardData since jsdom implementation doesn't have a constructor + const clipboardData = { + dropEffect: null, + effectAllowed: null, + files: null, + items: null, + types: null, + }; + const eventHandler = event => { + expect(event.clipboardData).toBe(clipboardData); + expectedCount++; + }; + const div = ReactDOM.render( +
, + container, + ); + + let event; + event = document.createEvent('Event'); + event.initEvent('copy', true, true); + event.clipboardData = clipboardData; + div.dispatchEvent(event); + + event = document.createEvent('Event'); + event.initEvent('cut', true, true); + event.clipboardData = clipboardData; + div.dispatchEvent(event); + + event = document.createEvent('Event'); + event.initEvent('paste', true, true); + event.clipboardData = clipboardData; + div.dispatchEvent(event); + + expect(expectedCount).toBe(3); + }); }); }); }); - }); - describe('EventInterface', () => { - it('is able to `preventDefault` and `stopPropagation`', () => { - let expectedCount = 0; - - const eventHandler = event => { - expect(event.isDefaultPrevented()).toBe(false); - event.preventDefault(); - expect(event.isDefaultPrevented()).toBe(true); - expect(event.isPropagationStopped()).toBe(false); - event.stopPropagation(); - expect(event.isPropagationStopped()).toBe(true); - expectedCount++; - }; - - const div = ReactDOM.render( -
, - container, - ); - - let event; - event = document.createEvent('Event'); - event.initEvent('copy', true, true); - div.dispatchEvent(event); - - event = document.createEvent('Event'); - event.initEvent('cut', true, true); - div.dispatchEvent(event); - - event = document.createEvent('Event'); - event.initEvent('paste', true, true); - div.dispatchEvent(event); - - expect(expectedCount).toBe(3); - }); + describe('EventInterface', () => { + it('is able to `preventDefault` and `stopPropagation`', () => { + let expectedCount = 0; + + const eventHandler = event => { + expect(event.isDefaultPrevented()).toBe(false); + event.preventDefault(); + expect(event.isDefaultPrevented()).toBe(true); + expect(event.isPropagationStopped()).toBe(false); + event.stopPropagation(); + expect(event.isPropagationStopped()).toBe(true); + expectedCount++; + }; + + const div = ReactDOM.render( +
, + container, + ); + + let event; + event = document.createEvent('Event'); + event.initEvent('copy', true, true); + div.dispatchEvent(event); + + event = document.createEvent('Event'); + event.initEvent('cut', true, true); + div.dispatchEvent(event); + + event = document.createEvent('Event'); + event.initEvent('paste', true, true); + div.dispatchEvent(event); + + expect(expectedCount).toBe(3); + }); - it('is able to `persist`', () => { - const persistentEvents = []; - const eventHandler = event => { - expect(event.isPersistent()).toBe(false); - event.persist(); - expect(event.isPersistent()).toBe(true); - persistentEvents.push(event); - }; - - const div = ReactDOM.render( -
, - container, - ); - - let event; - event = document.createEvent('Event'); - event.initEvent('copy', true, true); - div.dispatchEvent(event); - - event = document.createEvent('Event'); - event.initEvent('cut', true, true); - div.dispatchEvent(event); - - event = document.createEvent('Event'); - event.initEvent('paste', true, true); - div.dispatchEvent(event); - - expect(persistentEvents.length).toBe(3); - expect(persistentEvents[0].type).toBe('copy'); - expect(persistentEvents[1].type).toBe('cut'); - expect(persistentEvents[2].type).toBe('paste'); + it('is able to `persist`', () => { + const persistentEvents = []; + const eventHandler = event => { + expect(event.isPersistent()).toBe(false); + event.persist(); + expect(event.isPersistent()).toBe(true); + persistentEvents.push(event); + }; + + const div = ReactDOM.render( +
, + container, + ); + + let event; + event = document.createEvent('Event'); + event.initEvent('copy', true, true); + div.dispatchEvent(event); + + event = document.createEvent('Event'); + event.initEvent('cut', true, true); + div.dispatchEvent(event); + + event = document.createEvent('Event'); + event.initEvent('paste', true, true); + div.dispatchEvent(event); + + expect(persistentEvents.length).toBe(3); + expect(persistentEvents[0].type).toBe('copy'); + expect(persistentEvents[1].type).toBe('cut'); + expect(persistentEvents[2].type).toBe('paste'); + }); }); }); -}); +} diff --git a/packages/react-dom/src/events/__tests__/SyntheticEvent-test.js b/packages/react-dom/src/events/__tests__/SyntheticEvent-test.js index 906ae127cf4..5c1d196ffa2 100644 --- a/packages/react-dom/src/events/__tests__/SyntheticEvent-test.js +++ b/packages/react-dom/src/events/__tests__/SyntheticEvent-test.js @@ -11,351 +11,357 @@ let React; let ReactDOM; +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); -describe('SyntheticEvent', () => { - let container; +// SyntheticEvent is no longer supported in React Fire +if (ReactFeatureFlags.enableReactDOMFire) { + it('Empty test', () => {}); +} else { + describe('SyntheticEvent', () => { + let container; - beforeEach(() => { - React = require('react'); - ReactDOM = require('react-dom'); + beforeEach(() => { + React = require('react'); + ReactDOM = require('react-dom'); - container = document.createElement('div'); - document.body.appendChild(container); - }); + container = document.createElement('div'); + document.body.appendChild(container); + }); - afterEach(() => { - document.body.removeChild(container); - container = null; - }); + afterEach(() => { + document.body.removeChild(container); + container = null; + }); - it('should be able to `preventDefault`', () => { - let node; - let expectedCount = 0; + it('should be able to `preventDefault`', () => { + let node; + let expectedCount = 0; - const eventHandler = syntheticEvent => { - expect(syntheticEvent.isDefaultPrevented()).toBe(false); - syntheticEvent.preventDefault(); - expect(syntheticEvent.isDefaultPrevented()).toBe(true); - expect(syntheticEvent.defaultPrevented).toBe(true); + const eventHandler = syntheticEvent => { + expect(syntheticEvent.isDefaultPrevented()).toBe(false); + syntheticEvent.preventDefault(); + expect(syntheticEvent.isDefaultPrevented()).toBe(true); + expect(syntheticEvent.defaultPrevented).toBe(true); - expectedCount++; - }; - node = ReactDOM.render(
, container); + expectedCount++; + }; + node = ReactDOM.render(
, container); - const event = document.createEvent('Event'); - event.initEvent('click', true, true); - node.dispatchEvent(event); + const event = document.createEvent('Event'); + event.initEvent('click', true, true); + node.dispatchEvent(event); - expect(expectedCount).toBe(1); - }); + expect(expectedCount).toBe(1); + }); + + it('should be prevented if nativeEvent is prevented', () => { + let node; + let expectedCount = 0; + + const eventHandler = syntheticEvent => { + expect(syntheticEvent.isDefaultPrevented()).toBe(true); + + expectedCount++; + }; + node = ReactDOM.render(
, container); + + let event; + event = document.createEvent('Event'); + event.initEvent('click', true, true); + event.preventDefault(); + node.dispatchEvent(event); + + event = document.createEvent('Event'); + event.initEvent('click', true, true); + // Emulate IE8 + Object.defineProperty(event, 'defaultPrevented', { + get() {}, + }); + Object.defineProperty(event, 'returnValue', { + get() { + return false; + }, + }); + node.dispatchEvent(event); + + expect(expectedCount).toBe(2); + }); - it('should be prevented if nativeEvent is prevented', () => { - let node; - let expectedCount = 0; + it('should be able to `stopPropagation`', () => { + let node; + let expectedCount = 0; - const eventHandler = syntheticEvent => { - expect(syntheticEvent.isDefaultPrevented()).toBe(true); + const eventHandler = syntheticEvent => { + expect(syntheticEvent.isPropagationStopped()).toBe(false); + syntheticEvent.stopPropagation(); + expect(syntheticEvent.isPropagationStopped()).toBe(true); - expectedCount++; - }; - node = ReactDOM.render(
, container); + expectedCount++; + }; + node = ReactDOM.render(
, container); - let event; - event = document.createEvent('Event'); - event.initEvent('click', true, true); - event.preventDefault(); - node.dispatchEvent(event); + const event = document.createEvent('Event'); + event.initEvent('click', true, true); + node.dispatchEvent(event); - event = document.createEvent('Event'); - event.initEvent('click', true, true); - // Emulate IE8 - Object.defineProperty(event, 'defaultPrevented', { - get() {}, + expect(expectedCount).toBe(1); }); - Object.defineProperty(event, 'returnValue', { - get() { - return false; - }, + + it('should be able to `persist`', () => { + let node; + let expectedCount = 0; + let syntheticEvent; + + const eventHandler = e => { + expect(e.isPersistent()).toBe(false); + e.persist(); + syntheticEvent = e; + expect(e.isPersistent()).toBe(true); + + expectedCount++; + }; + node = ReactDOM.render(
, container); + + const event = document.createEvent('Event'); + event.initEvent('click', true, true); + node.dispatchEvent(event); + + expect(syntheticEvent.type).toBe('click'); + expect(syntheticEvent.bubbles).toBe(true); + expect(syntheticEvent.cancelable).toBe(true); + expect(expectedCount).toBe(1); }); - node.dispatchEvent(event); - expect(expectedCount).toBe(2); - }); + it('should be nullified and log warnings if the synthetic event has not been persisted', () => { + let node; + let expectedCount = 0; + let syntheticEvent; - it('should be able to `stopPropagation`', () => { - let node; - let expectedCount = 0; + const eventHandler = e => { + syntheticEvent = e; - const eventHandler = syntheticEvent => { - expect(syntheticEvent.isPropagationStopped()).toBe(false); - syntheticEvent.stopPropagation(); - expect(syntheticEvent.isPropagationStopped()).toBe(true); + expectedCount++; + }; + node = ReactDOM.render(
, container); - expectedCount++; - }; - node = ReactDOM.render(
, container); + const event = document.createEvent('Event'); + event.initEvent('click', true, true); + node.dispatchEvent(event); - const event = document.createEvent('Event'); - event.initEvent('click', true, true); - node.dispatchEvent(event); + const getExpectedWarning = property => + 'Warning: This synthetic event is reused for performance reasons. If ' + + `you're seeing this, you're accessing the property \`${property}\` on a ` + + 'released/nullified synthetic event. This is set to null. If you must ' + + 'keep the original synthetic event around, use event.persist(). ' + + 'See https://fb.me/react-event-pooling for more information.'; - expect(expectedCount).toBe(1); - }); + // once for each property accessed + expect(() => expect(syntheticEvent.type).toBe(null)).toWarnDev( + getExpectedWarning('type'), + {withoutStack: true}, + ); + expect(() => expect(syntheticEvent.nativeEvent).toBe(null)).toWarnDev( + getExpectedWarning('nativeEvent'), + {withoutStack: true}, + ); + expect(() => expect(syntheticEvent.target).toBe(null)).toWarnDev( + getExpectedWarning('target'), + {withoutStack: true}, + ); - it('should be able to `persist`', () => { - let node; - let expectedCount = 0; - let syntheticEvent; - - const eventHandler = e => { - expect(e.isPersistent()).toBe(false); - e.persist(); - syntheticEvent = e; - expect(e.isPersistent()).toBe(true); - - expectedCount++; - }; - node = ReactDOM.render(
, container); - - const event = document.createEvent('Event'); - event.initEvent('click', true, true); - node.dispatchEvent(event); - - expect(syntheticEvent.type).toBe('click'); - expect(syntheticEvent.bubbles).toBe(true); - expect(syntheticEvent.cancelable).toBe(true); - expect(expectedCount).toBe(1); - }); + expect(expectedCount).toBe(1); + }); - it('should be nullified and log warnings if the synthetic event has not been persisted', () => { - let node; - let expectedCount = 0; - let syntheticEvent; - - const eventHandler = e => { - syntheticEvent = e; - - expectedCount++; - }; - node = ReactDOM.render(
, container); - - const event = document.createEvent('Event'); - event.initEvent('click', true, true); - node.dispatchEvent(event); - - const getExpectedWarning = property => - 'Warning: This synthetic event is reused for performance reasons. If ' + - `you're seeing this, you're accessing the property \`${property}\` on a ` + - 'released/nullified synthetic event. This is set to null. If you must ' + - 'keep the original synthetic event around, use event.persist(). ' + - 'See https://fb.me/react-event-pooling for more information.'; - - // once for each property accessed - expect(() => expect(syntheticEvent.type).toBe(null)).toWarnDev( - getExpectedWarning('type'), - {withoutStack: true}, - ); - expect(() => expect(syntheticEvent.nativeEvent).toBe(null)).toWarnDev( - getExpectedWarning('nativeEvent'), - {withoutStack: true}, - ); - expect(() => expect(syntheticEvent.target).toBe(null)).toWarnDev( - getExpectedWarning('target'), - {withoutStack: true}, - ); - - expect(expectedCount).toBe(1); - }); + it('should warn when setting properties of a synthetic event that has not been persisted', () => { + let node; + let expectedCount = 0; + let syntheticEvent; + + const eventHandler = e => { + syntheticEvent = e; - it('should warn when setting properties of a synthetic event that has not been persisted', () => { - let node; - let expectedCount = 0; - let syntheticEvent; + expectedCount++; + }; + node = ReactDOM.render(
, container); - const eventHandler = e => { - syntheticEvent = e; + const event = document.createEvent('Event'); + event.initEvent('click', true, true); + node.dispatchEvent(event); + + expect(() => { + syntheticEvent.type = 'MouseEvent'; + }).toWarnDev( + 'Warning: This synthetic event is reused for performance reasons. If ' + + "you're seeing this, you're setting the property `type` on a " + + 'released/nullified synthetic event. This is effectively a no-op. If you must ' + + 'keep the original synthetic event around, use event.persist(). ' + + 'See https://fb.me/react-event-pooling for more information.', + {withoutStack: true}, + ); + expect(expectedCount).toBe(1); + }); - expectedCount++; - }; - node = ReactDOM.render(
, container); + it('should warn when calling `preventDefault` if the synthetic event has not been persisted', () => { + let node; + let expectedCount = 0; + let syntheticEvent; - const event = document.createEvent('Event'); - event.initEvent('click', true, true); - node.dispatchEvent(event); + const eventHandler = e => { + syntheticEvent = e; + expectedCount++; + }; + node = ReactDOM.render(
, container); - expect(() => { - syntheticEvent.type = 'MouseEvent'; - }).toWarnDev( - 'Warning: This synthetic event is reused for performance reasons. If ' + - "you're seeing this, you're setting the property `type` on a " + - 'released/nullified synthetic event. This is effectively a no-op. If you must ' + - 'keep the original synthetic event around, use event.persist(). ' + - 'See https://fb.me/react-event-pooling for more information.', - {withoutStack: true}, - ); - expect(expectedCount).toBe(1); - }); + const event = document.createEvent('Event'); + event.initEvent('click', true, true); + node.dispatchEvent(event); - it('should warn when calling `preventDefault` if the synthetic event has not been persisted', () => { - let node; - let expectedCount = 0; - let syntheticEvent; - - const eventHandler = e => { - syntheticEvent = e; - expectedCount++; - }; - node = ReactDOM.render(
, container); - - const event = document.createEvent('Event'); - event.initEvent('click', true, true); - node.dispatchEvent(event); - - expect(() => syntheticEvent.preventDefault()).toWarnDev( - 'Warning: This synthetic event is reused for performance reasons. If ' + - "you're seeing this, you're accessing the method `preventDefault` on a " + - 'released/nullified synthetic event. This is a no-op function. If you must ' + - 'keep the original synthetic event around, use event.persist(). ' + - 'See https://fb.me/react-event-pooling for more information.', - {withoutStack: true}, - ); - expect(expectedCount).toBe(1); - }); + expect(() => syntheticEvent.preventDefault()).toWarnDev( + 'Warning: This synthetic event is reused for performance reasons. If ' + + "you're seeing this, you're accessing the method `preventDefault` on a " + + 'released/nullified synthetic event. This is a no-op function. If you must ' + + 'keep the original synthetic event around, use event.persist(). ' + + 'See https://fb.me/react-event-pooling for more information.', + {withoutStack: true}, + ); + expect(expectedCount).toBe(1); + }); - it('should warn when calling `stopPropagation` if the synthetic event has not been persisted', () => { - let node; - let expectedCount = 0; - let syntheticEvent; + it('should warn when calling `stopPropagation` if the synthetic event has not been persisted', () => { + let node; + let expectedCount = 0; + let syntheticEvent; - const eventHandler = e => { - syntheticEvent = e; - expectedCount++; - }; - node = ReactDOM.render(
, container); + const eventHandler = e => { + syntheticEvent = e; + expectedCount++; + }; + node = ReactDOM.render(
, container); - const event = document.createEvent('Event'); - event.initEvent('click', true, true); + const event = document.createEvent('Event'); + event.initEvent('click', true, true); - node.dispatchEvent(event); + node.dispatchEvent(event); - expect(() => syntheticEvent.stopPropagation()).toWarnDev( - 'Warning: This synthetic event is reused for performance reasons. If ' + - "you're seeing this, you're accessing the method `stopPropagation` on a " + - 'released/nullified synthetic event. This is a no-op function. If you must ' + - 'keep the original synthetic event around, use event.persist(). ' + - 'See https://fb.me/react-event-pooling for more information.', - {withoutStack: true}, - ); - expect(expectedCount).toBe(1); - }); + expect(() => syntheticEvent.stopPropagation()).toWarnDev( + 'Warning: This synthetic event is reused for performance reasons. If ' + + "you're seeing this, you're accessing the method `stopPropagation` on a " + + 'released/nullified synthetic event. This is a no-op function. If you must ' + + 'keep the original synthetic event around, use event.persist(). ' + + 'See https://fb.me/react-event-pooling for more information.', + {withoutStack: true}, + ); + expect(expectedCount).toBe(1); + }); - it('should warn when calling `isPropagationStopped` if the synthetic event has not been persisted', () => { - let node; - let expectedCount = 0; - let syntheticEvent; - - const eventHandler = e => { - syntheticEvent = e; - expectedCount++; - }; - node = ReactDOM.render(
, container); - - const event = document.createEvent('Event'); - event.initEvent('click', true, true); - node.dispatchEvent(event); - - expect(() => - expect(syntheticEvent.isPropagationStopped()).toBe(false), - ).toWarnDev( - 'Warning: This synthetic event is reused for performance reasons. If ' + - "you're seeing this, you're accessing the method `isPropagationStopped` on a " + - 'released/nullified synthetic event. This is a no-op function. If you must ' + - 'keep the original synthetic event around, use event.persist(). ' + - 'See https://fb.me/react-event-pooling for more information.', - {withoutStack: true}, - ); - expect(expectedCount).toBe(1); - }); + it('should warn when calling `isPropagationStopped` if the synthetic event has not been persisted', () => { + let node; + let expectedCount = 0; + let syntheticEvent; - it('should warn when calling `isDefaultPrevented` if the synthetic event has not been persisted', () => { - let node; - let expectedCount = 0; - let syntheticEvent; - - const eventHandler = e => { - syntheticEvent = e; - expectedCount++; - }; - node = ReactDOM.render(
, container); - - const event = document.createEvent('Event'); - event.initEvent('click', true, true); - node.dispatchEvent(event); - - expect(() => - expect(syntheticEvent.isDefaultPrevented()).toBe(false), - ).toWarnDev( - 'Warning: This synthetic event is reused for performance reasons. If ' + - "you're seeing this, you're accessing the method `isDefaultPrevented` on a " + - 'released/nullified synthetic event. This is a no-op function. If you must ' + - 'keep the original synthetic event around, use event.persist(). ' + - 'See https://fb.me/react-event-pooling for more information.', - {withoutStack: true}, - ); - expect(expectedCount).toBe(1); - }); + const eventHandler = e => { + syntheticEvent = e; + expectedCount++; + }; + node = ReactDOM.render(
, container); - it('should properly log warnings when events simulated with rendered components', () => { - let event; - function assignEvent(e) { - event = e; - } - const node = ReactDOM.render(
, container); - node.click(); - - // access a property to cause the warning - expect(() => { - event.nativeEvent; // eslint-disable-line no-unused-expressions - }).toWarnDev( - 'Warning: This synthetic event is reused for performance reasons. If ' + - "you're seeing this, you're accessing the property `nativeEvent` on a " + - 'released/nullified synthetic event. This is set to null. If you must ' + - 'keep the original synthetic event around, use event.persist(). ' + - 'See https://fb.me/react-event-pooling for more information.', - {withoutStack: true}, - ); - }); + const event = document.createEvent('Event'); + event.initEvent('click', true, true); + node.dispatchEvent(event); - // TODO: we might want to re-add a warning like this in the future, - // but it shouldn't use Proxies because they make debugging difficult. - // Or we might disallow this pattern altogether: - // https://github.com/facebook/react/issues/13224 - xit('should warn if a property is added to the synthetic event', () => { - let node; - let expectedCount = 0; - let syntheticEvent; + expect(() => + expect(syntheticEvent.isPropagationStopped()).toBe(false), + ).toWarnDev( + 'Warning: This synthetic event is reused for performance reasons. If ' + + "you're seeing this, you're accessing the method `isPropagationStopped` on a " + + 'released/nullified synthetic event. This is a no-op function. If you must ' + + 'keep the original synthetic event around, use event.persist(). ' + + 'See https://fb.me/react-event-pooling for more information.', + {withoutStack: true}, + ); + expect(expectedCount).toBe(1); + }); - const eventHandler = e => { - expect(() => { - e.foo = 'bar'; - }).toWarnDev( + it('should warn when calling `isDefaultPrevented` if the synthetic event has not been persisted', () => { + let node; + let expectedCount = 0; + let syntheticEvent; + + const eventHandler = e => { + syntheticEvent = e; + expectedCount++; + }; + node = ReactDOM.render(
, container); + + const event = document.createEvent('Event'); + event.initEvent('click', true, true); + node.dispatchEvent(event); + + expect(() => + expect(syntheticEvent.isDefaultPrevented()).toBe(false), + ).toWarnDev( 'Warning: This synthetic event is reused for performance reasons. If ' + - "you're seeing this, you're adding a new property in the synthetic " + - 'event object. The property is never released. ' + + "you're seeing this, you're accessing the method `isDefaultPrevented` on a " + + 'released/nullified synthetic event. This is a no-op function. If you must ' + + 'keep the original synthetic event around, use event.persist(). ' + 'See https://fb.me/react-event-pooling for more information.', {withoutStack: true}, ); - syntheticEvent = e; - expectedCount++; - }; - node = ReactDOM.render(
, container); + expect(expectedCount).toBe(1); + }); - const event = document.createEvent('Event'); - event.initEvent('click', true, true); + it('should properly log warnings when events simulated with rendered components', () => { + let event; + function assignEvent(e) { + event = e; + } + const node = ReactDOM.render(
, container); + node.click(); - node.dispatchEvent(event); + // access a property to cause the warning + expect(() => { + event.nativeEvent; // eslint-disable-line no-unused-expressions + }).toWarnDev( + 'Warning: This synthetic event is reused for performance reasons. If ' + + "you're seeing this, you're accessing the property `nativeEvent` on a " + + 'released/nullified synthetic event. This is set to null. If you must ' + + 'keep the original synthetic event around, use event.persist(). ' + + 'See https://fb.me/react-event-pooling for more information.', + {withoutStack: true}, + ); + }); - expect(syntheticEvent.foo).toBe('bar'); - expect(expectedCount).toBe(1); + // TODO: we might want to re-add a warning like this in the future, + // but it shouldn't use Proxies because they make debugging difficult. + // Or we might disallow this pattern altogether: + // https://github.com/facebook/react/issues/13224 + xit('should warn if a property is added to the synthetic event', () => { + let node; + let expectedCount = 0; + let syntheticEvent; + + const eventHandler = e => { + expect(() => { + e.foo = 'bar'; + }).toWarnDev( + 'Warning: This synthetic event is reused for performance reasons. If ' + + "you're seeing this, you're adding a new property in the synthetic " + + 'event object. The property is never released. ' + + 'See https://fb.me/react-event-pooling for more information.', + {withoutStack: true}, + ); + syntheticEvent = e; + expectedCount++; + }; + node = ReactDOM.render(
, container); + + const event = document.createEvent('Event'); + event.initEvent('click', true, true); + + node.dispatchEvent(event); + + expect(syntheticEvent.foo).toBe('bar'); + expect(expectedCount).toBe(1); + }); }); -}); +} diff --git a/packages/react-dom/src/events/__tests__/SyntheticKeyboardEvent-test.js b/packages/react-dom/src/events/__tests__/SyntheticKeyboardEvent-test.js index 06673f0fbe0..97706e9886b 100644 --- a/packages/react-dom/src/events/__tests__/SyntheticKeyboardEvent-test.js +++ b/packages/react-dom/src/events/__tests__/SyntheticKeyboardEvent-test.js @@ -11,538 +11,544 @@ let React; let ReactDOM; +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); -describe('SyntheticKeyboardEvent', () => { - let container; +// KeyboardEvent is no longer polyfilled synthetically in React Fire +if (ReactFeatureFlags.enableReactDOMFire) { + it('Empty test', () => {}); +} else { + describe('SyntheticKeyboardEvent', () => { + let container; - beforeEach(() => { - React = require('react'); - ReactDOM = require('react-dom'); - // The container has to be attached for events to fire. - container = document.createElement('div'); - document.body.appendChild(container); - }); + beforeEach(() => { + React = require('react'); + ReactDOM = require('react-dom'); + // The container has to be attached for events to fire. + container = document.createElement('div'); + document.body.appendChild(container); + }); - afterEach(() => { - document.body.removeChild(container); - container = null; - }); + afterEach(() => { + document.body.removeChild(container); + container = null; + }); + + describe('KeyboardEvent interface', () => { + describe('charCode', () => { + describe('when event is `keypress`', () => { + describe('when charCode is present in nativeEvent', () => { + it('when charCode is 0 and keyCode is 13, returns 13', () => { + let charCode = null; + const node = ReactDOM.render( + { + charCode = e.charCode; + }} + />, + container, + ); + node.dispatchEvent( + new KeyboardEvent('keypress', { + charCode: 0, + keyCode: 13, + bubbles: true, + cancelable: true, + }), + ); + expect(charCode).toBe(13); + }); + + it('when charCode is 32 or bigger and keyCode is missing, returns charCode', () => { + let charCode = null; + const node = ReactDOM.render( + { + charCode = e.charCode; + }} + />, + container, + ); + node.dispatchEvent( + new KeyboardEvent('keypress', { + charCode: 32, + bubbles: true, + cancelable: true, + }), + ); + expect(charCode).toBe(32); + }); + + it('when charCode is 13 and keyCode is missing, returns charCode', () => { + let charCode = null; + const node = ReactDOM.render( + { + charCode = e.charCode; + }} + />, + container, + ); + node.dispatchEvent( + new KeyboardEvent('keypress', { + charCode: 13, + bubbles: true, + cancelable: true, + }), + ); + expect(charCode).toBe(13); + }); + + // Firefox creates a keypress event for function keys too. This removes + // the unwanted keypress events. Enter is however both printable and + // non-printable. One would expect Tab to be as well (but it isn't). + it('when charCode is smaller than 32 but is not 13, and keyCode is missing, ignores keypress', () => { + let called = false; + const node = ReactDOM.render( + { + called = true; + }} + />, + container, + ); + node.dispatchEvent( + new KeyboardEvent('keypress', { + charCode: 31, + bubbles: true, + cancelable: true, + }), + ); + expect(called).toBe(false); + }); + + it('when charCode is 10, returns 13', () => { + let charCode = null; + const node = ReactDOM.render( + { + charCode = e.charCode; + }} + />, + container, + ); + node.dispatchEvent( + new KeyboardEvent('keypress', { + charCode: 10, + bubbles: true, + cancelable: true, + }), + ); + expect(charCode).toBe(13); + }); - describe('KeyboardEvent interface', () => { - describe('charCode', () => { - describe('when event is `keypress`', () => { - describe('when charCode is present in nativeEvent', () => { - it('when charCode is 0 and keyCode is 13, returns 13', () => { - let charCode = null; + it('when charCode is 10 and ctrl is pressed, returns 13', () => { + let charCode = null; + const node = ReactDOM.render( + { + charCode = e.charCode; + }} + />, + container, + ); + node.dispatchEvent( + new KeyboardEvent('keypress', { + charCode: 10, + ctrlKey: true, + bubbles: true, + cancelable: true, + }), + ); + expect(charCode).toBe(13); + }); + }); + + // TODO: this seems IE8 specific. + // We can probably remove this normalization. + describe('when charCode is not present in nativeEvent', () => { + let charCodeDescriptor; + + beforeEach(() => { + charCodeDescriptor = Object.getOwnPropertyDescriptor( + KeyboardEvent.prototype, + 'charCode', + ); + delete KeyboardEvent.prototype.charCode; + }); + + afterEach(() => { + // Don't forget to restore for other tests. + Object.defineProperty( + KeyboardEvent.prototype, + 'charCode', + charCodeDescriptor, + ); + charCodeDescriptor = null; + }); + + it('when keyCode is 32 or bigger, returns keyCode', () => { + let charCode = null; + const node = ReactDOM.render( + { + charCode = e.charCode; + }} + />, + container, + ); + node.dispatchEvent( + new KeyboardEvent('keypress', { + keyCode: 32, + bubbles: true, + cancelable: true, + }), + ); + expect(charCode).toBe(32); + }); + + it('when keyCode is 13, returns 13', () => { + let charCode = null; + const node = ReactDOM.render( + { + charCode = e.charCode; + }} + />, + container, + ); + node.dispatchEvent( + new KeyboardEvent('keypress', { + keyCode: 13, + bubbles: true, + cancelable: true, + }), + ); + expect(charCode).toBe(13); + }); + + it('when keyCode is smaller than 32 and is not 13, ignores keypress', () => { + let called = false; + const node = ReactDOM.render( + { + called = true; + }} + />, + container, + ); + node.dispatchEvent( + new KeyboardEvent('keypress', { + keyCode: 31, + bubbles: true, + cancelable: true, + }), + ); + expect(called).toBe(false); + }); + }); + }); + + describe('when event is not `keypress`', () => { + it('returns 0', () => { + let charCodeDown = null; + let charCodeUp = null; const node = ReactDOM.render( { - charCode = e.charCode; + onKeyDown={e => { + charCodeDown = e.charCode; + }} + onKeyUp={e => { + charCodeUp = e.charCode; }} />, container, ); node.dispatchEvent( - new KeyboardEvent('keypress', { - charCode: 0, - keyCode: 13, + new KeyboardEvent('keydown', { + key: 'Del', bubbles: true, cancelable: true, }), ); - expect(charCode).toBe(13); - }); - - it('when charCode is 32 or bigger and keyCode is missing, returns charCode', () => { - let charCode = null; - const node = ReactDOM.render( - { - charCode = e.charCode; - }} - />, - container, - ); node.dispatchEvent( - new KeyboardEvent('keypress', { - charCode: 32, + new KeyboardEvent('keyup', { + key: 'Del', bubbles: true, cancelable: true, }), ); - expect(charCode).toBe(32); + expect(charCodeDown).toBe(0); + expect(charCodeUp).toBe(0); }); + }); + + it('when charCode is smaller than 32 but is not 13, and keyCode is missing, charCode is 0', () => { + let charCode = null; + const node = ReactDOM.render( + { + charCode = e.charCode; + }} + />, + container, + ); + node.dispatchEvent( + new KeyboardEvent('keydown', { + charCode: 31, + bubbles: true, + cancelable: true, + }), + ); + expect(charCode).toBe(0); + }); + }); - it('when charCode is 13 and keyCode is missing, returns charCode', () => { - let charCode = null; + describe('keyCode', () => { + describe('when event is `keydown` or `keyup`', () => { + it('returns a passed keyCode', () => { + let keyCodeDown = null; + let keyCodeUp = null; const node = ReactDOM.render( { - charCode = e.charCode; + onKeyDown={e => { + keyCodeDown = e.keyCode; + }} + onKeyUp={e => { + keyCodeUp = e.keyCode; }} />, container, ); node.dispatchEvent( - new KeyboardEvent('keypress', { - charCode: 13, + new KeyboardEvent('keydown', { + keyCode: 40, bubbles: true, cancelable: true, }), ); - expect(charCode).toBe(13); - }); - - // Firefox creates a keypress event for function keys too. This removes - // the unwanted keypress events. Enter is however both printable and - // non-printable. One would expect Tab to be as well (but it isn't). - it('when charCode is smaller than 32 but is not 13, and keyCode is missing, ignores keypress', () => { - let called = false; - const node = ReactDOM.render( - { - called = true; - }} - />, - container, - ); node.dispatchEvent( - new KeyboardEvent('keypress', { - charCode: 31, + new KeyboardEvent('keyup', { + keyCode: 40, bubbles: true, cancelable: true, }), ); - expect(called).toBe(false); + expect(keyCodeDown).toBe(40); + expect(keyCodeUp).toBe(40); }); + }); - it('when charCode is 10, returns 13', () => { - let charCode = null; + describe('when event is `keypress`', () => { + it('returns 0', () => { + let keyCode = null; const node = ReactDOM.render( { - charCode = e.charCode; + keyCode = e.keyCode; }} />, container, ); node.dispatchEvent( new KeyboardEvent('keypress', { - charCode: 10, + charCode: 65, bubbles: true, cancelable: true, }), ); - expect(charCode).toBe(13); + expect(keyCode).toBe(0); }); + }); + }); - it('when charCode is 10 and ctrl is pressed, returns 13', () => { - let charCode = null; + describe('which', () => { + describe('when event is `keypress`', () => { + it('is consistent with `charCode`', () => { + let calls = 0; const node = ReactDOM.render( { - charCode = e.charCode; + expect(e.which).toBe(e.charCode); + calls++; }} />, container, ); + // Try different combinations from other tests. node.dispatchEvent( new KeyboardEvent('keypress', { - charCode: 10, - ctrlKey: true, + charCode: 0, + keyCode: 13, bubbles: true, cancelable: true, }), ); - expect(charCode).toBe(13); - }); - }); - - // TODO: this seems IE8 specific. - // We can probably remove this normalization. - describe('when charCode is not present in nativeEvent', () => { - let charCodeDescriptor; - - beforeEach(() => { - charCodeDescriptor = Object.getOwnPropertyDescriptor( - KeyboardEvent.prototype, - 'charCode', + node.dispatchEvent( + new KeyboardEvent('keypress', { + charCode: 32, + bubbles: true, + cancelable: true, + }), ); - delete KeyboardEvent.prototype.charCode; - }); - - afterEach(() => { - // Don't forget to restore for other tests. - Object.defineProperty( - KeyboardEvent.prototype, - 'charCode', - charCodeDescriptor, + node.dispatchEvent( + new KeyboardEvent('keypress', { + charCode: 13, + bubbles: true, + cancelable: true, + }), ); - charCodeDescriptor = null; + expect(calls).toBe(3); }); + }); - it('when keyCode is 32 or bigger, returns keyCode', () => { - let charCode = null; + describe('when event is `keydown` or `keyup`', () => { + it('is consistent with `keyCode`', () => { + let calls = 0; const node = ReactDOM.render( { - charCode = e.charCode; + onKeyDown={e => { + expect(e.which).toBe(e.keyCode); + calls++; + }} + onKeyUp={e => { + expect(e.which).toBe(e.keyCode); + calls++; }} />, container, ); node.dispatchEvent( - new KeyboardEvent('keypress', { - keyCode: 32, + new KeyboardEvent('keydown', { + key: 'Del', bubbles: true, cancelable: true, }), ); - expect(charCode).toBe(32); - }); - - it('when keyCode is 13, returns 13', () => { - let charCode = null; - const node = ReactDOM.render( - { - charCode = e.charCode; - }} - />, - container, + node.dispatchEvent( + new KeyboardEvent('keydown', { + charCode: 31, + bubbles: true, + cancelable: true, + }), ); node.dispatchEvent( - new KeyboardEvent('keypress', { - keyCode: 13, + new KeyboardEvent('keydown', { + keyCode: 40, bubbles: true, cancelable: true, }), ); - expect(charCode).toBe(13); - }); - - it('when keyCode is smaller than 32 and is not 13, ignores keypress', () => { - let called = false; - const node = ReactDOM.render( - { - called = true; - }} - />, - container, + node.dispatchEvent( + new KeyboardEvent('keyup', { + key: 'Del', + bubbles: true, + cancelable: true, + }), ); node.dispatchEvent( - new KeyboardEvent('keypress', { - keyCode: 31, + new KeyboardEvent('keyup', { + keyCode: 40, bubbles: true, cancelable: true, }), ); - expect(called).toBe(false); + expect(calls).toBe(5); }); }); }); + }); - describe('when event is not `keypress`', () => { - it('returns 0', () => { - let charCodeDown = null; - let charCodeUp = null; - const node = ReactDOM.render( - { - charCodeDown = e.charCode; - }} - onKeyUp={e => { - charCodeUp = e.charCode; - }} - />, - container, - ); - node.dispatchEvent( - new KeyboardEvent('keydown', { - key: 'Del', - bubbles: true, - cancelable: true, - }), - ); - node.dispatchEvent( - new KeyboardEvent('keyup', { - key: 'Del', - bubbles: true, - cancelable: true, - }), - ); - expect(charCodeDown).toBe(0); - expect(charCodeUp).toBe(0); - }); - }); + describe('EventInterface', () => { + it('is able to `preventDefault` and `stopPropagation`', () => { + let expectedCount = 0; + const eventHandler = event => { + expect(event.isDefaultPrevented()).toBe(false); + event.preventDefault(); + expect(event.isDefaultPrevented()).toBe(true); - it('when charCode is smaller than 32 but is not 13, and keyCode is missing, charCode is 0', () => { - let charCode = null; - const node = ReactDOM.render( - { - charCode = e.charCode; - }} + expect(event.isPropagationStopped()).toBe(false); + event.stopPropagation(); + expect(event.isPropagationStopped()).toBe(true); + expectedCount++; + }; + let div = ReactDOM.render( +
, container, ); - node.dispatchEvent( + + div.dispatchEvent( new KeyboardEvent('keydown', { - charCode: 31, + keyCode: 40, bubbles: true, cancelable: true, }), ); - expect(charCode).toBe(0); - }); - }); - - describe('keyCode', () => { - describe('when event is `keydown` or `keyup`', () => { - it('returns a passed keyCode', () => { - let keyCodeDown = null; - let keyCodeUp = null; - const node = ReactDOM.render( - { - keyCodeDown = e.keyCode; - }} - onKeyUp={e => { - keyCodeUp = e.keyCode; - }} - />, - container, - ); - node.dispatchEvent( - new KeyboardEvent('keydown', { - keyCode: 40, - bubbles: true, - cancelable: true, - }), - ); - node.dispatchEvent( - new KeyboardEvent('keyup', { - keyCode: 40, - bubbles: true, - cancelable: true, - }), - ); - expect(keyCodeDown).toBe(40); - expect(keyCodeUp).toBe(40); - }); - }); - - describe('when event is `keypress`', () => { - it('returns 0', () => { - let keyCode = null; - const node = ReactDOM.render( - { - keyCode = e.keyCode; - }} - />, - container, - ); - node.dispatchEvent( - new KeyboardEvent('keypress', { - charCode: 65, - bubbles: true, - cancelable: true, - }), - ); - expect(keyCode).toBe(0); - }); + div.dispatchEvent( + new KeyboardEvent('keyup', { + keyCode: 40, + bubbles: true, + cancelable: true, + }), + ); + div.dispatchEvent( + new KeyboardEvent('keypress', { + charCode: 40, + keyCode: 40, + bubbles: true, + cancelable: true, + }), + ); + expect(expectedCount).toBe(3); }); - }); - describe('which', () => { - describe('when event is `keypress`', () => { - it('is consistent with `charCode`', () => { - let calls = 0; - const node = ReactDOM.render( - { - expect(e.which).toBe(e.charCode); - calls++; - }} - />, - container, - ); - // Try different combinations from other tests. - node.dispatchEvent( - new KeyboardEvent('keypress', { - charCode: 0, - keyCode: 13, - bubbles: true, - cancelable: true, - }), - ); - node.dispatchEvent( - new KeyboardEvent('keypress', { - charCode: 32, - bubbles: true, - cancelable: true, - }), - ); - node.dispatchEvent( - new KeyboardEvent('keypress', { - charCode: 13, - bubbles: true, - cancelable: true, - }), - ); - expect(calls).toBe(3); - }); - }); + it('is able to `persist`', () => { + const persistentEvents = []; + const eventHandler = event => { + expect(event.isPersistent()).toBe(false); + event.persist(); + expect(event.isPersistent()).toBe(true); + persistentEvents.push(event); + }; + let div = ReactDOM.render( +
, + container, + ); - describe('when event is `keydown` or `keyup`', () => { - it('is consistent with `keyCode`', () => { - let calls = 0; - const node = ReactDOM.render( - { - expect(e.which).toBe(e.keyCode); - calls++; - }} - onKeyUp={e => { - expect(e.which).toBe(e.keyCode); - calls++; - }} - />, - container, - ); - node.dispatchEvent( - new KeyboardEvent('keydown', { - key: 'Del', - bubbles: true, - cancelable: true, - }), - ); - node.dispatchEvent( - new KeyboardEvent('keydown', { - charCode: 31, - bubbles: true, - cancelable: true, - }), - ); - node.dispatchEvent( - new KeyboardEvent('keydown', { - keyCode: 40, - bubbles: true, - cancelable: true, - }), - ); - node.dispatchEvent( - new KeyboardEvent('keyup', { - key: 'Del', - bubbles: true, - cancelable: true, - }), - ); - node.dispatchEvent( - new KeyboardEvent('keyup', { - keyCode: 40, - bubbles: true, - cancelable: true, - }), - ); - expect(calls).toBe(5); - }); + div.dispatchEvent( + new KeyboardEvent('keydown', { + keyCode: 40, + bubbles: true, + cancelable: true, + }), + ); + div.dispatchEvent( + new KeyboardEvent('keyup', { + keyCode: 40, + bubbles: true, + cancelable: true, + }), + ); + div.dispatchEvent( + new KeyboardEvent('keypress', { + charCode: 40, + keyCode: 40, + bubbles: true, + cancelable: true, + }), + ); + expect(persistentEvents.length).toBe(3); + expect(persistentEvents[0].type).toBe('keydown'); + expect(persistentEvents[1].type).toBe('keyup'); + expect(persistentEvents[2].type).toBe('keypress'); }); }); }); - - describe('EventInterface', () => { - it('is able to `preventDefault` and `stopPropagation`', () => { - let expectedCount = 0; - const eventHandler = event => { - expect(event.isDefaultPrevented()).toBe(false); - event.preventDefault(); - expect(event.isDefaultPrevented()).toBe(true); - - expect(event.isPropagationStopped()).toBe(false); - event.stopPropagation(); - expect(event.isPropagationStopped()).toBe(true); - expectedCount++; - }; - let div = ReactDOM.render( -
, - container, - ); - - div.dispatchEvent( - new KeyboardEvent('keydown', { - keyCode: 40, - bubbles: true, - cancelable: true, - }), - ); - div.dispatchEvent( - new KeyboardEvent('keyup', { - keyCode: 40, - bubbles: true, - cancelable: true, - }), - ); - div.dispatchEvent( - new KeyboardEvent('keypress', { - charCode: 40, - keyCode: 40, - bubbles: true, - cancelable: true, - }), - ); - expect(expectedCount).toBe(3); - }); - - it('is able to `persist`', () => { - const persistentEvents = []; - const eventHandler = event => { - expect(event.isPersistent()).toBe(false); - event.persist(); - expect(event.isPersistent()).toBe(true); - persistentEvents.push(event); - }; - let div = ReactDOM.render( -
, - container, - ); - - div.dispatchEvent( - new KeyboardEvent('keydown', { - keyCode: 40, - bubbles: true, - cancelable: true, - }), - ); - div.dispatchEvent( - new KeyboardEvent('keyup', { - keyCode: 40, - bubbles: true, - cancelable: true, - }), - ); - div.dispatchEvent( - new KeyboardEvent('keypress', { - charCode: 40, - keyCode: 40, - bubbles: true, - cancelable: true, - }), - ); - expect(persistentEvents.length).toBe(3); - expect(persistentEvents[0].type).toBe('keydown'); - expect(persistentEvents[1].type).toBe('keyup'); - expect(persistentEvents[2].type).toBe('keypress'); - }); - }); -}); +} diff --git a/packages/react-dom/src/events/__tests__/SyntheticMouseEvent-test.js b/packages/react-dom/src/events/__tests__/SyntheticMouseEvent-test.js index 46a502e38e0..5a5b46d374f 100644 --- a/packages/react-dom/src/events/__tests__/SyntheticMouseEvent-test.js +++ b/packages/react-dom/src/events/__tests__/SyntheticMouseEvent-test.js @@ -11,70 +11,76 @@ let React; let ReactDOM; - -describe('SyntheticMouseEvent', () => { - let container; - - beforeEach(() => { - React = require('react'); - ReactDOM = require('react-dom'); - - // The container has to be attached for events to fire. - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - document.body.removeChild(container); - container = null; - }); - - it('should only use values from movementX/Y when event type is mousemove', () => { - const events = []; - const onMouseMove = event => { - events.push(event.movementX); - }; - - const onMouseDown = event => { - events.push(event.movementX); - }; - - const node = ReactDOM.render( -
, - container, - ); - - let event = new MouseEvent('mousemove', { - relatedTarget: null, - bubbles: true, - screenX: 2, - screenY: 2, +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); + +// MouseEvent is no longer polyfilled synthetically in React Fire +if (ReactFeatureFlags.enableReactDOMFire) { + it('Empty test', () => {}); +} else { + describe('SyntheticMouseEvent', () => { + let container; + + beforeEach(() => { + React = require('react'); + ReactDOM = require('react-dom'); + + // The container has to be attached for events to fire. + container = document.createElement('div'); + document.body.appendChild(container); }); - node.dispatchEvent(event); - - event = new MouseEvent('mousemove', { - relatedTarget: null, - bubbles: true, - screenX: 8, - screenY: 8, + afterEach(() => { + document.body.removeChild(container); + container = null; }); - node.dispatchEvent(event); - - // Now trigger a mousedown event to see if movementX has changed back to 0 - event = new MouseEvent('mousedown', { - relatedTarget: null, - bubbles: true, - screenX: 25, - screenY: 65, + it('should only use values from movementX/Y when event type is mousemove', () => { + const events = []; + const onMouseMove = event => { + events.push(event.movementX); + }; + + const onMouseDown = event => { + events.push(event.movementX); + }; + + const node = ReactDOM.render( +
, + container, + ); + + let event = new MouseEvent('mousemove', { + relatedTarget: null, + bubbles: true, + screenX: 2, + screenY: 2, + }); + + node.dispatchEvent(event); + + event = new MouseEvent('mousemove', { + relatedTarget: null, + bubbles: true, + screenX: 8, + screenY: 8, + }); + + node.dispatchEvent(event); + + // Now trigger a mousedown event to see if movementX has changed back to 0 + event = new MouseEvent('mousedown', { + relatedTarget: null, + bubbles: true, + screenX: 25, + screenY: 65, + }); + + node.dispatchEvent(event); + + expect(events.length).toBe(3); + expect(events[0]).toBe(0); + expect(events[1]).toBe(6); + expect(events[2]).toBe(0); // mousedown event should have movementX at 0 }); - - node.dispatchEvent(event); - - expect(events.length).toBe(3); - expect(events[0]).toBe(0); - expect(events[1]).toBe(6); - expect(events[2]).toBe(0); // mousedown event should have movementX at 0 }); -}); +} diff --git a/packages/react-dom/src/events/__tests__/SyntheticWheelEvent-test.js b/packages/react-dom/src/events/__tests__/SyntheticWheelEvent-test.js index 924a7f37628..ca5e66c4fdb 100644 --- a/packages/react-dom/src/events/__tests__/SyntheticWheelEvent-test.js +++ b/packages/react-dom/src/events/__tests__/SyntheticWheelEvent-test.js @@ -11,125 +11,131 @@ let React; let ReactDOM; - -describe('SyntheticWheelEvent', () => { - let container; - - beforeEach(() => { - React = require('react'); - ReactDOM = require('react-dom'); - - // The container has to be attached for events to fire. - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - document.body.removeChild(container); - container = null; - }); - - it('should normalize properties from the MouseEvent interface', () => { - const events = []; - const onWheel = event => { - event.persist(); - events.push(event); - }; - ReactDOM.render(
, container); - - container.firstChild.dispatchEvent( - new MouseEvent('wheel', { - bubbles: true, - button: 1, - }), - ); - - expect(events.length).toBe(1); - expect(events[0].button).toBe(1); - }); - - it('should normalize properties from the WheelEvent interface', () => { - const events = []; - const onWheel = event => { - event.persist(); - events.push(event); - }; - ReactDOM.render(
, container); - - let event = new MouseEvent('wheel', { - bubbles: true, - }); - // jsdom doesn't support these so we add them manually. - Object.assign(event, { - deltaX: 10, - deltaY: -50, +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); + +// WheelEvent is no longer polyfilled synthetically in React Fire +if (ReactFeatureFlags.enableReactDOMFire) { + it('Empty test', () => {}); +} else { + describe('SyntheticWheelEvent', () => { + let container; + + beforeEach(() => { + React = require('react'); + ReactDOM = require('react-dom'); + + // The container has to be attached for events to fire. + container = document.createElement('div'); + document.body.appendChild(container); }); - container.firstChild.dispatchEvent(event); - event = new MouseEvent('wheel', { - bubbles: true, + afterEach(() => { + document.body.removeChild(container); + container = null; }); - // jsdom doesn't support these so we add them manually. - Object.assign(event, { - wheelDeltaX: -10, - wheelDeltaY: 50, + + it('should normalize properties from the MouseEvent interface', () => { + const events = []; + const onWheel = event => { + event.persist(); + events.push(event); + }; + ReactDOM.render(
, container); + + container.firstChild.dispatchEvent( + new MouseEvent('wheel', { + bubbles: true, + button: 1, + }), + ); + + expect(events.length).toBe(1); + expect(events[0].button).toBe(1); }); - container.firstChild.dispatchEvent(event); - expect(events.length).toBe(2); - expect(events[0].deltaX).toBe(10); - expect(events[0].deltaY).toBe(-50); - expect(events[1].deltaX).toBe(10); - expect(events[1].deltaY).toBe(-50); - }); + it('should normalize properties from the WheelEvent interface', () => { + const events = []; + const onWheel = event => { + event.persist(); + events.push(event); + }; + ReactDOM.render(
, container); - it('should be able to `preventDefault` and `stopPropagation`', () => { - const events = []; - const onWheel = event => { - expect(event.isDefaultPrevented()).toBe(false); - event.preventDefault(); - expect(event.isDefaultPrevented()).toBe(true); - event.persist(); - events.push(event); - }; - ReactDOM.render(
, container); - - container.firstChild.dispatchEvent( - new MouseEvent('wheel', { + let event = new MouseEvent('wheel', { bubbles: true, + }); + // jsdom doesn't support these so we add them manually. + Object.assign(event, { deltaX: 10, deltaY: -50, - }), - ); + }); + container.firstChild.dispatchEvent(event); - container.firstChild.dispatchEvent( - new MouseEvent('wheel', { + event = new MouseEvent('wheel', { bubbles: true, - deltaX: 10, - deltaY: -50, - }), - ); - - expect(events.length).toBe(2); - }); + }); + // jsdom doesn't support these so we add them manually. + Object.assign(event, { + wheelDeltaX: -10, + wheelDeltaY: 50, + }); + container.firstChild.dispatchEvent(event); + + expect(events.length).toBe(2); + expect(events[0].deltaX).toBe(10); + expect(events[0].deltaY).toBe(-50); + expect(events[1].deltaX).toBe(10); + expect(events[1].deltaY).toBe(-50); + }); - it('should be able to `persist`', () => { - const events = []; - const onWheel = event => { - expect(event.isPersistent()).toBe(false); - event.persist(); - expect(event.isPersistent()).toBe(true); - events.push(event); - }; - ReactDOM.render(
, container); - - container.firstChild.dispatchEvent( - new MouseEvent('wheel', { - bubbles: true, - }), - ); + it('should be able to `preventDefault` and `stopPropagation`', () => { + const events = []; + const onWheel = event => { + expect(event.isDefaultPrevented()).toBe(false); + event.preventDefault(); + expect(event.isDefaultPrevented()).toBe(true); + event.persist(); + events.push(event); + }; + ReactDOM.render(
, container); + + container.firstChild.dispatchEvent( + new MouseEvent('wheel', { + bubbles: true, + deltaX: 10, + deltaY: -50, + }), + ); + + container.firstChild.dispatchEvent( + new MouseEvent('wheel', { + bubbles: true, + deltaX: 10, + deltaY: -50, + }), + ); + + expect(events.length).toBe(2); + }); - expect(events.length).toBe(1); - expect(events[0].type).toBe('wheel'); + it('should be able to `persist`', () => { + const events = []; + const onWheel = event => { + expect(event.isPersistent()).toBe(false); + event.persist(); + expect(event.isPersistent()).toBe(true); + events.push(event); + }; + ReactDOM.render(
, container); + + container.firstChild.dispatchEvent( + new MouseEvent('wheel', { + bubbles: true, + }), + ); + + expect(events.length).toBe(1); + expect(events[0].type).toBe('wheel'); + }); }); -}); +} diff --git a/packages/react-dom/src/events/__tests__/getEventKey-test.js b/packages/react-dom/src/events/__tests__/getEventKey-test.js index cb6b949e4eb..d62041db311 100644 --- a/packages/react-dom/src/events/__tests__/getEventKey-test.js +++ b/packages/react-dom/src/events/__tests__/getEventKey-test.js @@ -11,155 +11,161 @@ let React; let ReactDOM; - -describe('getEventKey', () => { - let container; - - beforeEach(() => { - React = require('react'); - ReactDOM = require('react-dom'); - - // The container has to be attached for events to fire. - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - document.body.removeChild(container); - container = null; - }); - - describe('when key is implemented in a browser', () => { - describe('when key is not normalized', () => { - it('returns a normalized value', () => { - let key = null; - class Comp extends React.Component { - render() { - return (key = e.key)} />; - } - } - - ReactDOM.render(, container); - - const nativeEvent = new KeyboardEvent('keydown', { - key: 'Del', - bubbles: true, - cancelable: true, - }); - container.firstChild.dispatchEvent(nativeEvent); - expect(key).toBe('Delete'); - }); +const ReactFeatureFlags = require('shared/ReactFeatureFlags'); + +// getEventKey is no longer polyfilled synthetically in React Fire +if (ReactFeatureFlags.enableReactDOMFire) { + it('Empty test', () => {}); +} else { + describe('getEventKey', () => { + let container; + + beforeEach(() => { + React = require('react'); + ReactDOM = require('react-dom'); + + // The container has to be attached for events to fire. + container = document.createElement('div'); + document.body.appendChild(container); }); - describe('when key is normalized', () => { - it('returns a key', () => { - let key = null; - class Comp extends React.Component { - render() { - return (key = e.key)} />; - } - } - - ReactDOM.render(, container); - - const nativeEvent = new KeyboardEvent('keydown', { - key: 'f', - bubbles: true, - cancelable: true, - }); - container.firstChild.dispatchEvent(nativeEvent); - expect(key).toBe('f'); - }); + afterEach(() => { + document.body.removeChild(container); + container = null; }); - }); - describe('when key is not implemented in a browser', () => { - describe('when event type is keypress', () => { - describe('when charCode is 13', () => { - it('returns "Enter"', () => { + describe('when key is implemented in a browser', () => { + describe('when key is not normalized', () => { + it('returns a normalized value', () => { let key = null; class Comp extends React.Component { render() { - return (key = e.key)} />; + return (key = e.key)} />; } } ReactDOM.render(, container); - const nativeEvent = new KeyboardEvent('keypress', { - charCode: 13, + const nativeEvent = new KeyboardEvent('keydown', { + key: 'Del', bubbles: true, cancelable: true, }); container.firstChild.dispatchEvent(nativeEvent); - expect(key).toBe('Enter'); + expect(key).toBe('Delete'); }); }); - describe('when charCode is not 13', () => { - it('returns a string from a charCode', () => { + describe('when key is normalized', () => { + it('returns a key', () => { let key = null; class Comp extends React.Component { render() { - return (key = e.key)} />; + return (key = e.key)} />; } } ReactDOM.render(, container); - const nativeEvent = new KeyboardEvent('keypress', { - charCode: 65, + const nativeEvent = new KeyboardEvent('keydown', { + key: 'f', bubbles: true, cancelable: true, }); container.firstChild.dispatchEvent(nativeEvent); - expect(key).toBe('A'); + expect(key).toBe('f'); }); }); }); - describe('when event type is keydown or keyup', () => { - describe('when keyCode is recognized', () => { - it('returns a translated key', () => { - let key = null; - class Comp extends React.Component { - render() { - return (key = e.key)} />; + describe('when key is not implemented in a browser', () => { + describe('when event type is keypress', () => { + describe('when charCode is 13', () => { + it('returns "Enter"', () => { + let key = null; + class Comp extends React.Component { + render() { + return (key = e.key)} />; + } } - } - ReactDOM.render(, container); + ReactDOM.render(, container); - const nativeEvent = new KeyboardEvent('keydown', { - keyCode: 45, - bubbles: true, - cancelable: true, + const nativeEvent = new KeyboardEvent('keypress', { + charCode: 13, + bubbles: true, + cancelable: true, + }); + container.firstChild.dispatchEvent(nativeEvent); + expect(key).toBe('Enter'); + }); + }); + + describe('when charCode is not 13', () => { + it('returns a string from a charCode', () => { + let key = null; + class Comp extends React.Component { + render() { + return (key = e.key)} />; + } + } + + ReactDOM.render(, container); + + const nativeEvent = new KeyboardEvent('keypress', { + charCode: 65, + bubbles: true, + cancelable: true, + }); + container.firstChild.dispatchEvent(nativeEvent); + expect(key).toBe('A'); }); - container.firstChild.dispatchEvent(nativeEvent); - expect(key).toBe('Insert'); }); }); - describe('when keyCode is not recognized', () => { - it('returns Unidentified', () => { - let key = null; - class Comp extends React.Component { - render() { - return (key = e.key)} />; + describe('when event type is keydown or keyup', () => { + describe('when keyCode is recognized', () => { + it('returns a translated key', () => { + let key = null; + class Comp extends React.Component { + render() { + return (key = e.key)} />; + } } - } - ReactDOM.render(, container); + ReactDOM.render(, container); - const nativeEvent = new KeyboardEvent('keydown', { - keyCode: 1337, - bubbles: true, - cancelable: true, + const nativeEvent = new KeyboardEvent('keydown', { + keyCode: 45, + bubbles: true, + cancelable: true, + }); + container.firstChild.dispatchEvent(nativeEvent); + expect(key).toBe('Insert'); + }); + }); + + describe('when keyCode is not recognized', () => { + it('returns Unidentified', () => { + let key = null; + class Comp extends React.Component { + render() { + return (key = e.key)} />; + } + } + + ReactDOM.render(, container); + + const nativeEvent = new KeyboardEvent('keydown', { + keyCode: 1337, + bubbles: true, + cancelable: true, + }); + container.firstChild.dispatchEvent(nativeEvent); + expect(key).toBe('Unidentified'); }); - container.firstChild.dispatchEvent(nativeEvent); - expect(key).toBe('Unidentified'); }); }); }); }); -}); +} diff --git a/packages/react-dom/src/fire/ReactFire.js b/packages/react-dom/src/fire/ReactFire.js index b1a7855ab24..1139e74d949 100644 --- a/packages/react-dom/src/fire/ReactFire.js +++ b/packages/react-dom/src/fire/ReactFire.js @@ -7,566 +7,159 @@ * @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} 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, 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'; + 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 './ReactFireEvents'; + 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'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; -let topLevelUpdateWarnings; -let warnOnInvalidCallback; -let didWarnAboutUnstableCreatePortal = false; - -if (__DEV__) { - if ( - typeof Map !== 'function' || - // $FlowIssue Flow incorrectly thinks Map has no prototype - Map.prototype == null || - typeof Map.prototype.forEach !== 'function' || - typeof Set !== 'function' || - // $FlowIssue Flow incorrectly thinks Set has no prototype - Set.prototype == null || - typeof Set.prototype.clear !== 'function' || - typeof Set.prototype.forEach !== 'function' - ) { - warningWithoutStack( - false, - 'React depends on Map and Set built-in types. Make sure that you load a ' + - 'polyfill in older browsers. https://fb.me/react-polyfills', - ); +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; } +}); - topLevelUpdateWarnings = (container: DOMContainer) => { - if (container._reactRootContainer && container.nodeType !== COMMENT_NODE) { - const hostInstance = findHostInstanceWithNoPortals( - container._reactRootContainer._internalRoot.current, - ); - if (hostInstance) { - warningWithoutStack( - hostInstance.parentNode === container, - 'render(...): It looks like the React-rendered content of this ' + - 'container was removed without using React. This is not ' + - 'supported and will cause errors. Instead, call ' + - 'ReactDOM.unmountComponentAtNode to empty a container.', - ); - } - } - - const isRootRenderedBySomeReact = !!container._reactRootContainer; - const rootEl = getReactRootElementInContainer(container); - const hasNonRootReactChild = !!(rootEl && getInstanceFromNode(rootEl)); - - warningWithoutStack( - !hasNonRootReactChild || isRootRenderedBySomeReact, - 'render(...): Replacing React-rendered children with a new root ' + - 'component. If you intended to update the children of this node, ' + - 'you should instead have the existing children update their state ' + - 'and render the new components instead of calling ReactDOM.render.', - ); - - warningWithoutStack( - container.nodeType !== ELEMENT_NODE || - !((container: any): Element).tagName || - ((container: any): Element).tagName.toUpperCase() !== 'BODY', - 'render(): Rendering components directly into document.body is ' + - 'discouraged, since its children are often manipulated by third-party ' + - 'scripts and browser extensions. This may lead to subtle ' + - 'reconciliation issues. Try rendering into a container element created ' + - '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, -}; - -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; - if (__DEV__) { - warnOnInvalidCallback(callback, 'render'); - } - if (callback !== null) { - work.then(callback); - } - 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; +function findDOMNode(componentOrElement: Element | ?React$Component) { if (__DEV__) { - warnOnInvalidCallback(callback, 'render'); - } - if (callback !== null) { - work.then(callback); - } - 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; + 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; } } - - 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 ')) - ); -} - -function getReactRootElementInContainer(container: any) { - if (!container) { + if (componentOrElement == null) { return null; } - - if (container.nodeType === DOCUMENT_NODE) { - return container.documentElement; - } else { - return container.firstChild; + if ((componentOrElement: any).nodeType === ELEMENT_NODE) { + return (componentOrElement: any); } + if (__DEV__) { + return findHostInstanceWithWarning(componentOrElement, 'findDOMNode'); + } + return findHostInstance(componentOrElement); } -function shouldHydrateDueToLegacyHeuristic(container) { - const rootElement = getReactRootElementInContainer(container); - return !!( - rootElement && - rootElement.nodeType === ELEMENT_NODE && - rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME) - ); -} - -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.', - ); - } +function cleanRootDOMContainer(container) { + 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.', - ); } + container.removeChild(rootSibling); } - // 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, - forceHydrate: boolean, - callback: ?Function, -) { - // TODO: Ensure all entry points contain this check - invariant( - isValidContainer(container), - 'Target container is not a DOM element.', - ); - - if (__DEV__) { - topLevelUpdateWarnings(container); - } - - // 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) { - // Initial mount - root = container._reactRootContainer = legacyCreateRootFromDOMContainer( - container, - forceHydrate, +function legacyRenderRoot(children, domContainer, callback, shouldHydrate) { + let root = roots.get(domContainer); + if (root === undefined) { + invariant( + isValidContainer(domContainer), + 'unmountComponentAtNode(...): Target container is not a DOM element.', ); + if (!shouldHydrate) { + cleanRootDOMContainer(domContainer); + } + root = new ReactRoot(domContainer, false, shouldHydrate); + roots.set(domContainer, root); if (typeof callback === 'function') { const originalCallback = callback; callback = function() { @@ -576,15 +169,7 @@ function legacyRenderSubtreeIntoContainer( } // Initial mount should not be batched. unbatchedUpdates(() => { - if (parentComponent != null) { - root.legacy_renderSubtreeIntoContainer( - parentComponent, - children, - callback, - ); - } else { - root.render(children, callback); - } + root.render(children, callback); }); } else { if (typeof callback === 'function') { @@ -594,262 +179,135 @@ function legacyRenderSubtreeIntoContainer( originalCallback.call(instance); }; } - // Update - if (parentComponent != null) { - root.legacy_renderSubtreeIntoContainer( - parentComponent, - children, - callback, - ); - } else { - root.render(children, callback); - } + root.render(children, callback); } return getPublicRootInstance(root._internalRoot); } -function createPortal( - children: ReactNodeList, - container: DOMContainer, - key: ?string = null, -) { - invariant( - isValidContainer(container), - 'Target container is not a DOM element.', - ); - // TODO: pass ReactDOM portal implementation as third argument - return createPortalImpl(children, container, null, key); +function render(children, domContainer, callback) { + return legacyRenderRoot(children, domContainer, callback, false); } -const ReactDOM: Object = { - createPortal, +function getReactRootElementInContainer(container: any) { + if (!container) { + return null; + } - 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 (container.nodeType === DOCUMENT_NODE) { + return container.documentElement; + } else { + return container.firstChild; + } +} + +function unmountComponentAtNode(domContainer) { + invariant( + isValidContainer(domContainer), + 'unmountComponentAtNode(...): Target container is not a DOM element.', + ); + if (roots.has(domContainer)) { if (__DEV__) { - return findHostInstanceWithWarning(componentOrElement, 'findDOMNode'); + const rootEl = getReactRootElementInContainer(domContainer); + const renderedByDifferentReact = rootEl && !getFiberFromDomNode(rootEl); + warningWithoutStack( + !renderedByDifferentReact, + "unmountComponentAtNode(): The node you're attempting to unmount " + + 'was rendered by another copy of React.', + ); } - return findHostInstance(componentOrElement); - }, - - hydrate(element: React$Node, container: DOMContainer, callback: ?Function) { - // TODO: throw or warn if we couldn't hydrate? - return legacyRenderSubtreeIntoContainer( - null, - element, - container, - true, - callback, - ); - }, + unbatchedUpdates(() => { + render(null, domContainer, () => { + roots.delete(domContainer); + }); + }); + return true; + } + if (__DEV__) { + const rootEl = getReactRootElementInContainer(domContainer); + const hasNonRootReactChild = !!(rootEl && getFiberFromDomNode(rootEl)); - render( - element: React$Element, - container: DOMContainer, - callback: ?Function, - ) { - return legacyRenderSubtreeIntoContainer( - null, - element, - container, - false, - callback, - ); - }, + // Check if the container itself is a React root node. + const isContainerReactRoot = + domContainer.nodeType === ELEMENT_NODE && + isValidContainer(domContainer.parentNode) && + !!domContainer.parentNode._reactRootContainer; - 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, - ); - }, - - 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.', - ); - } - - // 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)); - - // Check if the container itself is a React root node. - const isContainerReactRoot = - container.nodeType === ELEMENT_NODE && - isValidContainer(container.parentNode) && - !!container.parentNode._reactRootContainer; +function hydrate( + element: React$Node, + container: DOMContainer, + callback: ?Function, +) { + return legacyRenderRoot(element, container, callback, true); +} - 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.', - ); - } +/** + * Given a ReactDOMComponent or ReactDOMTextComponent, return the corresponding + * DOM node. + */ +function getDomNodeFromFiber(inst) { + 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; + } - return false; - } - }, + // 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.'); +} - // Temporary alias since we already shipped React 16 RC with it. - // TODO: remove in React 17. - unstable_createPortal(...args) { - if (!didWarnAboutUnstableCreatePortal) { - didWarnAboutUnstableCreatePortal = true; - lowPriorityWarning( - false, - 'The ReactDOM.unstable_createPortal() alias has been deprecated, ' + - 'and will be removed in React 17+. Update your code to use ' + - 'ReactDOM.createPortal() instead. It has the exact same API, ' + - 'but without the "unstable_" prefix.', - ); - } - return createPortal(...args); - }, +const noOp = () => {}; +const ReactDOM = { + createPortal, + findDOMNode, + flushSync, + hydrate, + render, + unmountComponentAtNode, + unstable_createRoot: undefined, unstable_batchedUpdates: batchedUpdates, - - unstable_interactiveUpdates: interactiveUpdates, - - flushSync: flushSync, - - unstable_createRoot: createRoot, unstable_flushControlled: flushControlled, - + unstable_interactiveUpdates: interactiveUpdates, __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..41a2d019a9f --- /dev/null +++ b/packages/react-dom/src/fire/ReactFireBatching.js @@ -0,0 +1,69 @@ +/** + * 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 { + 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..b90928d8464 --- /dev/null +++ b/packages/react-dom/src/fire/ReactFireComponent.js @@ -0,0 +1,635 @@ +/** + * 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, + isPropAnEvent, +} from './ReactFireUtils'; +import { + AUTOFOCUS, + CHILDREN, + DANGEROUSLY_SET_INNER_HTML, + HTML, + STYLE, + SUPPRESS_CONTENT_EDITABLE_WARNING, + SUPPRESS_HYDRATION_WARNING, +} from './ReactFireDOMConfig'; +import {mediaEventTypes} from './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, + getHostTextareaSelectProps, + initHostComponentTextareaWrapperState, +} from './controlled/ReactFireTextarea'; +import { + ensureListeningTo, + trapBubbledEvent, + trapClickOnNonInteractiveElement, +} from './ReactFireEvents'; +import {track} from './controlled/ReactFireValueTracking'; +import { + assertValidProps, + validateARIAProperties, + validateInputProperties, + validateShorthandPropertyCollisionInDev, + validateUnknownProperties, +} from './ReactFireValidation'; +import { + diffHydratedDOMElementProperties, + setDOMElementProperties, + updateDOMElementProperties, +} from './ReactFireComponentProperties'; + +import warning from 'shared/warning'; +import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber'; + +let didWarnShadyDOM = false; +let validatePropertiesInDevelopment; +let warnForInvalidEventListener; + +if (__DEV__) { + validatePropertiesInDevelopment = function(type, props) { + validateARIAProperties(type, props); + validateInputProperties(type, props); + validateUnknownProperties(type, props); + }; + + warnForInvalidEventListener = function(registrationName, listener) { + if (listener === false) { + warning( + false, + 'Expected `%s` listener to be a function, instead got `false`.\n\n' + + 'If you used to conditionally omit it with %s={condition && value}, ' + + 'pass %s={condition ? value : undefined} instead.', + registrationName, + registrationName, + registrationName, + ); + } else { + warning( + false, + 'Expected `%s` listener to be a function, instead got a value of `%s` type.', + registrationName, + typeof listener, + ); + } + }; +} + +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 = getHostTextareaSelectProps(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 < mediaEventTypes.length; i++) { + // TODO should this be bubbled still? I think it should be captured instead... + trapBubbledEvent(mediaEventTypes[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.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 = getHostTextareaSelectProps(domNode, lastRawProps); + nextProps = getHostTextareaSelectProps(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; + } + + assertValidProps(type, nextProps); + + let propName; + let styleName; + let styleUpdates = null; + for (propName in lastProps) { + if ( + nextProps.hasOwnProperty(propName) || + !lastProps.hasOwnProperty(propName) || + lastProps[propName] == null + ) { + continue; + } + if (propName === STYLE) { + const lastStyle = lastProps[propName]; + for (styleName in lastStyle) { + if (lastStyle.hasOwnProperty(styleName)) { + if (!styleUpdates) { + styleUpdates = {}; + } + styleUpdates[styleName] = ''; + } + } + } else if ( + propName === DANGEROUSLY_SET_INNER_HTML || + propName === CHILDREN + ) { + // Noop. This is handled by the clear text mechanism. + } else if ( + propName === SUPPRESS_CONTENT_EDITABLE_WARNING || + propName === SUPPRESS_HYDRATION_WARNING + ) { + // Noop + } else if (propName === AUTOFOCUS) { + // Noop. It doesn't work on updates anyway. + } else { + // For all other deleted properties we add it to the queue. We use + // the whitelist in the commit phase instead. + (updatePayload = updatePayload || []).push(propName, null); + } + } + for (propName in nextProps) { + const nextProp = nextProps[propName]; + const lastProp = lastProps != null ? lastProps[propName] : undefined; + if ( + !nextProps.hasOwnProperty(propName) || + nextProp === lastProp || + (nextProp == null && lastProp == null) + ) { + continue; + } + if (propName === STYLE) { + if (__DEV__) { + if (nextProp) { + // Freeze the next style object so that we can assume it won't be + // mutated. We have already warned for this in the past. + Object.freeze(nextProp); + } + } + if (lastProp) { + // Unset styles on `lastProp` but not on `nextProp`. + for (styleName in lastProp) { + if ( + lastProp.hasOwnProperty(styleName) && + (!nextProp || !nextProp.hasOwnProperty(styleName)) + ) { + if (!styleUpdates) { + styleUpdates = {}; + } + styleUpdates[styleName] = ''; + } + } + // Update styles that changed since `lastProp`. + for (styleName in nextProp) { + if ( + nextProp.hasOwnProperty(styleName) && + lastProp[styleName] !== nextProp[styleName] + ) { + if (!styleUpdates) { + styleUpdates = {}; + } + styleUpdates[styleName] = nextProp[styleName]; + } + } + } else { + // Relies on `updateStylesByID` not mutating `styleUpdates`. + if (!styleUpdates) { + if (!updatePayload) { + updatePayload = []; + } + updatePayload.push(propName, styleUpdates); + } + styleUpdates = nextProp; + } + } else if (propName === DANGEROUSLY_SET_INNER_HTML) { + const nextHtml = nextProp ? nextProp[HTML] : undefined; + const lastHtml = lastProp ? lastProp[HTML] : undefined; + if (nextHtml != null) { + if (lastHtml !== nextHtml) { + (updatePayload = updatePayload || []).push(propName, '' + nextHtml); + } + } else { + // TODO: It might be too late to clear this if we have children + // inserted already. + } + } else if (propName === CHILDREN) { + if ( + lastProp !== nextProp && + (typeof nextProp === 'string' || typeof nextProp === 'number') + ) { + (updatePayload = updatePayload || []).push(propName, '' + nextProp); + } + } else if ( + propName === SUPPRESS_CONTENT_EDITABLE_WARNING || + propName === SUPPRESS_HYDRATION_WARNING + ) { + // Noop + } else { + if (nextProp != null && isPropAnEvent(propName)) { + if (__DEV__ && typeof nextProp !== 'function') { + warnForInvalidEventListener(propName, nextProp); + } + ensureListeningTo(rootContainerElement, propName); + } + // For any other property we always add it to the queue and then we + // filter it out using the whitelist during the commit. + (updatePayload = updatePayload || []).push(propName, nextProp); + } + } + if (styleUpdates) { + if (__DEV__) { + validateShorthandPropertyCollisionInDev(styleUpdates, nextProps[STYLE]); + } + (updatePayload = updatePayload || []).push(STYLE, styleUpdates); + } + + return updatePayload; +} + +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': + //