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

[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