ReactDOM.useEvent: add support for beforeblur/afterblur (#18370)

* ReactDOM.useEvent: add support for beforeblur/afterblur
This commit is contained in:
Dominic Gannaway
2020-03-24 16:14:55 +00:00
committed by GitHub
parent a56309fb88
commit 0140118e8e
7 changed files with 271 additions and 25 deletions
+1
View File
@@ -113,6 +113,7 @@
"test-persistent": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-persistent.js",
"debug-test-persistent": "cross-env NODE_ENV=development node --inspect-brk node_modules/jest/bin/jest.js --config ./scripts/jest/config.source-persistent.js --runInBand",
"test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js",
"debug-test-prod": "cross-env NODE_ENV=production node --inspect-brk node_modules/jest/bin/jest.js --config ./scripts/jest/config.source.js --runInBand",
"test-prod-build": "yarn test-build-prod",
"test-build": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build.js",
"test-build-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.build.js",
+58 -24
View File
@@ -7,6 +7,7 @@
* @flow
*/
import type {TopLevelType} from 'legacy-events/TopLevelEventTypes';
import type {RootType} from './ReactDOMRoot';
import {
@@ -84,6 +85,7 @@ import {
attachTargetEventListener,
} from '../events/DOMModernPluginEventSystem';
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
export type ReactListenerEvent = ReactDOMListenerEvent;
export type ReactListenerMap = ReactDOMListenerMap;
@@ -238,11 +240,11 @@ export function resetAfterCommit(containerInfo: Container): void {
restoreSelection(selectionInformation);
ReactBrowserEventEmitterSetEnabled(eventsEnabled);
eventsEnabled = null;
if (enableDeprecatedFlareAPI) {
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
const activeElementDetached = (selectionInformation: any)
.activeElementDetached;
if (activeElementDetached !== null) {
dispatchDetachedBlur(activeElementDetached);
dispatchAfterDetachedBlur(activeElementDetached);
}
}
selectionInformation = null;
@@ -490,34 +492,66 @@ export function insertInContainerBefore(
}
}
function createEvent(type: TopLevelType): Event {
const event = document.createEvent('Event');
event.initEvent(((type: any): string), false, false);
return event;
}
function dispatchBeforeDetachedBlur(target: HTMLElement): void {
const targetInstance = getClosestInstanceFromNode(target);
((selectionInformation: any): SelectionInformation).activeElementDetached = target;
DEPRECATED_dispatchEventForResponderEventSystem(
'beforeblur',
targetInstance,
({
if (enableDeprecatedFlareAPI) {
DEPRECATED_dispatchEventForResponderEventSystem(
'beforeblur',
targetInstance,
({
target,
timeStamp: Date.now(),
}: any),
target,
timeStamp: Date.now(),
}: any),
target,
RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
);
RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
);
}
if (enableUseEventAPI) {
try {
// We need to temporarily enable the event system
// to dispatch the "beforeblur" event.
ReactBrowserEventEmitterSetEnabled(true);
const event = createEvent(TOP_BEFORE_BLUR);
// Dispatch "beforeblur" directly on the target,
// so it gets picked up by the event system and
// can propagate through the React internal tree.
target.dispatchEvent(event);
} finally {
ReactBrowserEventEmitterSetEnabled(false);
}
}
}
function dispatchDetachedBlur(target: HTMLElement): void {
DEPRECATED_dispatchEventForResponderEventSystem(
'blur',
null,
({
isTargetAttached: false,
function dispatchAfterDetachedBlur(target: HTMLElement): void {
if (enableDeprecatedFlareAPI) {
DEPRECATED_dispatchEventForResponderEventSystem(
'blur',
null,
({
isTargetAttached: false,
target,
timeStamp: Date.now(),
}: any),
target,
timeStamp: Date.now(),
}: any),
target,
RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
);
RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
);
}
if (enableUseEventAPI) {
const event = createEvent(TOP_AFTER_BLUR);
// So we know what was detached, make the relatedTarget the
// detached target on the "afterblur" event.
(event: any).relatedTarget = target;
// Dispatch the event on the document.
document.dispatchEvent(event);
}
}
// This is a specific event for the React Flare
@@ -528,7 +562,7 @@ export function beforeRemoveInstance(
instance: Instance | TextInstance | SuspenseInstance,
): void {
if (
enableDeprecatedFlareAPI &&
(enableDeprecatedFlareAPI || enableUseEventAPI) &&
selectionInformation &&
instance === selectionInformation.focusedElem
) {
@@ -639,7 +673,7 @@ export function hideInstance(instance: Instance): void {
// 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) {
if ((enableDeprecatedFlareAPI || enableUseEventAPI) && selectionInformation) {
const focusedElem = selectionInformation.focusedElem;
if (focusedElem !== null && instanceContainsElem(instance, focusedElem)) {
dispatchBeforeDetachedBlur(((focusedElem: any): HTMLElement));
+8
View File
@@ -23,6 +23,7 @@ import {
UserBlockingEvent,
ContinuousEvent,
} from 'shared/ReactTypes';
import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
// Needed for SimpleEventPlugin, rather than
// do it in two places, which duplicates logic
@@ -95,6 +96,13 @@ const otherDiscreteEvents = [
DOMTopLevelEventTypes.TOP_COMPOSITION_UPDATE,
];
if (enableUseEventAPI) {
otherDiscreteEvents.push(
DOMTopLevelEventTypes.TOP_BEFORE_BLUR,
DOMTopLevelEventTypes.TOP_AFTER_BLUR,
);
}
// prettier-ignore
const userBlockingPairsForSimpleEventPlugin = [
DOMTopLevelEventTypes.TOP_DRAG, 'drag',
+11 -1
View File
@@ -75,6 +75,8 @@ import {
TOP_PROGRESS,
TOP_PLAYING,
TOP_CLICK,
TOP_BEFORE_BLUR,
TOP_AFTER_BLUR,
} from './DOMTopLevelEventTypes';
import {
getClosestInstanceFromNode,
@@ -84,7 +86,10 @@ import {
import {COMMENT_NODE} from '../shared/HTMLNodeType';
import {topLevelEventsToDispatchConfig} from './DOMEventProperties';
import {enableLegacyFBSupport} from 'shared/ReactFeatureFlags';
import {
enableLegacyFBSupport,
enableUseEventAPI,
} from 'shared/ReactFeatureFlags';
const capturePhaseEvents = new Set([
TOP_FOCUS,
@@ -122,6 +127,11 @@ const capturePhaseEvents = new Set([
TOP_WAITING,
]);
if (enableUseEventAPI) {
capturePhaseEvents.add(TOP_BEFORE_BLUR);
capturePhaseEvents.add(TOP_AFTER_BLUR);
}
const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {
customEvent: true,
phasedRegistrationNames: {
@@ -149,6 +149,9 @@ export const TOP_VOLUME_CHANGE = unsafeCastStringToDOMTopLevelType(
export const TOP_WAITING = unsafeCastStringToDOMTopLevelType('waiting');
export const TOP_WHEEL = unsafeCastStringToDOMTopLevelType('wheel');
export const TOP_AFTER_BLUR = unsafeCastStringToDOMTopLevelType('afterblur');
export const TOP_BEFORE_BLUR = unsafeCastStringToDOMTopLevelType('beforeblur');
// List of events that need to be individually attached to media elements.
// Note that events in this list will *not* be listened to at the top level
// unless they're explicitly whitelisted in `ReactBrowserEventEmitter.listenTo`.
+2
View File
@@ -105,6 +105,8 @@ const SimpleEventPlugin: PluginModule<MouseEvent> = {
break;
case DOMTopLevelEventTypes.TOP_BLUR:
case DOMTopLevelEventTypes.TOP_FOCUS:
case DOMTopLevelEventTypes.TOP_BEFORE_BLUR:
case DOMTopLevelEventTypes.TOP_AFTER_BLUR:
EventConstructor = SyntheticFocusEvent;
break;
case DOMTopLevelEventTypes.TOP_CLICK:
@@ -9,10 +9,13 @@
'use strict';
import {createEventTarget} from 'dom-event-testing-library';
let React;
let ReactFeatureFlags;
let ReactDOM;
let ReactDOMServer;
let ReactTestUtils;
let Scheduler;
function dispatchEvent(element, type) {
@@ -64,6 +67,7 @@ describe('DOMModernPluginEventSystem', () => {
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
ReactDOMServer = require('react-dom/server');
ReactTestUtils = require('react-dom/test-utils');
container = document.createElement('div');
document.body.appendChild(container);
startNativeEventListenerClearDown();
@@ -1092,6 +1096,7 @@ describe('DOMModernPluginEventSystem', () => {
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
ReactDOMServer = require('react-dom/server');
ReactTestUtils = require('react-dom/test-utils');
});
if (!__EXPERIMENTAL__) {
@@ -2139,6 +2144,189 @@ describe('DOMModernPluginEventSystem', () => {
expect(log[4]).toEqual(['bubble', divElement]);
expect(log[5]).toEqual(['bubble', buttonElement]);
});
it('beforeblur and afterblur are called after a focused element is unmounted', () => {
const log = [];
// We have to persist here because we want to read relatedTarget later.
const onAfterBlur = jest.fn(e => {
e.persist();
log.push(e.type);
});
const onBeforeBlur = jest.fn(e => log.push(e.type));
const innerRef = React.createRef();
const innerRef2 = React.createRef();
const Component = ({show}) => {
const ref = React.useRef(null);
const afterBlurHandle = ReactDOM.unstable_useEvent('afterblur');
const beforeBlurHandle = ReactDOM.unstable_useEvent('beforeblur');
React.useEffect(() => {
afterBlurHandle.setListener(document, onAfterBlur);
beforeBlurHandle.setListener(ref.current, onBeforeBlur);
});
return (
<div ref={ref}>
{show && <input ref={innerRef} />}
<div ref={innerRef2} />
</div>
);
};
ReactDOM.render(<Component show={true} />, container);
Scheduler.unstable_flushAll();
const inner = innerRef.current;
const target = createEventTarget(inner);
target.focus();
expect(onBeforeBlur).toHaveBeenCalledTimes(0);
expect(onAfterBlur).toHaveBeenCalledTimes(0);
ReactDOM.render(<Component show={false} />, container);
Scheduler.unstable_flushAll();
expect(onBeforeBlur).toHaveBeenCalledTimes(1);
expect(onAfterBlur).toHaveBeenCalledTimes(1);
expect(onAfterBlur).toHaveBeenCalledWith(
expect.objectContaining({relatedTarget: inner}),
);
expect(log).toEqual(['beforeblur', 'afterblur']);
});
it('beforeblur and afterblur are called after a nested focused element is unmounted', () => {
const log = [];
// We have to persist here because we want to read relatedTarget later.
const onAfterBlur = jest.fn(e => {
e.persist();
log.push(e.type);
});
const onBeforeBlur = jest.fn(e => log.push(e.type));
const innerRef = React.createRef();
const innerRef2 = React.createRef();
const Component = ({show}) => {
const ref = React.useRef(null);
const afterBlurHandle = ReactDOM.unstable_useEvent('afterblur');
const beforeBlurHandle = ReactDOM.unstable_useEvent('beforeblur');
React.useEffect(() => {
afterBlurHandle.setListener(document, onAfterBlur);
beforeBlurHandle.setListener(ref.current, onBeforeBlur);
});
return (
<div ref={ref}>
{show && (
<div>
<input ref={innerRef} />
</div>
)}
<div ref={innerRef2} />
</div>
);
};
ReactDOM.render(<Component show={true} />, container);
Scheduler.unstable_flushAll();
const inner = innerRef.current;
const target = createEventTarget(inner);
target.focus();
expect(onBeforeBlur).toHaveBeenCalledTimes(0);
expect(onAfterBlur).toHaveBeenCalledTimes(0);
ReactDOM.render(<Component show={false} />, container);
Scheduler.unstable_flushAll();
expect(onBeforeBlur).toHaveBeenCalledTimes(1);
expect(onAfterBlur).toHaveBeenCalledTimes(1);
expect(onAfterBlur).toHaveBeenCalledWith(
expect.objectContaining({relatedTarget: inner}),
);
expect(log).toEqual(['beforeblur', 'afterblur']);
});
it.experimental(
'beforeblur and afterblur are called after a focused element is suspended',
() => {
const log = [];
// We have to persist here because we want to read relatedTarget later.
const onAfterBlur = jest.fn(e => {
e.persist();
log.push(e.type);
});
const onBeforeBlur = jest.fn(e => log.push(e.type));
const innerRef = React.createRef();
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 = () => {
const ref = React.useRef(null);
const afterBlurHandle = ReactDOM.unstable_useEvent('afterblur');
const beforeBlurHandle = ReactDOM.unstable_useEvent(
'beforeblur',
);
React.useEffect(() => {
afterBlurHandle.setListener(document, onAfterBlur);
beforeBlurHandle.setListener(ref.current, onBeforeBlur);
});
return (
<div ref={ref}>
<Suspense fallback="Loading...">
<Child />
</Suspense>
</div>
);
};
const container2 = document.createElement('div');
document.body.appendChild(container2);
let root = ReactDOM.createRoot(container2);
ReactTestUtils.act(() => {
root.render(<Component />);
});
jest.runAllTimers();
const inner = innerRef.current;
const target = createEventTarget(inner);
target.focus();
expect(onBeforeBlur).toHaveBeenCalledTimes(0);
expect(onAfterBlur).toHaveBeenCalledTimes(0);
suspend = true;
ReactTestUtils.act(() => {
root.render(<Component />);
});
jest.runAllTimers();
expect(onBeforeBlur).toHaveBeenCalledTimes(1);
expect(onAfterBlur).toHaveBeenCalledTimes(1);
expect(onAfterBlur).toHaveBeenCalledWith(
expect.objectContaining({relatedTarget: inner}),
);
resolve();
expect(log).toEqual(['beforeblur', 'afterblur']);
document.body.removeChild(container2);
},
);
});
},
);