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

File tree

2 files changed

+66
-39
lines changed

2 files changed

+66
-39
lines changed

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
function dispatchFocusInEvents(
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+
const target = ((state.focusTarget: any): Element | Document);
6658
if (props.onFocus) {
67-
const syntheticEvent = createFocusEvent(
68-
'focus',
69-
((state.focusTarget: any): Element | Document),
70-
);
59+
const syntheticEvent = createFocusEvent('focus', target);
7160
context.dispatchEvent(syntheticEvent, props.onFocus, {discrete: true});
7261
}
7362
if (props.onFocusChange) {
7463
const listener = () => {
7564
props.onFocusChange(true);
7665
};
77-
const syntheticEvent = createFocusEvent(
78-
'focuschange',
79-
((state.focusTarget: any): Element | Document),
80-
);
66+
const syntheticEvent = createFocusEvent('focuschange', target);
8167
context.dispatchEvent(syntheticEvent, listener, {discrete: true});
8268
}
8369
}
8470

8571
function dispatchFocusOutEvents(
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+
const target = ((state.focusTarget: any): Element | Document);
9977
if (props.onBlur) {
100-
const syntheticEvent = createFocusEvent(
101-
'blur',
102-
((state.focusTarget: any): Element | Document),
103-
);
78+
const syntheticEvent = createFocusEvent('blur', target);
10479
context.dispatchEvent(syntheticEvent, props.onBlur, {discrete: true});
10580
}
10681
if (props.onFocusChange) {
10782
const listener = () => {
10883
props.onFocusChange(false);
10984
};
110-
const syntheticEvent = createFocusEvent(
111-
'focuschange',
112-
((state.focusTarget: any): Element | Document),
113-
);
85+
const syntheticEvent = 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+
const shouldStopPropagation =
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+
let currentTarget = (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-
return false;
149+
return shouldStopPropagation;
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+
const events = [];
111+
const innerRef = React.createRef();
112+
const outerRef = React.createRef();
113+
const createEventHandler = msg => () => {
114+
events.push(msg);
115+
};
116+
117+
const element = (
118+
<Focus
119+
onBlur={createEventHandler('outer: onBlur')}
120+
onFocus={createEventHandler('outer: onFocus')}
121+
onFocusChange={createEventHandler('outer: onFocusChange')}>
122+
<div ref={outerRef}>
123+
<Focus
124+
onBlur={createEventHandler('inner: onBlur')}
125+
onFocus={createEventHandler('inner: onFocus')}
126+
onFocusChange={createEventHandler('inner: onFocusChange')}>
127+
<div ref={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)