mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
ReactDOM.useEvent: add support for beforeblur/afterblur (#18370)
* ReactDOM.useEvent: add support for beforeblur/afterblur
This commit is contained in:
@@ -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
@@ -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));
|
||||
|
||||
@@ -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
@@ -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`.
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user