[react-interactions] Change unmount blur logic to a dedicated event (#17291)
Dominic Gannaway committed
Nov 7, 2019 at 10:27 UTC
e701632ad4112038c4dd8e7302b90c0bb3fc4f2b
3 files changed
+60
-23
packages/react-dom/src/client/ReactDOMHostConfig.js
+8
-6
@@ -452,7 +452,11 @@ export function insertInContainerBefore(
452
}
453
}
454
455
-function handleSimulateChildBlur(
455
+// This is a specific event for the React Flare
456
+// event system, so event responders can act
457
+// accordingly to a DOM node being unmounted that
458
+// previously had active document focus.
459
+function dispatchDetachedVisibleNodeEvent(
460
child: Instance | TextInstance | SuspenseInstance,
461
): void {
462
if (
@@ -463,13 +467,11 @@ function handleSimulateChildBlur(
467
const targetFiber = getClosestInstanceFromNode(child);
468
// Simlulate a blur event to the React Flare responder system.
469
dispatchEventForResponderEventSystem(
466
- 'blur',
470
+ 'detachedvisiblenode',
471
targetFiber,
472
({
469
- relatedTarget: null,
473
target: child,
474
timeStamp: Date.now(),
472
- type: 'blur',
475
}: any),
476
((child: any): Document | Element),
477
RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
@@ -481,7 +483,7 @@ export function removeChild(
483
parentInstance: Instance,
484
child: Instance | TextInstance | SuspenseInstance,
485
): void {
484
- handleSimulateChildBlur(child);
486
+ dispatchDetachedVisibleNodeEvent(child);
487
parentInstance.removeChild(child);
488
}
489
@@ -492,7 +494,7 @@ export function removeChildFromContainer(
494
if (container.nodeType === COMMENT_NODE) {
495
(container.parentNode: any).removeChild(child);
496
} else {
495
- handleSimulateChildBlur(child);
497
+ dispatchDetachedVisibleNodeEvent(child);
498
container.removeChild(child);
499
}
500
}
packages/react-interactions/events/src/dom/Focus.js
+26
-3
@@ -45,7 +45,12 @@ type FocusProps = {
45
onFocusVisibleChange: boolean => void,
46
};
47
48
-type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
48
+type FocusEventType =
49
+ | 'focus'
50
+ | 'blur'
51
+ | 'focuschange'
52
+ | 'focusvisiblechange'
53
+ | 'detachedvisiblenode';
54
55
type FocusWithinProps = {
56
disabled?: boolean,
@@ -53,13 +58,15 @@ type FocusWithinProps = {
58
onBlurWithin?: (e: FocusEvent) => void,
59
onFocusWithinChange?: boolean => void,
60
onFocusWithinVisibleChange?: boolean => void,
61
+ onDetachedVisibleNode?: (e: FocusEvent) => void,
62
};
63
64
type FocusWithinEventType =
65
| 'focuswithinvisiblechange'
66
| 'focuswithinchange'
67
| 'blurwithin'
62
- | 'focuswithin';
68
+ | 'focuswithin'
69
+ | 'detachedvisiblenode';
70
71
/**
72
* Shared between Focus and FocusWithin
@@ -72,7 +79,7 @@ const isMac =
79
? /^Mac/.test(window.navigator.platform)
80
: false;
81
75
-const targetEventTypes = ['focus', 'blur'];
82
+const targetEventTypes = ['focus', 'blur', 'detachedvisiblenode'];
83
84
const hasPointerEvents =
85
typeof window !== 'undefined' && window.PointerEvent != null;
@@ -507,6 +514,22 @@ const focusWithinResponderImpl = {
514
}
515
break;
516
}
517
+ case 'detachedvisiblenode': {
518
+ const onDetachedVisibleNode = (props.onDetachedVisibleNode: any);
519
+ if (isFunction(onDetachedVisibleNode)) {
520
+ const syntheticEvent = createFocusEvent(
521
+ context,
522
+ 'detachedvisiblenode',
523
+ event.target,
524
+ state.pointerType,
525
+ );
526
+ context.dispatchEvent(
527
+ syntheticEvent,
528
+ onDetachedVisibleNode,
529
+ DiscreteEvent,
530
+ );
531
+ }
532
+ }
533
}
534
},
535
onRootEvent(
packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
+26
-14
@@ -141,16 +141,6 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
141
expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
142
expect(onFocusWithinChange).toHaveBeenCalledWith(false);
143
});
144
-
145
- it('is called after a focused element is unmounted', () => {
146
- const target = createEventTarget(innerRef.current);
147
- target.focus();
148
- expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
149
- expect(onFocusWithinChange).toHaveBeenCalledWith(true);
150
- ReactDOM.render(<Component show={false} />, container);
151
- expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
152
- expect(onFocusWithinChange).toHaveBeenCalledWith(false);
153
- });
144
});
145
146
describe('onFocusWithinVisibleChange', () => {
@@ -270,17 +260,39 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
260
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
261
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
262
});
263
+ });
264
+
265
+ describe('onDetachedVisibleNode', () => {
266
+ let onDetachedVisibleNode, ref, innerRef, innerRef2;
267
+
268
+ const Component = ({show}) => {
269
+ const listener = useFocusWithin({
270
+ onDetachedVisibleNode,
271
+ });
272
+ return (
273
+ <div ref={ref} listeners={listener}>
274
+ {show && <input ref={innerRef} />}
275
+ <div ref={innerRef2} />
276
+ </div>
277
+ );
278
+ };
279
+
280
+ beforeEach(() => {
281
+ onDetachedVisibleNode = jest.fn();
282
+ ref = React.createRef();
283
+ innerRef = React.createRef();
284
+ innerRef2 = React.createRef();
285
+ ReactDOM.render(<Component show={true} />, container);
286
+ });
287
288
it('is called after a focused element is unmounted', () => {
289
const inner = innerRef.current;
290
const target = createEventTarget(inner);
291
target.keydown({key: 'Tab'});
292
target.focus();
279
- expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
280
- expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
293
+ expect(onDetachedVisibleNode).toHaveBeenCalledTimes(0);
294
ReactDOM.render(<Component show={false} />, container);
282
- expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
283
- expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
295
+ expect(onDetachedVisibleNode).toHaveBeenCalledTimes(1);
296
});
297
});
298