Skip to content

Commit

Permalink
[react-interactions] Add Listener API + useEvent/useDelegatedEvent
Browse files Browse the repository at this point in the history
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
  • Loading branch information
trueadm committed Dec 18, 2019
1 parent 4c27037 commit 72ed512
Show file tree
Hide file tree
Showing 33 changed files with 1,968 additions and 23 deletions.
11 changes: 6 additions & 5 deletions packages/legacy-events/EventSystemFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ export type EventSystemFlags = number;

export const PLUGIN_EVENT_SYSTEM = 1;
export const RESPONDER_EVENT_SYSTEM = 1 << 1;
export const IS_PASSIVE = 1 << 2;
export const IS_ACTIVE = 1 << 3;
export const PASSIVE_NOT_SUPPORTED = 1 << 4;
export const IS_REPLAYED = 1 << 5;
export const IS_FIRST_ANCESTOR = 1 << 6;
export const LISTENER_EVENT_SYSTEM = 1 << 2;
export const IS_PASSIVE = 1 << 3;
export const IS_ACTIVE = 1 << 4;
export const PASSIVE_NOT_SUPPORTED = 1 << 5;
export const IS_REPLAYED = 1 << 6;
export const IS_FIRST_ANCESTOR = 1 << 7;
2 changes: 1 addition & 1 deletion packages/legacy-events/PluginModuleType.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';

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

export type AnyNativeEvent = Event | KeyboardEvent | MouseEvent | Touch;
export type AnyNativeEvent = Event | KeyboardEvent | MouseEvent | TouchEvent;

export type PluginName = string;

Expand Down
7 changes: 5 additions & 2 deletions packages/legacy-events/ReactGenericBatching.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
restoreStateIfNeeded,
} from './ReactControlledComponent';

import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags';
import {
enableDeprecatedFlareAPI,
enableListenerAPI,
} from 'shared/ReactFeatureFlags';
import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';

// Used as a way to call batchedUpdates when we don't have a reference to
Expand Down Expand Up @@ -118,7 +121,7 @@ export function flushDiscreteUpdatesIfNeeded(timeStamp: number) {
// behaviour as we had before this change, so the risks are low.
if (
!isInsideEventHandler &&
(!enableDeprecatedFlareAPI ||
((!enableDeprecatedFlareAPI && !enableListenerAPI) ||
(timeStamp === 0 || lastFlushedEventTimeStamp !== timeStamp))
) {
lastFlushedEventTimeStamp = timeStamp;
Expand Down
15 changes: 15 additions & 0 deletions packages/react-art/src/ReactARTHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -469,3 +469,18 @@ export function getInstanceFromNode(node) {
export function beforeRemoveInstance(instance) {
// noop
}

export function registerEventListener(
eventListener: any,
rootContainerInstance: any,
): void {
// noop
}

export function addDelegatedEventListener(delegatedEventListener: any) {
// noop
}

export function removeDelegatedEventListener(delegatedEventListener: any) {
// noop
}
15 changes: 15 additions & 0 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,19 @@ function useResponder(
};
}

function useEvent(listener: any): void {
hookLog.push({primitive: 'Event', stackError: new Error(), value: listener});
}

function useDelegatedEvent(delegatedListener: any): [() => void, () => void] {
hookLog.push({
primitive: 'DelegatedEvent',
stackError: new Error(),
value: delegatedListener,
});
return [() => {}, () => {}];
}

function useTransition(
config: SuspenseConfig | null | void,
): [(() => void) => void, boolean] {
Expand Down Expand Up @@ -274,6 +287,8 @@ const Dispatcher: DispatcherType = {
useResponder,
useTransition,
useDeferredValue,
useEvent,
useDelegatedEvent,
};

// Inspect
Expand Down
11 changes: 10 additions & 1 deletion packages/react-dom/src/client/ReactDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ import {
} from 'legacy-events/EventPropagators';
import ReactVersion from 'shared/ReactVersion';
import invariant from 'shared/invariant';
import {exposeConcurrentModeAPIs} from 'shared/ReactFeatureFlags';
import {
exposeConcurrentModeAPIs,
enableListenerAPI,
} from 'shared/ReactFeatureFlags';

import {
getInstanceFromNode,
Expand All @@ -70,6 +73,7 @@ import {
setAttemptHydrationAtCurrentPriority,
queueExplicitHydrationTarget,
} from '../events/ReactDOMEventReplaying';
import {useEvent, useDelegatedEvent} from './ReactDOMEventListenerHooks';

setAttemptSynchronousHydration(attemptSynchronousHydration);
setAttemptUserBlockingHydration(attemptUserBlockingHydration);
Expand Down Expand Up @@ -193,6 +197,11 @@ if (exposeConcurrentModeAPIs) {
};
}

if (enableListenerAPI) {
ReactDOM.unstable_useEvent = useEvent;
ReactDOM.unstable_useDelegatedEvent = useDelegatedEvent;
}

const foundDevTools = injectIntoDevTools({
findFiberByHostInstance: getClosestInstanceFromNode,
bundleType: __DEV__ ? 1 : 0,
Expand Down
39 changes: 39 additions & 0 deletions packages/react-dom/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ import {
import {
addResponderEventSystemEvent,
removeActiveResponderEventSystemEvent,
addListenerSystemEvent,
removeListenerSystemEvent,
} from '../events/ReactDOMEventListener.js';
import {mediaEventTypes} from '../events/DOMTopLevelEventTypes';
import {
Expand All @@ -90,6 +92,7 @@ import {toStringOrTrustedType} from './ToStringValue';
import {
enableDeprecatedFlareAPI,
enableTrustedTypesIntegration,
enableListenerAPI,
} from 'shared/ReactFeatureFlags';

let didWarnInvalidHydration = false;
Expand Down Expand Up @@ -1345,6 +1348,42 @@ export function listenToEventResponderEventTypes(
}
}

export function listenToEventListener(
type: string,
passive: boolean,
document: Document,
): void {
if (enableListenerAPI) {
// Get the listening Map for this element. We use this to track
// what events we're listening to.
const listenerMap = getListenerMapForElement(document);
const passiveKey = type + '_passive';
const activeKey = type + '_active';
const eventKey = passive ? passiveKey : activeKey;

if (!listenerMap.has(eventKey)) {
if (passive) {
if (listenerMap.has(activeKey)) {
// If we have an active event listener, do not register
// a passive event listener. We use the same active event
// listener.
return;
} else {
// If we have a passive event listener, remove the
// existing passive event listener before we add the
// active event listener.
const passiveListener = listenerMap.get(passiveKey);
if (passiveListener != null) {
removeListenerSystemEvent(document, type, passiveListener);
}
}
}
const eventListener = addListenerSystemEvent(document, type, passive);
listenerMap.set(eventKey, eventListener);
}
}
}

// We can remove this once the event API is stable and out of a flag
if (enableDeprecatedFlareAPI) {
setListenToResponderEventTypes(listenToEventResponderEventTypes);
Expand Down
134 changes: 134 additions & 0 deletions packages/react-dom/src/client/ReactDOMEventListenerHooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type {RefObject, ReactDOMEventListener} from 'shared/ReactDOMTypes';

import React from 'react';
import invariant from 'shared/invariant';
import {getEventPriority} from '../events/SimpleEventPlugin';

const ReactCurrentDispatcher =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher;

type EventOptions = {|
bind?: RefObject,
capture?: boolean,
kind?: null | number | Symbol,
passive?: boolean,
priority?: number,
|};

function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
invariant(
dispatcher !== null,
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
return dispatcher;
}

function createEventListener(
type: string,
callback: Event => void,
options: EventOptions | void,
isDelegated: boolean,
): ReactDOMEventListener {
let bind = null;
let capture = false;
let kind = null;
let passive = false;
let priority = getEventPriority((type: any));

if (options != null) {
const optionsBind = options.bind;
const optionsCapture = options && options.capture;
const optionsKind = options && options.kind;
const optionsPassive = options && options.passive;
const optionsPriority = options && options.priority;

if (!isDelegated) {
if (optionsBind != null) {
invariant(
'current' in optionsBind,
'An object ref (via the useRef hook) is ' +
'required for the "bind" option in useEvent',
);
bind = optionsBind;
}
}
if (typeof optionsCapture === 'boolean') {
capture = optionsCapture;
}
if (
(!isDelegated && typeof optionsKind === 'number') ||
// $FlowFixMe: we are using an older version of Flow that incorrectly recognizes the typeof for "symbol".
typeof optionsKind === 'symbol'
) {
// Because of the Flow bug, we have to do this :(
kind = (optionsKind: any);
}
if (typeof optionsPassive === 'boolean') {
passive = optionsPassive;
}
if (typeof optionsPriority === 'number') {
priority = optionsPriority;
}
}
return {
attached: false,
bind,
callback,
capture,
depth: 0,
delegated: isDelegated,
kind,
passive,
priority,
target: null,
type,
};
}

export function useEvent(
type: string,
callback: Event => void,
options?: EventOptions,
): void {
const dispatcher = resolveDispatcher();
const isDelegated = false;
const reactEventListener = createEventListener(
type,
callback,
options,
isDelegated,
);
dispatcher.useEvent(reactEventListener);
}

export function useDelegatedEvent(
type: string,
callback: Event => void,
options?: EventOptions,
): [() => void, () => void] {
const dispatcher = resolveDispatcher();
const isDelegated = true;
const reactDelegatedEventListener = createEventListener(
type,
callback,
options,
isDelegated,
);
return dispatcher.useDelegatedEvent(reactDelegatedEventListener);
}
29 changes: 29 additions & 0 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
warnForInsertedHydratedElement,
warnForInsertedHydratedText,
listenToEventResponderEventTypes,
listenToEventListener,
} from './ReactDOMComponent';
import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
import setTextContent from './setTextContent';
Expand All @@ -50,6 +51,7 @@ import type {
ReactDOMEventResponder,
ReactDOMEventResponderInstance,
ReactDOMFundamentalComponentInstance,
ReactDOMEventListener,
} from 'shared/ReactDOMTypes';
import {
mountEventResponder,
Expand All @@ -58,6 +60,8 @@ import {
} from '../events/DeprecatedDOMEventResponderSystem';
import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying';

export type ReactEventListener = ReactDOMEventListener;

export type Type = string;
export type Props = {
autoFocus?: boolean,
Expand Down Expand Up @@ -118,6 +122,10 @@ import {
RESPONDER_EVENT_SYSTEM,
IS_PASSIVE,
} from 'legacy-events/EventSystemFlags';
import {
addDelegatedListener,
removeDelegatedListener,
} from '../events/DOMEventListenerSystem';

let SUPPRESS_HYDRATION_WARNING;
if (__DEV__) {
Expand Down Expand Up @@ -1040,3 +1048,24 @@ export function unmountFundamentalComponent(
export function getInstanceFromNode(node: HTMLElement): null | Object {
return getClosestInstanceFromNode(node) || null;
}

export function registerEventListener(
eventListener: ReactDOMEventListener,
rootContainerInstance: Container,
): void {
const {type, passive} = eventListener;
const doc = rootContainerInstance.ownerDocument;
listenToEventListener(type, passive, doc);
}

export function addDelegatedEventListener(
delegatedEventListener: ReactDOMEventListener,
) {
addDelegatedListener(delegatedEventListener);
}

export function removeDelegatedEventListener(
delegatedEventListener: ReactDOMEventListener,
) {
removeDelegatedListener(delegatedEventListener);
}
Loading

0 comments on commit 72ed512

Please sign in to comment.