ReactDOM.useEvent: add support for beforeblur/afterblur (#18370)
* ReactDOM.useEvent: add support for beforeblur/afterblur
Dominic Gannaway committed
Mar 24, 2020 at 16:14 UTC
0140118e8e2766abaac2c833775434f83df81d56
7 files changed
+271
-25
package.json
+1
@@ -113,6 +113,7 @@
113
"test-persistent": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-persistent.js",
114
"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",
115
"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",
117
"test-prod-build": "yarn test-build-prod",
118
"test-build": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build.js",
119
"test-build-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.build.js",
packages/react-dom/src/client/ReactDOMHostConfig.js
+58
-24
@@ -7,6 +7,7 @@
7
* @flow
8
*/
9
10
+import type {TopLevelType} from 'legacy-events/TopLevelEventTypes';
11
import type {RootType} from './ReactDOMRoot';
12
13
import {
@@ -84,6 +85,7 @@ import {
85
attachTargetEventListener,
86
} from '../events/DOMModernPluginEventSystem';
87
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
88
+import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
89
90
export type ReactListenerEvent = ReactDOMListenerEvent;
91
export type ReactListenerMap = ReactDOMListenerMap;
@@ -238,11 +240,11 @@ export function resetAfterCommit(containerInfo: Container): void {
240
restoreSelection(selectionInformation);
241
ReactBrowserEventEmitterSetEnabled(eventsEnabled);
242
eventsEnabled = null;
241
- if (enableDeprecatedFlareAPI) {
243
+ if (enableDeprecatedFlareAPI || enableUseEventAPI) {
244
const activeElementDetached = (selectionInformation: any)
245
.activeElementDetached;
246
if (activeElementDetached !== null) {
245
- dispatchDetachedBlur(activeElementDetached);
247
+ dispatchAfterDetachedBlur(activeElementDetached);
248
}
249
}
250
selectionInformation = null;
@@ -490,34 +492,66 @@ export function insertInContainerBefore(
492
}
493
}
494
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
+
501
function dispatchBeforeDetachedBlur(target: HTMLElement): void {
502
const targetInstance = getClosestInstanceFromNode(target);
503
((selectionInformation: any): SelectionInformation).activeElementDetached = target;
504
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),
513
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
+ }
531
}
532
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),
543
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
+ }
555
}
556
557
// This is a specific event for the React Flare
@@ -528,7 +562,7 @@ export function beforeRemoveInstance(
562
instance: Instance | TextInstance | SuspenseInstance,
563
): void {
564
if (
531
- enableDeprecatedFlareAPI &&
565
+ (enableDeprecatedFlareAPI || enableUseEventAPI) &&
566
selectionInformation &&
567
instance === selectionInformation.focusedElem
568
) {
@@ -639,7 +673,7 @@ export function hideInstance(instance: Instance): void {
673
// is ether the instance of a child or the instance. We need
674
// to traverse the Fiber tree here rather than use node.contains()
675
// as the child node might be inside a Portal.
642
- if (enableDeprecatedFlareAPI && selectionInformation) {
676
+ if ((enableDeprecatedFlareAPI || enableUseEventAPI) && selectionInformation) {
677
const focusedElem = selectionInformation.focusedElem;
678
if (focusedElem !== null && instanceContainsElem(instance, focusedElem)) {
679
dispatchBeforeDetachedBlur(((focusedElem: any): HTMLElement));
packages/react-dom/src/events/DOMEventProperties.js
+8
@@ -23,6 +23,7 @@ import {
23
UserBlockingEvent,
24
ContinuousEvent,
25
} from 'shared/ReactTypes';
26
+import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
27
28
// Needed for SimpleEventPlugin, rather than
29
// do it in two places, which duplicates logic
@@ -95,6 +96,13 @@ const otherDiscreteEvents = [
96
DOMTopLevelEventTypes.TOP_COMPOSITION_UPDATE,
97
];
98
99
+if (enableUseEventAPI) {
100
+ otherDiscreteEvents.push(
101
+ DOMTopLevelEventTypes.TOP_BEFORE_BLUR,
102
+ DOMTopLevelEventTypes.TOP_AFTER_BLUR,
103
+ );
104
+}
105
+
106
// prettier-ignore
107
const userBlockingPairsForSimpleEventPlugin = [
108
DOMTopLevelEventTypes.TOP_DRAG, 'drag',
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+11
-1
@@ -75,6 +75,8 @@ import {
75
TOP_PROGRESS,
76
TOP_PLAYING,
77
TOP_CLICK,
78
+ TOP_BEFORE_BLUR,
79
+ TOP_AFTER_BLUR,
80
} from './DOMTopLevelEventTypes';
81
import {
82
getClosestInstanceFromNode,
@@ -84,7 +86,10 @@ import {
86
import {COMMENT_NODE} from '../shared/HTMLNodeType';
87
import {topLevelEventsToDispatchConfig} from './DOMEventProperties';
88
87
-import {enableLegacyFBSupport} from 'shared/ReactFeatureFlags';
89
+import {
90
+ enableLegacyFBSupport,
91
+ enableUseEventAPI,
92
+} from 'shared/ReactFeatureFlags';
93
94
const capturePhaseEvents = new Set([
95
TOP_FOCUS,
@@ -122,6 +127,11 @@ const capturePhaseEvents = new Set([
127
TOP_WAITING,
128
]);
129
130
+if (enableUseEventAPI) {
131
+ capturePhaseEvents.add(TOP_BEFORE_BLUR);
132
+ capturePhaseEvents.add(TOP_AFTER_BLUR);
133
+}
134
+
135
const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {
136
customEvent: true,
137
phasedRegistrationNames: {
packages/react-dom/src/events/DOMTopLevelEventTypes.js
+3
@@ -149,6 +149,9 @@ export const TOP_VOLUME_CHANGE = unsafeCastStringToDOMTopLevelType(
149
export const TOP_WAITING = unsafeCastStringToDOMTopLevelType('waiting');
150
export const TOP_WHEEL = unsafeCastStringToDOMTopLevelType('wheel');
151
152
+export const TOP_AFTER_BLUR = unsafeCastStringToDOMTopLevelType('afterblur');
153
+export const TOP_BEFORE_BLUR = unsafeCastStringToDOMTopLevelType('beforeblur');
154
+
155
// List of events that need to be individually attached to media elements.
156
// Note that events in this list will *not* be listened to at the top level
157
// unless they're explicitly whitelisted in `ReactBrowserEventEmitter.listenTo`.
packages/react-dom/src/events/SimpleEventPlugin.js
+2
@@ -105,6 +105,8 @@ const SimpleEventPlugin: PluginModule<MouseEvent> = {
105
break;
106
case DOMTopLevelEventTypes.TOP_BLUR:
107
case DOMTopLevelEventTypes.TOP_FOCUS:
108
+ case DOMTopLevelEventTypes.TOP_BEFORE_BLUR:
109
+ case DOMTopLevelEventTypes.TOP_AFTER_BLUR:
110
EventConstructor = SyntheticFocusEvent;
111
break;
112
case DOMTopLevelEventTypes.TOP_CLICK:
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+188
@@ -9,10 +9,13 @@
9
10
'use strict';
11
12
+import {createEventTarget} from 'dom-event-testing-library';
13
+
14
let React;
15
let ReactFeatureFlags;
16
let ReactDOM;
17
let ReactDOMServer;
18
+let ReactTestUtils;
19
let Scheduler;
20
21
function dispatchEvent(element, type) {
@@ -64,6 +67,7 @@ describe('DOMModernPluginEventSystem', () => {
67
ReactDOM = require('react-dom');
68
Scheduler = require('scheduler');
69
ReactDOMServer = require('react-dom/server');
70
+ ReactTestUtils = require('react-dom/test-utils');
71
container = document.createElement('div');
72
document.body.appendChild(container);
73
startNativeEventListenerClearDown();
@@ -1092,6 +1096,7 @@ describe('DOMModernPluginEventSystem', () => {
1096
ReactDOM = require('react-dom');
1097
Scheduler = require('scheduler');
1098
ReactDOMServer = require('react-dom/server');
1099
+ ReactTestUtils = require('react-dom/test-utils');
1100
});
1101
1102
if (!__EXPERIMENTAL__) {
@@ -2139,6 +2144,189 @@ describe('DOMModernPluginEventSystem', () => {
2144
expect(log[4]).toEqual(['bubble', divElement]);
2145
expect(log[5]).toEqual(['bubble', buttonElement]);
2146
});
2147
+
2148
+ it('beforeblur and afterblur are called after a focused element is unmounted', () => {
2149
+ const log = [];
2150
+ // We have to persist here because we want to read relatedTarget later.
2151
+ const onAfterBlur = jest.fn(e => {
2152
+ e.persist();
2153
+ log.push(e.type);
2154
+ });
2155
+ const onBeforeBlur = jest.fn(e => log.push(e.type));
2156
+ const innerRef = React.createRef();
2157
+ const innerRef2 = React.createRef();
2158
+
2159
+ const Component = ({show}) => {
2160
+ const ref = React.useRef(null);
2161
+ const afterBlurHandle = ReactDOM.unstable_useEvent('afterblur');
2162
+ const beforeBlurHandle = ReactDOM.unstable_useEvent('beforeblur');
2163
+
2164
+ React.useEffect(() => {
2165
+ afterBlurHandle.setListener(document, onAfterBlur);
2166
+ beforeBlurHandle.setListener(ref.current, onBeforeBlur);
2167
+ });
2168
+
2169
+ return (
2170
+ <div ref={ref}>
2171
+ {show && <input ref={innerRef} />}
2172
+ <div ref={innerRef2} />
2173
+ </div>
2174
+ );
2175
+ };
2176
+
2177
+ ReactDOM.render(<Component show={true} />, container);
2178
+ Scheduler.unstable_flushAll();
2179
+
2180
+ const inner = innerRef.current;
2181
+ const target = createEventTarget(inner);
2182
+ target.focus();
2183
+ expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2184
+ expect(onAfterBlur).toHaveBeenCalledTimes(0);
2185
+
2186
+ ReactDOM.render(<Component show={false} />, container);
2187
+ Scheduler.unstable_flushAll();
2188
+
2189
+ expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2190
+ expect(onAfterBlur).toHaveBeenCalledTimes(1);
2191
+ expect(onAfterBlur).toHaveBeenCalledWith(
2192
+ expect.objectContaining({relatedTarget: inner}),
2193
+ );
2194
+ expect(log).toEqual(['beforeblur', 'afterblur']);
2195
+ });
2196
+
2197
+ it('beforeblur and afterblur are called after a nested focused element is unmounted', () => {
2198
+ const log = [];
2199
+ // We have to persist here because we want to read relatedTarget later.
2200
+ const onAfterBlur = jest.fn(e => {
2201
+ e.persist();
2202
+ log.push(e.type);
2203
+ });
2204
+ const onBeforeBlur = jest.fn(e => log.push(e.type));
2205
+ const innerRef = React.createRef();
2206
+ const innerRef2 = React.createRef();
2207
+
2208
+ const Component = ({show}) => {
2209
+ const ref = React.useRef(null);
2210
+ const afterBlurHandle = ReactDOM.unstable_useEvent('afterblur');
2211
+ const beforeBlurHandle = ReactDOM.unstable_useEvent('beforeblur');
2212
+
2213
+ React.useEffect(() => {
2214
+ afterBlurHandle.setListener(document, onAfterBlur);
2215
+ beforeBlurHandle.setListener(ref.current, onBeforeBlur);
2216
+ });
2217
+
2218
+ return (
2219
+ <div ref={ref}>
2220
+ {show && (
2221
+ <div>
2222
+ <input ref={innerRef} />
2223
+ </div>
2224
+ )}
2225
+ <div ref={innerRef2} />
2226
+ </div>
2227
+ );
2228
+ };
2229
+
2230
+ ReactDOM.render(<Component show={true} />, container);
2231
+ Scheduler.unstable_flushAll();
2232
+
2233
+ const inner = innerRef.current;
2234
+ const target = createEventTarget(inner);
2235
+ target.focus();
2236
+ expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2237
+ expect(onAfterBlur).toHaveBeenCalledTimes(0);
2238
+
2239
+ ReactDOM.render(<Component show={false} />, container);
2240
+ Scheduler.unstable_flushAll();
2241
+
2242
+ expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2243
+ expect(onAfterBlur).toHaveBeenCalledTimes(1);
2244
+ expect(onAfterBlur).toHaveBeenCalledWith(
2245
+ expect.objectContaining({relatedTarget: inner}),
2246
+ );
2247
+ expect(log).toEqual(['beforeblur', 'afterblur']);
2248
+ });
2249
+
2250
+ it.experimental(
2251
+ 'beforeblur and afterblur are called after a focused element is suspended',
2252
+ () => {
2253
+ const log = [];
2254
+ // We have to persist here because we want to read relatedTarget later.
2255
+ const onAfterBlur = jest.fn(e => {
2256
+ e.persist();
2257
+ log.push(e.type);
2258
+ });
2259
+ const onBeforeBlur = jest.fn(e => log.push(e.type));
2260
+ const innerRef = React.createRef();
2261
+ const Suspense = React.Suspense;
2262
+ let suspend = false;
2263
+ let resolve;
2264
+ let promise = new Promise(
2265
+ resolvePromise => (resolve = resolvePromise),
2266
+ );
2267
+
2268
+ function Child() {
2269
+ if (suspend) {
2270
+ throw promise;
2271
+ } else {
2272
+ return <input ref={innerRef} />;
2273
+ }
2274
+ }
2275
+
2276
+ const Component = () => {
2277
+ const ref = React.useRef(null);
2278
+ const afterBlurHandle = ReactDOM.unstable_useEvent('afterblur');
2279
+ const beforeBlurHandle = ReactDOM.unstable_useEvent(
2280
+ 'beforeblur',
2281
+ );
2282
+
2283
+ React.useEffect(() => {
2284
+ afterBlurHandle.setListener(document, onAfterBlur);
2285
+ beforeBlurHandle.setListener(ref.current, onBeforeBlur);
2286
+ });
2287
+
2288
+ return (
2289
+ <div ref={ref}>
2290
+ <Suspense fallback="Loading...">
2291
+ <Child />
2292
+ </Suspense>
2293
+ </div>
2294
+ );
2295
+ };
2296
+
2297
+ const container2 = document.createElement('div');
2298
+ document.body.appendChild(container2);
2299
+
2300
+ let root = ReactDOM.createRoot(container2);
2301
+
2302
+ ReactTestUtils.act(() => {
2303
+ root.render(<Component />);
2304
+ });
2305
+ jest.runAllTimers();
2306
+
2307
+ const inner = innerRef.current;
2308
+ const target = createEventTarget(inner);
2309
+ target.focus();
2310
+ expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2311
+ expect(onAfterBlur).toHaveBeenCalledTimes(0);
2312
+
2313
+ suspend = true;
2314
+ ReactTestUtils.act(() => {
2315
+ root.render(<Component />);
2316
+ });
2317
+ jest.runAllTimers();
2318
+
2319
+ expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2320
+ expect(onAfterBlur).toHaveBeenCalledTimes(1);
2321
+ expect(onAfterBlur).toHaveBeenCalledWith(
2322
+ expect.objectContaining({relatedTarget: inner}),
2323
+ );
2324
+ resolve();
2325
+ expect(log).toEqual(['beforeblur', 'afterblur']);
2326
+
2327
+ document.body.removeChild(container2);
2328
+ },
2329
+ );
2330
});
2331
},
2332
);