From 664213e678bf89139b9bb4e3055590f9a6d50352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 20 Feb 2020 23:56:40 -0800 Subject: [PATCH] Rename Chunks API to Blocks (#18086) Sounds like this is the name we're going with. This also helps us distinguish it from other "chunking" implementation details. Add Modern Event System fork Fix FIX FIX Refine comment Support handling of listening to comment nodes Update error codes FXI FXI Add test cases and revise traversal Fix Refactor twak Rename Address traversal of comment nodes --- .../react-debug-tools/src/ReactDebugHooks.js | 4 +- .../ReactBrowserEventEmitter-test.internal.js | 12 +- .../__tests__/ReactDOMEventListener-test.js | 95 ++- .../src/__tests__/ReactTreeTraversal-test.js | 119 ++- .../react-dom/src/client/ReactDOMComponent.js | 130 ++- .../src/events/DOMModernPluginEventSystem.js | 253 ++++++ .../src/events/EnterLeaveEventPlugin.js | 43 +- .../src/events/ReactDOMEventListener.js | 96 ++- .../src/events/ReactDOMEventReplaying.js | 32 +- ...OMModernPluginEventSystem-test.internal.js | 786 ++++++++++++++++++ .../react-reconciler/src/ReactChildFiber.js | 20 +- packages/react-reconciler/src/ReactFiber.js | 16 +- .../src/ReactFiberBeginWork.js | 28 +- .../src/ReactFiberCommitWork.js | 14 +- .../src/ReactFiberCompleteWork.js | 8 +- .../src/ReactFiberWorkLoop.js | 4 +- ...eactChunks-test.js => ReactBlocks-test.js} | 12 +- .../src/ReactTestRenderer.js | 8 +- packages/react/src/React.js | 8 +- packages/react/src/{chunk.js => block.js} | 24 +- packages/shared/ReactFeatureFlags.js | 2 +- packages/shared/ReactSymbols.js | 2 +- packages/shared/ReactTreeTraversal.js | 52 +- packages/shared/ReactWorkTags.js | 2 +- .../forks/ReactFeatureFlags.native-fb.js | 2 +- .../forks/ReactFeatureFlags.native-oss.js | 2 +- .../forks/ReactFeatureFlags.persistent.js | 2 +- .../forks/ReactFeatureFlags.test-renderer.js | 2 +- .../ReactFeatureFlags.test-renderer.www.js | 2 +- .../shared/forks/ReactFeatureFlags.testing.js | 2 +- .../forks/ReactFeatureFlags.testing.www.js | 2 +- .../shared/forks/ReactFeatureFlags.www.js | 2 +- packages/shared/getComponentName.js | 4 +- packages/shared/isValidElementType.js | 4 +- scripts/error-codes/codes.json | 3 +- 35 files changed, 1552 insertions(+), 245 deletions(-) create mode 100644 packages/react-dom/src/events/DOMModernPluginEventSystem.js create mode 100644 packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js rename packages/react-reconciler/src/__tests__/{ReactChunks-test.js => ReactBlocks-test.js} (95%) rename packages/react/src/{chunk.js => block.js} (74%) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 99f7ae89056f7..d515bac08e9b9 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -25,7 +25,7 @@ import { SimpleMemoComponent, ContextProvider, ForwardRef, - Chunk, + Block, } from 'shared/ReactWorkTags'; type CurrentDispatcherRef = typeof ReactSharedInternals.ReactCurrentDispatcher; @@ -628,7 +628,7 @@ export function inspectHooksOfFiber( fiber.tag !== FunctionComponent && fiber.tag !== SimpleMemoComponent && fiber.tag !== ForwardRef && - fiber.tag !== Chunk + fiber.tag !== Block ) { throw new Error( 'Unknown Fiber. Needs to be a function component to inspect hooks.', diff --git a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js index 64099b4b8fed4..44c94a4cff623 100644 --- a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js @@ -17,6 +17,7 @@ let ReactDOMComponentTree; let listenToEvent; let ReactDOMEventListener; let ReactTestUtils; +let ReactFeatureFlags; let idCallOrder; const recordID = function(id) { @@ -60,13 +61,20 @@ describe('ReactBrowserEventEmitter', () => { jest.resetModules(); LISTENER.mockClear(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); EventPluginGetListener = require('legacy-events/getListener').default; EventPluginRegistry = require('legacy-events/EventPluginRegistry'); React = require('react'); ReactDOM = require('react-dom'); ReactDOMComponentTree = require('../client/ReactDOMComponentTree'); - listenToEvent = require('../events/DOMLegacyEventPluginSystem') - .legacyListenToEvent; + if (ReactFeatureFlags.enableModernEventSystem) { + listenToEvent = require('../events/DOMModernPluginEventSystem') + .listenToEvent; + } else { + listenToEvent = require('../events/DOMLegacyEventPluginSystem') + .legacyListenToEvent; + } + ReactDOMEventListener = require('../events/ReactDOMEventListener'); ReactTestUtils = require('react-dom/test-utils'); diff --git a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js index a1cd76874beb7..729044b6b4b91 100644 --- a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js @@ -12,6 +12,7 @@ describe('ReactDOMEventListener', () => { let React; let ReactDOM; + let ReactFeatureFlags = require('shared/ReactFeatureFlags'); beforeEach(() => { jest.resetModules(); @@ -19,29 +20,33 @@ describe('ReactDOMEventListener', () => { ReactDOM = require('react-dom'); }); - it('should dispatch events from outside React tree', () => { - const mock = jest.fn(); + // We attached events to roots with the modern system, + // so this test is no longer valid. + if (!ReactFeatureFlags.enableModernEventSystem) { + it('should dispatch events from outside React tree', () => { + const mock = jest.fn(); - const container = document.createElement('div'); - const node = ReactDOM.render(
, container); - const otherNode = document.createElement('h1'); - document.body.appendChild(container); - document.body.appendChild(otherNode); + const container = document.createElement('div'); + const node = ReactDOM.render(
, container); + const otherNode = document.createElement('h1'); + document.body.appendChild(container); + document.body.appendChild(otherNode); - try { - otherNode.dispatchEvent( - new MouseEvent('mouseout', { - bubbles: true, - cancelable: true, - relatedTarget: node, - }), - ); - expect(mock).toBeCalled(); - } finally { - document.body.removeChild(container); - document.body.removeChild(otherNode); - } - }); + try { + otherNode.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: node, + }), + ); + expect(mock).toBeCalled(); + } finally { + document.body.removeChild(container); + document.body.removeChild(otherNode); + } + }); + } describe('Propagation', () => { it('should propagate events one level down', () => { @@ -189,9 +194,25 @@ describe('ReactDOMEventListener', () => { // The first call schedules a render of '1' into the 'Child'. // However, we're batching so it isn't flushed yet. expect(mock.mock.calls[0][0]).toBe('Child'); - // The first call schedules a render of '2' into the 'Child'. - // We're still batching so it isn't flushed yet either. - expect(mock.mock.calls[1][0]).toBe('Child'); + if (ReactFeatureFlags.enableModernEventSystem) { + // As we have two roots, it means we have two event listeners. + // This also means we enter the event batching phase twice, + // flushing the child to be 1. + + // We don't have any good way of knowing if another event will + // occur because another event handler might invoke + // stopPropagation() along the way. After discussions internally + // with Sebastian, it seems that for now over-flushing should + // be fine, especially as the new event system is a breaking + // change anyway. We can maybe revisit this later as part of + // the work to refine this in the scheduler (maybe by leveraging + // isInputPending?). + expect(mock.mock.calls[1][0]).toBe('1'); + } else { + // The first call schedules a render of '2' into the 'Child'. + // We're still batching so it isn't flushed yet either. + expect(mock.mock.calls[1][0]).toBe('Child'); + } // By the time we leave the handler, the second update is flushed. expect(childNode.textContent).toBe('2'); } finally { @@ -362,13 +383,25 @@ describe('ReactDOMEventListener', () => { bubbles: false, }), ); - // Historically, we happened to not support onLoadStart - // on , and this test documents that lack of support. - // If we decide to support it in the future, we should change - // this line to expect 1 call. Note that fixing this would - // be simple but would require attaching a handler to each - // . So far nobody asked us for it. - expect(handleImgLoadStart).toHaveBeenCalledTimes(0); + if (ReactFeatureFlags.enableModernEventSystem) { + // As of the modern event system refactor, we now support + // this on . The reason for this, is because we now + // attach all media events to the "root" or "portal" in the + // capture phase, rather than the bubble phase. This allows + // us to assign less event listeners to individual elements, + // which also nicely allows us to support more without needing + // to add more individual code paths to support various + // events that do not bubble. + expect(handleImgLoadStart).toHaveBeenCalledTimes(1); + } else { + // Historically, we happened to not support onLoadStart + // on , and this test documents that lack of support. + // If we decide to support it in the future, we should change + // this line to expect 1 call. Note that fixing this would + // be simple but would require attaching a handler to each + // . So far nobody asked us for it. + expect(handleImgLoadStart).toHaveBeenCalledTimes(0); + } videoRef.current.dispatchEvent( new ProgressEvent('loadstart', { diff --git a/packages/react-dom/src/__tests__/ReactTreeTraversal-test.js b/packages/react-dom/src/__tests__/ReactTreeTraversal-test.js index 11428dd429017..5b0379827f0ee 100644 --- a/packages/react-dom/src/__tests__/ReactTreeTraversal-test.js +++ b/packages/react-dom/src/__tests__/ReactTreeTraversal-test.js @@ -11,6 +11,7 @@ let React; let ReactDOM; +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); const ChildComponent = ({id, eventHandler}) => (
{ expect(mockFn.mock.calls).toEqual(expectedCalls); }); - it('should enter from the window', () => { - const enterNode = document.getElementById('P_P1_C1__DIV'); - - const expectedCalls = [ - ['P', 'mouseenter'], - ['P_P1', 'mouseenter'], - ['P_P1_C1__DIV', 'mouseenter'], - ]; - - outerNode1.dispatchEvent( - new MouseEvent('mouseout', { - bubbles: true, - cancelable: true, - relatedTarget: enterNode, - }), - ); - - expect(mockFn.mock.calls).toEqual(expectedCalls); - }); - - it('should enter from the window to the shallowest', () => { - const enterNode = document.getElementById('P'); - - const expectedCalls = [['P', 'mouseenter']]; - - outerNode1.dispatchEvent( - new MouseEvent('mouseout', { - bubbles: true, - cancelable: true, - relatedTarget: enterNode, - }), - ); - - expect(mockFn.mock.calls).toEqual(expectedCalls); - }); + // This will not work with the modern event system that + // attaches event listeners to roots as the event below + // is being triggered on a node that React does not listen + // to any more. Instead we should fire mouseover. + if (ReactFeatureFlags.enableModernEventSystem) { + it('should enter from the window', () => { + const enterNode = document.getElementById('P_P1_C1__DIV'); + + const expectedCalls = [ + ['P', 'mouseenter'], + ['P_P1', 'mouseenter'], + ['P_P1_C1__DIV', 'mouseenter'], + ]; + + enterNode.dispatchEvent( + new MouseEvent('mouseover', { + bubbles: true, + cancelable: true, + relatedTarget: outerNode1, + }), + ); + + expect(mockFn.mock.calls).toEqual(expectedCalls); + }); + } else { + it('should enter from the window', () => { + const enterNode = document.getElementById('P_P1_C1__DIV'); + + const expectedCalls = [ + ['P', 'mouseenter'], + ['P_P1', 'mouseenter'], + ['P_P1_C1__DIV', 'mouseenter'], + ]; + + outerNode1.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: enterNode, + }), + ); + + expect(mockFn.mock.calls).toEqual(expectedCalls); + }); + } + + // This will not work with the modern event system that + // attaches event listeners to roots as the event below + // is being triggered on a node that React does not listen + // to any more. Instead we should fire mouseover. + if (ReactFeatureFlags.enableModernEventSystem) { + it('should enter from the window to the shallowest', () => { + const enterNode = document.getElementById('P'); + + const expectedCalls = [['P', 'mouseenter']]; + + enterNode.dispatchEvent( + new MouseEvent('mouseover', { + bubbles: true, + cancelable: true, + relatedTarget: outerNode1, + }), + ); + + expect(mockFn.mock.calls).toEqual(expectedCalls); + }); + } else { + it('should enter from the window to the shallowest', () => { + const enterNode = document.getElementById('P'); + + const expectedCalls = [['P', 'mouseenter']]; + + outerNode1.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: enterNode, + }), + ); + + expect(mockFn.mock.calls).toEqual(expectedCalls); + }); + } it('should leave to the window', () => { const leaveNode = document.getElementById('P_P1_C1__DIV'); diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index 854137dec6fa6..c00fc93008d90 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -76,7 +76,12 @@ import { shouldRemoveAttribute, } from '../shared/DOMProperty'; import assertValidProps from '../shared/assertValidProps'; -import {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE} from '../shared/HTMLNodeType'; +import { + DOCUMENT_NODE, + DOCUMENT_FRAGMENT_NODE, + ELEMENT_NODE, + COMMENT_NODE, +} from '../shared/HTMLNodeType'; import isCustomComponent from '../shared/isCustomComponent'; import possibleStandardNames from '../shared/possibleStandardNames'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; @@ -86,8 +91,11 @@ import {validateProperties as validateUnknownProperties} from '../shared/ReactDO import { enableDeprecatedFlareAPI, enableTrustedTypesIntegration, + enableModernEventSystem, } from 'shared/ReactFeatureFlags'; +import invariant from 'shared/invariant'; import {legacyListenToEvent} from '../events/DOMLegacyEventPluginSystem'; +import {listenToEvent} from '../events/DOMModernPluginEventSystem'; let didWarnInvalidHydration = false; let didWarnShadyDOM = false; @@ -266,13 +274,31 @@ function ensureListeningTo( rootContainerElement: Element | Node, registrationName: string, ): void { - const isDocumentOrFragment = - rootContainerElement.nodeType === DOCUMENT_NODE || - rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE; - const doc = isDocumentOrFragment - ? rootContainerElement - : rootContainerElement.ownerDocument; - legacyListenToEvent(registrationName, doc); + if (enableModernEventSystem) { + // If we have a comment node, then use the parent node, + // which should be an element. + const container = + rootContainerElement.nodeType === COMMENT_NODE + ? rootContainerElement.parentNode + : rootContainerElement; + // Containers can only ever be element nodes. We do not + // want to register events to document fragments or documents + // with the modern plugin event system. + invariant( + container && container.nodeType === ELEMENT_NODE, + 'ensureListeningTo(): recieved a container that was not an element node. ' + + 'This is likely a bug in React.', + ); + listenToEvent(registrationName, ((container: any): Element)); + } else { + const isDocumentOrFragment = + rootContainerElement.nodeType === DOCUMENT_NODE || + rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE; + const doc = isDocumentOrFragment + ? rootContainerElement + : rootContainerElement.ownerDocument; + legacyListenToEvent(registrationName, doc); + } } function getOwnerDocumentFromRootContainer( @@ -529,41 +555,55 @@ export function setInitialProperties( case 'iframe': case 'object': case 'embed': - trapBubbledEvent(TOP_LOAD, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_LOAD, domElement); + } props = rawProps; break; case 'video': case 'audio': - // Create listener for each media event - for (let i = 0; i < mediaEventTypes.length; i++) { - trapBubbledEvent(mediaEventTypes[i], domElement); + if (!enableModernEventSystem) { + // Create listener for each media event + for (let i = 0; i < mediaEventTypes.length; i++) { + trapBubbledEvent(mediaEventTypes[i], domElement); + } } props = rawProps; break; case 'source': - trapBubbledEvent(TOP_ERROR, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_ERROR, domElement); + } props = rawProps; break; case 'img': case 'image': case 'link': - trapBubbledEvent(TOP_ERROR, domElement); - trapBubbledEvent(TOP_LOAD, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_ERROR, domElement); + trapBubbledEvent(TOP_LOAD, domElement); + } props = rawProps; break; case 'form': - trapBubbledEvent(TOP_RESET, domElement); - trapBubbledEvent(TOP_SUBMIT, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_RESET, domElement); + trapBubbledEvent(TOP_SUBMIT, domElement); + } props = rawProps; break; case 'details': - trapBubbledEvent(TOP_TOGGLE, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_TOGGLE, domElement); + } props = rawProps; break; case 'input': ReactDOMInputInitWrapperState(domElement, rawProps); props = ReactDOMInputGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_INVALID, domElement); + } // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -575,7 +615,9 @@ export function setInitialProperties( case 'select': ReactDOMSelectInitWrapperState(domElement, rawProps); props = ReactDOMSelectGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_INVALID, domElement); + } // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -583,7 +625,9 @@ export function setInitialProperties( case 'textarea': ReactDOMTextareaInitWrapperState(domElement, rawProps); props = ReactDOMTextareaGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_INVALID, domElement); + } // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -925,34 +969,48 @@ export function diffHydratedProperties( case 'iframe': case 'object': case 'embed': - trapBubbledEvent(TOP_LOAD, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_LOAD, domElement); + } break; case 'video': case 'audio': - // Create listener for each media event - for (let i = 0; i < mediaEventTypes.length; i++) { - trapBubbledEvent(mediaEventTypes[i], domElement); + if (!enableModernEventSystem) { + // Create listener for each media event + for (let i = 0; i < mediaEventTypes.length; i++) { + trapBubbledEvent(mediaEventTypes[i], domElement); + } } break; case 'source': - trapBubbledEvent(TOP_ERROR, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_ERROR, domElement); + } break; case 'img': case 'image': case 'link': - trapBubbledEvent(TOP_ERROR, domElement); - trapBubbledEvent(TOP_LOAD, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_ERROR, domElement); + trapBubbledEvent(TOP_LOAD, domElement); + } break; case 'form': - trapBubbledEvent(TOP_RESET, domElement); - trapBubbledEvent(TOP_SUBMIT, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_RESET, domElement); + trapBubbledEvent(TOP_SUBMIT, domElement); + } break; case 'details': - trapBubbledEvent(TOP_TOGGLE, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_TOGGLE, domElement); + } break; case 'input': ReactDOMInputInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_INVALID, domElement); + } // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -962,14 +1020,18 @@ export function diffHydratedProperties( break; case 'select': ReactDOMSelectInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_INVALID, domElement); + } // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); break; case 'textarea': ReactDOMTextareaInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + trapBubbledEvent(TOP_INVALID, domElement); + } // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); diff --git a/packages/react-dom/src/events/DOMModernPluginEventSystem.js b/packages/react-dom/src/events/DOMModernPluginEventSystem.js new file mode 100644 index 0000000000000..2bac4bf688bc3 --- /dev/null +++ b/packages/react-dom/src/events/DOMModernPluginEventSystem.js @@ -0,0 +1,253 @@ +/** + * 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 type {AnyNativeEvent} from 'legacy-events/PluginModuleType'; +import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags'; +import type {Fiber} from 'react-reconciler/src/ReactFiber'; +import type {PluginModule} from 'legacy-events/PluginModuleType'; + +import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry'; +import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching'; +import {executeDispatchesInOrder} from 'legacy-events/EventPluginUtils'; +import {plugins} from 'legacy-events/EventPluginRegistry'; + +import {HostRoot, HostPortal} from 'shared/ReactWorkTags'; + +import {getListenerMapForElement} from './DOMEventListenerMap'; +import { + TOP_FOCUS, + TOP_LOAD, + TOP_ABORT, + TOP_CANCEL, + TOP_INVALID, + TOP_BLUR, + TOP_SCROLL, + TOP_CLOSE, + TOP_RESET, + TOP_SUBMIT, + TOP_CAN_PLAY, + TOP_CAN_PLAY_THROUGH, + TOP_DURATION_CHANGE, + TOP_EMPTIED, + TOP_ENCRYPTED, + TOP_ENDED, + TOP_ERROR, + TOP_WAITING, + TOP_VOLUME_CHANGE, + TOP_TIME_UPDATE, + TOP_SUSPEND, + TOP_STALLED, + TOP_SEEKING, + TOP_SEEKED, + TOP_PLAY, + TOP_PAUSE, + TOP_LOAD_START, + TOP_LOADED_DATA, + TOP_LOADED_METADATA, + TOP_RATE_CHANGE, + TOP_PROGRESS, + TOP_PLAYING, +} from './DOMTopLevelEventTypes'; +import {trapEventForPluginEventSystem} from './ReactDOMEventListener'; +import getEventTarget from './getEventTarget'; +import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; +import {COMMENT_NODE} from '../shared/HTMLNodeType'; + +const capturePhaseEvents = new Set([ + TOP_FOCUS, + TOP_BLUR, + TOP_SCROLL, + TOP_LOAD, + TOP_ABORT, + TOP_CANCEL, + TOP_CLOSE, + TOP_INVALID, + TOP_RESET, + TOP_SUBMIT, + TOP_ABORT, + TOP_CAN_PLAY, + TOP_CAN_PLAY_THROUGH, + TOP_DURATION_CHANGE, + TOP_EMPTIED, + TOP_ENCRYPTED, + TOP_ENDED, + TOP_ERROR, + TOP_LOADED_DATA, + TOP_LOADED_METADATA, + TOP_LOAD_START, + TOP_PAUSE, + TOP_PLAY, + TOP_PLAYING, + TOP_PROGRESS, + TOP_RATE_CHANGE, + TOP_SEEKED, + TOP_SEEKING, + TOP_STALLED, + TOP_SUSPEND, + TOP_TIME_UPDATE, + TOP_VOLUME_CHANGE, + TOP_WAITING, +]); + +const isArray = Array.isArray; + +function getContainerInstance( + container: Document | Element | Node, +): Fiber | null { + const rootContainer = (container: any)._reactRootContainer; + if (rootContainer) { + return rootContainer._internalRoot.current; + } + return getClosestInstanceFromNode(container); +} + +function getContainerNode(instance: Fiber): Element | Node | Document { + return instance.stateNode.containerInfo; +} + +function getContainerAncestorInstance( + containerInst: Fiber, + targetInst: null | Fiber, +): null | Fiber { + const containerInstAlt = containerInst.alternate; + let ancestor = targetInst; + let node = targetInst; + + while (node !== null) { + const tag = node.tag; + if (node === containerInst || node === containerInstAlt) { + return ancestor; + } else if (tag === HostRoot) { + const hostNode = getContainerNode(node); + const hostInst = getClosestInstanceFromNode(hostNode); + // If the portal's container is the same as our container + // instance and it's a comment node, then use the current + // ancestor we have. + if ( + ((hostInst === containerInst || hostInst === containerInstAlt) && + hostNode.nodeType === COMMENT_NODE) || + hostInst === null + ) { + return ancestor; + } + ancestor = node = hostInst; + continue; + } else if (tag === HostPortal) { + const portalNode = getContainerNode(node); + const portalInst = getClosestInstanceFromNode(portalNode); + // If the portal's container is the same as the one passed + // through, then use the current ancestor we have. + if (portalInst === containerInst || portalInst === containerInstAlt) { + return ancestor; + } + ancestor = node.return; + } + node = node.return; + } + return ancestor; +} + +function dispatchEventsForPlugins( + topLevelType: DOMTopLevelEventType, + eventSystemFlags: EventSystemFlags, + nativeEvent: AnyNativeEvent, + targetInst: null | Fiber, + container: Document | Element | Node, +): void { + const nativeEventTarget = getEventTarget(nativeEvent); + const syntheticEvents = []; + + for (let i = 0; i < plugins.length; i++) { + const possiblePlugin: PluginModule = plugins[i]; + if (possiblePlugin) { + const extractedEvents = possiblePlugin.extractEvents( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget, + eventSystemFlags, + container, + ); + if (extractedEvents) { + if (isArray(extractedEvents)) { + syntheticEvents.push(...(extractedEvents: any)); + } else { + syntheticEvents.push(extractedEvents); + } + } + } + } + for (let i = 0; i < syntheticEvents.length; i++) { + const syntheticEvent = syntheticEvents[i]; + executeDispatchesInOrder(syntheticEvent); + // Release the event from the pool if needed + if (!syntheticEvent.isPersistent()) { + syntheticEvent.constructor.release(syntheticEvent); + } + } +} + +export function listenToTopLevelEvent( + topLevelType: DOMTopLevelEventType, + container: Element, + listenerMap: Map void)>, +): void { + if (!listenerMap.has(topLevelType)) { + listenerMap.set(topLevelType, null); + const isCapturePhase = capturePhaseEvents.has(topLevelType); + trapEventForPluginEventSystem(container, topLevelType, isCapturePhase); + } +} + +export function listenToEvent( + registrationName: string, + container: Element, +): void { + const listenerMap = getListenerMapForElement(container); + const dependencies = registrationNameDependencies[registrationName]; + + for (let i = 0; i < dependencies.length; i++) { + const dependency = dependencies[i]; + listenToTopLevelEvent(dependency, container, listenerMap); + } +} + +export function dispatchEventForPluginEventSystem( + topLevelType: DOMTopLevelEventType, + eventSystemFlags: EventSystemFlags, + nativeEvent: AnyNativeEvent, + targetInst: null | Fiber, + container: Document | Element | Node, +): void { + const containerInst = getContainerInstance(container); + let ancestorTargetInst = targetInst; + if (containerInst !== null) { + if (containerInst === targetInst) { + return; + } + // Due to the fact we can render multiple roots or portals + // within one another, we need to find the correct ancestor + // to use as our target fiber instance. + ancestorTargetInst = getContainerAncestorInstance( + containerInst, + targetInst, + ); + } + + batchedEventUpdates(() => + dispatchEventsForPlugins( + topLevelType, + eventSystemFlags, + nativeEvent, + ancestorTargetInst, + container, + ), + ); +} diff --git a/packages/react-dom/src/events/EnterLeaveEventPlugin.js b/packages/react-dom/src/events/EnterLeaveEventPlugin.js index de9b130462fb0..4130cd709ad20 100644 --- a/packages/react-dom/src/events/EnterLeaveEventPlugin.js +++ b/packages/react-dom/src/events/EnterLeaveEventPlugin.js @@ -22,6 +22,7 @@ import { } from '../client/ReactDOMComponentTree'; import {HostComponent, HostText} from 'shared/ReactWorkTags'; import {getNearestMountedFiber} from 'react-reconciler/reflection'; +import {enableModernEventSystem} from 'shared/ReactFeatureFlags'; const eventTypes = { mouseEnter: { @@ -64,16 +65,26 @@ const EnterLeaveEventPlugin = { const isOutEvent = topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_POINTER_OUT; - if ( - isOverEvent && - (eventSystemFlags & IS_REPLAYED) === 0 && - (nativeEvent.relatedTarget || nativeEvent.fromElement) - ) { - // If this is an over event with a target, then we've already dispatched - // the event in the out event of the other target. If this is replayed, - // then it's because we couldn't dispatch against this target previously - // so we have to do it now instead. - return null; + if (isOverEvent && (eventSystemFlags & IS_REPLAYED) === 0) { + const related = nativeEvent.relatedTarget || nativeEvent.fromElement; + if (related) { + if (enableModernEventSystem) { + // Due to the fact we don't add listeners to the document with the + // modern event system and instead attach listeners to roots, we + // need to handle the over event case. To ensure this, we just need to + // make sure the node that we're coming from is managed by React. + const inst = getClosestInstanceFromNode(related); + if (inst !== null) { + return null; + } + } else { + // If this is an over event with a target, then we've already dispatched + // the event in the out event of the other target. If this is replayed, + // then it's because we couldn't dispatch against this target previously + // so we have to do it now instead. + return null; + } + } } if (!isOutEvent && !isOverEvent) { @@ -163,11 +174,13 @@ const EnterLeaveEventPlugin = { accumulateEnterLeaveDispatches(leave, enter, from, to); - // If we are not processing the first ancestor, then we - // should not process the same nativeEvent again, as we - // will have already processed it in the first ancestor. - if ((eventSystemFlags & IS_FIRST_ANCESTOR) === 0) { - return [leave]; + if (!enableModernEventSystem) { + // If we are not processing the first ancestor, then we + // should not process the same nativeEvent again, as we + // will have already processed it in the first ancestor. + if ((eventSystemFlags & IS_FIRST_ANCESTOR) === 0) { + return [leave]; + } } return [leave, enter]; diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index c63dc0ee648dd..f2cc6fa068d12 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -53,7 +53,10 @@ import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; import {getRawEventName} from './DOMTopLevelEventTypes'; import {passiveBrowserEventsSupported} from './checkPassiveEvents'; -import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags'; +import { + enableDeprecatedFlareAPI, + enableModernEventSystem, +} from 'shared/ReactFeatureFlags'; import { UserBlockingEvent, ContinuousEvent, @@ -61,6 +64,7 @@ import { } from 'shared/ReactTypes'; import {getEventPriorityForPluginSystem} from './DOMEventProperties'; import {dispatchEventForLegacyPluginEventSystem} from './DOMLegacyEventPluginSystem'; +import {dispatchEventForPluginEventSystem} from './DOMModernPluginEventSystem'; const { unstable_UserBlockingPriority: UserBlockingPriority, @@ -149,7 +153,7 @@ export function removeActiveResponderEventSystemEvent( } } -function trapEventForPluginEventSystem( +export function trapEventForPluginEventSystem( container: Document | Element | Node, topLevelType: DOMTopLevelEventType, capture: boolean, @@ -293,12 +297,22 @@ export function dispatchEvent( // in case the event system needs to trace it. if (enableDeprecatedFlareAPI) { if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) { - dispatchEventForLegacyPluginEventSystem( - topLevelType, - eventSystemFlags, - nativeEvent, - null, - ); + if (enableModernEventSystem) { + dispatchEventForPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + null, + container, + ); + } else { + dispatchEventForLegacyPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + null, + ); + } } if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) { // React Flare event system @@ -311,12 +325,22 @@ export function dispatchEvent( ); } } else { - dispatchEventForLegacyPluginEventSystem( - topLevelType, - eventSystemFlags, - nativeEvent, - null, - ); + if (enableModernEventSystem) { + dispatchEventForPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + null, + container, + ); + } else { + dispatchEventForLegacyPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + null, + ); + } } } @@ -372,12 +396,22 @@ export function attemptToDispatchEvent( if (enableDeprecatedFlareAPI) { if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) { - dispatchEventForLegacyPluginEventSystem( - topLevelType, - eventSystemFlags, - nativeEvent, - targetInst, - ); + if (enableModernEventSystem) { + dispatchEventForPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + container, + ); + } else { + dispatchEventForLegacyPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + ); + } } if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) { // React Flare event system @@ -390,12 +424,22 @@ export function attemptToDispatchEvent( ); } } else { - dispatchEventForLegacyPluginEventSystem( - topLevelType, - eventSystemFlags, - nativeEvent, - targetInst, - ); + if (enableModernEventSystem) { + dispatchEventForPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + container, + ); + } else { + dispatchEventForLegacyPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + ); + } } // We're not blocked on anything. return null; diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index 48ff7a5b00190..c7b695cd00c8f 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -17,6 +17,7 @@ import type {DOMContainer} from '../client/ReactDOM'; import { enableDeprecatedFlareAPI, enableSelectiveHydration, + enableModernEventSystem, } from 'shared/ReactFeatureFlags'; import { unstable_runWithPriority as runWithPriority, @@ -119,6 +120,7 @@ import { } from './DOMTopLevelEventTypes'; import {IS_REPLAYED} from 'legacy-events/EventSystemFlags'; import {legacyListenToTopLevelEvent} from './DOMLegacyEventPluginSystem'; +import {listenToTopLevelEvent} from './DOMModernPluginEventSystem'; type QueuedReplayableEvent = {| blockedOn: null | Container | SuspenseInstance, @@ -212,12 +214,22 @@ export function isReplayableDiscreteEvent( return discreteReplayableEvents.indexOf(eventType) > -1; } +function trapReplayableEventForContainer( + topLevelType: DOMTopLevelEventType, + container: DOMContainer, + listenerMap: Map void)>, +) { + listenToTopLevelEvent(topLevelType, ((container: any): Element), listenerMap); +} + function trapReplayableEventForDocument( topLevelType: DOMTopLevelEventType, document: Document, listenerMap: Map void)>, ) { - legacyListenToTopLevelEvent(topLevelType, document, listenerMap); + if (!enableModernEventSystem) { + legacyListenToTopLevelEvent(topLevelType, document, listenerMap); + } if (enableDeprecatedFlareAPI) { // Trap events for the responder system. const topLevelTypeString = unsafeCastDOMTopLevelTypeToString(topLevelType); @@ -242,12 +254,30 @@ export function eagerlyTrapReplayableEvents( document: Document, ) { const listenerMapForDoc = getListenerMapForElement(document); + let listenerMapForContainer; + if (enableModernEventSystem) { + listenerMapForContainer = getListenerMapForElement(container); + } // Discrete discreteReplayableEvents.forEach(topLevelType => { + if (enableModernEventSystem) { + trapReplayableEventForContainer( + topLevelType, + container, + listenerMapForContainer, + ); + } trapReplayableEventForDocument(topLevelType, document, listenerMapForDoc); }); // Continuous continuousReplayableEvents.forEach(topLevelType => { + if (enableModernEventSystem) { + trapReplayableEventForContainer( + topLevelType, + container, + listenerMapForContainer, + ); + } trapReplayableEventForDocument(topLevelType, document, listenerMapForDoc); }); } diff --git a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js new file mode 100644 index 0000000000000..549dee97a01b9 --- /dev/null +++ b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js @@ -0,0 +1,786 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +let React; +let ReactFeatureFlags; +let ReactDOM; +let ReactDOMServer; +let Scheduler; + +function dispatchEvent(element, type) { + const event = document.createEvent('Event'); + event.initEvent(type, true, true); + element.dispatchEvent(event); +} + +function dispatchClickEvent(element) { + dispatchEvent(element, 'click'); +} + +describe('DOMModernPluginEventSystem', () => { + let container; + + beforeEach(() => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.enableModernEventSystem = true; + React = require('react'); + ReactDOM = require('react-dom'); + Scheduler = require('scheduler'); + ReactDOMServer = require('react-dom/server'); + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + container = null; + }); + + it('handle propagation of click events', () => { + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const log = []; + const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); + const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget])); + + function Test() { + return ( + + ); + } + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + dispatchClickEvent(buttonElement); + expect(onClick).toHaveBeenCalledTimes(1); + expect(onClickCapture).toHaveBeenCalledTimes(1); + expect(log[0]).toEqual(['capture', buttonElement]); + expect(log[1]).toEqual(['bubble', buttonElement]); + + let divElement = divRef.current; + dispatchClickEvent(divElement); + expect(onClick).toHaveBeenCalledTimes(3); + expect(onClickCapture).toHaveBeenCalledTimes(3); + expect(log[2]).toEqual(['capture', buttonElement]); + expect(log[3]).toEqual(['capture', divElement]); + expect(log[4]).toEqual(['bubble', divElement]); + expect(log[5]).toEqual(['bubble', buttonElement]); + }); + + it('handle propagation of click events between roots', () => { + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const childRef = React.createRef(); + const log = []; + const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); + const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget])); + + function Child() { + return ( +
+ Click me! +
+ ); + } + + function Parent() { + return ( + + ); + } + + ReactDOM.render(, container); + ReactDOM.render(, childRef.current); + + let buttonElement = buttonRef.current; + dispatchClickEvent(buttonElement); + expect(onClick).toHaveBeenCalledTimes(1); + expect(onClickCapture).toHaveBeenCalledTimes(1); + expect(log[0]).toEqual(['capture', buttonElement]); + expect(log[1]).toEqual(['bubble', buttonElement]); + + let divElement = divRef.current; + dispatchClickEvent(divElement); + expect(onClick).toHaveBeenCalledTimes(3); + expect(onClickCapture).toHaveBeenCalledTimes(3); + expect(log[2]).toEqual(['capture', divElement]); + expect(log[3]).toEqual(['bubble', divElement]); + expect(log[4]).toEqual(['capture', buttonElement]); + expect(log[5]).toEqual(['bubble', buttonElement]); + }); + + it('handle propagation of click events between disjointed roots', () => { + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const log = []; + const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); + const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget])); + + function Child() { + return ( +
+ Click me! +
+ ); + } + + function Parent() { + return ( + + ); + } + + // We use a comment node here, then mount to it + const disjointedNode = document.createComment( + ' react-mount-point-unstable ', + ); + ReactDOM.render(, container); + spanRef.current.appendChild(disjointedNode); + ReactDOM.render(, disjointedNode); + + let buttonElement = buttonRef.current; + dispatchClickEvent(buttonElement); + expect(onClick).toHaveBeenCalledTimes(1); + expect(onClickCapture).toHaveBeenCalledTimes(1); + expect(log[0]).toEqual(['capture', buttonElement]); + expect(log[1]).toEqual(['bubble', buttonElement]); + + let divElement = divRef.current; + dispatchClickEvent(divElement); + expect(onClick).toHaveBeenCalledTimes(3); + expect(onClickCapture).toHaveBeenCalledTimes(3); + expect(log[2]).toEqual(['capture', divElement]); + expect(log[3]).toEqual(['bubble', divElement]); + expect(log[4]).toEqual(['capture', buttonElement]); + expect(log[5]).toEqual(['bubble', buttonElement]); + }); + + it('handle propagation of click events between portals', () => { + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const log = []; + const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); + const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget])); + + const portalElement = document.createElement('div'); + document.body.appendChild(portalElement); + + function Child() { + return ( +
+ Click me! +
+ ); + } + + function Parent() { + return ( + + ); + } + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + dispatchClickEvent(buttonElement); + expect(onClick).toHaveBeenCalledTimes(1); + expect(onClickCapture).toHaveBeenCalledTimes(1); + expect(log[0]).toEqual(['capture', buttonElement]); + expect(log[1]).toEqual(['bubble', buttonElement]); + + let divElement = divRef.current; + dispatchClickEvent(divElement); + expect(onClick).toHaveBeenCalledTimes(3); + expect(onClickCapture).toHaveBeenCalledTimes(3); + expect(log[2]).toEqual(['capture', buttonElement]); + expect(log[3]).toEqual(['capture', divElement]); + expect(log[4]).toEqual(['bubble', divElement]); + expect(log[5]).toEqual(['bubble', buttonElement]); + + document.body.removeChild(portalElement); + }); + + it('handle click events on document.body portals', () => { + const log = []; + + function Child({label}) { + return
log.push(label)}>{label}
; + } + + function Parent() { + return ( + <> + {ReactDOM.createPortal(, document.body)} + {ReactDOM.createPortal(, document.body)} + + ); + } + + ReactDOM.render(, container); + + const second = document.body.lastChild; + expect(second.textContent).toEqual('second'); + dispatchClickEvent(second); + + expect(log).toEqual(['second']); + + const first = second.previousSibling; + expect(first.textContent).toEqual('first'); + dispatchClickEvent(first); + + expect(log).toEqual(['second', 'first']); + }); + + it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => { + let suspend = false; + let resolve; + let promise = new Promise(resolvePromise => (resolve = resolvePromise)); + + let clicks = 0; + let childSlotRef = React.createRef(); + + function Parent() { + return
clicks++} ref={childSlotRef} />; + } + + function Child({text}) { + if (suspend) { + throw promise; + } else { + return Click me; + } + } + + function App() { + // The root is a Suspense boundary. + return ( + + + + ); + } + + suspend = false; + let finalHTML = ReactDOMServer.renderToString(); + + let parentContainer = document.createElement('div'); + let childContainer = document.createElement('div'); + + // We need this to be in the document since we'll dispatch events on it. + document.body.appendChild(parentContainer); + + // We're going to use a different root as a parent. + // This lets us detect whether an event goes through React's event system. + let parentRoot = ReactDOM.createRoot(parentContainer); + parentRoot.render(); + Scheduler.unstable_flushAll(); + + childSlotRef.current.appendChild(childContainer); + + childContainer.innerHTML = finalHTML; + + let a = childContainer.getElementsByTagName('a')[0]; + + suspend = true; + + // Hydrate asynchronously. + let root = ReactDOM.createRoot(childContainer, {hydrate: true}); + root.render(); + jest.runAllTimers(); + Scheduler.unstable_flushAll(); + + // The Suspense boundary is not yet hydrated. + a.click(); + expect(clicks).toBe(0); + + // Resolving the promise so that rendering can complete. + suspend = false; + resolve(); + await promise; + + Scheduler.unstable_flushAll(); + jest.runAllTimers(); + + // We're now full hydrated. + + expect(clicks).toBe(1); + + document.body.removeChild(parentContainer); + }); + + it('handle click events on document.body dynamic portals', () => { + const log = []; + + function Parent() { + const ref = React.useRef(null); + const [portal, setPortal] = React.useState(null); + + React.useEffect(() => { + setPortal( + ReactDOM.createPortal( + log.push('child')} id="child" />, + ref.current, + ), + ); + }); + + return ( +
log.push('parent')} id="parent"> + {portal} +
+ ); + } + + ReactDOM.render(, container); + + const parent = container.lastChild; + expect(parent.id).toEqual('parent'); + dispatchClickEvent(parent); + + expect(log).toEqual(['parent']); + + const child = parent.lastChild; + expect(child.id).toEqual('child'); + dispatchClickEvent(child); + + // we add both 'child' and 'parent' due to bubbling + expect(log).toEqual(['parent', 'child', 'parent']); + }); + + // Slight alteration to the last test, to catch + // a subtle difference in traversal. + it('handle click events on document.body dynamic portals #2', () => { + const log = []; + + function Parent() { + const ref = React.useRef(null); + const [portal, setPortal] = React.useState(null); + + React.useEffect(() => { + setPortal( + ReactDOM.createPortal( + log.push('child')} id="child" />, + ref.current, + ), + ); + }); + + return ( +
log.push('parent')} id="parent"> +
{portal}
+
+ ); + } + + ReactDOM.render(, container); + + const parent = container.lastChild; + expect(parent.id).toEqual('parent'); + dispatchClickEvent(parent); + + expect(log).toEqual(['parent']); + + const child = parent.lastChild; + expect(child.id).toEqual('child'); + dispatchClickEvent(child); + + // we add both 'child' and 'parent' due to bubbling + expect(log).toEqual(['parent', 'child', 'parent']); + }); + + it('native stopPropagation on click events between portals', () => { + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const middelDivRef = React.createRef(); + const log = []; + const onClick = jest.fn(e => log.push(['bubble', e.currentTarget])); + const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget])); + + const portalElement = document.createElement('div'); + document.body.appendChild(portalElement); + + function Child() { + return ( +
+
+ Click me! +
+
+ ); + } + + function Parent() { + React.useLayoutEffect(() => { + // This should prevent the portalElement listeners from + // capturing the events in the bubble phase. + middelDivRef.current.addEventListener('click', e => { + e.stopPropagation(); + }); + }); + + return ( + + ); + } + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + dispatchClickEvent(buttonElement); + expect(onClick).toHaveBeenCalledTimes(1); + expect(onClickCapture).toHaveBeenCalledTimes(1); + expect(log[0]).toEqual(['capture', buttonElement]); + expect(log[1]).toEqual(['bubble', buttonElement]); + + let divElement = divRef.current; + dispatchClickEvent(divElement); + expect(onClick).toHaveBeenCalledTimes(1); + expect(onClickCapture).toHaveBeenCalledTimes(1); + + document.body.removeChild(portalElement); + }); + + it('handle propagation of focus events', () => { + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const log = []; + const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget])); + const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget])); + + function Test() { + return ( + + ); + } + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + buttonElement.focus(); + expect(onFocus).toHaveBeenCalledTimes(1); + expect(onFocusCapture).toHaveBeenCalledTimes(1); + expect(log[0]).toEqual(['capture', buttonElement]); + expect(log[1]).toEqual(['bubble', buttonElement]); + + let divElement = divRef.current; + divElement.focus(); + expect(onFocus).toHaveBeenCalledTimes(3); + expect(onFocusCapture).toHaveBeenCalledTimes(3); + expect(log[2]).toEqual(['capture', buttonElement]); + expect(log[3]).toEqual(['capture', divElement]); + expect(log[4]).toEqual(['bubble', divElement]); + expect(log[5]).toEqual(['bubble', buttonElement]); + }); + + it('handle propagation of focus events between roots', () => { + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const childRef = React.createRef(); + const log = []; + const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget])); + const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget])); + + function Child() { + return ( +
+ Click me! +
+ ); + } + + function Parent() { + return ( + + ); + } + + ReactDOM.render(, container); + ReactDOM.render(, childRef.current); + + let buttonElement = buttonRef.current; + buttonElement.focus(); + expect(onFocus).toHaveBeenCalledTimes(1); + expect(onFocusCapture).toHaveBeenCalledTimes(1); + expect(log[0]).toEqual(['capture', buttonElement]); + expect(log[1]).toEqual(['bubble', buttonElement]); + + let divElement = divRef.current; + divElement.focus(); + expect(onFocus).toHaveBeenCalledTimes(3); + expect(onFocusCapture).toHaveBeenCalledTimes(3); + expect(log[2]).toEqual(['capture', buttonElement]); + expect(log[3]).toEqual(['bubble', buttonElement]); + expect(log[4]).toEqual(['capture', divElement]); + expect(log[5]).toEqual(['bubble', divElement]); + }); + + it('handle propagation of focus events between portals', () => { + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const log = []; + const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget])); + const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget])); + + const portalElement = document.createElement('div'); + document.body.appendChild(portalElement); + + function Child() { + return ( +
+ Click me! +
+ ); + } + + function Parent() { + return ( + + ); + } + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + buttonElement.focus(); + expect(onFocus).toHaveBeenCalledTimes(1); + expect(onFocusCapture).toHaveBeenCalledTimes(1); + expect(log[0]).toEqual(['capture', buttonElement]); + expect(log[1]).toEqual(['bubble', buttonElement]); + + let divElement = divRef.current; + divElement.focus(); + expect(onFocus).toHaveBeenCalledTimes(3); + expect(onFocusCapture).toHaveBeenCalledTimes(3); + expect(log[2]).toEqual(['capture', buttonElement]); + expect(log[3]).toEqual(['capture', divElement]); + expect(log[4]).toEqual(['bubble', divElement]); + expect(log[5]).toEqual(['bubble', buttonElement]); + + document.body.removeChild(portalElement); + }); + + it('native stopPropagation on focus events between portals', () => { + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const middelDivRef = React.createRef(); + const log = []; + const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget])); + const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget])); + + const portalElement = document.createElement('div'); + document.body.appendChild(portalElement); + + function Child() { + return ( +
+
+ Click me! +
+
+ ); + } + + function Parent() { + React.useLayoutEffect(() => { + // This should prevent the portalElement listeners from + // capturing the events in the bubble phase. + middelDivRef.current.addEventListener('click', e => { + e.stopPropagation(); + }); + }); + + return ( + + ); + } + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + buttonElement.focus(); + expect(onFocus).toHaveBeenCalledTimes(1); + expect(onFocusCapture).toHaveBeenCalledTimes(1); + expect(log[0]).toEqual(['capture', buttonElement]); + expect(log[1]).toEqual(['bubble', buttonElement]); + + let divElement = divRef.current; + divElement.focus(); + expect(onFocus).toHaveBeenCalledTimes(1); + expect(onFocusCapture).toHaveBeenCalledTimes(1); + + document.body.removeChild(portalElement); + }); +}); diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 7888d7a34a64a..59508f776e1ad 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -19,7 +19,7 @@ import { REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE, REACT_PORTAL_TYPE, - REACT_CHUNK_TYPE, + REACT_BLOCK_TYPE, } from 'shared/ReactSymbols'; import { FunctionComponent, @@ -27,10 +27,10 @@ import { HostText, HostPortal, Fragment, - Chunk, + Block, } from 'shared/ReactWorkTags'; import invariant from 'shared/invariant'; -import {warnAboutStringRefs, enableChunksAPI} from 'shared/ReactFeatureFlags'; +import {warnAboutStringRefs, enableBlocksAPI} from 'shared/ReactFeatureFlags'; import { createWorkInProgress, @@ -416,9 +416,9 @@ function ChildReconciler(shouldTrackSideEffects) { } return existing; } else if ( - enableChunksAPI && - current.tag === Chunk && - element.type.$$typeof === REACT_CHUNK_TYPE && + enableBlocksAPI && + current.tag === Block && + element.type.$$typeof === REACT_BLOCK_TYPE && element.type.render === current.type.render ) { // Same as above but also update the .type field. @@ -1175,10 +1175,10 @@ function ChildReconciler(shouldTrackSideEffects) { } break; } - case Chunk: - if (enableChunksAPI) { + case Block: + if (enableBlocksAPI) { if ( - element.type.$$typeof === REACT_CHUNK_TYPE && + element.type.$$typeof === REACT_BLOCK_TYPE && element.type.render === child.type.render ) { deleteRemainingChildren(returnFiber, child.sibling); @@ -1192,7 +1192,7 @@ function ChildReconciler(shouldTrackSideEffects) { return existing; } } - // We intentionally fallthrough here if enableChunksAPI is not on. + // We intentionally fallthrough here if enableBlocksAPI is not on. // eslint-disable-next-lined no-fallthrough default: { if ( diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 67aabe830bdfb..35a6c1e6e77a7 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -33,7 +33,7 @@ import { enableFundamentalAPI, enableUserTimingAPI, enableScopeAPI, - enableChunksAPI, + enableBlocksAPI, } from 'shared/ReactFeatureFlags'; import {NoEffect, Placement} from 'shared/ReactSideEffectTags'; import {ConcurrentRoot, BlockingRoot} from 'shared/ReactRootTags'; @@ -59,7 +59,7 @@ import { LazyComponent, FundamentalComponent, ScopeComponent, - Chunk, + Block, } from 'shared/ReactWorkTags'; import getComponentName from 'shared/getComponentName'; @@ -91,7 +91,7 @@ import { REACT_LAZY_TYPE, REACT_FUNDAMENTAL_TYPE, REACT_SCOPE_TYPE, - REACT_CHUNK_TYPE, + REACT_BLOCK_TYPE, } from 'shared/ReactSymbols'; let hasBadMapPolyfill; @@ -391,9 +391,9 @@ export function resolveLazyComponentTag(Component: Function): WorkTag { if ($$typeof === REACT_MEMO_TYPE) { return MemoComponent; } - if (enableChunksAPI) { - if ($$typeof === REACT_CHUNK_TYPE) { - return Chunk; + if (enableBlocksAPI) { + if ($$typeof === REACT_BLOCK_TYPE) { + return Block; } } } @@ -676,8 +676,8 @@ export function createFiberFromTypeAndProps( fiberTag = LazyComponent; resolvedType = null; break getTag; - case REACT_CHUNK_TYPE: - fiberTag = Chunk; + case REACT_BLOCK_TYPE: + fiberTag = Block; break getTag; case REACT_FUNDAMENTAL_TYPE: if (enableFundamentalAPI) { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index ff990e5801a99..8eb9f9ffb9502 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -42,7 +42,7 @@ import { IncompleteClassComponent, FundamentalComponent, ScopeComponent, - Chunk, + Block, } from 'shared/ReactWorkTags'; import { NoEffect, @@ -65,7 +65,7 @@ import { enableFundamentalAPI, warnAboutDefaultPropsOnFunctionComponents, enableScopeAPI, - enableChunksAPI, + enableBlocksAPI, } from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import shallowEqual from 'shared/shallowEqual'; @@ -701,10 +701,10 @@ function updateFunctionComponent( return workInProgress.child; } -function updateChunk( +function updateBlock( current: Fiber | null, workInProgress: Fiber, - chunk: any, + block: any, nextProps: any, renderExpirationTime: ExpirationTime, ) { @@ -712,8 +712,8 @@ function updateChunk( // hasn't yet mounted. This happens after the first render suspends. // We'll need to figure out if this is fine or can cause issues. - const render = chunk.render; - const data = chunk.query(); + const render = block.render; + const data = block.query(); // The rest is a fork of updateFunctionComponent let nextChildren; @@ -1215,10 +1215,10 @@ function mountLazyComponent( ); return child; } - case Chunk: { - if (enableChunksAPI) { + case Block: { + if (enableBlocksAPI) { // TODO: Resolve for Hot Reloading. - child = updateChunk( + child = updateBlock( null, workInProgress, Component, @@ -3289,14 +3289,14 @@ function beginWork( } break; } - case Chunk: { - if (enableChunksAPI) { - const chunk = workInProgress.type; + case Block: { + if (enableBlocksAPI) { + const block = workInProgress.type; const props = workInProgress.pendingProps; - return updateChunk( + return updateBlock( current, workInProgress, - chunk, + block, props, renderExpirationTime, ); diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 73942936e9a4a..857928aa4c3dd 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -53,7 +53,7 @@ import { SuspenseListComponent, FundamentalComponent, ScopeComponent, - Chunk, + Block, } from 'shared/ReactWorkTags'; import { invokeGuardedCallback, @@ -249,7 +249,7 @@ function commitBeforeMutationLifeCycles( case FunctionComponent: case ForwardRef: case SimpleMemoComponent: - case Chunk: { + case Block: { return; } case ClassComponent: { @@ -426,7 +426,7 @@ export function commitPassiveHookEffects(finishedWork: Fiber): void { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: - case Chunk: { + case Block: { // TODO (#17945) We should call all passive destroy functions (for all fibers) // before calling any create functions. The current approach only serializes // these for a single fiber. @@ -450,7 +450,7 @@ function commitLifeCycles( case FunctionComponent: case ForwardRef: case SimpleMemoComponent: - case Chunk: { + case Block: { // At this point layout effects have already been destroyed (during mutation phase). // This is done to prevent sibling component effects from interfering with each other, // e.g. a destroy function in one component should never override a ref set @@ -779,7 +779,7 @@ function commitUnmount( case ForwardRef: case MemoComponent: case SimpleMemoComponent: - case Chunk: { + case Block: { const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any); if (updateQueue !== null) { const lastEffect = updateQueue.lastEffect; @@ -1360,7 +1360,7 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { case ForwardRef: case MemoComponent: case SimpleMemoComponent: - case Chunk: { + case Block: { // Layout effects are destroyed during the mutation phase so that all // destroy functions for all fibers are called before any create functions. // This prevents sibling component effects from interfering with each other, @@ -1403,7 +1403,7 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { case ForwardRef: case MemoComponent: case SimpleMemoComponent: - case Chunk: { + case Block: { // Layout effects are destroyed during the mutation phase so that all // destroy functions for all fibers are called before any create functions. // This prevents sibling component effects from interfering with each other, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 1138dc57116b9..e7ecaf3343b82 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -51,7 +51,7 @@ import { IncompleteClassComponent, FundamentalComponent, ScopeComponent, - Chunk, + Block, } from 'shared/ReactWorkTags'; import {NoMode, BlockingMode} from './ReactTypeOfMode'; import { @@ -119,7 +119,7 @@ import { enableDeprecatedFlareAPI, enableFundamentalAPI, enableScopeAPI, - enableChunksAPI, + enableBlocksAPI, } from 'shared/ReactFeatureFlags'; import { markSpawnedWork, @@ -1295,8 +1295,8 @@ function completeWork( } break; } - case Chunk: - if (enableChunksAPI) { + case Block: + if (enableBlocksAPI) { return null; } break; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 1c03f8f50a412..8c3e4dd170a8b 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -88,7 +88,7 @@ import { ForwardRef, MemoComponent, SimpleMemoComponent, - Chunk, + Block, } from 'shared/ReactWorkTags'; import { NoEffect, @@ -2682,7 +2682,7 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { tag !== ForwardRef && tag !== MemoComponent && tag !== SimpleMemoComponent && - tag !== Chunk + tag !== Block ) { // Only warn for user-defined components, not internal ones like Suspense. return; diff --git a/packages/react-reconciler/src/__tests__/ReactChunks-test.js b/packages/react-reconciler/src/__tests__/ReactBlocks-test.js similarity index 95% rename from packages/react-reconciler/src/__tests__/ReactChunks-test.js rename to packages/react-reconciler/src/__tests__/ReactBlocks-test.js index 28ed482ebdb84..280eef0609a14 100644 --- a/packages/react-reconciler/src/__tests__/ReactChunks-test.js +++ b/packages/react-reconciler/src/__tests__/ReactBlocks-test.js @@ -12,17 +12,17 @@ let React; let ReactNoop; let useState; let Suspense; -let chunk; +let block; let readString; -describe('ReactChunks', () => { +describe('ReactBlocks', () => { beforeEach(() => { jest.resetModules(); React = require('react'); ReactNoop = require('react-noop-renderer'); - chunk = React.chunk; + block = React.block; useState = React.useState; Suspense = React.Suspense; let cache = new Map(); @@ -63,7 +63,7 @@ describe('ReactChunks', () => { ); } - let loadUser = chunk(Query, Render); + let loadUser = block(Query, Render); function App({User}) { return ( @@ -102,7 +102,7 @@ describe('ReactChunks', () => { ); } - let loadUser = chunk(Query, Render); + let loadUser = block(Query, Render); function App({User}) { return ( @@ -164,7 +164,7 @@ describe('ReactChunks', () => { ); } - let loadUser = chunk(Query, Render); + let loadUser = block(Query, Render); function App({User}) { return ( diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index e8ec6cbd53e6a..0bd57d4702645 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -37,7 +37,7 @@ import { Profiler, MemoComponent, SimpleMemoComponent, - Chunk, + Block, IncompleteClassComponent, ScopeComponent, } from 'shared/ReactWorkTags'; @@ -188,9 +188,9 @@ function toTree(node: ?Fiber) { instance: null, rendered: childrenToTree(node.child), }; - case Chunk: + case Block: return { - nodeType: 'chunk', + nodeType: 'block', type: node.type, props: {...node.memoizedProps}, instance: null, @@ -233,7 +233,7 @@ const validWrapperTypes = new Set([ ForwardRef, MemoComponent, SimpleMemoComponent, - Chunk, + Block, // Normally skipped, but used when there's more than one root child. HostRoot, ]); diff --git a/packages/react/src/React.js b/packages/react/src/React.js index ad515bd321dfc..23fb0f87917b6 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -28,7 +28,7 @@ import {createContext} from './ReactContext'; import {lazy} from './ReactLazy'; import forwardRef from './forwardRef'; import memo from './memo'; -import chunk from './chunk'; +import block from './block'; import { useCallback, useContext, @@ -63,7 +63,7 @@ import { enableFundamentalAPI, enableScopeAPI, exposeConcurrentModeAPIs, - enableChunksAPI, + enableBlocksAPI, disableCreateFactory, } from 'shared/ReactFeatureFlags'; const React = { @@ -120,8 +120,8 @@ if (exposeConcurrentModeAPIs) { React.unstable_withSuspenseConfig = withSuspenseConfig; } -if (enableChunksAPI) { - React.chunk = chunk; +if (enableBlocksAPI) { + React.block = block; } if (enableDeprecatedFlareAPI) { diff --git a/packages/react/src/chunk.js b/packages/react/src/block.js similarity index 74% rename from packages/react/src/chunk.js rename to packages/react/src/block.js index 90bf8ac56ffdb..575f8b66ea322 100644 --- a/packages/react/src/chunk.js +++ b/packages/react/src/block.js @@ -6,47 +6,47 @@ */ import { - REACT_CHUNK_TYPE, + REACT_BLOCK_TYPE, REACT_MEMO_TYPE, REACT_FORWARD_REF_TYPE, } from 'shared/ReactSymbols'; -opaque type Chunk: React$AbstractComponent< +opaque type Block: React$AbstractComponent< Props, null, > = React$AbstractComponent; -export default function chunk( +export default function block( query: (...args: Args) => Data, render: (props: Props, data: Data) => React$Node, -): (...args: Args) => Chunk { +): (...args: Args) => Block { if (__DEV__) { if (typeof query !== 'function') { console.error( - 'Chunks require a query function but was given %s.', + 'Blocks require a query function but was given %s.', query === null ? 'null' : typeof query, ); } if (render != null && render.$$typeof === REACT_MEMO_TYPE) { console.error( - 'Chunks require a render function but received a `memo` ' + + 'Blocks require a render function but received a `memo` ' + 'component. Use `memo` on an inner component instead.', ); } else if (render != null && render.$$typeof === REACT_FORWARD_REF_TYPE) { console.error( - 'Chunks require a render function but received a `forwardRef` ' + + 'Blocks require a render function but received a `forwardRef` ' + 'component. Use `forwardRef` on an inner component instead.', ); } else if (typeof render !== 'function') { console.error( - 'Chunks require a render function but was given %s.', + 'Blocks require a render function but was given %s.', render === null ? 'null' : typeof render, ); } else if (render.length !== 0 && render.length !== 2) { // Warn if it's not accepting two args. // Do not warn for 0 arguments because it could be due to usage of the 'arguments' object console.error( - 'Chunk render functions accept exactly two parameters: props and data. %s', + 'Block render functions accept exactly two parameters: props and data. %s', render.length === 1 ? 'Did you forget to use the data parameter?' : 'Any additional parameter will be undefined.', @@ -58,15 +58,15 @@ export default function chunk( (render.defaultProps != null || render.propTypes != null) ) { console.error( - 'Chunk render functions do not support propTypes or defaultProps. ' + + 'Block render functions do not support propTypes or defaultProps. ' + 'Did you accidentally pass a React component?', ); } } - return function(): Chunk { + return function(): Block { let args = arguments; return { - $$typeof: REACT_CHUNK_TYPE, + $$typeof: REACT_BLOCK_TYPE, query: function() { return query.apply(null, args); }, diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 383bbe3c248c0..1008cd05fd6a2 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -31,7 +31,7 @@ export const enableSuspenseServerRenderer = __EXPERIMENTAL__; export const enableSelectiveHydration = __EXPERIMENTAL__; // Flight experiments -export const enableChunksAPI = __EXPERIMENTAL__; +export const enableBlocksAPI = __EXPERIMENTAL__; // Only used in www builds. export const enableSchedulerDebugging = false; diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index acc990d04c93c..6e6dd6c30690c 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -51,7 +51,7 @@ export const REACT_SUSPENSE_LIST_TYPE = hasSymbol : 0xead8; export const REACT_MEMO_TYPE = hasSymbol ? Symbol.for('react.memo') : 0xead3; export const REACT_LAZY_TYPE = hasSymbol ? Symbol.for('react.lazy') : 0xead4; -export const REACT_CHUNK_TYPE = hasSymbol ? Symbol.for('react.chunk') : 0xead9; +export const REACT_BLOCK_TYPE = hasSymbol ? Symbol.for('react.block') : 0xead9; export const REACT_FUNDAMENTAL_TYPE = hasSymbol ? Symbol.for('react.fundamental') : 0xead5; diff --git a/packages/shared/ReactTreeTraversal.js b/packages/shared/ReactTreeTraversal.js index 08a1a3d87c2cc..24d177f8c7349 100644 --- a/packages/shared/ReactTreeTraversal.js +++ b/packages/shared/ReactTreeTraversal.js @@ -5,21 +5,49 @@ * LICENSE file in the root directory of this source tree. */ -import {HostComponent} from './ReactWorkTags'; +import {HostComponent, HostPortal, HostRoot} from './ReactWorkTags'; +import {enableModernEventSystem} from './ReactFeatureFlags'; function getParent(inst) { - do { - inst = inst.return; - // TODO: If this is a HostRoot we might want to bail out. - // That is depending on if we want nested subtrees (layers) to bubble - // events to their parent. We could also go through parentNode on the - // host node but that wouldn't work for React Native and doesn't let us - // do the portal feature. - } while (inst && inst.tag !== HostComponent); - if (inst) { - return inst; + if (enableModernEventSystem) { + let node = inst.return; + + while (node !== null) { + if (node.tag === HostPortal) { + let grandNode = node; + const portalNode = node.stateNode.containerInfo; + while (grandNode !== null) { + // If we find a root that is actually a parent in the DOM tree + // then we don't continue with getting the parent, as that root + // will have its own event listener. + if ( + grandNode.tag === HostRoot && + grandNode.stateNode.containerInfo.contains(portalNode) + ) { + return null; + } + grandNode = grandNode.return; + } + } else if (node.tag === HostComponent) { + return node; + } + node = node.return; + } + return null; + } else { + do { + inst = inst.return; + // TODO: If this is a HostRoot we might want to bail out. + // That is depending on if we want nested subtrees (layers) to bubble + // events to their parent. We could also go through parentNode on the + // host node but that wouldn't work for React Native and doesn't let us + // do the portal feature. + } while (inst && inst.tag !== HostComponent); + if (inst) { + return inst; + } + return null; } - return null; } /** diff --git a/packages/shared/ReactWorkTags.js b/packages/shared/ReactWorkTags.js index 9fa017983f816..c8675c72259cf 100644 --- a/packages/shared/ReactWorkTags.js +++ b/packages/shared/ReactWorkTags.js @@ -54,4 +54,4 @@ export const DehydratedFragment = 18; export const SuspenseListComponent = 19; export const FundamentalComponent = 20; export const ScopeComponent = 21; -export const Chunk = 22; +export const Block = 22; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 44fa3a158fd54..58bf3098dbb74 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -23,7 +23,7 @@ export const enableProfilerTimer = __PROFILE__; export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; -export const enableChunksAPI = false; +export const enableBlocksAPI = false; export const exposeConcurrentModeAPIs = __EXPERIMENTAL__; export const warnAboutShorthandPropertyCollision = true; export const enableSchedulerDebugging = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index a637eb75d0c2f..91d29d5f5a32c 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -20,7 +20,7 @@ export const enableProfilerTimer = __PROFILE__; export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; -export const enableChunksAPI = false; +export const enableBlocksAPI = false; export const disableJavaScriptURLs = false; export const disableInputAttributeSyncing = false; export const exposeConcurrentModeAPIs = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index 17a66058dc8da..05bd97dd2e297 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -20,7 +20,7 @@ export const enableProfilerTimer = __PROFILE__; export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; -export const enableChunksAPI = false; +export const enableBlocksAPI = false; export const disableJavaScriptURLs = false; export const disableInputAttributeSyncing = false; export const exposeConcurrentModeAPIs = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 05375f1f04223..d1edf59b7cab7 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -20,7 +20,7 @@ export const enableProfilerTimer = __PROFILE__; export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; -export const enableChunksAPI = false; +export const enableBlocksAPI = false; export const disableJavaScriptURLs = false; export const disableInputAttributeSyncing = false; export const exposeConcurrentModeAPIs = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 423947d8b6eb9..175bf15fe7448 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -20,7 +20,7 @@ export const enableProfilerTimer = __PROFILE__; export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; -export const enableChunksAPI = false; +export const enableBlocksAPI = false; export const exposeConcurrentModeAPIs = __EXPERIMENTAL__; export const warnAboutShorthandPropertyCollision = true; export const enableSchedulerDebugging = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index f19cecb601e02..2969b38630afa 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -20,7 +20,7 @@ export const enableProfilerTimer = __PROFILE__; export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; -export const enableChunksAPI = false; +export const enableBlocksAPI = false; export const disableJavaScriptURLs = false; export const disableInputAttributeSyncing = false; export const exposeConcurrentModeAPIs = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index d82f90c473875..11c5ddbe4e701 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -20,7 +20,7 @@ export const enableProfilerTimer = false; export const enableSchedulerTracing = false; export const enableSuspenseServerRenderer = true; export const enableSelectiveHydration = true; -export const enableChunksAPI = true; +export const enableBlocksAPI = true; export const disableJavaScriptURLs = true; export const disableInputAttributeSyncing = false; export const exposeConcurrentModeAPIs = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index e9d2d9b3f306c..2972553b8a890 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -50,7 +50,7 @@ export const exposeConcurrentModeAPIs = true; export const enableSuspenseServerRenderer = true; export const enableSelectiveHydration = true; -export const enableChunksAPI = true; +export const enableBlocksAPI = true; export const disableJavaScriptURLs = true; diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index f90ca1fbe35e0..e2ad538e584a9 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -21,7 +21,7 @@ import { REACT_SUSPENSE_TYPE, REACT_SUSPENSE_LIST_TYPE, REACT_LAZY_TYPE, - REACT_CHUNK_TYPE, + REACT_BLOCK_TYPE, } from 'shared/ReactSymbols'; import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent'; @@ -80,7 +80,7 @@ function getComponentName(type: mixed): string | null { return getWrappedName(type, type.render, 'ForwardRef'); case REACT_MEMO_TYPE: return getComponentName(type.type); - case REACT_CHUNK_TYPE: + case REACT_BLOCK_TYPE: return getComponentName(type.render); case REACT_LAZY_TYPE: { const thenable: LazyComponent = (type: any); diff --git a/packages/shared/isValidElementType.js b/packages/shared/isValidElementType.js index c4a6ce0d4e338..187dcf8c8f987 100644 --- a/packages/shared/isValidElementType.js +++ b/packages/shared/isValidElementType.js @@ -22,7 +22,7 @@ import { REACT_FUNDAMENTAL_TYPE, REACT_RESPONDER_TYPE, REACT_SCOPE_TYPE, - REACT_CHUNK_TYPE, + REACT_BLOCK_TYPE, } from 'shared/ReactSymbols'; export default function isValidElementType(type: mixed) { @@ -46,6 +46,6 @@ export default function isValidElementType(type: mixed) { type.$$typeof === REACT_FUNDAMENTAL_TYPE || type.$$typeof === REACT_RESPONDER_TYPE || type.$$typeof === REACT_SCOPE_TYPE || - type.$$typeof === REACT_CHUNK_TYPE)) + type.$$typeof === REACT_BLOCK_TYPE)) ); } diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 5a2e9b8075e1e..851ad137a0fc8 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -345,5 +345,6 @@ "344": "Expected prepareToHydrateHostSuspenseInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.", "345": "Root did not complete. This is a bug in React.", "346": "An event responder context was used outside of an event cycle.", - "347": "Maps are not valid as a React child (found: %s). Consider converting children to an array of keyed ReactElements instead." + "347": "Maps are not valid as a React child (found: %s). Consider converting children to an array of keyed ReactElements instead.", + "348": "ensureListeningTo(): recieved a container that was not an element node. This is likely a bug in React." }