Skip to content

Commit

Permalink
[react-interactions] Change unmount blur logic to a dedicated event (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm authored Nov 7, 2019
1 parent ce4b3e9 commit e701632
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 23 deletions.
14 changes: 8 additions & 6 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,11 @@ export function insertInContainerBefore(
}
}

function handleSimulateChildBlur(
// This is a specific event for the React Flare
// event system, so event responders can act
// accordingly to a DOM node being unmounted that
// previously had active document focus.
function dispatchDetachedVisibleNodeEvent(
child: Instance | TextInstance | SuspenseInstance,
): void {
if (
Expand All @@ -463,13 +467,11 @@ function handleSimulateChildBlur(
const targetFiber = getClosestInstanceFromNode(child);
// Simlulate a blur event to the React Flare responder system.
dispatchEventForResponderEventSystem(
'blur',
'detachedvisiblenode',
targetFiber,
({
relatedTarget: null,
target: child,
timeStamp: Date.now(),
type: 'blur',
}: any),
((child: any): Document | Element),
RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
Expand All @@ -481,7 +483,7 @@ export function removeChild(
parentInstance: Instance,
child: Instance | TextInstance | SuspenseInstance,
): void {
handleSimulateChildBlur(child);
dispatchDetachedVisibleNodeEvent(child);
parentInstance.removeChild(child);
}

Expand All @@ -492,7 +494,7 @@ export function removeChildFromContainer(
if (container.nodeType === COMMENT_NODE) {
(container.parentNode: any).removeChild(child);
} else {
handleSimulateChildBlur(child);
dispatchDetachedVisibleNodeEvent(child);
container.removeChild(child);
}
}
Expand Down
29 changes: 26 additions & 3 deletions packages/react-interactions/events/src/dom/Focus.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,28 @@ type FocusProps = {
onFocusVisibleChange: boolean => void,
};

type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
type FocusEventType =
| 'focus'
| 'blur'
| 'focuschange'
| 'focusvisiblechange'
| 'detachedvisiblenode';

type FocusWithinProps = {
disabled?: boolean,
onFocusWithin?: (e: FocusEvent) => void,
onBlurWithin?: (e: FocusEvent) => void,
onFocusWithinChange?: boolean => void,
onFocusWithinVisibleChange?: boolean => void,
onDetachedVisibleNode?: (e: FocusEvent) => void,
};

type FocusWithinEventType =
| 'focuswithinvisiblechange'
| 'focuswithinchange'
| 'blurwithin'
| 'focuswithin';
| 'focuswithin'
| 'detachedvisiblenode';

/**
* Shared between Focus and FocusWithin
Expand All @@ -72,7 +79,7 @@ const isMac =
? /^Mac/.test(window.navigator.platform)
: false;

const targetEventTypes = ['focus', 'blur'];
const targetEventTypes = ['focus', 'blur', 'detachedvisiblenode'];

const hasPointerEvents =
typeof window !== 'undefined' && window.PointerEvent != null;
Expand Down Expand Up @@ -507,6 +514,22 @@ const focusWithinResponderImpl = {
}
break;
}
case 'detachedvisiblenode': {
const onDetachedVisibleNode = (props.onDetachedVisibleNode: any);
if (isFunction(onDetachedVisibleNode)) {
const syntheticEvent = createFocusEvent(
context,
'detachedvisiblenode',
event.target,
state.pointerType,
);
context.dispatchEvent(
syntheticEvent,
onDetachedVisibleNode,
DiscreteEvent,
);
}
}
}
},
onRootEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,6 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
expect(onFocusWithinChange).toHaveBeenCalledWith(false);
});

it('is called after a focused element is unmounted', () => {
const target = createEventTarget(innerRef.current);
target.focus();
expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
expect(onFocusWithinChange).toHaveBeenCalledWith(true);
ReactDOM.render(<Component show={false} />, container);
expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
expect(onFocusWithinChange).toHaveBeenCalledWith(false);
});
});

describe('onFocusWithinVisibleChange', () => {
Expand Down Expand Up @@ -270,17 +260,39 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
});
});

describe('onDetachedVisibleNode', () => {
let onDetachedVisibleNode, ref, innerRef, innerRef2;

const Component = ({show}) => {
const listener = useFocusWithin({
onDetachedVisibleNode,
});
return (
<div ref={ref} listeners={listener}>
{show && <input ref={innerRef} />}
<div ref={innerRef2} />
</div>
);
};

beforeEach(() => {
onDetachedVisibleNode = jest.fn();
ref = React.createRef();
innerRef = React.createRef();
innerRef2 = React.createRef();
ReactDOM.render(<Component show={true} />, container);
});

it('is called after a focused element is unmounted', () => {
const inner = innerRef.current;
const target = createEventTarget(inner);
target.keydown({key: 'Tab'});
target.focus();
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
expect(onDetachedVisibleNode).toHaveBeenCalledTimes(0);
ReactDOM.render(<Component show={false} />, container);
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
expect(onDetachedVisibleNode).toHaveBeenCalledTimes(1);
});
});

Expand Down

0 comments on commit e701632

Please sign in to comment.