Skip to content

Commit cb09dbe

Browse files
authored
[react-interactions] Add handleSimulateChildBlur upon DOM node removal (#17225)
* [react-interactions] Add handleSimulateChildBlur upon DOM node removal
1 parent 6095993 commit cb09dbe

File tree

3 files changed

+81
-24
lines changed

3 files changed

+81
-24
lines changed

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
addRootEventTypesForResponderInstance,
5656
mountEventResponder,
5757
unmountEventResponder,
58+
dispatchEventForResponderEventSystem,
5859
} from '../events/DOMEventResponderSystem';
5960
import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying';
6061

@@ -108,6 +109,10 @@ import {
108109
enableFlareAPI,
109110
enableFundamentalAPI,
110111
} from 'shared/ReactFeatureFlags';
112+
import {
113+
RESPONDER_EVENT_SYSTEM,
114+
IS_PASSIVE,
115+
} from 'legacy-events/EventSystemFlags';
111116

112117
let SUPPRESS_HYDRATION_WARNING;
113118
if (__DEV__) {
@@ -447,10 +452,36 @@ export function insertInContainerBefore(
447452
}
448453
}
449454

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+
450480
export function removeChild(
451481
parentInstance: Instance,
452482
child: Instance | TextInstance | SuspenseInstance,
453483
): void {
484+
handleSimulateChildBlur(child);
454485
parentInstance.removeChild(child);
455486
}
456487

@@ -461,6 +492,7 @@ export function removeChildFromContainer(
461492
if (container.nodeType === COMMENT_NODE) {
462493
(container.parentNode: any).removeChild(child);
463494
} else {
495+
handleSimulateChildBlur(child);
464496
container.removeChild(child);
465497
}
466498
}

packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -77,23 +77,24 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
7777
describe('onFocusWithinChange', () => {
7878
let onFocusWithinChange, ref, innerRef, innerRef2;
7979

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+
8092
beforeEach(() => {
8193
onFocusWithinChange = jest.fn();
8294
ref = React.createRef();
8395
innerRef = React.createRef();
8496
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);
9798
});
9899

99100
it('is called after "blur" and "focus" events on focus target', () => {
@@ -140,28 +141,39 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
140141
expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
141142
expect(onFocusWithinChange).toHaveBeenCalledWith(false);
142143
});
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+
});
143154
});
144155

145156
describe('onFocusWithinVisibleChange', () => {
146157
let onFocusWithinVisibleChange, ref, innerRef, innerRef2;
147158

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+
148171
beforeEach(() => {
149172
onFocusWithinVisibleChange = jest.fn();
150173
ref = React.createRef();
151174
innerRef = React.createRef();
152175
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);
165177
});
166178

167179
it('is called after "focus" and "blur" on focus target if keyboard was used', () => {
@@ -258,6 +270,18 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
258270
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
259271
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
260272
});
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+
});
261285
});
262286

263287
it('expect displayName to show up for event component', () => {

packages/react-interactions/events/src/dom/testing-library/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const createEventTarget = node => ({
3434
},
3535
focus(payload) {
3636
node.dispatchEvent(domEvents.focus(payload));
37+
node.focus();
3738
},
3839
scroll(payload) {
3940
node.dispatchEvent(domEvents.scroll(payload));

0 commit comments

Comments
 (0)