Skip to content

Commit 4f59a14

Browse files
authored
Modern event system: fix selectionchange bug (facebook#18680)
1 parent a152827 commit 4f59a14

File tree

6 files changed

+105
-39
lines changed

6 files changed

+105
-39
lines changed

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,28 @@ export function isListeningToAllDependencies(
4343
registrationName: string,
4444
mountAt: Document | Element,
4545
): boolean {
46-
const listenerMap = getListenerMapForElement(mountAt);
4746
const dependencies = registrationNameDependencies[registrationName];
47+
return isListeningToEvents(dependencies, mountAt);
48+
}
4849

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)) {
5258
return false;
5359
}
5460
}
5561
return true;
5662
}
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

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import {
8282
TOP_CLICK,
8383
TOP_BEFORE_BLUR,
8484
TOP_AFTER_BLUR,
85+
TOP_SELECTION_CHANGE,
8586
} from './DOMTopLevelEventTypes';
8687
import {
8788
getClosestInstanceFromNode,
@@ -250,23 +251,21 @@ function dispatchEventsForPlugins(
250251

251252
for (let i = 0; i < plugins.length; i++) {
252253
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);
270269
}
271270
}
272271
dispatchEventsInBatch(syntheticEvents);
@@ -314,6 +313,14 @@ export function listenToTopLevelEvent(
314313
capture === undefined
315314
? topLevelType
316315
: `${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+
}
317324
const listenerEntry = listenerMap.get(listenerMapKey);
318325
const shouldUpgrade = shouldUpgradeListener(listenerEntry, passive);
319326
if (listenerEntry === undefined || shouldUpgrade) {

packages/react-dom/src/events/plugins/ModernSelectEventPlugin.js

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,29 +25,30 @@ import getActiveElement from '../../client/getActiveElement';
2525
import {getNodeFromInstance} from '../../client/ReactDOMComponentTree';
2626
import {hasSelectionCapabilities} from '../../client/ReactInputSelection';
2727
import {DOCUMENT_NODE} from '../../shared/HTMLNodeType';
28-
import {isListeningToAllDependencies} from '../DOMEventListenerMap';
28+
import {isListeningToEvent, isListeningToEvents} from '../DOMEventListenerMap';
2929
import {accumulateTwoPhaseListeners} from '../DOMModernPluginEventSystem';
3030

3131
const skipSelectionChangeEvent =
3232
canUseDOM && 'documentMode' in document && document.documentMode <= 11;
3333

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+
3445
const eventTypes = {
3546
select: {
3647
phasedRegistrationNames: {
3748
bubbled: 'onSelect',
3849
captured: 'onSelectCapture',
3950
},
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],
5152
},
5253
};
5354

@@ -168,13 +169,19 @@ const SelectEventPlugin = {
168169
eventSystemFlags,
169170
container,
170171
) {
171-
const containerOrDoc =
172-
container || getEventTargetDocument(nativeEventTarget);
172+
const doc = getEventTargetDocument(nativeEventTarget);
173173
// Track whether all listeners exists for this plugin. If none exist, we do
174174
// not extract events. See #3639.
175175
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))
178185
) {
179186
return null;
180187
}

packages/react-dom/src/events/plugins/__tests__/LegacySelectEventPlugin-test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,23 @@ describe('SelectEventPlugin', () => {
142142
node.dispatchEvent(nativeEvent);
143143
expect(select).toHaveBeenCalledTimes(1);
144144
});
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+
});
145164
});

packages/react-dom/src/events/plugins/__tests__/ModernBeforeInputEventPlugin-test.internal.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ describe('BeforeInputEventPlugin', () => {
2121
if (envSimulator) {
2222
envSimulator();
2323
}
24+
ReactFeatureFlags = require('shared/ReactFeatureFlags');
25+
ReactFeatureFlags.enableModernEventSystem = true;
2426
return require('react-dom');
2527
}
2628

@@ -78,8 +80,6 @@ describe('BeforeInputEventPlugin', () => {
7880
}
7981

8082
beforeEach(() => {
81-
ReactFeatureFlags = require('shared/ReactFeatureFlags');
82-
ReactFeatureFlags.enableModernEventSystem = true;
8383
React = require('react');
8484
container = document.createElement('div');
8585
document.body.appendChild(container);

packages/react-dom/src/events/plugins/__tests__/ModernSelectEventPlugin-test.internal.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,23 @@ describe('SelectEventPlugin', () => {
145145
node.dispatchEvent(nativeEvent);
146146
expect(select).toHaveBeenCalledTimes(1);
147147
});
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+
});
148167
});

0 commit comments

Comments
 (0)