[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,