Skip to content

Commit

Permalink
Modern Event System: Add support for internal FB Primer (#18210)
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm committed Mar 4, 2020
1 parent 45c172d commit 503fd82
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 2 deletions.
52 changes: 52 additions & 0 deletions packages/react-dom/src/events/DOMModernPluginEventSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 <a> 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,
Expand All @@ -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(
Expand Down
36 changes: 34 additions & 2 deletions packages/react-dom/src/events/ReactDOMEventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {passiveBrowserEventsSupported} from './checkPassiveEvents';
import {
enableDeprecatedFlareAPI,
enableModernEventSystem,
enableLegacyFBPrimerSupport,
} from 'shared/ReactFeatureFlags';
import {
UserBlockingEvent,
Expand Down Expand Up @@ -143,6 +144,7 @@ export function trapEventForPluginEventSystem(
container: Document | Element,
topLevelType: DOMTopLevelEventType,
capture: boolean,
legacyFBSupport?: boolean,
): void {
let listener;
let listenerWrapper;
Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div onClick={onDivClick}>
<a ref={aRef} href="#" onClick={onClick} rel="dialog">
Click me
</a>
</div>
);
}
ReactDOM.render(<Test />, 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 (
<div onClick={onDivClick}>
<a ref={aRef} href="#" onClick={onClick} rel="dialog-foo">
Click me
</a>
</div>
);
}
ReactDOM.render(<Test2 />, container);
dispatchClickEvent(aElement);
expect(onClick).toHaveBeenCalledTimes(1);
expect(log).toEqual([]);
expect(onDivClick).toHaveBeenCalledTimes(0);
});
});
3 changes: 3 additions & 0 deletions packages/shared/ReactFeatureFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-oss.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.persistent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.test-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.testing.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.testing.www.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/forks/ReactFeatureFlags.www.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit 503fd82

Please sign in to comment.