Skip to content

Commit acba0bd

Browse files
trueadmkoto
authored andcommitted
[EventSystem] Revise onBeforeBlur propagation mechanics (facebook#20020)
1 parent 3c12274 commit acba0bd

File tree

10 files changed

+174
-12
lines changed

10 files changed

+174
-12
lines changed

packages/react-art/src/ReactARTHostConfig.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
467467
throw new Error('Not yet implemented');
468468
}
469469

470-
export function beforeActiveInstanceBlur() {
470+
export function beforeActiveInstanceBlur(internalInstanceHandle: Object) {
471471
// noop
472472
}
473473

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

+11-3
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,13 @@ export function prepareForCommit(containerInfo: Container): Object | null {
232232
return activeInstance;
233233
}
234234

235-
export function beforeActiveInstanceBlur(): void {
235+
export function beforeActiveInstanceBlur(internalInstanceHandle: Object): void {
236236
if (enableCreateEventHandleAPI) {
237237
ReactBrowserEventEmitterSetEnabled(true);
238-
dispatchBeforeDetachedBlur((selectionInformation: any).focusedElem);
238+
dispatchBeforeDetachedBlur(
239+
(selectionInformation: any).focusedElem,
240+
internalInstanceHandle,
241+
);
239242
ReactBrowserEventEmitterSetEnabled(false);
240243
}
241244
}
@@ -499,12 +502,17 @@ function createEvent(type: DOMEventName, bubbles: boolean): Event {
499502
return event;
500503
}
501504

502-
function dispatchBeforeDetachedBlur(target: HTMLElement): void {
505+
function dispatchBeforeDetachedBlur(
506+
target: HTMLElement,
507+
internalInstanceHandle: Object,
508+
): void {
503509
if (enableCreateEventHandleAPI) {
504510
const event = createEvent('beforeblur', true);
505511
// Dispatch "beforeblur" directly on the target,
506512
// so it gets picked up by the event system and
507513
// can propagate through the React internal tree.
514+
// $FlowFixMe: internal field
515+
event._detachedInterceptFiber = internalInstanceHandle;
508516
target.dispatchEvent(event);
509517
}
510518
}

packages/react-dom/src/events/DOMPluginEventSystem.js

+19-1
Original file line numberDiff line numberDiff line change
@@ -658,10 +658,11 @@ export function accumulateSinglePhaseListeners(
658658
nativeEventType: string,
659659
inCapturePhase: boolean,
660660
accumulateTargetOnly: boolean,
661+
nativeEvent: AnyNativeEvent,
661662
): Array<DispatchListener> {
662663
const captureName = reactName !== null ? reactName + 'Capture' : null;
663664
const reactEventName = inCapturePhase ? captureName : reactName;
664-
const listeners: Array<DispatchListener> = [];
665+
let listeners: Array<DispatchListener> = [];
665666

666667
let instance = targetFiber;
667668
let lastHostComponent = null;
@@ -740,6 +741,23 @@ export function accumulateSinglePhaseListeners(
740741
if (accumulateTargetOnly) {
741742
break;
742743
}
744+
// If we are processing the onBeforeBlur event, then we need to take
745+
// into consideration that part of the React tree might have been hidden
746+
// or deleted (as we're invoking this event during commit). We can find
747+
// this out by checking if intercept fiber set on the event matches the
748+
// current instance fiber. In which case, we should clear all existing
749+
// listeners.
750+
if (enableCreateEventHandleAPI && nativeEvent.type === 'beforeblur') {
751+
// $FlowFixMe: internal field
752+
const detachedInterceptFiber = nativeEvent._detachedInterceptFiber;
753+
if (
754+
detachedInterceptFiber !== null &&
755+
(detachedInterceptFiber === instance ||
756+
detachedInterceptFiber === instance.alternate)
757+
) {
758+
listeners = [];
759+
}
760+
}
743761
instance = instance.return;
744762
}
745763
return listeners;

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

+135
Original file line numberDiff line numberDiff line change
@@ -2424,6 +2424,60 @@ describe('DOMPluginEventSystem', () => {
24242424
expect(log).toEqual(['beforeblur', 'afterblur']);
24252425
});
24262426

2427+
// @gate experimental
2428+
it('beforeblur should skip handlers from a deleted subtree after the focused element is unmounted', () => {
2429+
const onBeforeBlur = jest.fn();
2430+
const innerRef = React.createRef();
2431+
const innerRef2 = React.createRef();
2432+
const setBeforeBlurHandle = ReactDOM.unstable_createEventHandle(
2433+
'beforeblur',
2434+
);
2435+
const ref2 = React.createRef();
2436+
2437+
const Component = ({show}) => {
2438+
const ref = React.useRef(null);
2439+
2440+
React.useEffect(() => {
2441+
const clear1 = setBeforeBlurHandle(ref.current, onBeforeBlur);
2442+
let clear2;
2443+
if (ref2.current) {
2444+
clear2 = setBeforeBlurHandle(ref2.current, onBeforeBlur);
2445+
}
2446+
2447+
return () => {
2448+
clear1();
2449+
if (clear2) {
2450+
clear2();
2451+
}
2452+
};
2453+
});
2454+
2455+
return (
2456+
<div ref={ref}>
2457+
{show && (
2458+
<div ref={ref2}>
2459+
<input ref={innerRef} />
2460+
</div>
2461+
)}
2462+
<div ref={innerRef2} />
2463+
</div>
2464+
);
2465+
};
2466+
2467+
ReactDOM.render(<Component show={true} />, container);
2468+
Scheduler.unstable_flushAll();
2469+
2470+
const inner = innerRef.current;
2471+
const target = createEventTarget(inner);
2472+
target.focus();
2473+
expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2474+
2475+
ReactDOM.render(<Component show={false} />, container);
2476+
Scheduler.unstable_flushAll();
2477+
2478+
expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2479+
});
2480+
24272481
// @gate experimental
24282482
it('beforeblur and afterblur are called after a focused element is suspended', () => {
24292483
const log = [];
@@ -2510,6 +2564,87 @@ describe('DOMPluginEventSystem', () => {
25102564
document.body.removeChild(container2);
25112565
});
25122566

2567+
// @gate experimental
2568+
it('beforeblur should skip handlers from a deleted subtree after the focused element is suspended', () => {
2569+
const onBeforeBlur = jest.fn();
2570+
const innerRef = React.createRef();
2571+
const innerRef2 = React.createRef();
2572+
const setBeforeBlurHandle = ReactDOM.unstable_createEventHandle(
2573+
'beforeblur',
2574+
);
2575+
const ref2 = React.createRef();
2576+
const Suspense = React.Suspense;
2577+
let suspend = false;
2578+
let resolve;
2579+
const promise = new Promise(
2580+
resolvePromise => (resolve = resolvePromise),
2581+
);
2582+
2583+
function Child() {
2584+
if (suspend) {
2585+
throw promise;
2586+
} else {
2587+
return <input ref={innerRef} />;
2588+
}
2589+
}
2590+
2591+
const Component = () => {
2592+
const ref = React.useRef(null);
2593+
2594+
React.useEffect(() => {
2595+
const clear1 = setBeforeBlurHandle(ref.current, onBeforeBlur);
2596+
let clear2;
2597+
if (ref2.current) {
2598+
clear2 = setBeforeBlurHandle(ref2.current, onBeforeBlur);
2599+
}
2600+
2601+
return () => {
2602+
clear1();
2603+
if (clear2) {
2604+
clear2();
2605+
}
2606+
};
2607+
});
2608+
2609+
return (
2610+
<div ref={ref}>
2611+
<Suspense fallback="Loading...">
2612+
<div ref={ref2}>
2613+
<Child />
2614+
</div>
2615+
</Suspense>
2616+
<div ref={innerRef2} />
2617+
</div>
2618+
);
2619+
};
2620+
2621+
const container2 = document.createElement('div');
2622+
document.body.appendChild(container2);
2623+
2624+
const root = ReactDOM.createRoot(container2);
2625+
2626+
act(() => {
2627+
root.render(<Component />);
2628+
});
2629+
jest.runAllTimers();
2630+
2631+
const inner = innerRef.current;
2632+
const target = createEventTarget(inner);
2633+
target.focus();
2634+
expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2635+
2636+
suspend = true;
2637+
act(() => {
2638+
root.render(<Component />);
2639+
});
2640+
jest.runAllTimers();
2641+
2642+
expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2643+
resolve();
2644+
2645+
document.body.removeChild(container2);
2646+
});
2647+
25132648
// @gate experimental
25142649
it('regression: does not fire beforeblur/afterblur if target is already hidden', () => {
25152650
const Suspense = React.Suspense;

packages/react-dom/src/events/plugins/SimpleEventPlugin.js

+1
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ function extractEvents(
200200
nativeEvent.type,
201201
inCapturePhase,
202202
accumulateTargetOnly,
203+
nativeEvent,
203204
);
204205
if (listeners.length > 0) {
205206
// Intentionally create event lazily.

packages/react-native-renderer/src/ReactFabricHostConfig.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
481481
throw new Error('Not yet implemented');
482482
}
483483

484-
export function beforeActiveInstanceBlur() {
484+
export function beforeActiveInstanceBlur(internalInstanceHandle: Object) {
485485
// noop
486486
}
487487

packages/react-native-renderer/src/ReactNativeHostConfig.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
534534
throw new Error('Not yet implemented');
535535
}
536536

537-
export function beforeActiveInstanceBlur() {
537+
export function beforeActiveInstanceBlur(internalInstanceHandle: Object) {
538538
// noop
539539
}
540540

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2172,7 +2172,7 @@ function commitBeforeMutationEffectsImpl(fiber: Fiber) {
21722172
doesFiberContain(fiber, focusedInstanceHandle)
21732173
) {
21742174
shouldFireAfterActiveInstanceBlur = true;
2175-
beforeActiveInstanceBlur();
2175+
beforeActiveInstanceBlur(fiber);
21762176
}
21772177
}
21782178

@@ -2206,7 +2206,7 @@ function commitBeforeMutationEffectsDeletions(deletions: Array<Fiber>) {
22062206

22072207
if (doesFiberContain(fiber, ((focusedInstanceHandle: any): Fiber))) {
22082208
shouldFireAfterActiveInstanceBlur = true;
2209-
beforeActiveInstanceBlur();
2209+
beforeActiveInstanceBlur(fiber);
22102210
}
22112211
}
22122212
}

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2261,7 +2261,7 @@ function commitBeforeMutationEffects() {
22612261
if ((nextEffect.flags & Deletion) !== NoFlags) {
22622262
if (doesFiberContain(nextEffect, focusedInstanceHandle)) {
22632263
shouldFireAfterActiveInstanceBlur = true;
2264-
beforeActiveInstanceBlur();
2264+
beforeActiveInstanceBlur(nextEffect);
22652265
}
22662266
} else {
22672267
// TODO: Move this out of the hot path using a dedicated effect tag.
@@ -2271,7 +2271,7 @@ function commitBeforeMutationEffects() {
22712271
doesFiberContain(nextEffect, focusedInstanceHandle)
22722272
) {
22732273
shouldFireAfterActiveInstanceBlur = true;
2274-
beforeActiveInstanceBlur();
2274+
beforeActiveInstanceBlur(nextEffect);
22752275
}
22762276
}
22772277
}

packages/react-test-renderer/src/ReactTestHostConfig.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ export function makeOpaqueHydratingObject(
377377
};
378378
}
379379

380-
export function beforeActiveInstanceBlur() {
380+
export function beforeActiveInstanceBlur(internalInstanceHandle: Object) {
381381
// noop
382382
}
383383

0 commit comments

Comments
 (0)