@samitouri / QOS-React-2 / commits / 4f59a149b8

Modern event system: fix selectionchange bug (#18680)

Dominic Gannaway committed Apr 20, 2020 at 19:54 UTC 4f59a149b8dd99e0e3f2b515e15b069c276c085b
6 files changed +105 -39
packages/react-dom/src/events/DOMEventListenerMap.js
+18 -4
@@ -43,14 +43,28 @@ export function isListeningToAllDependencies(
43 registrationName: string,
44 mountAt: Document | Element,
45 ): boolean {
46 - const listenerMap = getListenerMapForElement(mountAt);
46 const dependencies = registrationNameDependencies[registrationName];
47 + return isListeningToEvents(dependencies, mountAt);
48 +}
49
49 - for (let i = 0; i < dependencies.length; i++) {
50 - const dependency = dependencies[i];
51 - if (!listenerMap.has(dependency)) {
50 +export function isListeningToEvents(
51 + events: Array<string>,
52 + mountAt: Document | Element,
53 +): boolean {
54 + const listenerMap = getListenerMapForElement(mountAt);
55 + for (let i = 0; i < events.length; i++) {
56 + const event = events[i];
57 + if (!listenerMap.has(event)) {
58 return false;
59 }
60 }
61 return true;
62 }
63 +
64 +export function isListeningToEvent(
65 + registrationName: string,
66 + mountAt: Document | Element,
67 +): boolean {
68 + const listenerMap = getListenerMapForElement(mountAt);
69 + return listenerMap.has(registrationName);
70 +}
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+24 -17
@@ -82,6 +82,7 @@ import {
82 TOP_CLICK,
83 TOP_BEFORE_BLUR,
84 TOP_AFTER_BLUR,
85 + TOP_SELECTION_CHANGE,
86 } from './DOMTopLevelEventTypes';
87 import {
88 getClosestInstanceFromNode,
@@ -250,23 +251,21 @@ function dispatchEventsForPlugins(
251
252 for (let i = 0; i < plugins.length; i++) {
253 const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
253 - if (possiblePlugin !== undefined) {
254 - const extractedEvents = possiblePlugin.extractEvents(
255 - topLevelType,
256 - targetInst,
257 - nativeEvent,
258 - nativeEventTarget,
259 - eventSystemFlags,
260 - targetContainer,
261 - );
262 - if (isArray(extractedEvents)) {
263 - // Flow complains about @@iterator being missing in ReactSyntheticEvent,
264 - // so we cast to avoid the Flow error.
265 - const arrOfExtractedEvents = ((extractedEvents: any): Array<ReactSyntheticEvent>);
266 - syntheticEvents.push(...arrOfExtractedEvents);
267 - } else if (extractedEvents != null) {
268 - syntheticEvents.push(extractedEvents);
269 - }
254 + const extractedEvents = possiblePlugin.extractEvents(
255 + topLevelType,
256 + targetInst,
257 + nativeEvent,
258 + nativeEventTarget,
259 + eventSystemFlags,
260 + targetContainer,
261 + );
262 + if (isArray(extractedEvents)) {
263 + // Flow complains about @@iterator being missing in ReactSyntheticEvent,
264 + // so we cast to avoid the Flow error.
265 + const arrOfExtractedEvents = ((extractedEvents: any): Array<ReactSyntheticEvent>);
266 + syntheticEvents.push(...arrOfExtractedEvents);
267 + } else if (extractedEvents != null) {
268 + syntheticEvents.push(extractedEvents);
269 }
270 }
271 dispatchEventsInBatch(syntheticEvents);
@@ -314,6 +313,14 @@ export function listenToTopLevelEvent(
313 capture === undefined
314 ? topLevelType
315 : `${typeStr}_${capture ? 'capture' : 'bubble'}`;
316 +
317 + // TOP_SELECTION_CHANGE needs to be attached to the document
318 + // otherwise it won't capture incoming events that are only
319 + // triggered on the document directly.
320 + if (topLevelType === TOP_SELECTION_CHANGE) {
321 + targetContainer = (targetContainer: any).ownerDocument || targetContainer;
322 + listenerMap = getListenerMapForElement(targetContainer);
323 + }
324 const listenerEntry = listenerMap.get(listenerMapKey);
325 const shouldUpgrade = shouldUpgradeListener(listenerEntry, passive);
326 if (listenerEntry === undefined || shouldUpgrade) {
packages/react-dom/src/events/plugins/ModernSelectEventPlugin.js
+23 -16
@@ -25,29 +25,30 @@ import getActiveElement from '../../client/getActiveElement';
25 import {getNodeFromInstance} from '../../client/ReactDOMComponentTree';
26 import {hasSelectionCapabilities} from '../../client/ReactInputSelection';
27 import {DOCUMENT_NODE} from '../../shared/HTMLNodeType';
28 -import {isListeningToAllDependencies} from '../DOMEventListenerMap';
28 +import {isListeningToEvent, isListeningToEvents} from '../DOMEventListenerMap';
29 import {accumulateTwoPhaseListeners} from '../DOMModernPluginEventSystem';
30
31 const skipSelectionChangeEvent =
32 canUseDOM && 'documentMode' in document && document.documentMode <= 11;
33
34 +const rootTargetDependencies = [
35 + TOP_BLUR,
36 + TOP_CONTEXT_MENU,
37 + TOP_DRAG_END,
38 + TOP_FOCUS,
39 + TOP_KEY_DOWN,
40 + TOP_KEY_UP,
41 + TOP_MOUSE_DOWN,
42 + TOP_MOUSE_UP,
43 +];
44 +
45 const eventTypes = {
46 select: {
47 phasedRegistrationNames: {
48 bubbled: 'onSelect',
49 captured: 'onSelectCapture',
50 },
40 - dependencies: [
41 - TOP_BLUR,
42 - TOP_CONTEXT_MENU,
43 - TOP_DRAG_END,
44 - TOP_FOCUS,
45 - TOP_KEY_DOWN,
46 - TOP_KEY_UP,
47 - TOP_MOUSE_DOWN,
48 - TOP_MOUSE_UP,
49 - TOP_SELECTION_CHANGE,
50 - ],
51 + dependencies: [...rootTargetDependencies, TOP_SELECTION_CHANGE],
52 },
53 };
54
@@ -168,13 +169,19 @@ const SelectEventPlugin = {
169 eventSystemFlags,
170 container,
171 ) {
171 - const containerOrDoc =
172 - container || getEventTargetDocument(nativeEventTarget);
172 + const doc = getEventTargetDocument(nativeEventTarget);
173 // Track whether all listeners exists for this plugin. If none exist, we do
174 // not extract events. See #3639.
175 if (
176 - !containerOrDoc ||
177 - !isListeningToAllDependencies('onSelect', containerOrDoc)
176 + // We only listen to TOP_SELECTION_CHANGE on the document, never the
177 + // root.
178 + !isListeningToEvent(TOP_SELECTION_CHANGE, doc) ||
179 + // If we are handling TOP_SELECTION_CHANGE, then we don't need to
180 + // check for the other dependencies, as TOP_SELECTION_CHANGE is only
181 + // event attached from the onChange plugin and we don't expose an
182 + // onSelectionChange event from React.
183 + (topLevelType !== TOP_SELECTION_CHANGE &&
184 + !isListeningToEvents(rootTargetDependencies, container))
185 ) {
186 return null;
187 }
packages/react-dom/src/events/plugins/__tests__/LegacySelectEventPlugin-test.js
+19
@@ -142,4 +142,23 @@ describe('SelectEventPlugin', () => {
142 node.dispatchEvent(nativeEvent);
143 expect(select).toHaveBeenCalledTimes(1);
144 });
145 +
146 + it('should handle selectionchange events', function() {
147 + const onSelect = jest.fn();
148 + const node = ReactDOM.render(
149 + <input type="text" onSelect={onSelect} />,
150 + container,
151 + );
152 + node.focus();
153 +
154 + // Make sure the event was not called before we emit the selection change event
155 + expect(onSelect).toHaveBeenCalledTimes(0);
156 +
157 + // This is dispatched e.g. when using CMD+a on macOS
158 + document.dispatchEvent(
159 + new Event('selectionchange', {bubbles: false, cancelable: false}),
160 + );
161 +
162 + expect(onSelect).toHaveBeenCalledTimes(1);
163 + });
164 });
packages/react-dom/src/events/plugins/__tests__/ModernBeforeInputEventPlugin-test.internal.js
+2 -2
@@ -21,6 +21,8 @@ describe('BeforeInputEventPlugin', () => {
21 if (envSimulator) {
22 envSimulator();
23 }
24 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
25 + ReactFeatureFlags.enableModernEventSystem = true;
26 return require('react-dom');
27 }
28
@@ -78,8 +80,6 @@ describe('BeforeInputEventPlugin', () => {
80 }
81
82 beforeEach(() => {
81 - ReactFeatureFlags = require('shared/ReactFeatureFlags');
82 - ReactFeatureFlags.enableModernEventSystem = true;
83 React = require('react');
84 container = document.createElement('div');
85 document.body.appendChild(container);
packages/react-dom/src/events/plugins/__tests__/ModernSelectEventPlugin-test.internal.js
+19
@@ -145,4 +145,23 @@ describe('SelectEventPlugin', () => {
145 node.dispatchEvent(nativeEvent);
146 expect(select).toHaveBeenCalledTimes(1);
147 });
148 +
149 + it('should handle selectionchange events', function() {
150 + const onSelect = jest.fn();
151 + const node = ReactDOM.render(
152 + <input type="text" onSelect={onSelect} />,
153 + container,
154 + );
155 + node.focus();
156 +
157 + // Make sure the event was not called before we emit the selection change event
158 + expect(onSelect).toHaveBeenCalledTimes(0);
159 +
160 + // This is dispatched e.g. when using CMD+a on macOS
161 + document.dispatchEvent(
162 + new Event('selectionchange', {bubbles: false, cancelable: false}),
163 + );
164 +
165 + expect(onSelect).toHaveBeenCalledTimes(1);
166 + });
167 });