Skip to content

Commit f774ae1

Browse files
committed
[EventSystem] Revise onBeforeBlur propagation mechancis
Fix host configs
1 parent e614e69 commit f774ae1

File tree

10 files changed

+169
-12
lines changed

10 files changed

+169
-12
lines changed

packages/react-art/src/ReactARTHostConfig.js

Lines changed: 1 addition & 1 deletion
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

Lines changed: 11 additions & 3 deletions
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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
HostComponent,
3535
HostText,
3636
ScopeComponent,
37+
SuspenseComponent,
3738
} from 'react-reconciler/src/ReactWorkTags';
3839

3940
import getEventTarget from './getEventTarget';
@@ -70,6 +71,7 @@ import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
7071
import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
7172
import * as SelectEventPlugin from './plugins/SelectEventPlugin';
7273
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';
74+
import {Deletion} from 'react-reconciler/src/ReactFiberFlags';
7375

7476
type DispatchListener = {|
7577
instance: null | Fiber,
@@ -658,10 +660,11 @@ export function accumulateSinglePhaseListeners(
658660
nativeEventType: string,
659661
inCapturePhase: boolean,
660662
accumulateTargetOnly: boolean,
663+
nativeEvent: AnyNativeEvent,
661664
): Array<DispatchListener> {
662665
const captureName = reactName !== null ? reactName + 'Capture' : null;
663666
const reactEventName = inCapturePhase ? captureName : reactName;
664-
const listeners: Array<DispatchListener> = [];
667+
let listeners: Array<DispatchListener> = [];
665668

666669
let instance = targetFiber;
667670
let lastHostComponent = null;
@@ -740,6 +743,20 @@ export function accumulateSinglePhaseListeners(
740743
if (accumulateTargetOnly) {
741744
break;
742745
}
746+
// If we are processing the onBeforeBlur event, then we need to take
747+
// into consideration that part of the React tree might have been hidden
748+
// or deleted (as we're invoking this event during commit). We can find
749+
// this out by checking if intercept fiber set on the event matches the
750+
// current instance fiber. In which case, we should clear all existing
751+
// listeners.
752+
if (
753+
enableCreateEventHandleAPI &&
754+
nativeEvent.type === 'beforeblur' &&
755+
// $FlowFixMe: internal field
756+
nativeEvent._detachedInterceptFiber === instance
757+
) {
758+
listeners = [];
759+
}
743760
instance = instance.return;
744761
}
745762
return listeners;

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

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2424,6 +2424,58 @@ describe('DOMPluginEventSystem', () => {
24242424
expect(log).toEqual(['beforeblur', 'afterblur']);
24252425
});
24262426

2427+
// @gate experimental
2428+
it('beforeblur has the correct propagation mechancis after a nested 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+
clear2 && clear2();
2450+
};
2451+
});
2452+
2453+
return (
2454+
<div ref={ref}>
2455+
{show && (
2456+
<div ref={ref2}>
2457+
<input ref={innerRef} />
2458+
</div>
2459+
)}
2460+
<div ref={innerRef2} />
2461+
</div>
2462+
);
2463+
};
2464+
2465+
ReactDOM.render(<Component show={true} />, container);
2466+
Scheduler.unstable_flushAll();
2467+
2468+
const inner = innerRef.current;
2469+
const target = createEventTarget(inner);
2470+
target.focus();
2471+
expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2472+
2473+
ReactDOM.render(<Component show={false} />, container);
2474+
Scheduler.unstable_flushAll();
2475+
2476+
expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2477+
});
2478+
24272479
// @gate experimental
24282480
it('beforeblur and afterblur are called after a focused element is suspended', () => {
24292481
const log = [];
@@ -2510,6 +2562,85 @@ describe('DOMPluginEventSystem', () => {
25102562
document.body.removeChild(container2);
25112563
});
25122564

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

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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 2 additions & 2 deletions
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(current);
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

Lines changed: 2 additions & 2 deletions
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(current);
22752275
}
22762276
}
22772277
}

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

Lines changed: 1 addition & 1 deletion
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)