Skip to content

Commit 7126a37

Browse files
authored
[react-events] Keyboard responder propagation handling (#16657)
1 parent 539640d commit 7126a37

5 files changed

Lines changed: 117 additions & 14 deletions

File tree

‎packages/legacy-events/ReactGenericBatching.js‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import {
1111
}from'./ReactControlledComponent';
1212
import{enableFlareAPI}from'shared/ReactFeatureFlags';
1313

14-
import{invokeGuardedCallbackAndCatchFirstError}from'shared/ReactErrorUtils';
15-
1614
// Used as a way to call batchedUpdates when we don't have a reference to
1715
// the renderer. Such as when we're dispatching events or if third party
1816
// libraries need to call batchedUpdates. Eventually, this API will go away when
@@ -77,12 +75,12 @@ export function batchedEventUpdates(fn, a, b) {
7775
}
7876
}
7977

80-
exportfunctionexecuteUserEventHandler(fn: any=>void,value: any){
78+
// This is for the React Flare event system
79+
exportfunctionexecuteUserEventHandler(fn: any=>void,value: any): any{
8180
constpreviouslyInEventHandler=isInsideEventHandler;
8281
try{
8382
isInsideEventHandler=true;
84-
consttype=typeofvalue==='object'&&value!==null ? value.type : '';
85-
invokeGuardedCallbackAndCatchFirstError(type,fn,undefined,value);
83+
returnfn(value);
8684
}finally{
8785
isInsideEventHandler=previouslyInEventHandler;
8886
}

‎packages/react-dom/src/events/DOMEventResponderSystem.js‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,42 +76,45 @@ const rootEventTypesToEventResponderInstances: Map<
7676
Set<ReactDOMEventResponderInstance>,
7777
>=newMap();
7878

79+
typePropagationBehavior=0|1;
80+
81+
constDoNotPropagateToNextResponder=0;
82+
constPropagateToNextResponder=1;
83+
7984
letcurrentTimeStamp=0;
8085
letcurrentTimers=newMap();
8186
let currentInstance: null|ReactDOMEventResponderInstance=null;
8287
letcurrentTimerIDCounter=0;
8388
let currentDocument: null|Document=null;
89+
let currentPropagationBehavior: PropagationBehavior=DoNotPropagateToNextResponder;
8490
let currentTargetFiber: null|Fiber=null;
8591

8692
consteventResponderContext: ReactDOMResponderContext={
8793
dispatchEvent(
8894
eventValue: any,
8995
eventListener: any=>void,
9096
eventPriority: EventPriority,
91-
): void{
97+
): any{
9298
validateResponderContext();
9399
validateEventValue(eventValue);
94100
switch(eventPriority){
95101
case DiscreteEvent: {
96102
flushDiscreteUpdatesIfNeeded(currentTimeStamp);
97-
discreteUpdates(()=>
103+
returndiscreteUpdates(()=>
98104
executeUserEventHandler(eventListener,eventValue),
99105
);
100-
break;
101106
}
102107
case UserBlockingEvent: {
103108
if(enableUserBlockingEvents){
104-
runWithPriority(UserBlockingPriority,()=>
109+
returnrunWithPriority(UserBlockingPriority,()=>
105110
executeUserEventHandler(eventListener,eventValue),
106111
);
107112
}else{
108-
executeUserEventHandler(eventListener,eventValue);
113+
returnexecuteUserEventHandler(eventListener,eventValue);
109114
}
110-
break;
111115
}
112116
case ContinuousEvent: {
113-
executeUserEventHandler(eventListener,eventValue);
114-
break;
117+
returnexecuteUserEventHandler(eventListener,eventValue);
115118
}
116119
}
117120
},
@@ -266,6 +269,9 @@ const eventResponderContext: ReactDOMResponderContext = {
266269
}
267270
returnfalse;
268271
},
272+
continuePropagation(){
273+
currentPropagationBehavior=PropagateToNextResponder;
274+
},
269275
enqueueStateRestore,
270276
getCurrentTarget(): Element|null{
271277
validateResponderContext();
@@ -489,6 +495,10 @@ function traverseAndHandleEventResponderInstances(
489495
if(onEvent!==null){
490496
currentInstance=responderInstance;
491497
onEvent(responderEvent,eventResponderContext,props,state);
498+
if(currentPropagationBehavior===PropagateToNextResponder){
499+
visitedResponders.delete(responder);
500+
currentPropagationBehavior=DoNotPropagateToNextResponder;
501+
}
492502
}
493503
}
494504
}
@@ -588,7 +598,9 @@ export function dispatchEventForResponderEventSystem(
588598
constpreviousTimers=currentTimers;
589599
constpreviousTimeStamp=currentTimeStamp;
590600
constpreviousDocument=currentDocument;
601+
constpreviousPropagationBehavior=currentPropagationBehavior;
591602
constpreviousTargetFiber=currentTargetFiber;
603+
currentPropagationBehavior=DoNotPropagateToNextResponder;
592604
currentTimers=null;
593605
currentTargetFiber=targetFiber;
594606
// nodeType 9 is DOCUMENT_NODE
@@ -613,6 +625,7 @@ export function dispatchEventForResponderEventSystem(
613625
currentInstance=previousInstance;
614626
currentTimeStamp=previousTimeStamp;
615627
currentDocument=previousDocument;
628+
currentPropagationBehavior=previousPropagationBehavior;
616629
currentTargetFiber=previousTargetFiber;
617630
}
618631
}

‎packages/react-events/src/dom/Keyboard.js‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,14 @@ function dispatchKeyboardEvent(
174174
type,
175175
defaultPrevented,
176176
);
177-
context.dispatchEvent(syntheticEvent,listener,DiscreteEvent);
177+
constshouldPropagate=context.dispatchEvent(
178+
syntheticEvent,
179+
listener,
180+
DiscreteEvent,
181+
);
182+
if(shouldPropagate){
183+
context.continuePropagation();
184+
}
178185
}
179186

180187
constkeyboardResponderImpl={

‎packages/react-events/src/dom/__tests__/Keyboard-test.internal.js‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,88 @@ describe('Keyboard event responder', () => {
251251
);
252252
});
253253
});
254+
255+
describe('correctly handles responder propagation',()=>{
256+
describe('onKeyDown',()=>{
257+
letonKeyDownInner,onKeyDownOuter,ref;
258+
259+
functionrenderPropagationTest(propagates){
260+
onKeyDownInner=jest.fn(()=>propagates);
261+
onKeyDownOuter=jest.fn();
262+
ref=React.createRef();
263+
constComponent=()=>{
264+
constlistenerInner=useKeyboard({
265+
onKeyDown: onKeyDownInner,
266+
});
267+
constlistenerOuter=useKeyboard({
268+
onKeyDown: onKeyDownOuter,
269+
});
270+
return(
271+
<divlisteners={listenerOuter}>
272+
<divref={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+
consttarget=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+
consttarget=createEventTarget(ref.current);
290+
target.keydown();
291+
expect(onKeyDownInner).toBeCalled();
292+
expect(onKeyDownOuter).not.toBeCalled();
293+
});
294+
});
295+
296+
describe('onKeyUp',()=>{
297+
letonKeyUpInner,onKeyUpOuter,ref;
298+
299+
functionrenderPropagationTest(propagates){
300+
onKeyUpInner=jest.fn(()=>propagates);
301+
onKeyUpOuter=jest.fn();
302+
ref=React.createRef();
303+
constComponent=()=>{
304+
constlistenerInner=useKeyboard({
305+
onKeyUp: onKeyUpInner,
306+
});
307+
constlistenerOuter=useKeyboard({
308+
onKeyUp: onKeyUpOuter,
309+
});
310+
return(
311+
<divlisteners={listenerOuter}>
312+
<divref={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+
consttarget=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+
consttarget=createEventTarget(ref.current);
331+
target.keydown();
332+
target.keyup();
333+
expect(onKeyUpInner).toBeCalled();
334+
expect(onKeyUpOuter).not.toBeCalled();
335+
});
336+
});
337+
});
254338
});

‎packages/shared/ReactDOMTypes.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export type ReactDOMResponderContext = {
7272
target: Element|Document,
7373
elementType: string,
7474
)=>boolean,
75+
continuePropagation(): void,
7576
// Used for controller components
7677
enqueueStateRestore(Element|Document): void,
7778
getCurrentTarget(): Element|null,

0 commit comments

Comments
 (0)