@samitouri / QOS-React / commits / 0140118e8e

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 );