@samitouri / QOS-React / commits / c73ab39c1f

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 });