Skip to content

Commit 0140118

Browse files
authored
ReactDOM.useEvent: add support for beforeblur/afterblur (#18370)
* ReactDOM.useEvent: add support for beforeblur/afterblur
1 parent a56309f commit 0140118

7 files changed

+271
-25
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"test-persistent": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-persistent.js",
114114
"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",
115115
"test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js",
116+
"debug-test-prod": "cross-env NODE_ENV=production node --inspect-brk node_modules/jest/bin/jest.js --config ./scripts/jest/config.source.js --runInBand",
116117
"test-prod-build": "yarn test-build-prod",
117118
"test-build": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build.js",
118119
"test-build-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.build.js",

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @flow
88
*/
99

10+
import type {TopLevelType} from 'legacy-events/TopLevelEventTypes';
1011
import type {RootType} from './ReactDOMRoot';
1112

1213
import {
@@ -84,6 +85,7 @@ import {
8485
attachTargetEventListener,
8586
} from '../events/DOMModernPluginEventSystem';
8687
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
88+
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
8789

8890
export type ReactListenerEvent = ReactDOMListenerEvent;
8991
export type ReactListenerMap = ReactDOMListenerMap;
@@ -238,11 +240,11 @@ export function resetAfterCommit(containerInfo: Container): void {
238240
restoreSelection(selectionInformation);
239241
ReactBrowserEventEmitterSetEnabled(eventsEnabled);
240242
eventsEnabled = null;
241-
if (enableDeprecatedFlareAPI) {
243+
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
242244
const activeElementDetached = (selectionInformation: any)
243245
.activeElementDetached;
244246
if (activeElementDetached !== null) {
245-
dispatchDetachedBlur(activeElementDetached);
247+
dispatchAfterDetachedBlur(activeElementDetached);
246248
}
247249
}
248250
selectionInformation = null;
@@ -490,34 +492,66 @@ export function insertInContainerBefore(
490492
}
491493
}
492494

495+
function createEvent(type: TopLevelType): Event {
496+
const event = document.createEvent('Event');
497+
event.initEvent(((type: any): string), false, false);
498+
return event;
499+
}
500+
493501
function dispatchBeforeDetachedBlur(target: HTMLElement): void {
494502
const targetInstance = getClosestInstanceFromNode(target);
495503
((selectionInformation: any): SelectionInformation).activeElementDetached = target;
496504

497-
DEPRECATED_dispatchEventForResponderEventSystem(
498-
'beforeblur',
499-
targetInstance,
500-
({
505+
if (enableDeprecatedFlareAPI) {
506+
DEPRECATED_dispatchEventForResponderEventSystem(
507+
'beforeblur',
508+
targetInstance,
509+
({
510+
target,
511+
timeStamp: Date.now(),
512+
}: any),
501513
target,
502-
timeStamp: Date.now(),
503-
}: any),
504-
target,
505-
RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
506-
);
514+
RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
515+
);
516+
}
517+
if (enableUseEventAPI) {
518+
try {
519+
// We need to temporarily enable the event system
520+
// to dispatch the "beforeblur" event.
521+
ReactBrowserEventEmitterSetEnabled(true);
522+
const event = createEvent(TOP_BEFORE_BLUR);
523+
// Dispatch "beforeblur" directly on the target,
524+
// so it gets picked up by the event system and
525+
// can propagate through the React internal tree.
526+
target.dispatchEvent(event);
527+
} finally {
528+
ReactBrowserEventEmitterSetEnabled(false);
529+
}
530+
}
507531
}
508532

509-
function dispatchDetachedBlur(target: HTMLElement): void {
510-
DEPRECATED_dispatchEventForResponderEventSystem(
511-
'blur',
512-
null,
513-
({
514-
isTargetAttached: false,
533+
function dispatchAfterDetachedBlur(target: HTMLElement): void {
534+
if (enableDeprecatedFlareAPI) {
535+
DEPRECATED_dispatchEventForResponderEventSystem(
536+
'blur',
537+
null,
538+
({
539+
isTargetAttached: false,
540+
target,
541+
timeStamp: Date.now(),
542+
}: any),
515543
target,
516-
timeStamp: Date.now(),
517-
}: any),
518-
target,
519-
RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
520-
);
544+
RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
545+
);
546+
}
547+
if (enableUseEventAPI) {
548+
const event = createEvent(TOP_AFTER_BLUR);
549+
// So we know what was detached, make the relatedTarget the
550+
// detached target on the "afterblur" event.
551+
(event: any).relatedTarget = target;
552+
// Dispatch the event on the document.
553+
document.dispatchEvent(event);
554+
}
521555
}
522556

523557
// This is a specific event for the React Flare
@@ -528,7 +562,7 @@ export function beforeRemoveInstance(
528562
instance: Instance | TextInstance | SuspenseInstance,
529563
): void {
530564
if (
531-
enableDeprecatedFlareAPI &&
565+
(enableDeprecatedFlareAPI || enableUseEventAPI) &&
532566
selectionInformation &&
533567
instance === selectionInformation.focusedElem
534568
) {
@@ -639,7 +673,7 @@ export function hideInstance(instance: Instance): void {
639673
// is ether the instance of a child or the instance. We need
640674
// to traverse the Fiber tree here rather than use node.contains()
641675
// as the child node might be inside a Portal.
642-
if (enableDeprecatedFlareAPI && selectionInformation) {
676+
if ((enableDeprecatedFlareAPI || enableUseEventAPI) && selectionInformation) {
643677
const focusedElem = selectionInformation.focusedElem;
644678
if (focusedElem !== null && instanceContainsElem(instance, focusedElem)) {
645679
dispatchBeforeDetachedBlur(((focusedElem: any): HTMLElement));

packages/react-dom/src/events/DOMEventProperties.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
UserBlockingEvent,
2424
ContinuousEvent,
2525
} from 'shared/ReactTypes';
26+
import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
2627

2728
// Needed for SimpleEventPlugin, rather than
2829
// do it in two places, which duplicates logic
@@ -95,6 +96,13 @@ const otherDiscreteEvents = [
9596
DOMTopLevelEventTypes.TOP_COMPOSITION_UPDATE,
9697
];
9798

99+
if (enableUseEventAPI) {
100+
otherDiscreteEvents.push(
101+
DOMTopLevelEventTypes.TOP_BEFORE_BLUR,
102+
DOMTopLevelEventTypes.TOP_AFTER_BLUR,
103+
);
104+
}
105+
98106
// prettier-ignore
99107
const userBlockingPairsForSimpleEventPlugin = [
100108
DOMTopLevelEventTypes.TOP_DRAG, 'drag',

packages/react-dom/src/events/DOMModernPluginEventSystem.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ import {
7575
TOP_PROGRESS,
7676
TOP_PLAYING,
7777
TOP_CLICK,
78+
TOP_BEFORE_BLUR,
79+
TOP_AFTER_BLUR,
7880
} from './DOMTopLevelEventTypes';
7981
import {
8082
getClosestInstanceFromNode,
@@ -84,7 +86,10 @@ import {
8486
import {COMMENT_NODE} from '../shared/HTMLNodeType';
8587
import {topLevelEventsToDispatchConfig} from './DOMEventProperties';
8688

87-
import {enableLegacyFBSupport} from 'shared/ReactFeatureFlags';
89+
import {
90+
enableLegacyFBSupport,
91+
enableUseEventAPI,
92+
} from 'shared/ReactFeatureFlags';
8893

8994
const capturePhaseEvents = new Set([
9095
TOP_FOCUS,
@@ -122,6 +127,11 @@ const capturePhaseEvents = new Set([
122127
TOP_WAITING,
123128
]);
124129

130+
if (enableUseEventAPI) {
131+
capturePhaseEvents.add(TOP_BEFORE_BLUR);
132+
capturePhaseEvents.add(TOP_AFTER_BLUR);
133+
}
134+
125135
const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {
126136
customEvent: true,
127137
phasedRegistrationNames: {

packages/react-dom/src/events/DOMTopLevelEventTypes.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ export const TOP_VOLUME_CHANGE = unsafeCastStringToDOMTopLevelType(
149149
export const TOP_WAITING = unsafeCastStringToDOMTopLevelType('waiting');
150150
export const TOP_WHEEL = unsafeCastStringToDOMTopLevelType('wheel');
151151

152+
export const TOP_AFTER_BLUR = unsafeCastStringToDOMTopLevelType('afterblur');
153+
export const TOP_BEFORE_BLUR = unsafeCastStringToDOMTopLevelType('beforeblur');
154+
152155
// List of events that need to be individually attached to media elements.
153156
// Note that events in this list will *not* be listened to at the top level
154157
// unless they're explicitly whitelisted in `ReactBrowserEventEmitter.listenTo`.

packages/react-dom/src/events/SimpleEventPlugin.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ const SimpleEventPlugin: PluginModule<MouseEvent> = {
105105
break;
106106
case DOMTopLevelEventTypes.TOP_BLUR:
107107
case DOMTopLevelEventTypes.TOP_FOCUS:
108+
case DOMTopLevelEventTypes.TOP_BEFORE_BLUR:
109+
case DOMTopLevelEventTypes.TOP_AFTER_BLUR:
108110
EventConstructor = SyntheticFocusEvent;
109111
break;
110112
case DOMTopLevelEventTypes.TOP_CLICK:

0 commit comments

Comments
 (0)