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