[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', () => {