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

cleanup

Address feedback

Fix flow

Major refactor and re-design
  • Loading branch information
trueadm committed Dec 20, 2019
1 parent 4c27037 commit 0bec034
Show file tree
Hide file tree
Showing 30 changed files with 1,719 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
17 changes: 17 additions & 0 deletions packages/react-art/src/ReactARTHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -469,3 +469,20 @@ export function getInstanceFromNode(node) {
export function beforeRemoveInstance(instance) {
// noop
}

export function registerListenerEvent(
event: any,
rootContainerInstance: Container,
): void {
// noop
}

export function attachListenerToInstance(listener: any): boolean {
// noop
return false;
}

export function detachListenerFromInstance(listener: any): boolean {
// noop
return false;
}
12 changes: 12 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,17 @@ function useResponder(
};
}

const noOp = () => {};

function useEvent(options: any): any {
hookLog.push({primitive: 'Event', stackError: new Error(), value: options});
return {
clear: noOp,
listen: noOp,
unlisten: noOp,
};
}

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

// Inspect
Expand Down
10 changes: 9 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} from './ReactDOMEventListenerHooks';

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

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

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
9 changes: 9 additions & 0 deletions packages/react-dom/src/client/ReactDOMComponentTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const randomKey = Math.random()
.slice(2);
const internalInstanceKey = '__reactInternalInstance$' + randomKey;
const internalEventHandlersKey = '__reactEventHandlers$' + randomKey;
const internalEventListenersKey = '__reactEventListeners$' + randomKey;
const internalContainerInstanceKey = '__reactContainere$' + randomKey;

export function precacheFiberNode(hostInst, node) {
Expand Down Expand Up @@ -164,3 +165,11 @@ export function getFiberCurrentPropsFromNode(node) {
export function updateFiberProps(node, props) {
node[internalEventHandlersKey] = props;
}

export function getListenersFromNode(node) {
return node[internalEventListenersKey] || null;
}

export function initListenersSet(node, value) {
node[internalEventListenersKey] = value;
}
74 changes: 74 additions & 0 deletions packages/react-dom/src/client/ReactDOMEventListenerHooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* 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 {
ReactDOMListenerEvent,
ReactDOMListenerHook,
} 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 = {|
capture?: boolean,
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;
}

export function useEvent(
type: string,
options?: EventOptions,
): ReactDOMListenerHook {
const dispatcher = resolveDispatcher();
let capture = false;
let passive = false;
let priority = getEventPriority((type: any));

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

if (typeof optionsCapture === 'boolean') {
capture = optionsCapture;
}
if (typeof optionsPassive === 'boolean') {
passive = optionsPassive;
}
if (typeof optionsPriority === 'number') {
priority = optionsPriority;
}
}
const event: ReactDOMListenerEvent = {
capture,
passive,
priority,
type,
};
return dispatcher.useEvent(event);
}
57 changes: 56 additions & 1 deletion 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,9 @@ import type {
ReactDOMEventResponder,
ReactDOMEventResponderInstance,
ReactDOMFundamentalComponentInstance,
ReactDOMListener,
ReactDOMListenerEvent,
ReactDOMListenerHook,
} from 'shared/ReactDOMTypes';
import {
mountEventResponder,
Expand All @@ -58,6 +62,10 @@ import {
} from '../events/DeprecatedDOMEventResponderSystem';
import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying';

export type ReactListenerEvent = ReactDOMListenerEvent;
export type ReactListenerHook = ReactDOMListenerHook;
export type ReactListener = ReactDOMListener;

export type Type = string;
export type Props = {
autoFocus?: boolean,
Expand Down Expand Up @@ -118,6 +126,12 @@ import {
RESPONDER_EVENT_SYSTEM,
IS_PASSIVE,
} from 'legacy-events/EventSystemFlags';
import {
attachElementListener,
detachElementListener,
attachDocumentListener,
detachDocumentListener,
} from '../events/DOMEventListenerSystem';

let SUPPRESS_HYDRATION_WARNING;
if (__DEV__) {
Expand Down Expand Up @@ -1037,6 +1051,47 @@ export function unmountFundamentalComponent(
}
}

export function getInstanceFromNode(node: HTMLElement): null | Object {
export function getInstanceFromNode(node: Instance): null | Object {
return getClosestInstanceFromNode(node) || null;
}

export function registerListenerEvent(
event: ReactDOMListenerEvent,
rootContainerInstance: Container,
): void {
const {type, passive} = event;
const doc = rootContainerInstance.ownerDocument;
listenToEventListener(type, passive, doc);
}

export function attachListenerToInstance(listener: ReactDOMListener): boolean {
const {instance} = listener;
if (instance.nodeType === DOCUMENT_NODE) {
attachDocumentListener(listener);
return true;
}
const internalInstanceHandle = getClosestInstanceFromNode(instance);

if (!internalInstanceHandle) {
return false;
}
attachElementListener(listener);
return true;
}

export function detachListenerFromInstance(
listener: ReactDOMListener,
): boolean {
const {instance} = listener;
if (instance.nodeType === DOCUMENT_NODE) {
detachDocumentListener(listener);
return true;
}
const internalInstanceHandle = getClosestInstanceFromNode(instance);

if (!internalInstanceHandle) {
return false;
}
detachElementListener(listener);
return true;
}
Loading

0 comments on commit 0bec034

Please sign in to comment.