@samitouri / QOS-React-2 / commits / 7126a37bf4

[react-events] Keyboard responder propagation handling (#16657)

Dominic Gannaway committed Sep 4, 2019 at 18:25 UTC 7126a37bf4284a30377e08b2ef84f61b64fdb8cd
5 files changed +117 -14
packages/legacy-events/ReactGenericBatching.js
+3 -5
@@ -11,8 +11,6 @@ import {
11 } from './ReactControlledComponent';
12 import {enableFlareAPI} from 'shared/ReactFeatureFlags';
13
14 -import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
15 -
14 // Used as a way to call batchedUpdates when we don't have a reference to
15 // the renderer. Such as when we're dispatching events or if third party
16 // libraries need to call batchedUpdates. Eventually, this API will go away when
@@ -77,12 +75,12 @@ export function batchedEventUpdates(fn, a, b) {
75 }
76 }
77
80 -export function executeUserEventHandler(fn: any => void, value: any) {
78 +// This is for the React Flare event system
79 +export function executeUserEventHandler(fn: any => void, value: any): any {
80 const previouslyInEventHandler = isInsideEventHandler;
81 try {
82 isInsideEventHandler = true;
84 - const type = typeof value === 'object' && value !== null ? value.type : '';
85 - invokeGuardedCallbackAndCatchFirstError(type, fn, undefined, value);
83 + return fn(value);
84 } finally {
85 isInsideEventHandler = previouslyInEventHandler;
86 }
packages/react-dom/src/events/DOMEventResponderSystem.js
+21 -8
@@ -76,11 +76,17 @@ const rootEventTypesToEventResponderInstances: Map<
76 Set<ReactDOMEventResponderInstance>,
77 > = new Map();
78
79 +type PropagationBehavior = 0 | 1;
80 +
81 +const DoNotPropagateToNextResponder = 0;
82 +const PropagateToNextResponder = 1;
83 +
84 let currentTimeStamp = 0;
85 let currentTimers = new Map();
86 let currentInstance: null | ReactDOMEventResponderInstance = null;
87 let currentTimerIDCounter = 0;
88 let currentDocument: null | Document = null;
89 +let currentPropagationBehavior: PropagationBehavior = DoNotPropagateToNextResponder;
90 let currentTargetFiber: null | Fiber = null;
91
92 const eventResponderContext: ReactDOMResponderContext = {
@@ -88,30 +94,27 @@ const eventResponderContext: ReactDOMResponderContext = {
94 eventValue: any,
95 eventListener: any => void,
96 eventPriority: EventPriority,
91 - ): void {
97 + ): any {
98 validateResponderContext();
99 validateEventValue(eventValue);
100 switch (eventPriority) {
101 case DiscreteEvent: {
102 flushDiscreteUpdatesIfNeeded(currentTimeStamp);
97 - discreteUpdates(() =>
103 + return discreteUpdates(() =>
104 executeUserEventHandler(eventListener, eventValue),
105 );
100 - break;
106 }
107 case UserBlockingEvent: {
108 if (enableUserBlockingEvents) {
104 - runWithPriority(UserBlockingPriority, () =>
109 + return runWithPriority(UserBlockingPriority, () =>
110 executeUserEventHandler(eventListener, eventValue),
111 );
112 } else {
108 - executeUserEventHandler(eventListener, eventValue);
113 + return executeUserEventHandler(eventListener, eventValue);
114 }
110 - break;
115 }
116 case ContinuousEvent: {
113 - executeUserEventHandler(eventListener, eventValue);
114 - break;
117 + return executeUserEventHandler(eventListener, eventValue);
118 }
119 }
120 },
@@ -266,6 +269,9 @@ const eventResponderContext: ReactDOMResponderContext = {
269 }
270 return false;
271 },
272 + continuePropagation() {
273 + currentPropagationBehavior = PropagateToNextResponder;
274 + },
275 enqueueStateRestore,
276 getCurrentTarget(): Element | null {
277 validateResponderContext();
@@ -489,6 +495,10 @@ function traverseAndHandleEventResponderInstances(
495 if (onEvent !== null) {
496 currentInstance = responderInstance;
497 onEvent(responderEvent, eventResponderContext, props, state);
498 + if (currentPropagationBehavior === PropagateToNextResponder) {
499 + visitedResponders.delete(responder);
500 + currentPropagationBehavior = DoNotPropagateToNextResponder;
501 + }
502 }
503 }
504 }
@@ -588,7 +598,9 @@ export function dispatchEventForResponderEventSystem(
598 const previousTimers = currentTimers;
599 const previousTimeStamp = currentTimeStamp;
600 const previousDocument = currentDocument;
601 + const previousPropagationBehavior = currentPropagationBehavior;
602 const previousTargetFiber = currentTargetFiber;
603 + currentPropagationBehavior = DoNotPropagateToNextResponder;
604 currentTimers = null;
605 currentTargetFiber = targetFiber;
606 // nodeType 9 is DOCUMENT_NODE
@@ -613,6 +625,7 @@ export function dispatchEventForResponderEventSystem(
625 currentInstance = previousInstance;
626 currentTimeStamp = previousTimeStamp;
627 currentDocument = previousDocument;
628 + currentPropagationBehavior = previousPropagationBehavior;
629 currentTargetFiber = previousTargetFiber;
630 }
631 }
packages/react-events/src/dom/Keyboard.js
+8 -1
@@ -174,7 +174,14 @@ function dispatchKeyboardEvent(
174 type,
175 defaultPrevented,
176 );
177 - context.dispatchEvent(syntheticEvent, listener, DiscreteEvent);
177 + const shouldPropagate = context.dispatchEvent(
178 + syntheticEvent,
179 + listener,
180 + DiscreteEvent,
181 + );
182 + if (shouldPropagate) {
183 + context.continuePropagation();
184 + }
185 }
186
187 const keyboardResponderImpl = {
packages/react-events/src/dom/__tests__/Keyboard-test.internal.js
+84
@@ -251,4 +251,88 @@ describe('Keyboard event responder', () => {
251 );
252 });
253 });
254 +
255 + describe('correctly handles responder propagation', () => {
256 + describe('onKeyDown', () => {
257 + let onKeyDownInner, onKeyDownOuter, ref;
258 +
259 + function renderPropagationTest(propagates) {
260 + onKeyDownInner = jest.fn(() => propagates);
261 + onKeyDownOuter = jest.fn();
262 + ref = React.createRef();
263 + const Component = () => {
264 + const listenerInner = useKeyboard({
265 + onKeyDown: onKeyDownInner,
266 + });
267 + const listenerOuter = useKeyboard({
268 + onKeyDown: onKeyDownOuter,
269 + });
270 + return (
271 + <div listeners={listenerOuter}>
272 + <div ref={ref} listeners={listenerInner} />
273 + </div>
274 + );
275 + };
276 + ReactDOM.render(<Component />, container);
277 + }
278 +
279 + it('propagates when cb returns true', () => {
280 + renderPropagationTest(true);
281 + const target = createEventTarget(ref.current);
282 + target.keydown();
283 + expect(onKeyDownInner).toBeCalled();
284 + expect(onKeyDownOuter).toBeCalled();
285 + });
286 +
287 + it('does not propagate when cb returns false', () => {
288 + renderPropagationTest(false);
289 + const target = createEventTarget(ref.current);
290 + target.keydown();
291 + expect(onKeyDownInner).toBeCalled();
292 + expect(onKeyDownOuter).not.toBeCalled();
293 + });
294 + });
295 +
296 + describe('onKeyUp', () => {
297 + let onKeyUpInner, onKeyUpOuter, ref;
298 +
299 + function renderPropagationTest(propagates) {
300 + onKeyUpInner = jest.fn(() => propagates);
301 + onKeyUpOuter = jest.fn();
302 + ref = React.createRef();
303 + const Component = () => {
304 + const listenerInner = useKeyboard({
305 + onKeyUp: onKeyUpInner,
306 + });
307 + const listenerOuter = useKeyboard({
308 + onKeyUp: onKeyUpOuter,
309 + });
310 + return (
311 + <div listeners={listenerOuter}>
312 + <div ref={ref} listeners={listenerInner} />
313 + </div>
314 + );
315 + };
316 + ReactDOM.render(<Component />, container);
317 + }
318 +
319 + it('propagates when cb returns true', () => {
320 + renderPropagationTest(true);
321 + const target = createEventTarget(ref.current);
322 + target.keydown();
323 + target.keyup();
324 + expect(onKeyUpInner).toBeCalled();
325 + expect(onKeyUpOuter).toBeCalled();
326 + });
327 +
328 + it('does not propagate when cb returns false', () => {
329 + renderPropagationTest(false);
330 + const target = createEventTarget(ref.current);
331 + target.keydown();
332 + target.keyup();
333 + expect(onKeyUpInner).toBeCalled();
334 + expect(onKeyUpOuter).not.toBeCalled();
335 + });
336 + });
337 + });
338 });
packages/shared/ReactDOMTypes.js
+1
@@ -72,6 +72,7 @@ export type ReactDOMResponderContext = {
72 target: Element | Document,
73 elementType: string,
74 ) => boolean,
75 + continuePropagation(): void,
76 // Used for controller components
77 enqueueStateRestore(Element | Document): void,
78 getCurrentTarget(): Element | null,