[react-interactions] Add handleSimulateChildBlur upon DOM node removal (#17225)
* [react-interactions] Add handleSimulateChildBlur upon DOM node removal
Dominic Gannaway committed
Nov 4, 2019 at 20:02 UTC
cb09dbe0ab0f42185c4892c045b0c32e04f6b9cb
3 files changed
+81
-24
packages/react-dom/src/client/ReactDOMHostConfig.js
+32
@@ -55,6 +55,7 @@ import {
55
addRootEventTypesForResponderInstance,
56
mountEventResponder,
57
unmountEventResponder,
58
+ dispatchEventForResponderEventSystem,
59
} from '../events/DOMEventResponderSystem';
60
import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying';
61
@@ -108,6 +109,10 @@ import {
109
enableFlareAPI,
110
enableFundamentalAPI,
111
} from 'shared/ReactFeatureFlags';
112
+import {
113
+ RESPONDER_EVENT_SYSTEM,
114
+ IS_PASSIVE,
115
+} from 'legacy-events/EventSystemFlags';
116
117
let SUPPRESS_HYDRATION_WARNING;
118
if (__DEV__) {
@@ -447,10 +452,36 @@ export function insertInContainerBefore(
452
}
453
}
454
455
+function handleSimulateChildBlur(
456
+ child: Instance | TextInstance | SuspenseInstance,
457
+): void {
458
+ if (
459
+ enableFlareAPI &&
460
+ selectionInformation &&
461
+ child === selectionInformation.focusedElem
462
+ ) {
463
+ const targetFiber = getClosestInstanceFromNode(child);
464
+ // Simlulate a blur event to the React Flare responder system.
465
+ dispatchEventForResponderEventSystem(
466
+ 'blur',
467
+ targetFiber,
468
+ ({
469
+ relatedTarget: null,
470
+ target: child,
471
+ timeStamp: Date.now(),
472
+ type: 'blur',
473
+ }: any),
474
+ ((child: any): Document | Element),
475
+ RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
476
+ );
477
+ }
478
+}
479
+
480
export function removeChild(
481
parentInstance: Instance,
482
child: Instance | TextInstance | SuspenseInstance,
483
): void {
484
+ handleSimulateChildBlur(child);
485
parentInstance.removeChild(child);
486
}
487
@@ -461,6 +492,7 @@ export function removeChildFromContainer(
492
if (container.nodeType === COMMENT_NODE) {
493
(container.parentNode: any).removeChild(child);
494
} else {
495
+ handleSimulateChildBlur(child);
496
container.removeChild(child);
497
}
498
}
packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
+48
-24
@@ -77,23 +77,24 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
77
describe('onFocusWithinChange', () => {
78
let onFocusWithinChange, ref, innerRef, innerRef2;
79
80
+ const Component = ({show}) => {
81
+ const listener = useFocusWithin({
82
+ onFocusWithinChange,
83
+ });
84
+ return (
85
+ <div ref={ref} listeners={listener}>
86
+ {show && <input ref={innerRef} />}
87
+ <div ref={innerRef2} />
88
+ </div>
89
+ );
90
+ };
91
+
92
beforeEach(() => {
93
onFocusWithinChange = jest.fn();
94
ref = React.createRef();
95
innerRef = React.createRef();
96
innerRef2 = React.createRef();
85
- const Component = () => {
86
- const listener = useFocusWithin({
87
- onFocusWithinChange,
88
- });
89
- return (
90
- <div ref={ref} listeners={listener}>
91
- <div ref={innerRef} />
92
- <div ref={innerRef2} />
93
- </div>
94
- );
95
- };
96
- ReactDOM.render(<Component />, container);
97
+ ReactDOM.render(<Component show={true} />, container);
98
});
99
100
it('is called after "blur" and "focus" events on focus target', () => {
@@ -140,28 +141,39 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
141
expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
142
expect(onFocusWithinChange).toHaveBeenCalledWith(false);
143
});
144
+
145
+ it('is called after a focused element is unmounted', () => {
146
+ const target = createEventTarget(innerRef.current);
147
+ target.focus();
148
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
149
+ expect(onFocusWithinChange).toHaveBeenCalledWith(true);
150
+ ReactDOM.render(<Component show={false} />, container);
151
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
152
+ expect(onFocusWithinChange).toHaveBeenCalledWith(false);
153
+ });
154
});
155
156
describe('onFocusWithinVisibleChange', () => {
157
let onFocusWithinVisibleChange, ref, innerRef, innerRef2;
158
159
+ const Component = ({show}) => {
160
+ const listener = useFocusWithin({
161
+ onFocusWithinVisibleChange,
162
+ });
163
+ return (
164
+ <div ref={ref} listeners={listener}>
165
+ {show && <input ref={innerRef} />}
166
+ <div ref={innerRef2} />
167
+ </div>
168
+ );
169
+ };
170
+
171
beforeEach(() => {
172
onFocusWithinVisibleChange = jest.fn();
173
ref = React.createRef();
174
innerRef = React.createRef();
175
innerRef2 = React.createRef();
153
- const Component = () => {
154
- const listener = useFocusWithin({
155
- onFocusWithinVisibleChange,
156
- });
157
- return (
158
- <div ref={ref} listeners={listener}>
159
- <div ref={innerRef} />
160
- <div ref={innerRef2} />
161
- </div>
162
- );
163
- };
164
- ReactDOM.render(<Component />, container);
176
+ ReactDOM.render(<Component show={true} />, container);
177
});
178
179
it('is called after "focus" and "blur" on focus target if keyboard was used', () => {
@@ -258,6 +270,18 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
270
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
271
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
272
});
273
+
274
+ it('is called after a focused element is unmounted', () => {
275
+ const inner = innerRef.current;
276
+ const target = createEventTarget(inner);
277
+ target.keydown({key: 'Tab'});
278
+ target.focus();
279
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
280
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
281
+ ReactDOM.render(<Component show={false} />, container);
282
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
283
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
284
+ });
285
});
286
287
it('expect displayName to show up for event component', () => {
packages/react-interactions/events/src/dom/testing-library/index.js
+1
@@ -34,6 +34,7 @@ const createEventTarget = node => ({
34
},
35
focus(payload) {
36
node.dispatchEvent(domEvents.focus(payload));
37
+ node.focus();
38
},
39
scroll(payload) {
40
node.dispatchEvent(domEvents.scroll(payload));