diff --git a/packages/react-dom/src/events/DOMModernPluginEventSystem.js b/packages/react-dom/src/events/DOMModernPluginEventSystem.js index b8c07cc4e914f..32c56b15c62fb 100644 --- a/packages/react-dom/src/events/DOMModernPluginEventSystem.js +++ b/packages/react-dom/src/events/DOMModernPluginEventSystem.js @@ -56,6 +56,9 @@ import { TOP_PROGRESS, TOP_PLAYING, } from './DOMTopLevelEventTypes'; +import {DOCUMENT_NODE} from '../shared/HTMLNodeType'; + +import {enableLegacyFBPrimerSupport} from 'shared/ReactFeatureFlags'; const capturePhaseEvents = new Set([ TOP_FOCUS, @@ -165,6 +168,44 @@ export function listenToEvent( } } +const validFBLegacyPrimerRels = new Set([ + 'dialog', + 'dialog-post', + 'async', + 'async-post', + 'theater', + 'toggle', +]); + +function willDeferLaterForFBLegacyPrimer(nativeEvent: any): boolean { + let node = nativeEvent.target; + const type = nativeEvent.type; + if (type !== 'click') { + return false; + } + while (node !== null) { + // Primer works by intercepting a click event on an element + // that has a "rel" attribute that matches one of the valid ones + // in the Set above. If we intercept this before Primer does, we + // will need to defer the current event till later and discontinue + // execution of the current event. To do this we can add a document + // event listener and continue again later after propagation. + if (node.tagName === 'A' && validFBLegacyPrimerRels.has(node.rel)) { + const legacyFBSupport = true; + const isCapture = nativeEvent.eventPhase === 1; + trapEventForPluginEventSystem( + document, + ((type: any): DOMTopLevelEventType), + isCapture, + legacyFBSupport, + ); + return true; + } + node = node.parentNode; + } + return false; +} + export function dispatchEventForPluginEventSystem( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, @@ -173,6 +214,17 @@ export function dispatchEventForPluginEventSystem( rootContainer: Document | Element, ): void { let ancestorInst = targetInst; + if (rootContainer.nodeType !== DOCUMENT_NODE) { + // If we detect the FB legacy primer system, we + // defer the event to the "document" with a one + // time event listener so we can defer the event. + if ( + enableLegacyFBPrimerSupport && + willDeferLaterForFBLegacyPrimer(nativeEvent) + ) { + return; + } + } batchedEventUpdates(() => dispatchEventsForPlugins( diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index e6913dbeed5b2..647b7dc239297 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -56,6 +56,7 @@ import {passiveBrowserEventsSupported} from './checkPassiveEvents'; import { enableDeprecatedFlareAPI, enableModernEventSystem, + enableLegacyFBPrimerSupport, } from 'shared/ReactFeatureFlags'; import { UserBlockingEvent, @@ -143,6 +144,7 @@ export function trapEventForPluginEventSystem( container: Document | Element, topLevelType: DOMTopLevelEventType, capture: boolean, + legacyFBSupport?: boolean, ): void { let listener; let listenerWrapper; @@ -166,10 +168,40 @@ export function trapEventForPluginEventSystem( ); const rawEventName = getRawEventName(topLevelType); + let fbListener; + + // When legacyFBSupport is enabled, it's for when we + // want to add a one time event listener to a container. + // This should only be used with enableLegacyFBPrimerSupport + // due to requirement to provide compatibility with + // internal FB www event tooling. This works by removing + // the event listener as soon as it is invoked. We could + // also attempt to use the {once: true} param on + // addEventListener, but that requires support and some + // browsers do not support this today, and given this is + // to support legacy code patterns, it's likely they'll + // need support for such browsers. + if (enableLegacyFBPrimerSupport && legacyFBSupport) { + const originalListener = listener; + listener = function(...p) { + try { + return originalListener.apply(this, p); + } finally { + if (fbListener) { + fbListener.remove(); + } else { + container.removeEventListener( + ((rawEventName: any): string), + (listener: any), + ); + } + } + }; + } if (capture) { - addEventCaptureListener(container, rawEventName, listener); + fbListener = addEventCaptureListener(container, rawEventName, listener); } else { - addEventBubbleListener(container, rawEventName, listener); + fbListener = addEventBubbleListener(container, rawEventName, listener); } } diff --git a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js index 66c670b7ca3ec..009b74ef69bdc 100644 --- a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js @@ -119,4 +119,54 @@ describe('DOMModernPluginEventSystem', () => { expect(log[4]).toEqual(['bubble', divElement]); expect(log[5]).toEqual(['bubble', buttonElement]); }); + + it('handle propagation of click events correctly with FB primer', () => { + ReactFeatureFlags.enableLegacyFBPrimerSupport = true; + const aRef = React.createRef(); + + const log = []; + // Stop propagation throught the React system + const onClick = jest.fn(e => e.stopPropagation()); + const onDivClick = jest.fn(); + + function Test() { + return ( +
+ + Click me + +
+ ); + } + ReactDOM.render(, container); + + // Fake primer + document.addEventListener('click', e => { + if (e.target.rel === 'dialog') { + log.push('primer'); + } + }); + let aElement = aRef.current; + dispatchClickEvent(aElement); + expect(onClick).toHaveBeenCalledTimes(1); + expect(log).toEqual(['primer']); + expect(onDivClick).toHaveBeenCalledTimes(0); + + log.length = 0; + // This isn't something that should be picked up by Primer + function Test2() { + return ( +
+ + Click me + +
+ ); + } + ReactDOM.render(, container); + dispatchClickEvent(aElement); + expect(onClick).toHaveBeenCalledTimes(1); + expect(log).toEqual([]); + expect(onDivClick).toHaveBeenCalledTimes(0); + }); }); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 412ec5f9eef73..f4e95b44d275d 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -128,3 +128,6 @@ export const warnUnstableRenderSubtreeIntoContainer = false; // Modern event system where events get registered at roots export const enableModernEventSystem = false; + +// Support legacy Primer support on internal FB www +export const enableLegacyFBPrimerSupport = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index bce5af20b237d..922074b145477 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -44,6 +44,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false; export const runAllPassiveEffectDestroysBeforeCreates = false; export const enableModernEventSystem = false; export const warnAboutSpreadingKeyToJSX = false; +export const enableLegacyFBPrimerSupport = false; // Internal-only attempt to debug a React Native issue. See D20130868. export const throwEarlyForMysteriousError = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index c30e273b75e3b..e37ca75167d1e 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false; export const runAllPassiveEffectDestroysBeforeCreates = false; export const enableModernEventSystem = false; export const warnAboutSpreadingKeyToJSX = false; +export const enableLegacyFBPrimerSupport = false; // Internal-only attempt to debug a React Native issue. See D20130868. export const throwEarlyForMysteriousError = false; diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index 5bd1551a9cedf..e0a9d87759774 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false; export const runAllPassiveEffectDestroysBeforeCreates = false; export const enableModernEventSystem = false; export const warnAboutSpreadingKeyToJSX = false; +export const enableLegacyFBPrimerSupport = false; // Internal-only attempt to debug a React Native issue. See D20130868. export const throwEarlyForMysteriousError = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index a785da9ec1251..e303ea72d6dbf 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false; export const runAllPassiveEffectDestroysBeforeCreates = false; export const enableModernEventSystem = false; export const warnAboutSpreadingKeyToJSX = false; +export const enableLegacyFBPrimerSupport = false; // Internal-only attempt to debug a React Native issue. See D20130868. export const throwEarlyForMysteriousError = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index c00f54a5e8985..c4314beff7c60 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false; export const runAllPassiveEffectDestroysBeforeCreates = false; export const enableModernEventSystem = false; export const warnAboutSpreadingKeyToJSX = false; +export const enableLegacyFBPrimerSupport = false; // Internal-only attempt to debug a React Native issue. See D20130868. export const throwEarlyForMysteriousError = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index c8e7e7248415b..c4b82f4d2b456 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false; export const runAllPassiveEffectDestroysBeforeCreates = false; export const enableModernEventSystem = false; export const warnAboutSpreadingKeyToJSX = false; +export const enableLegacyFBPrimerSupport = false; // Internal-only attempt to debug a React Native issue. See D20130868. export const throwEarlyForMysteriousError = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 4b4db20654698..92dda04448347 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false; export const runAllPassiveEffectDestroysBeforeCreates = false; export const enableModernEventSystem = false; export const warnAboutSpreadingKeyToJSX = false; +export const enableLegacyFBPrimerSupport = !__EXPERIMENTAL__; // Internal-only attempt to debug a React Native issue. See D20130868. export const throwEarlyForMysteriousError = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index aea6f29596145..b34679d751582 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -97,6 +97,8 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const enableModernEventSystem = false; +export const enableLegacyFBPrimerSupport = !__EXPERIMENTAL__; + // Internal-only attempt to debug a React Native issue. See D20130868. export const throwEarlyForMysteriousError = false;