@samitouri / QOS-React-2 / commits / 1a6d8179b6

[react-interactions] Ensure onBeforeBlur fires for hideInstance (#18064)

Dominic Gannaway committed Feb 18, 2020 at 18:50 UTC 1a6d8179b6dd427fdf7ee50d5ac45ae5a40979eb
2 files changed +96 -10
packages/react-dom/src/client/ReactDOMHostConfig.js
+32 -10
@@ -58,6 +58,17 @@ import {
58 } from '../events/DeprecatedDOMEventResponderSystem';
59 import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying';
60
61 +import {
62 + enableSuspenseServerRenderer,
63 + enableDeprecatedFlareAPI,
64 + enableFundamentalAPI,
65 +} from 'shared/ReactFeatureFlags';
66 +import {HostComponent} from 'shared/ReactWorkTags';
67 +import {
68 + RESPONDER_EVENT_SYSTEM,
69 + IS_PASSIVE,
70 +} from 'legacy-events/EventSystemFlags';
71 +
72 export type Type = string;
73 export type Props = {
74 autoFocus?: boolean,
@@ -112,16 +123,6 @@ type SelectionInformation = {|
123 selectionRange: mixed,
124 |};
125
115 -import {
116 - enableSuspenseServerRenderer,
117 - enableDeprecatedFlareAPI,
118 - enableFundamentalAPI,
119 -} from 'shared/ReactFeatureFlags';
120 -import {
121 - RESPONDER_EVENT_SYSTEM,
122 - IS_PASSIVE,
123 -} from 'legacy-events/EventSystemFlags';
124 -
126 let SUPPRESS_HYDRATION_WARNING;
127 if (__DEV__) {
128 SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
@@ -584,7 +585,28 @@ export function clearSuspenseBoundaryFromContainer(
585 retryIfBlockedOn(container);
586 }
587
588 +function instanceContainsElem(instance: Instance, element: HTMLElement) {
589 + let fiber = getClosestInstanceFromNode(element);
590 + while (fiber !== null) {
591 + if (fiber.tag === HostComponent && fiber.stateNode === element) {
592 + return true;
593 + }
594 + fiber = fiber.return;
595 + }
596 + return false;
597 +}
598 +
599 export function hideInstance(instance: Instance): void {
600 + // Ensure we trigger `onBeforeBlur` if the active focused elment
601 + // is ether the instance of a child or the instance. We need
602 + // to traverse the Fiber tree here rather than use node.contains()
603 + // as the child node might be inside a Portal.
604 + if (enableDeprecatedFlareAPI && selectionInformation) {
605 + const focusedElem = selectionInformation.focusedElem;
606 + if (focusedElem !== null && instanceContainsElem(instance, focusedElem)) {
607 + dispatchBeforeDetachedBlur(((focusedElem: any): HTMLElement));
608 + }
609 + }
610 // TODO: Does this work for all element types? What about MathML? Should we
611 // pass host context to this method?
612 instance = ((instance: any): HTMLElement);
packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
+64
@@ -16,6 +16,7 @@ let ReactFeatureFlags;
16 let ReactDOM;
17 let FocusWithinResponder;
18 let useFocusWithin;
19 +let Scheduler;
20
21 const initializeModules = hasPointerEvents => {
22 setPointerEvent(hasPointerEvents);
@@ -27,6 +28,7 @@ const initializeModules = hasPointerEvents => {
28 FocusWithinResponder = require('react-interactions/events/focus')
29 .FocusWithinResponder;
30 useFocusWithin = require('react-interactions/events/focus').useFocusWithin;
31 + Scheduler = require('scheduler');
32 };
33
34 const forcePointerEvents = true;
@@ -336,6 +338,68 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
338 expect.objectContaining({isTargetAttached: false}),
339 );
340 });
341 +
342 + it.experimental(
343 + 'is called after a focused suspended element is hidden',
344 + () => {
345 + const Suspense = React.Suspense;
346 + let suspend = false;
347 + let resolve;
348 + let promise = new Promise(resolvePromise => (resolve = resolvePromise));
349 +
350 + function Child() {
351 + if (suspend) {
352 + throw promise;
353 + } else {
354 + return <input ref={innerRef} />;
355 + }
356 + }
357 +
358 + const Component = ({show}) => {
359 + const listener = useFocusWithin({
360 + onBeforeBlurWithin,
361 + onBlurWithin,
362 + });
363 +
364 + return (
365 + <div DEPRECATED_flareListeners={listener}>
366 + <Suspense fallback="Loading...">
367 + <Child />
368 + </Suspense>
369 + </div>
370 + );
371 + };
372 +
373 + const container2 = document.createElement('div');
374 + document.body.appendChild(container2);
375 +
376 + let root = ReactDOM.createRoot(container2);
377 + root.render(<Component />);
378 + Scheduler.unstable_flushAll();
379 + jest.runAllTimers();
380 + expect(container2.innerHTML).toBe('<div><input></div>');
381 +
382 + const inner = innerRef.current;
383 + const target = createEventTarget(inner);
384 + target.keydown({key: 'Tab'});
385 + target.focus();
386 + expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
387 + expect(onBlurWithin).toHaveBeenCalledTimes(0);
388 +
389 + suspend = true;
390 + root.render(<Component />);
391 + Scheduler.unstable_flushAll();
392 + jest.runAllTimers();
393 + expect(container2.innerHTML).toBe(
394 + '<div><input style="display: none;">Loading...</div>',
395 + );
396 + expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
397 + expect(onBlurWithin).toHaveBeenCalledTimes(1);
398 + resolve();
399 +
400 + document.body.removeChild(container2);
401 + },
402 + );
403 });
404
405 it('expect displayName to show up for event component', () => {