[EventSystem] Revise onBeforeBlur propagation mechanics (#20020)
Dominic Gannaway committed
Oct 14, 2020 at 23:38 UTC
d95c4938df670a8f0a13267bd89173737bb185e4
10 files changed
+174
-12
packages/react-art/src/ReactARTHostConfig.js
+1
-1
@@ -467,7 +467,7 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
467
throw new Error('Not yet implemented');
468
}
469
470
-export function beforeActiveInstanceBlur() {
470
+export function beforeActiveInstanceBlur(internalInstanceHandle: Object) {
471
// noop
472
}
473
packages/react-dom/src/client/ReactDOMHostConfig.js
+11
-3
@@ -232,10 +232,13 @@ export function prepareForCommit(containerInfo: Container): Object | null {
232
return activeInstance;
233
}
234
235
-export function beforeActiveInstanceBlur(): void {
235
+export function beforeActiveInstanceBlur(internalInstanceHandle: Object): void {
236
if (enableCreateEventHandleAPI) {
237
ReactBrowserEventEmitterSetEnabled(true);
238
- dispatchBeforeDetachedBlur((selectionInformation: any).focusedElem);
238
+ dispatchBeforeDetachedBlur(
239
+ (selectionInformation: any).focusedElem,
240
+ internalInstanceHandle,
241
+ );
242
ReactBrowserEventEmitterSetEnabled(false);
243
}
244
}
@@ -499,12 +502,17 @@ function createEvent(type: DOMEventName, bubbles: boolean): Event {
502
return event;
503
}
504
502
-function dispatchBeforeDetachedBlur(target: HTMLElement): void {
505
+function dispatchBeforeDetachedBlur(
506
+ target: HTMLElement,
507
+ internalInstanceHandle: Object,
508
+): void {
509
if (enableCreateEventHandleAPI) {
510
const event = createEvent('beforeblur', true);
511
// Dispatch "beforeblur" directly on the target,
512
// so it gets picked up by the event system and
513
// can propagate through the React internal tree.
514
+ // $FlowFixMe: internal field
515
+ event._detachedInterceptFiber = internalInstanceHandle;
516
target.dispatchEvent(event);
517
}
518
}
packages/react-dom/src/events/DOMPluginEventSystem.js
+19
-1
@@ -658,10 +658,11 @@ export function accumulateSinglePhaseListeners(
658
nativeEventType: string,
659
inCapturePhase: boolean,
660
accumulateTargetOnly: boolean,
661
+ nativeEvent: AnyNativeEvent,
662
): Array<DispatchListener> {
663
const captureName = reactName !== null ? reactName + 'Capture' : null;
664
const reactEventName = inCapturePhase ? captureName : reactName;
664
- const listeners: Array<DispatchListener> = [];
665
+ let listeners: Array<DispatchListener> = [];
666
667
let instance = targetFiber;
668
let lastHostComponent = null;
@@ -740,6 +741,23 @@ export function accumulateSinglePhaseListeners(
741
if (accumulateTargetOnly) {
742
break;
743
}
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
+ }
761
instance = instance.return;
762
}
763
return listeners;
packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+135
@@ -2424,6 +2424,60 @@ describe('DOMPluginEventSystem', () => {
2424
expect(log).toEqual(['beforeblur', 'afterblur']);
2425
});
2426
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
+
2481
// @gate experimental
2482
it('beforeblur and afterblur are called after a focused element is suspended', () => {
2483
const log = [];
@@ -2510,6 +2564,87 @@ describe('DOMPluginEventSystem', () => {
2564
document.body.removeChild(container2);
2565
});
2566
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
+
2648
// @gate experimental
2649
it('regression: does not fire beforeblur/afterblur if target is already hidden', () => {
2650
const Suspense = React.Suspense;
packages/react-dom/src/events/plugins/SimpleEventPlugin.js
+1
@@ -200,6 +200,7 @@ function extractEvents(
200
nativeEvent.type,
201
inCapturePhase,
202
accumulateTargetOnly,
203
+ nativeEvent,
204
);
205
if (listeners.length > 0) {
206
// Intentionally create event lazily.
packages/react-native-renderer/src/ReactFabricHostConfig.js
+1
-1
@@ -481,7 +481,7 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
481
throw new Error('Not yet implemented');
482
}
483
484
-export function beforeActiveInstanceBlur() {
484
+export function beforeActiveInstanceBlur(internalInstanceHandle: Object) {
485
// noop
486
}
487
packages/react-native-renderer/src/ReactNativeHostConfig.js
+1
-1
@@ -534,7 +534,7 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
534
throw new Error('Not yet implemented');
535
}
536
537
-export function beforeActiveInstanceBlur() {
537
+export function beforeActiveInstanceBlur(internalInstanceHandle: Object) {
538
// noop
539
}
540
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+2
-2
@@ -2172,7 +2172,7 @@ function commitBeforeMutationEffectsImpl(fiber: Fiber) {
2172
doesFiberContain(fiber, focusedInstanceHandle)
2173
) {
2174
shouldFireAfterActiveInstanceBlur = true;
2175
- beforeActiveInstanceBlur();
2175
+ beforeActiveInstanceBlur(fiber);
2176
}
2177
}
2178
@@ -2206,7 +2206,7 @@ function commitBeforeMutationEffectsDeletions(deletions: Array<Fiber>) {
2206
2207
if (doesFiberContain(fiber, ((focusedInstanceHandle: any): Fiber))) {
2208
shouldFireAfterActiveInstanceBlur = true;
2209
- beforeActiveInstanceBlur();
2209
+ beforeActiveInstanceBlur(fiber);
2210
}
2211
}
2212
}
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+2
-2
@@ -2261,7 +2261,7 @@ function commitBeforeMutationEffects() {
2261
if ((nextEffect.flags & Deletion) !== NoFlags) {
2262
if (doesFiberContain(nextEffect, focusedInstanceHandle)) {
2263
shouldFireAfterActiveInstanceBlur = true;
2264
- beforeActiveInstanceBlur();
2264
+ beforeActiveInstanceBlur(nextEffect);
2265
}
2266
} else {
2267
// TODO: Move this out of the hot path using a dedicated effect tag.
@@ -2271,7 +2271,7 @@ function commitBeforeMutationEffects() {
2271
doesFiberContain(nextEffect, focusedInstanceHandle)
2272
) {
2273
shouldFireAfterActiveInstanceBlur = true;
2274
- beforeActiveInstanceBlur();
2274
+ beforeActiveInstanceBlur(nextEffect);
2275
}
2276
}
2277
}
packages/react-test-renderer/src/ReactTestHostConfig.js
+1
-1
@@ -377,7 +377,7 @@ export function makeOpaqueHydratingObject(
377
};
378
}
379
380
-export function beforeActiveInstanceBlur() {
380
+export function beforeActiveInstanceBlur(internalInstanceHandle: Object) {
381
// noop
382
}
383