@samitouri / QOS-React-2 / commits / d95c4938df

[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