@samitouri / QOS-React / commits / 503fd82b42

Modern Event System: Add support for internal FB Primer (#18210)

Dominic Gannaway committed Mar 4, 2020 at 23:41 UTC 503fd82b423bbce20cb366afcf670ae5329f3995
12 files changed +148 -2
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+52
@@ -56,6 +56,9 @@ import {
56 TOP_PROGRESS,
57 TOP_PLAYING,
58 } from './DOMTopLevelEventTypes';
59 +import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
60 +
61 +import {enableLegacyFBPrimerSupport} from 'shared/ReactFeatureFlags';
62
63 const capturePhaseEvents = new Set([
64 TOP_FOCUS,
@@ -165,6 +168,44 @@ export function listenToEvent(
168 }
169 }
170
171 +const validFBLegacyPrimerRels = new Set([
172 + 'dialog',
173 + 'dialog-post',
174 + 'async',
175 + 'async-post',
176 + 'theater',
177 + 'toggle',
178 +]);
179 +
180 +function willDeferLaterForFBLegacyPrimer(nativeEvent: any): boolean {
181 + let node = nativeEvent.target;
182 + const type = nativeEvent.type;
183 + if (type !== 'click') {
184 + return false;
185 + }
186 + while (node !== null) {
187 + // Primer works by intercepting a click event on an <a> element
188 + // that has a "rel" attribute that matches one of the valid ones
189 + // in the Set above. If we intercept this before Primer does, we
190 + // will need to defer the current event till later and discontinue
191 + // execution of the current event. To do this we can add a document
192 + // event listener and continue again later after propagation.
193 + if (node.tagName === 'A' && validFBLegacyPrimerRels.has(node.rel)) {
194 + const legacyFBSupport = true;
195 + const isCapture = nativeEvent.eventPhase === 1;
196 + trapEventForPluginEventSystem(
197 + document,
198 + ((type: any): DOMTopLevelEventType),
199 + isCapture,
200 + legacyFBSupport,
201 + );
202 + return true;
203 + }
204 + node = node.parentNode;
205 + }
206 + return false;
207 +}
208 +
209 export function dispatchEventForPluginEventSystem(
210 topLevelType: DOMTopLevelEventType,
211 eventSystemFlags: EventSystemFlags,
@@ -173,6 +214,17 @@ export function dispatchEventForPluginEventSystem(
214 rootContainer: Document | Element,
215 ): void {
216 let ancestorInst = targetInst;
217 + if (rootContainer.nodeType !== DOCUMENT_NODE) {
218 + // If we detect the FB legacy primer system, we
219 + // defer the event to the "document" with a one
220 + // time event listener so we can defer the event.
221 + if (
222 + enableLegacyFBPrimerSupport &&
223 + willDeferLaterForFBLegacyPrimer(nativeEvent)
224 + ) {
225 + return;
226 + }
227 + }
228
229 batchedEventUpdates(() =>
230 dispatchEventsForPlugins(
packages/react-dom/src/events/ReactDOMEventListener.js
+34 -2
@@ -56,6 +56,7 @@ import {passiveBrowserEventsSupported} from './checkPassiveEvents';
56 import {
57 enableDeprecatedFlareAPI,
58 enableModernEventSystem,
59 + enableLegacyFBPrimerSupport,
60 } from 'shared/ReactFeatureFlags';
61 import {
62 UserBlockingEvent,
@@ -143,6 +144,7 @@ export function trapEventForPluginEventSystem(
144 container: Document | Element,
145 topLevelType: DOMTopLevelEventType,
146 capture: boolean,
147 + legacyFBSupport?: boolean,
148 ): void {
149 let listener;
150 let listenerWrapper;
@@ -166,10 +168,40 @@ export function trapEventForPluginEventSystem(
168 );
169
170 const rawEventName = getRawEventName(topLevelType);
171 + let fbListener;
172 +
173 + // When legacyFBSupport is enabled, it's for when we
174 + // want to add a one time event listener to a container.
175 + // This should only be used with enableLegacyFBPrimerSupport
176 + // due to requirement to provide compatibility with
177 + // internal FB www event tooling. This works by removing
178 + // the event listener as soon as it is invoked. We could
179 + // also attempt to use the {once: true} param on
180 + // addEventListener, but that requires support and some
181 + // browsers do not support this today, and given this is
182 + // to support legacy code patterns, it's likely they'll
183 + // need support for such browsers.
184 + if (enableLegacyFBPrimerSupport && legacyFBSupport) {
185 + const originalListener = listener;
186 + listener = function(...p) {
187 + try {
188 + return originalListener.apply(this, p);
189 + } finally {
190 + if (fbListener) {
191 + fbListener.remove();
192 + } else {
193 + container.removeEventListener(
194 + ((rawEventName: any): string),
195 + (listener: any),
196 + );
197 + }
198 + }
199 + };
200 + }
201 if (capture) {
170 - addEventCaptureListener(container, rawEventName, listener);
202 + fbListener = addEventCaptureListener(container, rawEventName, listener);
203 } else {
172 - addEventBubbleListener(container, rawEventName, listener);
204 + fbListener = addEventBubbleListener(container, rawEventName, listener);
205 }
206 }
207
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+50
@@ -119,4 +119,54 @@ describe('DOMModernPluginEventSystem', () => {
119 expect(log[4]).toEqual(['bubble', divElement]);
120 expect(log[5]).toEqual(['bubble', buttonElement]);
121 });
122 +
123 + it('handle propagation of click events correctly with FB primer', () => {
124 + ReactFeatureFlags.enableLegacyFBPrimerSupport = true;
125 + const aRef = React.createRef();
126 +
127 + const log = [];
128 + // Stop propagation throught the React system
129 + const onClick = jest.fn(e => e.stopPropagation());
130 + const onDivClick = jest.fn();
131 +
132 + function Test() {
133 + return (
134 + <div onClick={onDivClick}>
135 + <a ref={aRef} href="#" onClick={onClick} rel="dialog">
136 + Click me
137 + </a>
138 + </div>
139 + );
140 + }
141 + ReactDOM.render(<Test />, container);
142 +
143 + // Fake primer
144 + document.addEventListener('click', e => {
145 + if (e.target.rel === 'dialog') {
146 + log.push('primer');
147 + }
148 + });
149 + let aElement = aRef.current;
150 + dispatchClickEvent(aElement);
151 + expect(onClick).toHaveBeenCalledTimes(1);
152 + expect(log).toEqual(['primer']);
153 + expect(onDivClick).toHaveBeenCalledTimes(0);
154 +
155 + log.length = 0;
156 + // This isn't something that should be picked up by Primer
157 + function Test2() {
158 + return (
159 + <div onClick={onDivClick}>
160 + <a ref={aRef} href="#" onClick={onClick} rel="dialog-foo">
161 + Click me
162 + </a>
163 + </div>
164 + );
165 + }
166 + ReactDOM.render(<Test2 />, container);
167 + dispatchClickEvent(aElement);
168 + expect(onClick).toHaveBeenCalledTimes(1);
169 + expect(log).toEqual([]);
170 + expect(onDivClick).toHaveBeenCalledTimes(0);
171 + });
172 });
packages/shared/ReactFeatureFlags.js
+3
@@ -128,3 +128,6 @@ export const warnUnstableRenderSubtreeIntoContainer = false;
128
129 // Modern event system where events get registered at roots
130 export const enableModernEventSystem = false;
131 +
132 +// Support legacy Primer support on internal FB www
133 +export const enableLegacyFBPrimerSupport = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -44,6 +44,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
44 export const runAllPassiveEffectDestroysBeforeCreates = false;
45 export const enableModernEventSystem = false;
46 export const warnAboutSpreadingKeyToJSX = false;
47 +export const enableLegacyFBPrimerSupport = false;
48
49 // Internal-only attempt to debug a React Native issue. See D20130868.
50 export const throwEarlyForMysteriousError = true;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
43 export const runAllPassiveEffectDestroysBeforeCreates = false;
44 export const enableModernEventSystem = false;
45 export const warnAboutSpreadingKeyToJSX = false;
46 +export const enableLegacyFBPrimerSupport = false;
47
48 // Internal-only attempt to debug a React Native issue. See D20130868.
49 export const throwEarlyForMysteriousError = false;
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
43 export const runAllPassiveEffectDestroysBeforeCreates = false;
44 export const enableModernEventSystem = false;
45 export const warnAboutSpreadingKeyToJSX = false;
46 +export const enableLegacyFBPrimerSupport = false;
47
48 // Internal-only attempt to debug a React Native issue. See D20130868.
49 export const throwEarlyForMysteriousError = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
43 export const runAllPassiveEffectDestroysBeforeCreates = false;
44 export const enableModernEventSystem = false;
45 export const warnAboutSpreadingKeyToJSX = false;
46 +export const enableLegacyFBPrimerSupport = false;
47
48 // Internal-only attempt to debug a React Native issue. See D20130868.
49 export const throwEarlyForMysteriousError = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
43 export const runAllPassiveEffectDestroysBeforeCreates = false;
44 export const enableModernEventSystem = false;
45 export const warnAboutSpreadingKeyToJSX = false;
46 +export const enableLegacyFBPrimerSupport = false;
47
48 // Internal-only attempt to debug a React Native issue. See D20130868.
49 export const throwEarlyForMysteriousError = false;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
43 export const runAllPassiveEffectDestroysBeforeCreates = false;
44 export const enableModernEventSystem = false;
45 export const warnAboutSpreadingKeyToJSX = false;
46 +export const enableLegacyFBPrimerSupport = false;
47
48 // Internal-only attempt to debug a React Native issue. See D20130868.
49 export const throwEarlyForMysteriousError = false;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -43,6 +43,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
43 export const runAllPassiveEffectDestroysBeforeCreates = false;
44 export const enableModernEventSystem = false;
45 export const warnAboutSpreadingKeyToJSX = false;
46 +export const enableLegacyFBPrimerSupport = !__EXPERIMENTAL__;
47
48 // Internal-only attempt to debug a React Native issue. See D20130868.
49 export const throwEarlyForMysteriousError = false;
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -97,6 +97,8 @@ export const warnUnstableRenderSubtreeIntoContainer = false;
97
98 export const enableModernEventSystem = false;
99
100 +export const enableLegacyFBPrimerSupport = !__EXPERIMENTAL__;
101 +
102 // Internal-only attempt to debug a React Native issue. See D20130868.
103 export const throwEarlyForMysteriousError = false;
104