Skip to content

Commit c73ab39

Browse files
authored
React events: make nested Focus work as expected (#15421)
This patch makes a change to the Focus module so that it only reports focus/blur on the host node that's a direct child of the event component. This brings the expected behaviour in line with the browser default of focus/blur events not bubbling for Pressable.
1 parent 4221565 commit c73ab39

2 files changed

Lines changed: 66 additions & 39 deletions

File tree

‎packages/react-events/src/Focus.js‎

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -50,67 +50,39 @@ function createFocusEvent(
5050
}
5151

5252
functiondispatchFocusInEvents(
53-
event: null|ReactResponderEvent,
5453
context: ReactResponderContext,
5554
props: FocusProps,
5655
state: FocusState,
5756
){
58-
if(event!=null){
59-
const{nativeEvent}=event;
60-
if(
61-
context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)
62-
){
63-
return;
64-
}
65-
}
57+
consttarget=((state.focusTarget: any): Element|Document);
6658
if(props.onFocus){
67-
constsyntheticEvent=createFocusEvent(
68-
'focus',
69-
((state.focusTarget: any): Element|Document),
70-
);
59+
constsyntheticEvent=createFocusEvent('focus',target);
7160
context.dispatchEvent(syntheticEvent,props.onFocus,{discrete: true});
7261
}
7362
if(props.onFocusChange){
7463
constlistener=()=>{
7564
props.onFocusChange(true);
7665
};
77-
constsyntheticEvent=createFocusEvent(
78-
'focuschange',
79-
((state.focusTarget: any): Element|Document),
80-
);
66+
constsyntheticEvent=createFocusEvent('focuschange',target);
8167
context.dispatchEvent(syntheticEvent,listener,{discrete: true});
8268
}
8369
}
8470

8571
functiondispatchFocusOutEvents(
86-
event: null|ReactResponderEvent,
8772
context: ReactResponderContext,
8873
props: FocusProps,
8974
state: FocusState,
9075
){
91-
if(event!=null){
92-
const{nativeEvent}=event;
93-
if(
94-
context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)
95-
){
96-
return;
97-
}
98-
}
76+
consttarget=((state.focusTarget: any): Element|Document);
9977
if(props.onBlur){
100-
constsyntheticEvent=createFocusEvent(
101-
'blur',
102-
((state.focusTarget: any): Element|Document),
103-
);
78+
constsyntheticEvent=createFocusEvent('blur',target);
10479
context.dispatchEvent(syntheticEvent,props.onBlur,{discrete: true});
10580
}
10681
if(props.onFocusChange){
10782
constlistener=()=>{
10883
props.onFocusChange(false);
10984
};
110-
constsyntheticEvent=createFocusEvent(
111-
'focuschange',
112-
((state.focusTarget: any): Element|Document),
113-
);
85+
constsyntheticEvent=createFocusEvent('focuschange',target);
11486
context.dispatchEvent(syntheticEvent,listener,{discrete: true});
11587
}
11688
}
@@ -121,7 +93,7 @@ function unmountResponder(
12193
state: FocusState,
12294
): void{
12395
if(state.isFocused){
124-
dispatchFocusOutEvents(null,context,props,state);
96+
dispatchFocusOutEvents(context,props,state);
12597
}
12698
}
12799

@@ -140,6 +112,8 @@ const FocusResponder = {
140112
state: FocusState,
141113
): boolean{
142114
const{type,phase,target}=event;
115+
constshouldStopPropagation=
116+
props.stopPropagation===undefined ? true : props.stopPropagation;
143117

144118
// Focus doesn't handle capture target events at this point
145119
if(phase===CAPTURE_PHASE){
@@ -148,22 +122,31 @@ const FocusResponder = {
148122
switch(type){
149123
case 'focus': {
150124
if(!state.isFocused){
151-
state.focusTarget=target;
152-
dispatchFocusInEvents(event,context,props,state);
125+
// Limit focus events to the direct child of the event component.
126+
// Browser focus is not expected to bubble.
127+
letcurrentTarget=(target: any);
128+
if(
129+
currentTarget.parentNode&&
130+
context.isTargetWithinEventComponent(currentTarget.parentNode)
131+
){
132+
break;
133+
}
134+
state.focusTarget=currentTarget;
135+
dispatchFocusInEvents(context,props,state);
153136
state.isFocused=true;
154137
}
155138
break;
156139
}
157140
case'blur': {
158141
if(state.isFocused){
159-
dispatchFocusOutEvents(event,context,props,state);
142+
dispatchFocusOutEvents(context,props,state);
160143
state.isFocused=false;
161144
state.focusTarget=null;
162145
}
163146
break;
164147
}
165148
}
166-
returnfalse;
149+
returnshouldStopPropagation;
167150
},
168151
onUnmount(
169152
context: ReactResponderContext,

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,50 @@ describe('Focus event responder', () => {
105105
});
106106
});
107107

108+
describe('nested Focus components',()=>{
109+
it('does not propagate events by default',()=>{
110+
constevents=[];
111+
constinnerRef=React.createRef();
112+
constouterRef=React.createRef();
113+
constcreateEventHandler=msg=>()=>{
114+
events.push(msg);
115+
};
116+
117+
constelement=(
118+
<Focus
119+
onBlur={createEventHandler('outer: onBlur')}
120+
onFocus={createEventHandler('outer: onFocus')}
121+
onFocusChange={createEventHandler('outer: onFocusChange')}>
122+
<divref={outerRef}>
123+
<Focus
124+
onBlur={createEventHandler('inner: onBlur')}
125+
onFocus={createEventHandler('inner: onFocus')}
126+
onFocusChange={createEventHandler('inner: onFocusChange')}>
127+
<divref={innerRef}/>
128+
</Focus>
129+
</div>
130+
</Focus>
131+
);
132+
133+
ReactDOM.render(element,container);
134+
135+
outerRef.current.dispatchEvent(createFocusEvent('focus'));
136+
outerRef.current.dispatchEvent(createFocusEvent('blur'));
137+
innerRef.current.dispatchEvent(createFocusEvent('focus'));
138+
innerRef.current.dispatchEvent(createFocusEvent('blur'));
139+
expect(events).toEqual([
140+
'outer: onFocus',
141+
'outer: onFocusChange',
142+
'outer: onBlur',
143+
'outer: onFocusChange',
144+
'inner: onFocus',
145+
'inner: onFocusChange',
146+
'inner: onBlur',
147+
'inner: onFocusChange',
148+
]);
149+
});
150+
});
151+
108152
it('expect displayName to show up for event component',()=>{
109153
expect(Focus.displayName).toBe('Focus');
110154
});

0 commit comments

Comments
 (0)