Skip to content

Commit 54451f6

Browse files
committed
[react-interactions] Add Listener API + useEvent/useDelegatedEvent
Update error codes fix lint DCE fix test Add event kinds to help propagation rules Add event kinds to help propagation rules #2 Fix kind bug cleanup
1 parent 4c27037 commit 54451f6

33 files changed

Lines changed: 1952 additions & 23 deletions

packages/legacy-events/EventSystemFlags.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ export type EventSystemFlags = number;
1111

1212
export const PLUGIN_EVENT_SYSTEM = 1;
1313
export const RESPONDER_EVENT_SYSTEM = 1 << 1;
14-
export const IS_PASSIVE = 1 << 2;
15-
export const IS_ACTIVE = 1 << 3;
16-
export const PASSIVE_NOT_SUPPORTED = 1 << 4;
17-
export const IS_REPLAYED = 1 << 5;
18-
export const IS_FIRST_ANCESTOR = 1 << 6;
14+
export const LISTENER_EVENT_SYSTEM = 1 << 2;
15+
export const IS_PASSIVE = 1 << 3;
16+
export const IS_ACTIVE = 1 << 4;
17+
export const PASSIVE_NOT_SUPPORTED = 1 << 5;
18+
export const IS_REPLAYED = 1 << 6;
19+
export const IS_FIRST_ANCESTOR = 1 << 7;

packages/legacy-events/PluginModuleType.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
1717

1818
export type EventTypes = {[key: string]: DispatchConfig};
1919

20-
export type AnyNativeEvent = Event | KeyboardEvent | MouseEvent | Touch;
20+
export type AnyNativeEvent = Event | KeyboardEvent | MouseEvent | TouchEvent;
2121

2222
export type PluginName = string;
2323

packages/legacy-events/ReactGenericBatching.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import {
1010
restoreStateIfNeeded,
1111
} from './ReactControlledComponent';
1212

13-
import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags';
13+
import {
14+
enableDeprecatedFlareAPI,
15+
enableListenerAPI,
16+
} from 'shared/ReactFeatureFlags';
1417
import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
1518

1619
// Used as a way to call batchedUpdates when we don't have a reference to
@@ -118,7 +121,7 @@ export function flushDiscreteUpdatesIfNeeded(timeStamp: number) {
118121
// behaviour as we had before this change, so the risks are low.
119122
if (
120123
!isInsideEventHandler &&
121-
(!enableDeprecatedFlareAPI ||
124+
((!enableDeprecatedFlareAPI && !enableListenerAPI) ||
122125
(timeStamp === 0 || lastFlushedEventTimeStamp !== timeStamp))
123126
) {
124127
lastFlushedEventTimeStamp = timeStamp;

packages/react-art/src/ReactARTHostConfig.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,3 +469,18 @@ export function getInstanceFromNode(node) {
469469
export function beforeRemoveInstance(instance) {
470470
// noop
471471
}
472+
473+
export function registerEventListener(
474+
eventListener: any,
475+
rootContainerInstance: any,
476+
): void {
477+
// noop
478+
}
479+
480+
export function addDelegatedEventListener(delegatedEventListener: any) {
481+
// noop
482+
}
483+
484+
export function removeDelegatedEventListener(delegatedEventListener: any) {
485+
// noop
486+
}

packages/react-debug-tools/src/ReactDebugHooks.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,19 @@ function useResponder(
237237
};
238238
}
239239

240+
function useEvent(listener: any): void {
241+
hookLog.push({primitive: 'Event', stackError: new Error(), value: listener});
242+
}
243+
244+
function useDelegatedEvent(delegatedListener: any): [() => void, () => void] {
245+
hookLog.push({
246+
primitive: 'DelegatedEvent',
247+
stackError: new Error(),
248+
value: delegatedListener,
249+
});
250+
return [() => {}, () => {}];
251+
}
252+
240253
function useTransition(
241254
config: SuspenseConfig | null | void,
242255
): [(() => void) => void, boolean] {
@@ -274,6 +287,8 @@ const Dispatcher: DispatcherType = {
274287
useResponder,
275288
useTransition,
276289
useDeferredValue,
290+
useEvent,
291+
useDelegatedEvent,
277292
};
278293

279294
// Inspect

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ import {
5353
} from 'legacy-events/EventPropagators';
5454
import ReactVersion from 'shared/ReactVersion';
5555
import invariant from 'shared/invariant';
56-
import {exposeConcurrentModeAPIs} from 'shared/ReactFeatureFlags';
56+
import {
57+
exposeConcurrentModeAPIs,
58+
enableListenerAPI,
59+
} from 'shared/ReactFeatureFlags';
5760

5861
import {
5962
getInstanceFromNode,
@@ -70,6 +73,7 @@ import {
7073
setAttemptHydrationAtCurrentPriority,
7174
queueExplicitHydrationTarget,
7275
} from '../events/ReactDOMEventReplaying';
76+
import {useEvent, useDelegatedEvent} from './ReactDOMEventListenerHooks';
7377

7478
setAttemptSynchronousHydration(attemptSynchronousHydration);
7579
setAttemptUserBlockingHydration(attemptUserBlockingHydration);
@@ -193,6 +197,11 @@ if (exposeConcurrentModeAPIs) {
193197
};
194198
}
195199

200+
if (enableListenerAPI) {
201+
ReactDOM.unstable_useEvent = useEvent;
202+
ReactDOM.unstable_useDelegatedEvent = useDelegatedEvent;
203+
}
204+
196205
const foundDevTools = injectIntoDevTools({
197206
findFiberByHostInstance: getClosestInstanceFromNode,
198207
bundleType: __DEV__ ? 1 : 0,

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ import {
6565
import {
6666
addResponderEventSystemEvent,
6767
removeActiveResponderEventSystemEvent,
68+
addListenerSystemEvent,
69+
removeListenerSystemEvent,
6870
} from '../events/ReactDOMEventListener.js';
6971
import {mediaEventTypes} from '../events/DOMTopLevelEventTypes';
7072
import {
@@ -90,6 +92,7 @@ import {toStringOrTrustedType} from './ToStringValue';
9092
import {
9193
enableDeprecatedFlareAPI,
9294
enableTrustedTypesIntegration,
95+
enableListenerAPI,
9396
} from 'shared/ReactFeatureFlags';
9497

9598
let didWarnInvalidHydration = false;
@@ -1345,6 +1348,42 @@ export function listenToEventResponderEventTypes(
13451348
}
13461349
}
13471350

1351+
export function listenToEventListener(
1352+
type: string,
1353+
passive: boolean,
1354+
document: Document,
1355+
): void {
1356+
if (enableListenerAPI) {
1357+
// Get the listening Map for this element. We use this to track
1358+
// what events we're listening to.
1359+
const listenerMap = getListenerMapForElement(document);
1360+
const passiveKey = type + '_passive';
1361+
const activeKey = type + '_active';
1362+
const eventKey = passive ? passiveKey : activeKey;
1363+
1364+
if (!listenerMap.has(eventKey)) {
1365+
if (passive) {
1366+
if (listenerMap.has(activeKey)) {
1367+
// If we have an active event listener, do not register
1368+
// a passive event listener. We use the same active event
1369+
// listener.
1370+
return;
1371+
} else {
1372+
// If we have a passive event listener, remove the
1373+
// existing passive event listener before we add the
1374+
// active event listener.
1375+
const passiveListener = listenerMap.get(passiveKey);
1376+
if (passiveListener != null) {
1377+
removeListenerSystemEvent(document, type, passiveListener);
1378+
}
1379+
}
1380+
}
1381+
const eventListener = addListenerSystemEvent(document, type, passive);
1382+
listenerMap.set(eventKey, eventListener);
1383+
}
1384+
}
1385+
}
1386+
13481387
// We can remove this once the event API is stable and out of a flag
13491388
if (enableDeprecatedFlareAPI) {
13501389
setListenToResponderEventTypes(listenToEventResponderEventTypes);
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {RefObject, ReactDOMEventListener} from 'shared/ReactDOMTypes';
11+
12+
import React from 'react';
13+
import invariant from 'shared/invariant';
14+
import {getEventPriority} from '../events/SimpleEventPlugin';
15+
16+
const ReactCurrentDispatcher =
17+
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
18+
.ReactCurrentDispatcher;
19+
20+
type EventOptions = {|
21+
bind?: RefObject,
22+
capture?: boolean,
23+
kind?: null | number | Symbol,
24+
passive?: boolean,
25+
priority?: number,
26+
|};
27+
28+
function resolveDispatcher() {
29+
const dispatcher = ReactCurrentDispatcher.current;
30+
invariant(
31+
dispatcher !== null,
32+
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
33+
' one of the following reasons:\n' +
34+
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
35+
'2. You might be breaking the Rules of Hooks\n' +
36+
'3. You might have more than one copy of React in the same app\n' +
37+
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
38+
);
39+
return dispatcher;
40+
}
41+
42+
function createEventListener(
43+
type: string,
44+
callback: Event => void,
45+
options: EventOptions | void,
46+
isDelegated: boolean,
47+
): ReactDOMEventListener {
48+
let bind = null;
49+
let capture = false;
50+
let kind = null;
51+
let passive = false;
52+
let priority = getEventPriority((type: any));
53+
54+
if (options != null) {
55+
const optionsBind = options.bind;
56+
const optionsCapture = options && options.capture;
57+
const optionsKind = options && options.kind;
58+
const optionsPassive = options && options.passive;
59+
const optionsPriority = options && options.priority;
60+
61+
if (!isDelegated) {
62+
if (optionsBind != null) {
63+
invariant(
64+
'current' in optionsBind,
65+
'An object ref (via the useRef hook) is ' +
66+
'required for the "bind" option in useEvent',
67+
);
68+
bind = optionsBind;
69+
}
70+
}
71+
if (typeof optionsCapture === 'boolean') {
72+
capture = optionsCapture;
73+
}
74+
if (
75+
(!isDelegated && typeof optionsKind === 'number') ||
76+
// $FlowFixMe: we are using an older version of Flow that incorrectly recognizes the typeof for "symbol".
77+
typeof optionsKind === 'symbol'
78+
) {
79+
// Because of the Flow bug, we have to do this :(
80+
kind = (optionsKind: any);
81+
}
82+
if (typeof optionsPassive === 'boolean') {
83+
passive = optionsPassive;
84+
}
85+
if (typeof optionsPriority === 'number') {
86+
priority = optionsPriority;
87+
}
88+
}
89+
return {
90+
attached: false,
91+
bind,
92+
callback,
93+
capture,
94+
depth: 0,
95+
delegated: isDelegated,
96+
kind,
97+
passive,
98+
priority,
99+
target: null,
100+
type,
101+
};
102+
}
103+
104+
export function useEvent(
105+
type: string,
106+
callback: Event => void,
107+
options?: EventOptions,
108+
): void {
109+
const dispatcher = resolveDispatcher();
110+
const isDelegated = false;
111+
const reactEventListener = createEventListener(
112+
type,
113+
callback,
114+
options,
115+
isDelegated,
116+
);
117+
dispatcher.useEvent(reactEventListener);
118+
}
119+
120+
export function useDelegatedEvent(
121+
type: string,
122+
callback: Event => void,
123+
options?: EventOptions,
124+
): [() => void, () => void] {
125+
const dispatcher = resolveDispatcher();
126+
const isDelegated = true;
127+
const reactDelegatedEventListener = createEventListener(
128+
type,
129+
callback,
130+
options,
131+
isDelegated,
132+
);
133+
return dispatcher.useDelegatedEvent(reactDelegatedEventListener);
134+
}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
warnForInsertedHydratedElement,
2828
warnForInsertedHydratedText,
2929
listenToEventResponderEventTypes,
30+
listenToEventListener,
3031
} from './ReactDOMComponent';
3132
import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
3233
import setTextContent from './setTextContent';
@@ -50,6 +51,7 @@ import type {
5051
ReactDOMEventResponder,
5152
ReactDOMEventResponderInstance,
5253
ReactDOMFundamentalComponentInstance,
54+
ReactDOMEventListener,
5355
} from 'shared/ReactDOMTypes';
5456
import {
5557
mountEventResponder,
@@ -58,6 +60,8 @@ import {
5860
} from '../events/DeprecatedDOMEventResponderSystem';
5961
import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying';
6062

63+
export type ReactEventListener = ReactDOMEventListener;
64+
6165
export type Type = string;
6266
export type Props = {
6367
autoFocus?: boolean,
@@ -118,6 +122,10 @@ import {
118122
RESPONDER_EVENT_SYSTEM,
119123
IS_PASSIVE,
120124
} from 'legacy-events/EventSystemFlags';
125+
import {
126+
addDelegatedListener,
127+
removeDelegatedListener,
128+
} from '../events/DOMEventListenerSystem';
121129

122130
let SUPPRESS_HYDRATION_WARNING;
123131
if (__DEV__) {
@@ -1040,3 +1048,24 @@ export function unmountFundamentalComponent(
10401048
export function getInstanceFromNode(node: HTMLElement): null | Object {
10411049
return getClosestInstanceFromNode(node) || null;
10421050
}
1051+
1052+
export function registerEventListener(
1053+
eventListener: ReactDOMEventListener,
1054+
rootContainerInstance: Container,
1055+
): void {
1056+
const {type, passive} = eventListener;
1057+
const doc = rootContainerInstance.ownerDocument;
1058+
listenToEventListener(type, passive, doc);
1059+
}
1060+
1061+
export function addDelegatedEventListener(
1062+
delegatedEventListener: ReactDOMEventListener,
1063+
) {
1064+
addDelegatedListener(delegatedEventListener);
1065+
}
1066+
1067+
export function removeDelegatedEventListener(
1068+
delegatedEventListener: ReactDOMEventListener,
1069+
) {
1070+
removeDelegatedListener(delegatedEventListener);
1071+
}

0 commit comments

Comments
 (0)