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.
Nicolas Gallagher committed
Apr 16, 2019 at 11:16 UTC
c73ab39c1f5f466991b97c9e0c782910e3b1cf37
2 files changed
+66
-39
packages/react-events/src/Focus.js
+22
-39
@@ -50,67 +50,39 @@ function createFocusEvent(
50
}
51
52
function dispatchFocusInEvents(
53
- event: null | ReactResponderEvent,
53
context: ReactResponderContext,
54
props: FocusProps,
55
state: FocusState,
56
) {
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);
58
if (props.onFocus) {
67
- const syntheticEvent = createFocusEvent(
68
- 'focus',
69
- ((state.focusTarget: any): Element | Document),
70
- );
59
+ const syntheticEvent = createFocusEvent('focus', target);
60
context.dispatchEvent(syntheticEvent, props.onFocus, {discrete: true});
61
}
62
if (props.onFocusChange) {
63
const listener = () => {
64
props.onFocusChange(true);
65
};
77
- const syntheticEvent = createFocusEvent(
78
- 'focuschange',
79
- ((state.focusTarget: any): Element | Document),
80
- );
66
+ const syntheticEvent = createFocusEvent('focuschange', target);
67
context.dispatchEvent(syntheticEvent, listener, {discrete: true});
68
}
69
}
70
71
function dispatchFocusOutEvents(
86
- event: null | ReactResponderEvent,
72
context: ReactResponderContext,
73
props: FocusProps,
74
state: FocusState,
75
) {
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);
77
if (props.onBlur) {
100
- const syntheticEvent = createFocusEvent(
101
- 'blur',
102
- ((state.focusTarget: any): Element | Document),
103
- );
78
+ const syntheticEvent = createFocusEvent('blur', target);
79
context.dispatchEvent(syntheticEvent, props.onBlur, {discrete: true});
80
}
81
if (props.onFocusChange) {
82
const listener = () => {
83
props.onFocusChange(false);
84
};
110
- const syntheticEvent = createFocusEvent(
111
- 'focuschange',
112
- ((state.focusTarget: any): Element | Document),
113
- );
85
+ const syntheticEvent = createFocusEvent('focuschange', target);
86
context.dispatchEvent(syntheticEvent, listener, {discrete: true});
87
}
88
}
@@ -121,7 +93,7 @@ function unmountResponder(
93
state: FocusState,
94
): void {
95
if (state.isFocused) {
124
- dispatchFocusOutEvents(null, context, props, state);
96
+ dispatchFocusOutEvents(context, props, state);
97
}
98
}
99
@@ -140,6 +112,8 @@ const FocusResponder = {
112
state: FocusState,
113
): boolean {
114
const {type, phase, target} = event;
115
+ const shouldStopPropagation =
116
+ props.stopPropagation === undefined ? true : props.stopPropagation;
117
118
// Focus doesn't handle capture target events at this point
119
if (phase === CAPTURE_PHASE) {
@@ -148,22 +122,31 @@ const FocusResponder = {
122
switch (type) {
123
case 'focus': {
124
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);
136
state.isFocused = true;
137
}
138
break;
139
}
140
case 'blur': {
141
if (state.isFocused) {
159
- dispatchFocusOutEvents(event, context, props, state);
142
+ dispatchFocusOutEvents(context, props, state);
143
state.isFocused = false;
144
state.focusTarget = null;
145
}
146
break;
147
}
148
}
166
- return false;
149
+ return shouldStopPropagation;
150
},
151
onUnmount(
152
context: ReactResponderContext,
packages/react-events/src/__tests__/Focus-test.internal.js
+44
@@ -105,6 +105,50 @@ describe('Focus event responder', () => {
105
});
106
});
107
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
+
152
it('expect displayName to show up for event component', () => {
153
expect(Focus.displayName).toBe('Focus');
154
});