Skip to content

Commit

Permalink
[react-interactions] Ensure onBeforeBlur fires for hideInstance (face…
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm committed Feb 18, 2020
1 parent 48c4867 commit 1a6d817
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 10 deletions.
42 changes: 32 additions & 10 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ import {
} from '../events/DeprecatedDOMEventResponderSystem';
import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying';

import {
enableSuspenseServerRenderer,
enableDeprecatedFlareAPI,
enableFundamentalAPI,
} from 'shared/ReactFeatureFlags';
import {HostComponent} from 'shared/ReactWorkTags';
import {
RESPONDER_EVENT_SYSTEM,
IS_PASSIVE,
} from 'legacy-events/EventSystemFlags';

export type Type = string;
export type Props = {
autoFocus?: boolean,
Expand Down Expand Up @@ -112,16 +123,6 @@ type SelectionInformation = {|
selectionRange: mixed,
|};

import {
enableSuspenseServerRenderer,
enableDeprecatedFlareAPI,
enableFundamentalAPI,
} from 'shared/ReactFeatureFlags';
import {
RESPONDER_EVENT_SYSTEM,
IS_PASSIVE,
} from 'legacy-events/EventSystemFlags';

let SUPPRESS_HYDRATION_WARNING;
if (__DEV__) {
SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
Expand Down Expand Up @@ -584,7 +585,28 @@ export function clearSuspenseBoundaryFromContainer(
retryIfBlockedOn(container);
}

function instanceContainsElem(instance: Instance, element: HTMLElement) {
let fiber = getClosestInstanceFromNode(element);
while (fiber !== null) {
if (fiber.tag === HostComponent && fiber.stateNode === element) {
return true;
}
fiber = fiber.return;
}
return false;
}

export function hideInstance(instance: Instance): void {
// Ensure we trigger `onBeforeBlur` if the active focused elment
// is ether the instance of a child or the instance. We need
// to traverse the Fiber tree here rather than use node.contains()
// as the child node might be inside a Portal.
if (enableDeprecatedFlareAPI && selectionInformation) {
const focusedElem = selectionInformation.focusedElem;
if (focusedElem !== null && instanceContainsElem(instance, focusedElem)) {
dispatchBeforeDetachedBlur(((focusedElem: any): HTMLElement));
}
}
// TODO: Does this work for all element types? What about MathML? Should we
// pass host context to this method?
instance = ((instance: any): HTMLElement);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ let ReactFeatureFlags;
let ReactDOM;
let FocusWithinResponder;
let useFocusWithin;
let Scheduler;

const initializeModules = hasPointerEvents => {
setPointerEvent(hasPointerEvents);
Expand All @@ -27,6 +28,7 @@ const initializeModules = hasPointerEvents => {
FocusWithinResponder = require('react-interactions/events/focus')
.FocusWithinResponder;
useFocusWithin = require('react-interactions/events/focus').useFocusWithin;
Scheduler = require('scheduler');
};

const forcePointerEvents = true;
Expand Down Expand Up @@ -336,6 +338,68 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
expect.objectContaining({isTargetAttached: false}),
);
});

it.experimental(
'is called after a focused suspended element is hidden',
() => {
const Suspense = React.Suspense;
let suspend = false;
let resolve;
let promise = new Promise(resolvePromise => (resolve = resolvePromise));

function Child() {
if (suspend) {
throw promise;
} else {
return <input ref={innerRef} />;
}
}

const Component = ({show}) => {
const listener = useFocusWithin({
onBeforeBlurWithin,
onBlurWithin,
});

return (
<div DEPRECATED_flareListeners={listener}>
<Suspense fallback="Loading...">
<Child />
</Suspense>
</div>
);
};

const container2 = document.createElement('div');
document.body.appendChild(container2);

let root = ReactDOM.createRoot(container2);
root.render(<Component />);
Scheduler.unstable_flushAll();
jest.runAllTimers();
expect(container2.innerHTML).toBe('<div><input></div>');

const inner = innerRef.current;
const target = createEventTarget(inner);
target.keydown({key: 'Tab'});
target.focus();
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
expect(onBlurWithin).toHaveBeenCalledTimes(0);

suspend = true;
root.render(<Component />);
Scheduler.unstable_flushAll();
jest.runAllTimers();
expect(container2.innerHTML).toBe(
'<div><input style="display: none;">Loading...</div>',
);
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
expect(onBlurWithin).toHaveBeenCalledTimes(1);
resolve();

document.body.removeChild(container2);
},
);
});

it('expect displayName to show up for event component', () => {
Expand Down

0 comments on commit 1a6d817

Please sign in to comment.