mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-06-11 21:37:09 +00:00
[change] Replace the ResponderEventPlugin
Replaces the ResponderEventPlugin with useResponderEvents hook. Also removes the event "normalization" of mouse, touch, and click events. These events are not part of the responder system and will no longer be modified from what ReactDOM dispatches. Fix #1589 Fix #1568 Fix #1571 Fix #829 Fix #693
This commit is contained in:
@@ -39,6 +39,7 @@
|
||||
"$ReadOnly": false,
|
||||
"$ReadOnlyArray": false,
|
||||
"CSSStyleSheet": false,
|
||||
"HTMLElement": false,
|
||||
"HTMLInputElement": false,
|
||||
"ReactClass": false,
|
||||
"ReactComponent": false,
|
||||
|
||||
+20
-17
@@ -15,6 +15,7 @@ import css from '../StyleSheet/css';
|
||||
import setAndForwardRef from '../../modules/setAndForwardRef';
|
||||
import useElementLayout from '../../hooks/useElementLayout';
|
||||
import usePlatformMethods from '../../hooks/usePlatformMethods';
|
||||
import useResponderEvents from '../../hooks/useResponderEvents';
|
||||
import React, { forwardRef, useContext, useRef } from 'react';
|
||||
import StyleSheet from '../StyleSheet';
|
||||
import TextAncestorContext from './TextAncestorContext';
|
||||
@@ -108,6 +109,24 @@ const Text = forwardRef<TextProps, *>((props, ref) => {
|
||||
|
||||
useElementLayout(hostRef, onLayout);
|
||||
usePlatformMethods(hostRef, ref, classList, style);
|
||||
useResponderEvents(hostRef, {
|
||||
onMoveShouldSetResponder,
|
||||
onMoveShouldSetResponderCapture,
|
||||
onResponderEnd,
|
||||
onResponderGrant,
|
||||
onResponderMove,
|
||||
onResponderReject,
|
||||
onResponderRelease,
|
||||
onResponderStart,
|
||||
onResponderTerminate,
|
||||
onResponderTerminationRequest,
|
||||
onScrollShouldSetResponder,
|
||||
onScrollShouldSetResponderCapture,
|
||||
onSelectionChangeShouldSetResponder,
|
||||
onSelectionChangeShouldSetResponderCapture,
|
||||
onStartShouldSetResponder,
|
||||
onStartShouldSetResponderCapture
|
||||
});
|
||||
|
||||
function createEnterHandler(fn) {
|
||||
return e => {
|
||||
@@ -140,29 +159,13 @@ const Text = forwardRef<TextProps, *>((props, ref) => {
|
||||
lang,
|
||||
nativeID,
|
||||
onBlur,
|
||||
onContextMenu,
|
||||
onFocus,
|
||||
onMoveShouldSetResponder,
|
||||
onMoveShouldSetResponderCapture,
|
||||
onResponderEnd,
|
||||
onResponderGrant,
|
||||
onResponderMove,
|
||||
onResponderReject,
|
||||
onResponderRelease,
|
||||
onResponderStart,
|
||||
onResponderTerminate,
|
||||
onResponderTerminationRequest,
|
||||
onScrollShouldSetResponder,
|
||||
onScrollShouldSetResponderCapture,
|
||||
onSelectionChangeShouldSetResponder,
|
||||
onSelectionChangeShouldSetResponderCapture,
|
||||
onStartShouldSetResponder,
|
||||
onStartShouldSetResponderCapture,
|
||||
ref: setRef,
|
||||
style,
|
||||
testID,
|
||||
// unstable
|
||||
onClick: onPress != null ? createPressHandler(onPress) : null,
|
||||
onContextMenu,
|
||||
onKeyDown: onPress != null ? createEnterHandler(onPress) : null,
|
||||
onMouseDown,
|
||||
onMouseEnter,
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ export type TextProps = {
|
||||
onResponderRelease?: (e: any) => void,
|
||||
onResponderStart?: (e: any) => void,
|
||||
onResponderTerminate?: (e: any) => void,
|
||||
onResponderTerminationRequest?: (e: any) => void,
|
||||
onResponderTerminationRequest?: (e: any) => boolean,
|
||||
onScrollShouldSetResponder?: (e: any) => boolean,
|
||||
onScrollShouldSetResponderCapture?: (e: any) => boolean,
|
||||
onSelectionChangeShouldSetResponder?: (e: any) => boolean,
|
||||
|
||||
+19
-16
@@ -16,6 +16,7 @@ import setAndForwardRef from '../../modules/setAndForwardRef';
|
||||
import useElementLayout from '../../hooks/useElementLayout';
|
||||
import useLayoutEffect from '../../hooks/useLayoutEffect';
|
||||
import { usePlatformInputMethods } from '../../hooks/usePlatformMethods';
|
||||
import useResponderEvents from '../../hooks/useResponderEvents';
|
||||
import { forwardRef, useRef } from 'react';
|
||||
import StyleSheet from '../StyleSheet';
|
||||
import TextInputState from '../../modules/TextInputState';
|
||||
@@ -286,6 +287,24 @@ const TextInput = forwardRef<TextInputProps, *>((props, ref) => {
|
||||
|
||||
useElementLayout(hostRef, onLayout);
|
||||
usePlatformInputMethods(hostRef, ref, classList, style);
|
||||
useResponderEvents(hostRef, {
|
||||
onMoveShouldSetResponder,
|
||||
onMoveShouldSetResponderCapture,
|
||||
onResponderEnd,
|
||||
onResponderGrant,
|
||||
onResponderMove,
|
||||
onResponderReject,
|
||||
onResponderRelease,
|
||||
onResponderStart,
|
||||
onResponderTerminate,
|
||||
onResponderTerminationRequest,
|
||||
onScrollShouldSetResponder,
|
||||
onScrollShouldSetResponderCapture,
|
||||
onSelectionChangeShouldSetResponder,
|
||||
onSelectionChangeShouldSetResponderCapture,
|
||||
onStartShouldSetResponder,
|
||||
onStartShouldSetResponderCapture
|
||||
});
|
||||
|
||||
return createElement(component, {
|
||||
accessibilityLabel,
|
||||
@@ -310,22 +329,6 @@ const TextInput = forwardRef<TextInputProps, *>((props, ref) => {
|
||||
onKeyDown: handleKeyDown,
|
||||
onScroll,
|
||||
onSelect: handleSelectionChange,
|
||||
onMoveShouldSetResponder,
|
||||
onMoveShouldSetResponderCapture,
|
||||
onResponderEnd,
|
||||
onResponderGrant,
|
||||
onResponderMove,
|
||||
onResponderReject,
|
||||
onResponderRelease,
|
||||
onResponderStart,
|
||||
onResponderTerminate,
|
||||
onResponderTerminationRequest,
|
||||
onScrollShouldSetResponder,
|
||||
onScrollShouldSetResponderCapture,
|
||||
onSelectionChangeShouldSetResponder,
|
||||
onSelectionChangeShouldSetResponderCapture,
|
||||
onStartShouldSetResponder,
|
||||
onStartShouldSetResponderCapture,
|
||||
placeholder,
|
||||
pointerEvents,
|
||||
testID,
|
||||
|
||||
+19
-16
@@ -15,6 +15,7 @@ import css from '../StyleSheet/css';
|
||||
import setAndForwardRef from '../../modules/setAndForwardRef';
|
||||
import useElementLayout from '../../hooks/useElementLayout';
|
||||
import usePlatformMethods from '../../hooks/usePlatformMethods';
|
||||
import useResponderEvents from '../../hooks/useResponderEvents';
|
||||
import StyleSheet from '../StyleSheet';
|
||||
import TextAncestorContext from '../Text/TextAncestorContext';
|
||||
import React, { forwardRef, useContext, useRef } from 'react';
|
||||
@@ -132,21 +133,7 @@ const View = forwardRef<ViewProps, *>((props, ref) => {
|
||||
|
||||
useElementLayout(hostRef, onLayout);
|
||||
usePlatformMethods(hostRef, ref, classList, style);
|
||||
|
||||
return createElement('div', {
|
||||
accessibilityLabel,
|
||||
accessibilityLiveRegion,
|
||||
accessibilityRelationship,
|
||||
accessibilityRole,
|
||||
accessibilityState,
|
||||
accessibilityValue,
|
||||
children,
|
||||
classList,
|
||||
importantForAccessibility,
|
||||
nativeID,
|
||||
onBlur,
|
||||
onContextMenu,
|
||||
onFocus,
|
||||
useResponderEvents(hostRef, {
|
||||
onMoveShouldSetResponder,
|
||||
onMoveShouldSetResponderCapture,
|
||||
onResponderEnd,
|
||||
@@ -162,7 +149,23 @@ const View = forwardRef<ViewProps, *>((props, ref) => {
|
||||
onSelectionChangeShouldSetResponder,
|
||||
onSelectionChangeShouldSetResponderCapture,
|
||||
onStartShouldSetResponder,
|
||||
onStartShouldSetResponderCapture,
|
||||
onStartShouldSetResponderCapture
|
||||
});
|
||||
|
||||
return createElement('div', {
|
||||
accessibilityLabel,
|
||||
accessibilityLiveRegion,
|
||||
accessibilityRelationship,
|
||||
accessibilityRole,
|
||||
accessibilityState,
|
||||
accessibilityValue,
|
||||
children,
|
||||
classList,
|
||||
importantForAccessibility,
|
||||
nativeID,
|
||||
onBlur,
|
||||
onContextMenu,
|
||||
onFocus,
|
||||
pointerEvents,
|
||||
ref: setRef,
|
||||
style,
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ export type ViewProps = {
|
||||
onResponderRelease?: (e: any) => void,
|
||||
onResponderStart?: (e: any) => void,
|
||||
onResponderTerminate?: (e: any) => void,
|
||||
onResponderTerminationRequest?: (e: any) => void,
|
||||
onResponderTerminationRequest?: (e: any) => boolean,
|
||||
onScrollShouldSetResponder?: (e: any) => boolean,
|
||||
onScrollShouldSetResponderCapture?: (e: any) => boolean,
|
||||
onSelectionChangeShouldSetResponder?: (e: any) => boolean,
|
||||
|
||||
-23
@@ -1,28 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`modules/createElement normalizes event.nativeEvent 1`] = `
|
||||
Object {
|
||||
"_normalized": true,
|
||||
"bubbles": undefined,
|
||||
"cancelable": undefined,
|
||||
"changedTouches": Array [],
|
||||
"defaultPrevented": undefined,
|
||||
"identifier": undefined,
|
||||
"locationX": undefined,
|
||||
"locationY": undefined,
|
||||
"pageX": undefined,
|
||||
"pageY": undefined,
|
||||
"preventDefault": [Function],
|
||||
"stopImmediatePropagation": [Function],
|
||||
"stopPropagation": [Function],
|
||||
"target": undefined,
|
||||
"timestamp": 1496876171255,
|
||||
"touches": Array [],
|
||||
"type": undefined,
|
||||
"which": undefined,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`modules/createElement renders different DOM elements 1`] = `<span />`;
|
||||
|
||||
exports[`modules/createElement renders different DOM elements 2`] = `<main />`;
|
||||
|
||||
@@ -12,18 +12,6 @@ describe('modules/createElement', () => {
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('normalizes event.nativeEvent', done => {
|
||||
const onClick = e => {
|
||||
e.nativeEvent.timestamp = 1496876171255;
|
||||
expect(e.nativeEvent).toMatchSnapshot();
|
||||
done();
|
||||
};
|
||||
const component = shallow(createElement('span', { onClick }));
|
||||
component.find('span').simulate('click', {
|
||||
nativeEvent: {}
|
||||
});
|
||||
});
|
||||
|
||||
describe('prop "accessibilityRole"', () => {
|
||||
test('and string component type', () => {
|
||||
const component = shallow(createElement('span', { accessibilityRole: 'link' }));
|
||||
|
||||
@@ -8,83 +8,18 @@
|
||||
*/
|
||||
|
||||
import AccessibilityUtil from '../../modules/AccessibilityUtil';
|
||||
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
|
||||
import createDOMProps from '../../modules/createDOMProps';
|
||||
import { injectEventPluginsByName } from 'react-dom/unstable-native-dependencies';
|
||||
import normalizeNativeEvent from '../../modules/normalizeNativeEvent';
|
||||
import React from 'react';
|
||||
import ResponderEventPlugin from '../../modules/ResponderEventPlugin';
|
||||
|
||||
if (canUseDOM) {
|
||||
try {
|
||||
injectEventPluginsByName({
|
||||
ResponderEventPlugin
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore errors caused by attempting to re-inject the plugin when app
|
||||
// scripts are being re-evaluated (e.g., development hot reloading) while
|
||||
// the ReactDOM instance is preserved.
|
||||
}
|
||||
}
|
||||
|
||||
const isModifiedEvent = event =>
|
||||
!!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
|
||||
|
||||
/**
|
||||
* Ensure event handlers receive an event of the expected shape. The 'button'
|
||||
* role – for accessibility reasons and functional equivalence to the native
|
||||
* button element – must also support synthetic keyboard activation of onclick,
|
||||
* and remove event handlers when disabled.
|
||||
*/
|
||||
const eventHandlerNames = {
|
||||
onBlur: true,
|
||||
onClick: true,
|
||||
onClickCapture: true,
|
||||
onContextMenu: true,
|
||||
onFocus: true,
|
||||
onResponderRelease: true,
|
||||
onTouchCancel: true,
|
||||
onTouchCancelCapture: true,
|
||||
onTouchEnd: true,
|
||||
onTouchEndCapture: true,
|
||||
onTouchMove: true,
|
||||
onTouchMoveCapture: true,
|
||||
onTouchStart: true,
|
||||
onTouchStartCapture: true
|
||||
};
|
||||
const adjustProps = domProps => {
|
||||
const { onClick, onResponderRelease, role } = domProps;
|
||||
const { onClick, role } = domProps;
|
||||
|
||||
const isButtonLikeRole = AccessibilityUtil.buttonLikeRoles[role];
|
||||
const isDisabled = AccessibilityUtil.isDisabled(domProps);
|
||||
const isLinkRole = role === 'link';
|
||||
|
||||
Object.keys(domProps).forEach(propName => {
|
||||
const prop = domProps[propName];
|
||||
const isEventHandler = typeof prop === 'function' && eventHandlerNames[propName];
|
||||
if (isEventHandler) {
|
||||
if (isButtonLikeRole && isDisabled) {
|
||||
domProps[propName] = undefined;
|
||||
} else {
|
||||
// TODO: move this out of the render path
|
||||
domProps[propName] = e => {
|
||||
e.nativeEvent = normalizeNativeEvent(e.nativeEvent);
|
||||
return prop(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel click events if the responder system is being used on a link
|
||||
// element. Click events are not an expected part of the React Native API,
|
||||
// and browsers dispatch click events that cannot otherwise be cancelled from
|
||||
// preceding mouse events in the responder system.
|
||||
if (isLinkRole && onResponderRelease) {
|
||||
domProps.onClick = function(e) {
|
||||
if (!e.isDefaultPrevented() && !isModifiedEvent(e.nativeEvent) && !domProps.target) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
// Button-like roles should not trigger 'onClick' if they are disabled.
|
||||
if (isButtonLikeRole && isDisabled && domProps.onClick != null) {
|
||||
domProps.onClick = undefined;
|
||||
}
|
||||
|
||||
// Button-like roles should trigger 'onClick' if SPACE or ENTER keys are pressed.
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
# Responder Event System
|
||||
|
||||
The Responder Event System is a gesture system that manages the lifecycle of gestures. It was designed for [React Native](https://reactnative.dev/docs/next/gesture-responder-system) to help support the development of native-quality gestures. A pointer may transition through several different phases while the gesture is being determined (e.g., tap, scroll, swipe) and be used simultaneously alongside other pointers. The Responder Event System provides a single, global “interaction lock” on views. For a view to become the “responder” means that pointer interactions are exclusive to that view and none other. A view can negotiate to become the “responder” without requiring knowledge of other views.
|
||||
|
||||
NOTE: Although the responder events mention only `touches`, this is for historical reasons (originating from React Native); the system does respond to mouse events which are converted into emulated touches. In the future we could adjust the events to align more with the `PointerEvent` API which would remove this ambiguity and surface more information to developers (e.g., `pointerType`).
|
||||
|
||||
## How it works
|
||||
|
||||
A view can become the "responder" after the following native events: `scroll`, `selectionchange`, `touchstart`, `touchmove`, `mousedown`, `mousemove`. If nothing is already the "responder", the event propagates to (capture) and from (bubble) the event target until a view returns `true` for `on*ShouldSetResponder(Capture)`.
|
||||
|
||||
If something is *already* the responder, the negotiation event propagates to (capture) and from (bubble) the lowest common ancestor of the event target and the current responder. Then negotiation happens between the current responder and the view that wants to become the responder.
|
||||
|
||||
## API
|
||||
|
||||
### useResponderEvents
|
||||
|
||||
The `useResponderEvents` hook takes a ref to a host element and an object of responder callbacks.
|
||||
|
||||
```js
|
||||
function View(props) {
|
||||
const hostRef = useRef(null);
|
||||
|
||||
const callbacks: ResponderCallbacks = {
|
||||
onMoveShouldSetResponder: props.onMoveShouldSetResponder,
|
||||
onMoveShouldSetResponderCapture: props.onMoveShouldSetResponderCapture,
|
||||
onResponderEnd: props.onResponderEnd,
|
||||
onResponderGrant: props.onResponderGrant,
|
||||
onResponderMove: props.onResponderMove,
|
||||
onResponderReject: props.onResponderReject,
|
||||
onResponderRelease: props.onResponderRelease,
|
||||
onResponderStart: props.onResponderStart,
|
||||
onResponderTerminate: props.onResponderTerminate,
|
||||
onResponderTerminationRequest: props.onResponderTerminationRequest,
|
||||
onScrollShouldSetResponder: props.onScrollShouldSetResponder,
|
||||
onScrollShouldSetResponderCapture: props.onScrollShouldSetResponderCapture,
|
||||
onSelectionChangeShouldSetResponder: props.onSelectionChangeShouldSetResponder,
|
||||
onSelectionChangeShouldSetResponderCapture: props.onSelectionChangeShouldSetResponderCapture,
|
||||
onStartShouldSetResponder: props.onStartShouldSetResponder,
|
||||
onStartShouldSetResponderCapture: props.onStartShouldSetResponderCapture
|
||||
}
|
||||
|
||||
useResponderEvents(hostRef, callbacks);
|
||||
|
||||
return (
|
||||
<div ref={hostRef} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Responder negotiation
|
||||
|
||||
A view can become the responder by using the negotiation methods. During the capture phase the deepest node is called last. During the bubble phase the deepest node is called first. The capture phase should be used when a view wants to prevent a descendant from becoming the responder. The first view to return `true` from any of the `on*ShouldSetResponderCapture`/`on*ShouldSetResponder` methods will either become the responder or enter into negotiation with the existing responder.
|
||||
|
||||
N.B. If `stopPropagation` is called on the event for any of the negotiation methods, it only stops further negotiation within the Responder System. It will not stop the propagation of the native event (which has already bubbled to the `document` by this time.)
|
||||
|
||||
#### onStartShouldSetResponder / onStartShouldSetResponderCapture
|
||||
|
||||
On pointer down, should this view attempt to become the responder? If the view is not the responder, these methods may be called for every pointer start on the view.
|
||||
|
||||
#### onMoveShouldSetResponder / onMoveShouldSetResponderCapture
|
||||
|
||||
On pointer move, should this view attempt to become the responder? If the view is not the responder, these methods may be called for every pointer move on the view.
|
||||
|
||||
#### onScrollShouldSetResponder / onScrollShouldSetResponderCapture
|
||||
|
||||
On scroll, should this view attempt to become the responder? If the view is not the responder, these methods may be called for every scroll on the view.
|
||||
|
||||
#### onSelectionChangeShouldSetResponder / onSelectionChangeShouldSetResponderCapture
|
||||
|
||||
On text selection change, should this view attempt to become the responder? Does not capture or bubble and is only called on the view that is the first ancestor of the selection `anchorNode`.
|
||||
|
||||
#### onResponderTerminationRequest
|
||||
|
||||
The view is the responder, but another view now wants to become the responder. Should this view release the responder? Returning `true` allows the responder to be released.
|
||||
|
||||
### Responder transfer
|
||||
|
||||
If a view returns `true` for a negotiation method then it will either become the responder (if none exists) or be involved in the responder transfer. The following methods are called only for the views involved in the responder transfer (i.e., no bubbling.)
|
||||
|
||||
#### onResponderGrant
|
||||
|
||||
The view is granted the responder and is now responding to pointer events. The lifecycle methods will be called for this view. This is the point at which you should provide visual feedback for users that the interaction has begun.
|
||||
|
||||
#### onResponderReject
|
||||
|
||||
The view was not granted the responder. It was rejected because another view is already the responder and will not release it.
|
||||
|
||||
#### onResponderTerminate
|
||||
|
||||
The responder has been taken from this view. It may have been taken by another view after a call to `onResponderTerminationRequest`, or it might have been taken by the browser without asking (e.g., window blur, document scroll, context menu open). This is the point at which you should provide visual feedback for users that the interaction has been cancelled.
|
||||
|
||||
### Responder lifecycle
|
||||
|
||||
If a view is the responder, the following methods will be called only for this view (i.e., no bubbling.) These methods are *always* bookended by `onResponderGrant` (before) and either `onResponderRelease` or `onResponderTerminate` (after).
|
||||
|
||||
#### onResponderStart
|
||||
|
||||
A pointer down event occured on the screen. The responder is notified of all start events, even if the pointer target is not this view (i.e., additional pointers are being used). Therefore, this method may be called multiple times while the view is the responder.
|
||||
|
||||
#### onResponderMove
|
||||
|
||||
A pointer move event occured on the screen. The responder is notified of all move events, even if the pointer target is not this view (i.e., additional pointers are being used). Therefore, this method may be called multiple times while the view is the responder.
|
||||
|
||||
#### onResponderEnd
|
||||
|
||||
A pointer up event occured on the screen. The responder is notified of all end events, even if the pointer target is not this view (i.e., additional pointers are being used). Therefore, this method may be called multiple times while the view is the responder.
|
||||
|
||||
#### onResponderRelease
|
||||
|
||||
As soon as there are no more pointers that *started* inside descendants of the responder, this method is called on the responder and the interaction lock is released. This is the point at which you should provide visual feedback for users that the interaction is over.
|
||||
|
||||
### Responder events
|
||||
|
||||
Every method is called with a responder event. The type of the event is shown below. The `currentTarget` of the event is always `null` for the negotiation methods. Data dervied from the native events, e.g., the native `target` and pointer coordinates, can be used to determine the return value of the negotiation methods, etc.
|
||||
|
||||
## Types
|
||||
|
||||
```js
|
||||
type ResponderCallbacks = {
|
||||
onResponderEnd?: ?(e: ResponderEvent) => void,
|
||||
onResponderGrant?: ?(e: ResponderEvent) => void,
|
||||
onResponderMove?: ?(e: ResponderEvent) => void,
|
||||
onResponderRelease?: ?(e: ResponderEvent) => void,
|
||||
onResponderReject?: ?(e: ResponderEvent) => void,
|
||||
onResponderStart?: ?(e: ResponderEvent) => void,
|
||||
onResponderTerminate?: ?(e: ResponderEvent) => void,
|
||||
onResponderTerminationRequest?: ?(e: ResponderEvent) => boolean,
|
||||
onStartShouldSetResponder?: ?(e: ResponderEvent) => boolean,
|
||||
onStartShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean,
|
||||
onMoveShouldSetResponder?: ?(e: ResponderEvent) => boolean,
|
||||
onMoveShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean,
|
||||
onScrollShouldSetResponder?: ?(e: ResponderEvent) => boolean,
|
||||
onScrollShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean,
|
||||
onSelectionChangeShouldSetResponder?: ?(e: ResponderEvent) => boolean,
|
||||
onSelectionChangeShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean
|
||||
};
|
||||
```
|
||||
|
||||
```js
|
||||
type ResponderEvent = {
|
||||
// The DOM element acting as the responder view
|
||||
currentTarget: ?HTMLElement,
|
||||
defaultPrevented: boolean,
|
||||
eventPhase: ?number,
|
||||
isDefaultPrevented: () => boolean,
|
||||
isPropagationStopped: () => boolean,
|
||||
isTrusted: boolean,
|
||||
preventDefault: () => void,
|
||||
stopPropagation: () => void,
|
||||
nativeEvent: TouchEvent,
|
||||
persist: () => void,
|
||||
target: HTMLElement,
|
||||
timeStamp: number,
|
||||
touchHistory: $ReadOnly<{|
|
||||
indexOfSingleActiveTouch: number,
|
||||
mostRecentTimeStamp: number,
|
||||
numberActiveTouches: number,
|
||||
touchBank: Array<{|
|
||||
currentPageX: number,
|
||||
currentPageY: number,
|
||||
currentTimeStamp: number,
|
||||
previousPageX: number,
|
||||
previousPageY: number,
|
||||
previousTimeStamp: number,
|
||||
startPageX: number,
|
||||
startPageY: number,
|
||||
startTimeStamp: number,
|
||||
touchActive: boolean
|
||||
|}>
|
||||
|}>
|
||||
};
|
||||
```
|
||||
|
||||
```js
|
||||
type TouchEvent = {
|
||||
// Array of all touch events that have changed since the last event
|
||||
changedTouches: Array<Touch>,
|
||||
force: number,
|
||||
// ID of the touch
|
||||
identifier: number,
|
||||
// The X position of the pointer, relative to the currentTarget
|
||||
locationX: number,
|
||||
// The Y position of the pointer, relative to the currentTarget
|
||||
locationY: number,
|
||||
// The X position of the pointer, relative to the page
|
||||
pageX: number,
|
||||
// The Y position of the pointer, relative to the page
|
||||
pageY: number,
|
||||
// The DOM element receiving the pointer event
|
||||
target: HTMLElement,
|
||||
// A time identifier for the pointer, useful for velocity calculation
|
||||
timestamp: number,
|
||||
// Array of all current touches on the screen
|
||||
touches: Array<Touch>
|
||||
};
|
||||
```
|
||||
|
||||
```js
|
||||
type Touch = {
|
||||
force: number,
|
||||
identifier: number,
|
||||
locationX: number,
|
||||
locationY: number,
|
||||
pageX: number,
|
||||
pageY: number,
|
||||
target: HTMLElement,
|
||||
timestamp: number
|
||||
};
|
||||
```
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) Nicolas Gallagher
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export type Touch = {
|
||||
force: number,
|
||||
identifier: number,
|
||||
// The locationX and locationY properties are non-standard additions
|
||||
locationX: any,
|
||||
locationY: any,
|
||||
pageX: number,
|
||||
pageY: number,
|
||||
target: any,
|
||||
// Touches in a list have a timestamp property
|
||||
timestamp: number
|
||||
};
|
||||
|
||||
export type TouchEvent = {
|
||||
altKey: boolean,
|
||||
ctrlKey: boolean,
|
||||
metaKey: boolean,
|
||||
shiftKey: boolean,
|
||||
// TouchList is an array in the Responder system
|
||||
changedTouches: Array<Touch>,
|
||||
force: number,
|
||||
// React Native adds properties to the "nativeEvent that are usually only found on W3C Touches ‾\_(ツ)_/‾
|
||||
identifier: number,
|
||||
locationX: any,
|
||||
locationY: any,
|
||||
pageX: number,
|
||||
pageY: number,
|
||||
target: any,
|
||||
// The timestamp has a lowercase "s" in the Responder system
|
||||
timestamp: number,
|
||||
// TouchList is an array in the Responder system
|
||||
touches: Array<Touch>
|
||||
};
|
||||
|
||||
export const BLUR = 'blur';
|
||||
export const CONTEXT_MENU = 'contextmenu';
|
||||
export const FOCUS_OUT = 'focusout';
|
||||
export const MOUSE_DOWN = 'mousedown';
|
||||
export const MOUSE_MOVE = 'mousemove';
|
||||
export const MOUSE_UP = 'mouseup';
|
||||
export const MOUSE_CANCEL = 'dragstart';
|
||||
export const TOUCH_START = 'touchstart';
|
||||
export const TOUCH_MOVE = 'touchmove';
|
||||
export const TOUCH_END = 'touchend';
|
||||
export const TOUCH_CANCEL = 'touchcancel';
|
||||
export const SCROLL = 'scroll';
|
||||
export const SELECT = 'select';
|
||||
export const SELECTION_CHANGE = 'selectionchange';
|
||||
|
||||
export function isStartish(eventType: mixed): boolean {
|
||||
return eventType === TOUCH_START || eventType === MOUSE_DOWN;
|
||||
}
|
||||
|
||||
export function isMoveish(eventType: mixed): boolean {
|
||||
return eventType === TOUCH_MOVE || eventType === MOUSE_MOVE;
|
||||
}
|
||||
|
||||
export function isEndish(eventType: mixed): boolean {
|
||||
return eventType === TOUCH_END || eventType === MOUSE_UP || isCancelish(eventType);
|
||||
}
|
||||
|
||||
export function isCancelish(eventType: mixed): boolean {
|
||||
return eventType === TOUCH_CANCEL || eventType === MOUSE_CANCEL;
|
||||
}
|
||||
|
||||
export function isScroll(eventType: mixed): boolean {
|
||||
return eventType === SCROLL;
|
||||
}
|
||||
|
||||
export function isSelectionChange(eventType: mixed): boolean {
|
||||
return eventType === SELECT || eventType === SELECTION_CHANGE;
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
/**
|
||||
* Copyright (c) Nicolas Gallagher
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
/**
|
||||
* RESPONDER EVENT SYSTEM
|
||||
*
|
||||
* A single, global "interaction lock" on views. For a view to be the "responder" means
|
||||
* that pointer interactions are exclusive to that view and none other. The "interaction
|
||||
* lock" can be transferred (only) to ancestors of the current "responder" as long as
|
||||
* pointers continue to be active.
|
||||
*
|
||||
* Responder being granted:
|
||||
*
|
||||
* A view can become the "responder" after the following events:
|
||||
* * "pointerdown" (implemented using "touchstart", "mousedown")
|
||||
* * "pointermove" (implemented using "touchmove", "mousemove")
|
||||
* * "scroll" (while a pointer is down)
|
||||
* * "selectionchange" (while a pointer is down)
|
||||
*
|
||||
* If nothing is already the "responder", the event propagates to (capture) and from
|
||||
* (bubble) the event target until a view returns `true` for
|
||||
* `on*ShouldSetResponder(Capture)`.
|
||||
*
|
||||
* If something is already the responder, the event propagates to (capture) and from
|
||||
* (bubble) the lowest common ancestor of the event target and the current "responder".
|
||||
* Then negotiation happens between the current "responder" and a view that wants to
|
||||
* become the "responder": see the timing diagram below.
|
||||
*
|
||||
* (NOTE: Scrolled views either automatically become the "responder" or release the
|
||||
* "interaction lock". A native scroll view that isn't built on top of the responder
|
||||
* system must result in the current "responder" being notified that it no longer has
|
||||
* the "interaction lock" - the native system has taken over.
|
||||
*
|
||||
* Responder being released:
|
||||
*
|
||||
* As soon as there are no more active pointers that *started* inside descendants
|
||||
* of the *current* "responder", an `onResponderRelease` event is dispatched to the
|
||||
* current "responder", and the responder lock is released.
|
||||
*
|
||||
* Typical sequence of events:
|
||||
* * startShouldSetResponder
|
||||
* * responderGrant/Reject
|
||||
* * responderStart
|
||||
* * responderMove
|
||||
* * responderEnd
|
||||
* * responderRelease
|
||||
*/
|
||||
|
||||
/* Negotiation Performed
|
||||
+-----------------------+
|
||||
/ \
|
||||
Process low level events to + Current Responder + wantsResponderID
|
||||
determine who to perform negot-| (if any exists at all) |
|
||||
iation/transition | Otherwise just pass through|
|
||||
-------------------------------+----------------------------+------------------+
|
||||
Bubble to find first ID | |
|
||||
to return true:wantsResponderID| |
|
||||
| |
|
||||
+--------------+ | |
|
||||
| onTouchStart | | |
|
||||
+------+-------+ none | |
|
||||
| return| |
|
||||
+-----------v-------------+true| +------------------------+ |
|
||||
|onStartShouldSetResponder|----->| onResponderStart (cur) |<-----------+
|
||||
+-----------+-------------+ | +------------------------+ | |
|
||||
| | | +--------+-------+
|
||||
| returned true for| false:REJECT +-------->|onResponderReject
|
||||
| wantsResponderID | | | +----------------+
|
||||
| (now attempt | +------------------+-----+ |
|
||||
| handoff) | | onResponder | |
|
||||
+------------------->| TerminationRequest | |
|
||||
| +------------------+-----+ |
|
||||
| | | +----------------+
|
||||
| true:GRANT +-------->|onResponderGrant|
|
||||
| | +--------+-------+
|
||||
| +------------------------+ | |
|
||||
| | onResponderTerminate |<-----------+
|
||||
| +------------------+-----+ |
|
||||
| | | +----------------+
|
||||
| +-------->|onResponderStart|
|
||||
| | +----------------+
|
||||
Bubble to find first ID | |
|
||||
to return true:wantsResponderID| |
|
||||
| |
|
||||
+-------------+ | |
|
||||
| onTouchMove | | |
|
||||
+------+------+ none | |
|
||||
| return| |
|
||||
+-----------v-------------+true| +------------------------+ |
|
||||
|onMoveShouldSetResponder |----->| onResponderMove (cur) |<-----------+
|
||||
+-----------+-------------+ | +------------------------+ | |
|
||||
| | | +--------+-------+
|
||||
| returned true for| false:REJECT +-------->|onResponderReject
|
||||
| wantsResponderID | | | +----------------+
|
||||
| (now attempt | +------------------+-----+ |
|
||||
| handoff) | | onResponder | |
|
||||
+------------------->| TerminationRequest| |
|
||||
| +------------------+-----+ |
|
||||
| | | +----------------+
|
||||
| true:GRANT +-------->|onResponderGrant|
|
||||
| | +--------+-------+
|
||||
| +------------------------+ | |
|
||||
| | onResponderTerminate |<-----------+
|
||||
| +------------------+-----+ |
|
||||
| | | +----------------+
|
||||
| +-------->|onResponderMove |
|
||||
| | +----------------+
|
||||
| |
|
||||
| |
|
||||
Some active touch started| |
|
||||
inside current responder | +------------------------+ |
|
||||
+------------------------->| onResponderEnd | |
|
||||
| | +------------------------+ |
|
||||
+---+---------+ | |
|
||||
| onTouchEnd | | |
|
||||
+---+---------+ | |
|
||||
| | +------------------------+ |
|
||||
+------------------------->| onResponderEnd | |
|
||||
No active touches started| +-----------+------------+ |
|
||||
inside current responder | | |
|
||||
| v |
|
||||
| +------------------------+ |
|
||||
| | onResponderRelease | |
|
||||
| +------------------------+ |
|
||||
| |
|
||||
+ + */
|
||||
|
||||
import type { ResponderEvent } from './createResponderEvent';
|
||||
|
||||
import createResponderEvent from './createResponderEvent';
|
||||
import {
|
||||
isCancelish,
|
||||
isEndish,
|
||||
isMoveish,
|
||||
isScroll,
|
||||
isSelectionChange,
|
||||
isStartish
|
||||
} from './ResponderEventTypes';
|
||||
import {
|
||||
getLowestCommonAncestor,
|
||||
getResponderPaths,
|
||||
hasTargetTouches,
|
||||
hasValidSelection,
|
||||
isPrimaryPointerDown,
|
||||
setResponderId
|
||||
} from './utils';
|
||||
import ResponderTouchHistoryStore from './ResponderTouchHistoryStore';
|
||||
|
||||
/* ------------ TYPES ------------ */
|
||||
|
||||
type ResponderId = number;
|
||||
|
||||
type ActiveResponderInstance = {
|
||||
id: ResponderId,
|
||||
idPath: Array<number>,
|
||||
node: any
|
||||
};
|
||||
|
||||
type EmptyResponderInstance = {
|
||||
id: null,
|
||||
idPath: null,
|
||||
node: null
|
||||
};
|
||||
|
||||
type ResponderInstance = ActiveResponderInstance | EmptyResponderInstance;
|
||||
|
||||
export type ResponderConfig = {
|
||||
// Direct responder events dispatched directly to responder. Do not bubble.
|
||||
onResponderEnd?: ?(e: ResponderEvent) => void,
|
||||
onResponderGrant?: ?(e: ResponderEvent) => void,
|
||||
onResponderMove?: ?(e: ResponderEvent) => void,
|
||||
onResponderRelease?: ?(e: ResponderEvent) => void,
|
||||
onResponderReject?: ?(e: ResponderEvent) => void,
|
||||
onResponderStart?: ?(e: ResponderEvent) => void,
|
||||
onResponderTerminate?: ?(e: ResponderEvent) => void,
|
||||
onResponderTerminationRequest?: ?(e: ResponderEvent) => boolean,
|
||||
// On pointer down, should this element become the responder?
|
||||
onStartShouldSetResponder?: ?(e: ResponderEvent) => boolean,
|
||||
onStartShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean,
|
||||
// On pointer move, should this element become the responder?
|
||||
onMoveShouldSetResponder?: ?(e: ResponderEvent) => boolean,
|
||||
onMoveShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean,
|
||||
// On scroll, should this element become the responder? Do no bubble
|
||||
onScrollShouldSetResponder?: ?(e: ResponderEvent) => boolean,
|
||||
onScrollShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean,
|
||||
// On text selection change, should this element become the responder?
|
||||
onSelectionChangeShouldSetResponder?: ?(e: ResponderEvent) => boolean,
|
||||
onSelectionChangeShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean
|
||||
};
|
||||
|
||||
const emptyObject = {};
|
||||
|
||||
/* ------------ IMPLEMENTATION ------------ */
|
||||
|
||||
const startRegistration = [
|
||||
'onStartShouldSetResponderCapture',
|
||||
'onStartShouldSetResponder',
|
||||
{ bubbles: true }
|
||||
];
|
||||
const moveRegistration = [
|
||||
'onMoveShouldSetResponderCapture',
|
||||
'onMoveShouldSetResponder',
|
||||
{ bubbles: true }
|
||||
];
|
||||
const scrollRegistration = [
|
||||
'onScrollShouldSetResponderCapture',
|
||||
'onScrollShouldSetResponder',
|
||||
{ bubbles: false }
|
||||
];
|
||||
const shouldSetResponderEvents = {
|
||||
touchstart: startRegistration,
|
||||
mousedown: startRegistration,
|
||||
touchmove: moveRegistration,
|
||||
mousemove: moveRegistration,
|
||||
scroll: scrollRegistration
|
||||
};
|
||||
|
||||
const emptyResponder = { id: null, idPath: null, node: null };
|
||||
const responderListenersMap = new Map();
|
||||
|
||||
let isEmulatingMouseEvents = false;
|
||||
let trackedTouchCount = 0;
|
||||
let currentResponder: ResponderInstance = {
|
||||
id: null,
|
||||
node: null,
|
||||
idPath: null
|
||||
};
|
||||
|
||||
function changeCurrentResponder(responder: ResponderInstance) {
|
||||
currentResponder = responder;
|
||||
}
|
||||
|
||||
function getResponderConfig(id: ResponderId): ResponderConfig | Object {
|
||||
const config = responderListenersMap.get(id);
|
||||
return config != null ? config : emptyObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process native events
|
||||
*
|
||||
* A single event listener is used to manage the responder system.
|
||||
* All pointers are tracked in the ResponderTouchHistoryStore. Native events
|
||||
* are interpreted in terms of the Responder System and checked to see if
|
||||
* the responder should be transferred. Each host node that is attached to
|
||||
* the Responder System has an ID, which is used to look up its associated
|
||||
* callbacks.
|
||||
*/
|
||||
function eventListener(domEvent: any) {
|
||||
const eventType = domEvent.type;
|
||||
const eventTarget = domEvent.target;
|
||||
|
||||
/**
|
||||
* Manage emulated events and early bailout.
|
||||
* Since PointerEvent is not used yet (lack of support in older Safari), it's
|
||||
* necessary to manually manage the mess of browser touch/mouse events.
|
||||
* And bailout early for termination events when there is no active responder.
|
||||
*/
|
||||
|
||||
// Flag when browser may produce emulated events
|
||||
if (eventType === 'touchstart') {
|
||||
isEmulatingMouseEvents = true;
|
||||
}
|
||||
// Remove flag when browser will not produce emulated events
|
||||
if (eventType === 'touchmove' || trackedTouchCount > 1) {
|
||||
isEmulatingMouseEvents = false;
|
||||
}
|
||||
// Ignore various events in particular circumstances
|
||||
if (
|
||||
// Ignore browser emulated mouse events
|
||||
(eventType === 'mousedown' && isEmulatingMouseEvents) ||
|
||||
(eventType === 'mousemove' && isEmulatingMouseEvents) ||
|
||||
// Ignore mousemove if a mousedown didn't occur first
|
||||
(eventType === 'mousemove' && trackedTouchCount < 1)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Remove flag after emulated events are finished
|
||||
if (isEmulatingMouseEvents && eventType === 'mouseup') {
|
||||
if (trackedTouchCount === 0) {
|
||||
isEmulatingMouseEvents = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isStartEvent = isStartish(eventType) && isPrimaryPointerDown(domEvent);
|
||||
const isMoveEvent = isMoveish(eventType);
|
||||
const isEndEvent = isEndish(eventType);
|
||||
const isScrollEvent = isScroll(eventType);
|
||||
const isSelectionChangeEvent = isSelectionChange(eventType);
|
||||
const responderEvent = createResponderEvent(domEvent);
|
||||
|
||||
/**
|
||||
* Record the state of active pointers
|
||||
*/
|
||||
|
||||
if (isStartEvent || isMoveEvent || isEndEvent) {
|
||||
if (domEvent.touches) {
|
||||
trackedTouchCount = domEvent.touches.length;
|
||||
} else {
|
||||
if (isStartEvent) {
|
||||
trackedTouchCount = 1;
|
||||
} else if (isEndEvent) {
|
||||
trackedTouchCount = 0;
|
||||
}
|
||||
}
|
||||
ResponderTouchHistoryStore.recordTouchTrack(eventType, responderEvent.nativeEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Responder System logic
|
||||
*/
|
||||
|
||||
let eventPaths = getResponderPaths(domEvent);
|
||||
let wasNegotiated = false;
|
||||
let wantsResponder;
|
||||
|
||||
// If an event occured that might change the current responder...
|
||||
if (isStartEvent || isMoveEvent || (isScrollEvent && trackedTouchCount > 0)) {
|
||||
// If there is already a responder, prune the event paths to the lowest common ancestor
|
||||
// of the existing responder and deepest target of the event.
|
||||
const currentResponderIdPath = currentResponder.idPath;
|
||||
const eventIdPath = eventPaths.idPath;
|
||||
|
||||
if (currentResponderIdPath != null && eventIdPath != null) {
|
||||
const lowestCommonAncestor = getLowestCommonAncestor(currentResponderIdPath, eventIdPath);
|
||||
const indexOfLowestCommonAncestor = eventIdPath.indexOf(lowestCommonAncestor);
|
||||
// Skip the current responder so it doesn't receive unexpected "shouldSet" events.
|
||||
const index =
|
||||
indexOfLowestCommonAncestor + (lowestCommonAncestor === currentResponder.id ? 1 : 0);
|
||||
eventPaths = {
|
||||
idPath: eventIdPath.slice(index),
|
||||
nodePath: eventPaths.nodePath.slice(index)
|
||||
};
|
||||
}
|
||||
// If a node wants to become the responder, attempt to transfer.
|
||||
wantsResponder = findWantsResponder(eventPaths, domEvent, responderEvent);
|
||||
if (wantsResponder != null) {
|
||||
// Sets responder if none exists, or negotates with existing responder.
|
||||
attemptTransfer(responderEvent, wantsResponder);
|
||||
wasNegotiated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is now a responder, invoke its callbacks for the lifecycle of the gesture.
|
||||
if (currentResponder.id != null && currentResponder.node != null) {
|
||||
const { id, node } = currentResponder;
|
||||
const {
|
||||
onResponderStart,
|
||||
onResponderMove,
|
||||
onResponderEnd,
|
||||
onResponderRelease,
|
||||
onResponderTerminate,
|
||||
onResponderTerminationRequest
|
||||
} = getResponderConfig(id);
|
||||
|
||||
responderEvent.bubbles = false;
|
||||
responderEvent.cancelable = false;
|
||||
responderEvent.currentTarget = node;
|
||||
|
||||
// Start
|
||||
if (isStartEvent) {
|
||||
if (onResponderStart != null) {
|
||||
onResponderStart(responderEvent);
|
||||
}
|
||||
}
|
||||
// Move
|
||||
else if (isMoveEvent) {
|
||||
if (onResponderMove != null) {
|
||||
onResponderMove(responderEvent);
|
||||
}
|
||||
} else {
|
||||
const isTerminateEvent =
|
||||
isCancelish(eventType) ||
|
||||
// native context menu
|
||||
eventType === 'contextmenu' ||
|
||||
// window blur
|
||||
(eventType === 'blur' && eventTarget === window) ||
|
||||
// responder (or ancestors) blur
|
||||
(eventType === 'blur' && (eventTarget.contains(node) && domEvent.relatedTarget !== node)) ||
|
||||
// native scroll without using a pointer
|
||||
(isScrollEvent && trackedTouchCount === 0) ||
|
||||
// native scroll on node that is parent of the responder (allow siblings to scroll)
|
||||
(isScrollEvent && eventTarget.contains(node) && eventTarget !== node) ||
|
||||
// native select/selectionchange on node
|
||||
(isSelectionChangeEvent && hasValidSelection(domEvent));
|
||||
|
||||
const isReleaseEvent =
|
||||
isEndEvent && !isTerminateEvent && !hasTargetTouches(node, domEvent.touches);
|
||||
|
||||
// End
|
||||
if (isEndEvent) {
|
||||
if (onResponderEnd != null) {
|
||||
onResponderEnd(responderEvent);
|
||||
}
|
||||
}
|
||||
// Release
|
||||
if (isReleaseEvent) {
|
||||
if (onResponderRelease != null) {
|
||||
onResponderRelease(responderEvent);
|
||||
}
|
||||
changeCurrentResponder(emptyResponder);
|
||||
}
|
||||
// Terminate
|
||||
if (isTerminateEvent) {
|
||||
let shouldTerminate = true;
|
||||
|
||||
// Responders can still avoid termination but only for these events.
|
||||
if (
|
||||
eventType === 'contextmenu' ||
|
||||
eventType === 'scroll' ||
|
||||
eventType === 'selectionchange'
|
||||
) {
|
||||
if (
|
||||
wasNegotiated ||
|
||||
// Only call this function is it wasn't already called during negotiation.
|
||||
(onResponderTerminationRequest != null &&
|
||||
onResponderTerminationRequest(responderEvent) === false)
|
||||
) {
|
||||
shouldTerminate = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldTerminate) {
|
||||
if (onResponderTerminate != null) {
|
||||
onResponderTerminate(responderEvent);
|
||||
}
|
||||
changeCurrentResponder(emptyResponder);
|
||||
isEmulatingMouseEvents = false;
|
||||
trackedTouchCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the event path to/from the target node. At each node, stop and call the
|
||||
* relevant "shouldSet" functions for the given event type. If any of those functions
|
||||
* call "stopPropagation" on the event, stop searching for a responder.
|
||||
*/
|
||||
function findWantsResponder(eventPaths, domEvent, responderEvent) {
|
||||
const shouldSetCallbacks = shouldSetResponderEvents[(domEvent.type: any)]; // for Flow
|
||||
|
||||
if (shouldSetCallbacks != null) {
|
||||
const { idPath, nodePath } = eventPaths;
|
||||
|
||||
const shouldSetCallbackCaptureName = shouldSetCallbacks[0];
|
||||
const shouldSetCallbackBubbleName = shouldSetCallbacks[1];
|
||||
const { bubbles } = shouldSetCallbacks[2];
|
||||
|
||||
const check = function(id, node, callbackName) {
|
||||
const config = getResponderConfig(id);
|
||||
const shouldSetCallback = config[callbackName];
|
||||
if (shouldSetCallback != null) {
|
||||
if (shouldSetCallback(responderEvent) === true) {
|
||||
return { id, node, idPath };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// capture
|
||||
for (let i = idPath.length - 1; i >= 0; i--) {
|
||||
const id = idPath[i];
|
||||
const node = nodePath[i];
|
||||
const result = check(id, node, shouldSetCallbackCaptureName);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
if (responderEvent.isPropagationStopped() === true) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// bubble
|
||||
if (bubbles) {
|
||||
for (let i = 0; i < idPath.length; i++) {
|
||||
const id = idPath[i];
|
||||
const node = nodePath[i];
|
||||
const result = check(id, node, shouldSetCallbackBubbleName);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
if (responderEvent.isPropagationStopped() === true) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const id = idPath[0];
|
||||
const node = nodePath[0];
|
||||
const target = domEvent.target;
|
||||
if (target === node) {
|
||||
return check(id, node, shouldSetCallbackBubbleName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to transfer the responder.
|
||||
*/
|
||||
function attemptTransfer(responderEvent: ResponderEvent, wantsResponder: ActiveResponderInstance) {
|
||||
const { id: currentId, node: currentNode } = currentResponder;
|
||||
const { id, node } = wantsResponder;
|
||||
|
||||
const { onResponderGrant, onResponderReject } = getResponderConfig(id);
|
||||
|
||||
responderEvent.bubbles = false;
|
||||
responderEvent.cancelable = false;
|
||||
responderEvent.currentTarget = node;
|
||||
// Set responder
|
||||
if (currentId == null) {
|
||||
if (onResponderGrant != null) {
|
||||
responderEvent.currentTarget = node;
|
||||
responderEvent.dispatchConfig.registrationName = 'onResponderGrant';
|
||||
onResponderGrant(responderEvent);
|
||||
}
|
||||
changeCurrentResponder(wantsResponder);
|
||||
}
|
||||
// Negotiate with current responder
|
||||
else {
|
||||
const { onResponderTerminate, onResponderTerminationRequest } = getResponderConfig(currentId);
|
||||
const allowTransfer =
|
||||
onResponderTerminationRequest != null && onResponderTerminationRequest(responderEvent);
|
||||
if (allowTransfer) {
|
||||
// Terminate existing responder
|
||||
if (onResponderTerminate != null) {
|
||||
responderEvent.currentTarget = currentNode;
|
||||
onResponderTerminate(responderEvent);
|
||||
}
|
||||
// Grant next responder
|
||||
if (onResponderGrant != null) {
|
||||
onResponderGrant(responderEvent);
|
||||
}
|
||||
changeCurrentResponder(wantsResponder);
|
||||
} else {
|
||||
// Reject responder request
|
||||
if (onResponderReject != null) {
|
||||
onResponderReject(responderEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------ PUBLIC API ------------ */
|
||||
|
||||
/**
|
||||
* Attach Listeners
|
||||
*
|
||||
* Use native events as ReactDOM doesn't have a non-plugin API to implement
|
||||
* this system.
|
||||
*/
|
||||
const documentEventsCapturePhase = ['blur', 'scroll'];
|
||||
const documentEventsBubblePhase = [
|
||||
// mouse
|
||||
'mousedown',
|
||||
'mousemove',
|
||||
'mouseup',
|
||||
'dragstart',
|
||||
// touch
|
||||
'touchstart',
|
||||
'touchmove',
|
||||
'touchend',
|
||||
'touchcancel',
|
||||
// other
|
||||
'contextmenu',
|
||||
'select',
|
||||
'selectionchange'
|
||||
];
|
||||
export function attachListeners() {
|
||||
if (window.__reactResponderSystemActive == null) {
|
||||
window.addEventListener('blur', eventListener);
|
||||
documentEventsBubblePhase.forEach(eventType => {
|
||||
document.addEventListener(eventType, eventListener);
|
||||
});
|
||||
documentEventsCapturePhase.forEach(eventType => {
|
||||
document.addEventListener(eventType, eventListener, true);
|
||||
});
|
||||
window.__reactResponderSystemActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a node with the ResponderSystem.
|
||||
*/
|
||||
export function addNode(id: ResponderId, node: any, config: ResponderConfig) {
|
||||
setResponderId(node, id);
|
||||
responderListenersMap.set(id, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a node with the ResponderSystem.
|
||||
*/
|
||||
export function removeNode(id: ResponderId) {
|
||||
if (currentResponder.id === id) {
|
||||
terminateResponder();
|
||||
}
|
||||
if (responderListenersMap.has(id)) {
|
||||
responderListenersMap.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow the current responder to be terminated from within components to support
|
||||
* more complex requirements, such as use with other React libraries for working
|
||||
* with scroll views, input views, etc.
|
||||
*/
|
||||
export function terminateResponder() {
|
||||
const { id, node } = currentResponder;
|
||||
if (id != null && node != null) {
|
||||
const { onResponderTerminate } = getResponderConfig(id);
|
||||
if (onResponderTerminate != null) {
|
||||
const event = createResponderEvent({});
|
||||
event.currentTarget = node;
|
||||
onResponderTerminate(event);
|
||||
}
|
||||
changeCurrentResponder(emptyResponder);
|
||||
}
|
||||
isEmulatingMouseEvents = false;
|
||||
trackedTouchCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow unit tests to inspect the current responder in the system.
|
||||
* FOR TESTING ONLY.
|
||||
*/
|
||||
export function getResponderNode(): any {
|
||||
return currentResponder.node;
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 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 { Touch, TouchEvent } from './ResponderEventTypes';
|
||||
import { isStartish, isMoveish, isEndish } from './ResponderEventTypes';
|
||||
|
||||
type TouchRecord = {|
|
||||
touchActive: boolean,
|
||||
startPageX: number,
|
||||
startPageY: number,
|
||||
startTimeStamp: number,
|
||||
currentPageX: number,
|
||||
currentPageY: number,
|
||||
currentTimeStamp: number,
|
||||
previousPageX: number,
|
||||
previousPageY: number,
|
||||
previousTimeStamp: number
|
||||
|};
|
||||
|
||||
/**
|
||||
* Tracks the position and time of each active touch by `touch.identifier`. We
|
||||
* should typically only see IDs in the range of 1-20 because IDs get recycled
|
||||
* when touches end and start again.
|
||||
*/
|
||||
|
||||
const __DEV__ = process.env.NODE_ENV !== 'production';
|
||||
const MAX_TOUCH_BANK = 20;
|
||||
const touchBank: Array<TouchRecord> = [];
|
||||
const touchHistory = {
|
||||
touchBank,
|
||||
numberActiveTouches: 0,
|
||||
// If there is only one active touch, we remember its location. This prevents
|
||||
// us having to loop through all of the touches all the time in the most
|
||||
// common case.
|
||||
indexOfSingleActiveTouch: -1,
|
||||
mostRecentTimeStamp: 0
|
||||
};
|
||||
|
||||
function timestampForTouch(touch: Touch): number {
|
||||
// The legacy internal implementation provides "timeStamp", which has been
|
||||
// renamed to "timestamp".
|
||||
return (touch: any).timeStamp || touch.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Instead of making gestures recompute filtered velocity, we could
|
||||
* include a built in velocity computation that can be reused globally.
|
||||
*/
|
||||
function createTouchRecord(touch: Touch): TouchRecord {
|
||||
return {
|
||||
touchActive: true,
|
||||
startPageX: touch.pageX,
|
||||
startPageY: touch.pageY,
|
||||
startTimeStamp: timestampForTouch(touch),
|
||||
currentPageX: touch.pageX,
|
||||
currentPageY: touch.pageY,
|
||||
currentTimeStamp: timestampForTouch(touch),
|
||||
previousPageX: touch.pageX,
|
||||
previousPageY: touch.pageY,
|
||||
previousTimeStamp: timestampForTouch(touch)
|
||||
};
|
||||
}
|
||||
|
||||
function resetTouchRecord(touchRecord: TouchRecord, touch: Touch): void {
|
||||
touchRecord.touchActive = true;
|
||||
touchRecord.startPageX = touch.pageX;
|
||||
touchRecord.startPageY = touch.pageY;
|
||||
touchRecord.startTimeStamp = timestampForTouch(touch);
|
||||
touchRecord.currentPageX = touch.pageX;
|
||||
touchRecord.currentPageY = touch.pageY;
|
||||
touchRecord.currentTimeStamp = timestampForTouch(touch);
|
||||
touchRecord.previousPageX = touch.pageX;
|
||||
touchRecord.previousPageY = touch.pageY;
|
||||
touchRecord.previousTimeStamp = timestampForTouch(touch);
|
||||
}
|
||||
|
||||
function getTouchIdentifier({ identifier }: Touch): number {
|
||||
if (identifier == null) {
|
||||
console.error('Touch object is missing identifier.');
|
||||
}
|
||||
// Safari produces very large identifiers that would cause the array length
|
||||
// to be so large as to crash the browser, if not normalized like this.
|
||||
return identifier > MAX_TOUCH_BANK ? identifier % 20 : identifier;
|
||||
}
|
||||
|
||||
function recordTouchStart(touch: Touch): void {
|
||||
const identifier = getTouchIdentifier(touch);
|
||||
const touchRecord = touchBank[identifier];
|
||||
if (touchRecord) {
|
||||
resetTouchRecord(touchRecord, touch);
|
||||
} else {
|
||||
touchBank[identifier] = createTouchRecord(touch);
|
||||
}
|
||||
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
|
||||
}
|
||||
|
||||
function recordTouchMove(touch: Touch): void {
|
||||
const touchRecord = touchBank[getTouchIdentifier(touch)];
|
||||
if (touchRecord) {
|
||||
touchRecord.touchActive = true;
|
||||
touchRecord.previousPageX = touchRecord.currentPageX;
|
||||
touchRecord.previousPageY = touchRecord.currentPageY;
|
||||
touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;
|
||||
touchRecord.currentPageX = touch.pageX;
|
||||
touchRecord.currentPageY = touch.pageY;
|
||||
touchRecord.currentTimeStamp = timestampForTouch(touch);
|
||||
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
|
||||
} else {
|
||||
console.warn(
|
||||
'Cannot record touch move without a touch start.\n',
|
||||
`Touch Move: ${printTouch(touch)}\n`,
|
||||
`Touch Bank: ${printTouchBank()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function recordTouchEnd(touch: Touch): void {
|
||||
const touchRecord = touchBank[getTouchIdentifier(touch)];
|
||||
if (touchRecord) {
|
||||
touchRecord.touchActive = false;
|
||||
touchRecord.previousPageX = touchRecord.currentPageX;
|
||||
touchRecord.previousPageY = touchRecord.currentPageY;
|
||||
touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;
|
||||
touchRecord.currentPageX = touch.pageX;
|
||||
touchRecord.currentPageY = touch.pageY;
|
||||
touchRecord.currentTimeStamp = timestampForTouch(touch);
|
||||
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
|
||||
} else {
|
||||
console.warn(
|
||||
'Cannot record touch end without a touch start.\n',
|
||||
`Touch End: ${printTouch(touch)}\n`,
|
||||
`Touch Bank: ${printTouchBank()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function printTouch(touch: Touch): string {
|
||||
return JSON.stringify({
|
||||
identifier: touch.identifier,
|
||||
pageX: touch.pageX,
|
||||
pageY: touch.pageY,
|
||||
timestamp: timestampForTouch(touch)
|
||||
});
|
||||
}
|
||||
|
||||
function printTouchBank(): string {
|
||||
let printed = JSON.stringify(touchBank.slice(0, MAX_TOUCH_BANK));
|
||||
if (touchBank.length > MAX_TOUCH_BANK) {
|
||||
printed += ' (original size: ' + touchBank.length + ')';
|
||||
}
|
||||
return printed;
|
||||
}
|
||||
|
||||
const ResponderTouchHistoryStore = {
|
||||
recordTouchTrack(topLevelType: string, nativeEvent: TouchEvent): void {
|
||||
if (isMoveish(topLevelType)) {
|
||||
nativeEvent.changedTouches.forEach(recordTouchMove);
|
||||
} else if (isStartish(topLevelType)) {
|
||||
nativeEvent.changedTouches.forEach(recordTouchStart);
|
||||
touchHistory.numberActiveTouches = nativeEvent.touches.length;
|
||||
if (touchHistory.numberActiveTouches === 1) {
|
||||
touchHistory.indexOfSingleActiveTouch = nativeEvent.touches[0].identifier;
|
||||
}
|
||||
} else if (isEndish(topLevelType)) {
|
||||
nativeEvent.changedTouches.forEach(recordTouchEnd);
|
||||
touchHistory.numberActiveTouches = nativeEvent.touches.length;
|
||||
if (touchHistory.numberActiveTouches === 1) {
|
||||
for (let i = 0; i < touchBank.length; i++) {
|
||||
const touchTrackToCheck = touchBank[i];
|
||||
if (touchTrackToCheck != null && touchTrackToCheck.touchActive) {
|
||||
touchHistory.indexOfSingleActiveTouch = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (__DEV__) {
|
||||
const activeRecord = touchBank[touchHistory.indexOfSingleActiveTouch];
|
||||
if (!(activeRecord != null && activeRecord.touchActive)) {
|
||||
console.error('Cannot find single active touch.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
touchHistory
|
||||
};
|
||||
|
||||
export default ResponderTouchHistoryStore;
|
||||
+2495
File diff suppressed because it is too large
Load Diff
+182
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Copyright (c) Nicolas Gallagher
|
||||
*
|
||||
* 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 { TouchEvent } from './ResponderEventTypes';
|
||||
|
||||
import getBoundingClientRect from '../../modules/getBoundingClientRect';
|
||||
import ResponderTouchHistoryStore from './ResponderTouchHistoryStore';
|
||||
|
||||
export type ResponderEvent = {|
|
||||
bubbles: boolean,
|
||||
cancelable: boolean,
|
||||
currentTarget: any,
|
||||
defaultPrevented: ?boolean,
|
||||
dispatchConfig: {
|
||||
registrationName?: string,
|
||||
phasedRegistrationNames?: {
|
||||
bubbled: string,
|
||||
captured: string
|
||||
}
|
||||
},
|
||||
eventPhase: ?number,
|
||||
isDefaultPrevented: () => boolean,
|
||||
isPropagationStopped: () => boolean,
|
||||
isTrusted: ?boolean,
|
||||
preventDefault: () => void,
|
||||
stopPropagation: () => void,
|
||||
nativeEvent: TouchEvent,
|
||||
persist: () => void,
|
||||
target: ?any,
|
||||
timeStamp: number,
|
||||
touchHistory: $ReadOnly<{|
|
||||
indexOfSingleActiveTouch: number,
|
||||
mostRecentTimeStamp: number,
|
||||
numberActiveTouches: number,
|
||||
touchBank: Array<{|
|
||||
currentPageX: number,
|
||||
currentPageY: number,
|
||||
currentTimeStamp: number,
|
||||
previousPageX: number,
|
||||
previousPageY: number,
|
||||
previousTimeStamp: number,
|
||||
startPageX: number,
|
||||
startPageY: number,
|
||||
startTimeStamp: number,
|
||||
touchActive: boolean
|
||||
|}>
|
||||
|}>
|
||||
|};
|
||||
|
||||
const emptyFunction = () => {};
|
||||
const emptyObject = {};
|
||||
const emptyArray = [];
|
||||
|
||||
/**
|
||||
* Converts a native DOM event to a ResponderEvent.
|
||||
* Mouse events are transformed into fake touch events.
|
||||
*/
|
||||
export default function createResponderEvent(domEvent: any): ResponderEvent {
|
||||
let rect;
|
||||
let propagationWasStopped = false;
|
||||
let changedTouches;
|
||||
let touches;
|
||||
|
||||
const domEventChangedTouches = domEvent.changedTouches;
|
||||
const domEventType = domEvent.type;
|
||||
|
||||
const metaKey = domEvent.metaKey === true;
|
||||
const shiftKey = domEvent.shiftKey === true;
|
||||
const force = (domEventChangedTouches && domEventChangedTouches[0].force) || 0;
|
||||
const identifier = (domEventChangedTouches && domEventChangedTouches[0].identifier) || 0;
|
||||
const clientX = (domEventChangedTouches && domEventChangedTouches[0].clientX) || domEvent.clientX;
|
||||
const clientY = (domEventChangedTouches && domEventChangedTouches[0].clientY) || domEvent.clientY;
|
||||
const pageX = (domEventChangedTouches && domEventChangedTouches[0].pageX) || domEvent.pageX;
|
||||
const pageY = (domEventChangedTouches && domEventChangedTouches[0].pageY) || domEvent.pageY;
|
||||
const preventDefault =
|
||||
typeof domEvent.preventDefault === 'function'
|
||||
? domEvent.preventDefault.bind(domEvent)
|
||||
: emptyFunction;
|
||||
const timestamp = domEvent.timeStamp;
|
||||
|
||||
function normalizeTouches(touches) {
|
||||
return Array.prototype.slice.call(touches).map(touch => {
|
||||
touch.timestamp = timestamp;
|
||||
return touch;
|
||||
});
|
||||
}
|
||||
|
||||
if (domEventChangedTouches != null) {
|
||||
changedTouches = normalizeTouches(domEventChangedTouches);
|
||||
touches = normalizeTouches(domEvent.touches);
|
||||
} else {
|
||||
const emulatedTouches = [
|
||||
{
|
||||
force,
|
||||
identifier,
|
||||
get locationX() {
|
||||
return locationX();
|
||||
},
|
||||
get locationY() {
|
||||
return locationY();
|
||||
},
|
||||
pageX,
|
||||
pageY,
|
||||
target: domEvent.target,
|
||||
timestamp
|
||||
}
|
||||
];
|
||||
changedTouches = emulatedTouches;
|
||||
touches =
|
||||
domEventType === 'mouseup' || domEventType === 'dragstart' ? emptyArray : emulatedTouches;
|
||||
}
|
||||
|
||||
const responderEvent = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
// `currentTarget` is set before dispatch
|
||||
currentTarget: null,
|
||||
defaultPrevented: domEvent.defaultPrevented,
|
||||
dispatchConfig: emptyObject,
|
||||
eventPhase: domEvent.eventPhase,
|
||||
isDefaultPrevented() {
|
||||
return domEvent.defaultPrevented;
|
||||
},
|
||||
isPropagationStopped() {
|
||||
return propagationWasStopped;
|
||||
},
|
||||
isTrusted: domEvent.isTrusted,
|
||||
nativeEvent: {
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey,
|
||||
shiftKey,
|
||||
changedTouches,
|
||||
force,
|
||||
identifier,
|
||||
get locationX() {
|
||||
return locationX();
|
||||
},
|
||||
get locationY() {
|
||||
return locationY();
|
||||
},
|
||||
pageX,
|
||||
pageY,
|
||||
target: domEvent.target,
|
||||
timestamp,
|
||||
touches,
|
||||
type: domEventType
|
||||
},
|
||||
persist: emptyFunction,
|
||||
preventDefault,
|
||||
stopPropagation() {
|
||||
propagationWasStopped = true;
|
||||
},
|
||||
target: domEvent.target,
|
||||
timeStamp: timestamp,
|
||||
touchHistory: ResponderTouchHistoryStore.touchHistory
|
||||
};
|
||||
|
||||
// Using getters and functions serves two purposes:
|
||||
// 1) The value of `currentTarget` is not initially available.
|
||||
// 2) Measuring the clientRect may cause layout jank and should only be done on-demand.
|
||||
function locationX() {
|
||||
rect = rect || getBoundingClientRect(responderEvent.currentTarget);
|
||||
if (rect) {
|
||||
return clientX - rect.left;
|
||||
}
|
||||
}
|
||||
function locationY() {
|
||||
rect = rect || getBoundingClientRect(responderEvent.currentTarget);
|
||||
if (rect) {
|
||||
return clientY - rect.top;
|
||||
}
|
||||
}
|
||||
|
||||
return responderEvent;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) Nicolas Gallagher
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
/**
|
||||
* Hook for integrating the Responder System into React
|
||||
*
|
||||
* function SomeComponent({ onStartShouldSetResponder }) {
|
||||
* const ref = useRef(null);
|
||||
* useResponderEvents(ref, { onStartShouldSetResponder });
|
||||
* return <div ref={ref} />
|
||||
* }
|
||||
*/
|
||||
|
||||
import type { ResponderConfig } from './ResponderSystem';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as ResponderSystem from './ResponderSystem';
|
||||
|
||||
const emptyObject = {};
|
||||
let idCounter = 0;
|
||||
|
||||
function useStable<T>(getInitialValue: () => T): T {
|
||||
const ref = React.useRef<T | null>(null);
|
||||
if (ref.current == null) {
|
||||
ref.current = getInitialValue();
|
||||
}
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
export default function useResponderEvents(hostRef: any, config: ResponderConfig = emptyObject) {
|
||||
const id = useStable(() => idCounter++);
|
||||
const isAttachedRef = React.useRef(false);
|
||||
|
||||
// This is a separate effects so it doesn't run when the config changes.
|
||||
// On initial mount, attach global listeners if needed.
|
||||
// On unmount, remove node potentially attached to the Responder System.
|
||||
React.useEffect(() => {
|
||||
ResponderSystem.attachListeners();
|
||||
return () => {
|
||||
ResponderSystem.removeNode(id);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
// Register and unregister with the Responder System as necessary
|
||||
React.useEffect(() => {
|
||||
const {
|
||||
onMoveShouldSetResponder,
|
||||
onMoveShouldSetResponderCapture,
|
||||
onScrollShouldSetResponder,
|
||||
onScrollShouldSetResponderCapture,
|
||||
onSelectionChangeShouldSetResponder,
|
||||
onSelectionChangeShouldSetResponderCapture,
|
||||
onStartShouldSetResponder,
|
||||
onStartShouldSetResponderCapture
|
||||
} = config;
|
||||
|
||||
const requiresResponderSystem =
|
||||
onMoveShouldSetResponder != null ||
|
||||
onMoveShouldSetResponderCapture != null ||
|
||||
onScrollShouldSetResponder != null ||
|
||||
onScrollShouldSetResponderCapture != null ||
|
||||
onSelectionChangeShouldSetResponder != null ||
|
||||
onSelectionChangeShouldSetResponderCapture != null ||
|
||||
onStartShouldSetResponder != null ||
|
||||
onStartShouldSetResponderCapture != null;
|
||||
|
||||
const node = hostRef.current;
|
||||
|
||||
if (requiresResponderSystem) {
|
||||
ResponderSystem.addNode(id, node, config);
|
||||
isAttachedRef.current = true;
|
||||
} else if (isAttachedRef.current) {
|
||||
ResponderSystem.removeNode(id);
|
||||
isAttachedRef.current = false;
|
||||
}
|
||||
}, [config, hostRef, id]);
|
||||
|
||||
React.useDebugValue({ isResponder: hostRef.current === ResponderSystem.getResponderNode() });
|
||||
React.useDebugValue(config);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Copyright (c) Nicolas Gallagher
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import isSelectionValid from '../../modules/isSelectionValid';
|
||||
|
||||
const keyName = '__reactResponderId';
|
||||
|
||||
function getEventPath(domEvent: any): Array<any> {
|
||||
// The 'selectionchange' event always has the 'document' as the target.
|
||||
// Use the anchor node as the initial target to reconstruct a path.
|
||||
// (We actually only need the first "responder" node in practice.)
|
||||
if (domEvent.type === 'selectionchange') {
|
||||
const target = window.getSelection().anchorNode;
|
||||
return composedPathFallback(target);
|
||||
} else {
|
||||
const path =
|
||||
domEvent.composedPath != null
|
||||
? domEvent.composedPath()
|
||||
: composedPathFallback(domEvent.target);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
function composedPathFallback(target: any): Array<any> {
|
||||
const path = [];
|
||||
while (target != null && target !== document.body) {
|
||||
path.push(target);
|
||||
target = target.parentNode;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the responderId from a host node
|
||||
*/
|
||||
function getResponderId(node: any): ?number {
|
||||
if (node != null) {
|
||||
return node[keyName];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the responderId on a host node
|
||||
*/
|
||||
export function setResponderId(node: any, id: number) {
|
||||
if (node != null) {
|
||||
node[keyName] = id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the event path to contain only the nodes attached to the responder system
|
||||
*/
|
||||
export function getResponderPaths(
|
||||
domEvent: any
|
||||
): {| idPath: Array<number>, nodePath: Array<any> |} {
|
||||
const idPath = [];
|
||||
const nodePath = [];
|
||||
const eventPath = getEventPath(domEvent);
|
||||
for (let i = 0; i < eventPath.length; i++) {
|
||||
const node = eventPath[i];
|
||||
const id = getResponderId(node);
|
||||
if (id != null) {
|
||||
idPath.push(id);
|
||||
nodePath.push(node);
|
||||
}
|
||||
}
|
||||
return { idPath, nodePath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the paths and find the first common ancestor
|
||||
*/
|
||||
export function getLowestCommonAncestor(pathA: Array<any>, pathB: Array<any>) {
|
||||
let pathALength = pathA.length;
|
||||
let pathBLength = pathB.length;
|
||||
if (
|
||||
// If either path is empty
|
||||
pathALength === 0 ||
|
||||
pathBLength === 0 ||
|
||||
// If the last elements aren't the same there can't be a common ancestor
|
||||
// that is connected to the responder system
|
||||
pathA[pathALength - 1] !== pathB[pathBLength - 1]
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let itemA = pathA[0];
|
||||
let indexA = 0;
|
||||
let itemB = pathB[0];
|
||||
let indexB = 0;
|
||||
|
||||
// If A is deeper, skip indices that can't match.
|
||||
if (pathALength - pathBLength > 0) {
|
||||
indexA = pathALength - pathBLength;
|
||||
itemA = pathA[indexA];
|
||||
pathALength = pathBLength;
|
||||
}
|
||||
|
||||
// If B is deeper, skip indices that can't match
|
||||
if (pathBLength - pathALength > 0) {
|
||||
indexB = pathBLength - pathALength;
|
||||
itemB = pathB[indexB];
|
||||
pathBLength = pathALength;
|
||||
}
|
||||
|
||||
// Walk in lockstep until a match is found
|
||||
let depth = pathALength;
|
||||
while (depth--) {
|
||||
if (itemA === itemB) {
|
||||
return itemA;
|
||||
}
|
||||
itemA = pathA[indexA++];
|
||||
itemB = pathB[indexB++];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether any of the active touches are within the current responder.
|
||||
* This cannot rely on W3C `targetTouches`, as neither IE11 nor Safari implement it.
|
||||
*/
|
||||
export function hasTargetTouches(target: any, touches: any): boolean {
|
||||
if (!touches || touches.length === 0) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < touches.length; i++) {
|
||||
const node = touches[i].target;
|
||||
if (node != null) {
|
||||
if (target.contains(node)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore 'selectionchange' events that don't correspond with a person's intent to
|
||||
* select text.
|
||||
*/
|
||||
export function hasValidSelection(domEvent: any) {
|
||||
if (domEvent.type === 'selectionchange') {
|
||||
return isSelectionValid();
|
||||
}
|
||||
return domEvent.type === 'select';
|
||||
}
|
||||
|
||||
/**
|
||||
* Events are only valid if the primary button was used without specific modifier keys.
|
||||
*/
|
||||
export function isPrimaryPointerDown(domEvent: any): boolean {
|
||||
const { altKey, button, buttons, ctrlKey, type } = domEvent;
|
||||
const isTouch = type === 'touchstart' || type === 'touchmove';
|
||||
const isPrimaryMouseDown = type === 'mousedown' && (button === 0 || buttons === 1);
|
||||
const isPrimaryMouseMove = type === 'mousemove' && buttons === 1;
|
||||
const noModifiers = altKey === false && ctrlKey === false;
|
||||
|
||||
if (isTouch || (isPrimaryMouseDown && noModifiers) || (isPrimaryMouseMove && noModifiers)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Nicolas Gallagher.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @noflow
|
||||
*/
|
||||
|
||||
// based on https://github.com/facebook/react/pull/4303/files
|
||||
|
||||
import normalizeNativeEvent from '../normalizeNativeEvent';
|
||||
import ReactDOMUnstableNativeDependencies from 'react-dom/unstable-native-dependencies';
|
||||
|
||||
const { ResponderEventPlugin, ResponderTouchHistoryStore } = ReactDOMUnstableNativeDependencies;
|
||||
|
||||
// On older versions of React (< 16.4) we have to inject the dependencies in
|
||||
// order for the plugin to work properly in the browser. This version still
|
||||
// uses `top*` strings to identify the internal event names.
|
||||
// https://github.com/facebook/react/pull/12629
|
||||
if (!ResponderEventPlugin.eventTypes.responderMove.dependencies) {
|
||||
const topMouseDown = 'topMouseDown';
|
||||
const topMouseMove = 'topMouseMove';
|
||||
const topMouseUp = 'topMouseUp';
|
||||
const topScroll = 'topScroll';
|
||||
const topSelectionChange = 'topSelectionChange';
|
||||
const topTouchCancel = 'topTouchCancel';
|
||||
const topTouchEnd = 'topTouchEnd';
|
||||
const topTouchMove = 'topTouchMove';
|
||||
const topTouchStart = 'topTouchStart';
|
||||
|
||||
const endDependencies = [topTouchCancel, topTouchEnd, topMouseUp];
|
||||
const moveDependencies = [topTouchMove, topMouseMove];
|
||||
const startDependencies = [topTouchStart, topMouseDown];
|
||||
|
||||
/**
|
||||
* Setup ResponderEventPlugin dependencies
|
||||
*/
|
||||
ResponderEventPlugin.eventTypes.responderMove.dependencies = moveDependencies;
|
||||
ResponderEventPlugin.eventTypes.responderEnd.dependencies = endDependencies;
|
||||
ResponderEventPlugin.eventTypes.responderStart.dependencies = startDependencies;
|
||||
ResponderEventPlugin.eventTypes.responderRelease.dependencies = endDependencies;
|
||||
ResponderEventPlugin.eventTypes.responderTerminationRequest.dependencies = [];
|
||||
ResponderEventPlugin.eventTypes.responderGrant.dependencies = [];
|
||||
ResponderEventPlugin.eventTypes.responderReject.dependencies = [];
|
||||
ResponderEventPlugin.eventTypes.responderTerminate.dependencies = [];
|
||||
ResponderEventPlugin.eventTypes.moveShouldSetResponder.dependencies = moveDependencies;
|
||||
ResponderEventPlugin.eventTypes.selectionChangeShouldSetResponder.dependencies = [
|
||||
topSelectionChange
|
||||
];
|
||||
ResponderEventPlugin.eventTypes.scrollShouldSetResponder.dependencies = [topScroll];
|
||||
ResponderEventPlugin.eventTypes.startShouldSetResponder.dependencies = startDependencies;
|
||||
}
|
||||
|
||||
let lastActiveTouchTimestamp = null;
|
||||
// The length of time after a touch that we ignore the browser's emulated mouse events
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Using_Touch_Events
|
||||
const EMULATED_MOUSE_THERSHOLD_MS = 1000;
|
||||
|
||||
const originalExtractEvents = ResponderEventPlugin.extractEvents;
|
||||
ResponderEventPlugin.extractEvents = (topLevelType, targetInst, nativeEvent, nativeEventTarget) => {
|
||||
const hasActiveTouches = ResponderTouchHistoryStore.touchHistory.numberActiveTouches > 0;
|
||||
const eventType = nativeEvent.type;
|
||||
|
||||
let shouldSkipMouseAfterTouch = false;
|
||||
if (eventType.indexOf('touch') > -1) {
|
||||
lastActiveTouchTimestamp = Date.now();
|
||||
} else if (lastActiveTouchTimestamp && eventType.indexOf('mouse') > -1) {
|
||||
const now = Date.now();
|
||||
shouldSkipMouseAfterTouch = now - lastActiveTouchTimestamp < EMULATED_MOUSE_THERSHOLD_MS;
|
||||
}
|
||||
|
||||
if (
|
||||
// Filter out mousemove and mouseup events when a touch hasn't started yet
|
||||
((eventType === 'mousemove' || eventType === 'mouseup') && !hasActiveTouches) ||
|
||||
// Filter out events from wheel/middle and right click.
|
||||
(nativeEvent.button === 1 || nativeEvent.button === 2) ||
|
||||
// Filter out mouse events that browsers dispatch immediately after touch events end
|
||||
// Prevents the REP from calling handlers twice for touch interactions.
|
||||
// See #802 and #932.
|
||||
shouldSkipMouseAfterTouch
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedEvent = normalizeNativeEvent(nativeEvent);
|
||||
|
||||
return originalExtractEvents.call(
|
||||
ResponderEventPlugin,
|
||||
topLevelType,
|
||||
targetInst,
|
||||
normalizedEvent,
|
||||
nativeEventTarget
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponderEventPlugin;
|
||||
@@ -7,10 +7,8 @@
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
/* global HTMLElement */
|
||||
|
||||
const getBoundingClientRect = (node: HTMLElement) => {
|
||||
if (node) {
|
||||
const getBoundingClientRect = (node: ?HTMLElement) => {
|
||||
if (node != null) {
|
||||
const isElement = node.nodeType === 1; /* Node.ELEMENT_NODE */
|
||||
if (isElement && typeof node.getBoundingClientRect === 'function') {
|
||||
return node.getBoundingClientRect();
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) Nicolas Gallagher
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export default function isSelectionValid() {
|
||||
const selection = window.getSelection();
|
||||
const string = selection.toString();
|
||||
const anchorNode = selection.anchorNode;
|
||||
const focusNode = selection.focusNode;
|
||||
const isTextNode =
|
||||
(anchorNode && anchorNode.nodeType === window.Node.TEXT_NODE) ||
|
||||
(focusNode && focusNode.nodeType === window.Node.TEXT_NODE);
|
||||
return string.length >= 1 && string !== '\n' && isTextNode;
|
||||
}
|
||||
-144
@@ -1,144 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`modules/normalizeNativeEvent mouse events simulated event 1`] = `
|
||||
Object {
|
||||
"_normalized": true,
|
||||
"bubbles": undefined,
|
||||
"cancelable": undefined,
|
||||
"changedTouches": Array [
|
||||
Object {
|
||||
"_normalized": true,
|
||||
"clientX": undefined,
|
||||
"clientY": undefined,
|
||||
"force": undefined,
|
||||
"identifier": 0,
|
||||
"locationX": undefined,
|
||||
"locationY": undefined,
|
||||
"pageX": undefined,
|
||||
"pageY": undefined,
|
||||
"screenX": undefined,
|
||||
"screenY": undefined,
|
||||
"target": undefined,
|
||||
"timestamp": 1496876171255,
|
||||
},
|
||||
],
|
||||
"defaultPrevented": undefined,
|
||||
"identifier": 0,
|
||||
"locationX": undefined,
|
||||
"locationY": undefined,
|
||||
"pageX": undefined,
|
||||
"pageY": undefined,
|
||||
"preventDefault": [Function],
|
||||
"stopImmediatePropagation": [Function],
|
||||
"stopPropagation": [Function],
|
||||
"target": undefined,
|
||||
"timestamp": 1496876171255,
|
||||
"touches": Array [],
|
||||
"type": "mouseup",
|
||||
"which": undefined,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`modules/normalizeNativeEvent mouse events synthetic event 1`] = `
|
||||
Object {
|
||||
"_normalized": true,
|
||||
"bubbles": undefined,
|
||||
"cancelable": undefined,
|
||||
"changedTouches": Array [
|
||||
Object {
|
||||
"_normalized": true,
|
||||
"clientX": 100,
|
||||
"clientY": 100,
|
||||
"force": false,
|
||||
"identifier": 0,
|
||||
"locationX": undefined,
|
||||
"locationY": undefined,
|
||||
"pageX": 300,
|
||||
"pageY": 300,
|
||||
"screenX": 400,
|
||||
"screenY": 400,
|
||||
"target": undefined,
|
||||
"timestamp": 1496876171255,
|
||||
},
|
||||
],
|
||||
"defaultPrevented": undefined,
|
||||
"identifier": 0,
|
||||
"locationX": undefined,
|
||||
"locationY": undefined,
|
||||
"pageX": 300,
|
||||
"pageY": 300,
|
||||
"preventDefault": [Function],
|
||||
"stopImmediatePropagation": [Function],
|
||||
"stopPropagation": [Function],
|
||||
"target": undefined,
|
||||
"timestamp": 1496876171255,
|
||||
"touches": Array [],
|
||||
"type": "mouseup",
|
||||
"which": undefined,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`modules/normalizeNativeEvent touch events simulated event 1`] = `
|
||||
Object {
|
||||
"_normalized": true,
|
||||
"bubbles": undefined,
|
||||
"cancelable": undefined,
|
||||
"changedTouches": Array [],
|
||||
"defaultPrevented": undefined,
|
||||
"identifier": undefined,
|
||||
"locationX": undefined,
|
||||
"locationY": undefined,
|
||||
"pageX": undefined,
|
||||
"pageY": undefined,
|
||||
"preventDefault": [Function],
|
||||
"stopImmediatePropagation": [Function],
|
||||
"stopPropagation": [Function],
|
||||
"target": undefined,
|
||||
"timestamp": 1496876171255,
|
||||
"touches": Array [],
|
||||
"type": "touchstart",
|
||||
"which": undefined,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`modules/normalizeNativeEvent touch events synthetic event 1`] = `
|
||||
Object {
|
||||
"_normalized": true,
|
||||
"bubbles": undefined,
|
||||
"cancelable": undefined,
|
||||
"changedTouches": Array [
|
||||
Object {
|
||||
"_normalized": true,
|
||||
"clientX": 100,
|
||||
"clientY": 100,
|
||||
"force": false,
|
||||
"identifier": undefined,
|
||||
"locationX": undefined,
|
||||
"locationY": undefined,
|
||||
"pageX": 300,
|
||||
"pageY": 300,
|
||||
"radiusX": 10,
|
||||
"radiusY": 10,
|
||||
"rotationAngle": 45,
|
||||
"screenX": 400,
|
||||
"screenY": 400,
|
||||
"target": undefined,
|
||||
"timestamp": 1496876171255,
|
||||
},
|
||||
],
|
||||
"defaultPrevented": undefined,
|
||||
"identifier": undefined,
|
||||
"locationX": undefined,
|
||||
"locationY": undefined,
|
||||
"pageX": 300,
|
||||
"pageY": 300,
|
||||
"preventDefault": [Function],
|
||||
"stopImmediatePropagation": [Function],
|
||||
"stopPropagation": [Function],
|
||||
"target": undefined,
|
||||
"timestamp": 1496876171255,
|
||||
"touches": Array [],
|
||||
"type": "touchstart",
|
||||
"which": undefined,
|
||||
}
|
||||
`;
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
/* eslint-env jasmine, jest */
|
||||
|
||||
import normalizeNativeEvent from '..';
|
||||
|
||||
const normalizeEvent = nativeEvent => {
|
||||
const result = normalizeNativeEvent(nativeEvent);
|
||||
result.timestamp = 1496876171255;
|
||||
if (result.changedTouches && result.changedTouches[0]) {
|
||||
result.changedTouches[0].timestamp = 1496876171255;
|
||||
}
|
||||
if (result.touches && result.touches[0]) {
|
||||
result.touches[0].timestamp = 1496876171255;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
describe('modules/normalizeNativeEvent', () => {
|
||||
describe('mouse events', () => {
|
||||
test('simulated event', () => {
|
||||
const nativeEvent = {
|
||||
type: 'mouseup'
|
||||
};
|
||||
|
||||
const result = normalizeEvent(nativeEvent);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('synthetic event', () => {
|
||||
const nativeEvent = {
|
||||
type: 'mouseup',
|
||||
clientX: 100,
|
||||
clientY: 100,
|
||||
force: false,
|
||||
offsetX: 200,
|
||||
offsetY: 200,
|
||||
pageX: 300,
|
||||
pageY: 300,
|
||||
screenX: 400,
|
||||
screenY: 400
|
||||
};
|
||||
|
||||
const result = normalizeEvent(nativeEvent);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('touch events', () => {
|
||||
test('simulated event', () => {
|
||||
const nativeEvent = {
|
||||
type: 'touchstart'
|
||||
};
|
||||
|
||||
const result = normalizeEvent(nativeEvent);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('synthetic event', () => {
|
||||
const nativeEvent = {
|
||||
type: 'touchstart',
|
||||
changedTouches: [
|
||||
{
|
||||
clientX: 100,
|
||||
clientY: 100,
|
||||
force: false,
|
||||
pageX: 300,
|
||||
pageY: 300,
|
||||
radiusX: 10,
|
||||
radiusY: 10,
|
||||
rotationAngle: 45,
|
||||
screenX: 400,
|
||||
screenY: 400
|
||||
}
|
||||
],
|
||||
pageX: 300,
|
||||
pageY: 300
|
||||
};
|
||||
|
||||
const result = normalizeEvent(nativeEvent);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,190 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Nicolas Gallagher.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import getBoundingClientRect from '../getBoundingClientRect';
|
||||
|
||||
const emptyArray = [];
|
||||
const emptyFunction = () => {};
|
||||
|
||||
// Mobile Safari re-uses touch objects, so we copy the properties we want and normalize the identifier
|
||||
const normalizeTouches = touches => {
|
||||
if (!touches) {
|
||||
return emptyArray;
|
||||
}
|
||||
|
||||
return Array.prototype.slice.call(touches).map(touch => {
|
||||
const identifier = touch.identifier > 20 ? touch.identifier % 20 : touch.identifier;
|
||||
let rect;
|
||||
|
||||
return {
|
||||
_normalized: true,
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
force: touch.force,
|
||||
get locationX() {
|
||||
rect = rect || getBoundingClientRect(touch.target);
|
||||
if (rect) {
|
||||
return touch.pageX - rect.left;
|
||||
}
|
||||
},
|
||||
get locationY() {
|
||||
rect = rect || getBoundingClientRect(touch.target);
|
||||
if (rect) {
|
||||
return touch.pageY - rect.top;
|
||||
}
|
||||
},
|
||||
identifier: identifier,
|
||||
pageX: touch.pageX,
|
||||
pageY: touch.pageY,
|
||||
radiusX: touch.radiusX,
|
||||
radiusY: touch.radiusY,
|
||||
rotationAngle: touch.rotationAngle,
|
||||
screenX: touch.screenX,
|
||||
screenY: touch.screenY,
|
||||
target: touch.target,
|
||||
// normalize the timestamp
|
||||
// https://stackoverflow.com/questions/26177087/ios-8-mobile-safari-wrong-timestamp-on-touch-events
|
||||
timestamp: Date.now()
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function normalizeTouchEvent(nativeEvent) {
|
||||
const changedTouches = normalizeTouches(nativeEvent.changedTouches);
|
||||
const touches = normalizeTouches(nativeEvent.touches);
|
||||
|
||||
const preventDefault =
|
||||
typeof nativeEvent.preventDefault === 'function'
|
||||
? nativeEvent.preventDefault.bind(nativeEvent)
|
||||
: emptyFunction;
|
||||
const stopImmediatePropagation =
|
||||
typeof nativeEvent.stopImmediatePropagation === 'function'
|
||||
? nativeEvent.stopImmediatePropagation.bind(nativeEvent)
|
||||
: emptyFunction;
|
||||
const stopPropagation =
|
||||
typeof nativeEvent.stopPropagation === 'function'
|
||||
? nativeEvent.stopPropagation.bind(nativeEvent)
|
||||
: emptyFunction;
|
||||
const singleChangedTouch = changedTouches[0];
|
||||
|
||||
const event = {
|
||||
_normalized: true,
|
||||
bubbles: nativeEvent.bubbles,
|
||||
cancelable: nativeEvent.cancelable,
|
||||
changedTouches,
|
||||
defaultPrevented: nativeEvent.defaultPrevented,
|
||||
identifier: singleChangedTouch ? singleChangedTouch.identifier : undefined,
|
||||
get locationX() {
|
||||
return singleChangedTouch ? singleChangedTouch.locationX : undefined;
|
||||
},
|
||||
get locationY() {
|
||||
return singleChangedTouch ? singleChangedTouch.locationY : undefined;
|
||||
},
|
||||
pageX: singleChangedTouch ? singleChangedTouch.pageX : nativeEvent.pageX,
|
||||
pageY: singleChangedTouch ? singleChangedTouch.pageY : nativeEvent.pageY,
|
||||
preventDefault,
|
||||
stopImmediatePropagation,
|
||||
stopPropagation,
|
||||
target: nativeEvent.target,
|
||||
// normalize the timestamp
|
||||
// https://stackoverflow.com/questions/26177087/ios-8-mobile-safari-wrong-timestamp-on-touch-events
|
||||
timestamp: Date.now(),
|
||||
touches,
|
||||
type: nativeEvent.type,
|
||||
which: nativeEvent.which
|
||||
};
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
function normalizeMouseEvent(nativeEvent) {
|
||||
let rect;
|
||||
|
||||
const touches = [
|
||||
{
|
||||
_normalized: true,
|
||||
clientX: nativeEvent.clientX,
|
||||
clientY: nativeEvent.clientY,
|
||||
force: nativeEvent.force,
|
||||
identifier: 0,
|
||||
get locationX() {
|
||||
rect = rect || getBoundingClientRect(nativeEvent.target);
|
||||
if (rect) {
|
||||
return nativeEvent.pageX - rect.left;
|
||||
}
|
||||
},
|
||||
get locationY() {
|
||||
rect = rect || getBoundingClientRect(nativeEvent.target);
|
||||
if (rect) {
|
||||
return nativeEvent.pageY - rect.top;
|
||||
}
|
||||
},
|
||||
pageX: nativeEvent.pageX,
|
||||
pageY: nativeEvent.pageY,
|
||||
screenX: nativeEvent.screenX,
|
||||
screenY: nativeEvent.screenY,
|
||||
target: nativeEvent.target,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
];
|
||||
|
||||
const preventDefault =
|
||||
typeof nativeEvent.preventDefault === 'function'
|
||||
? nativeEvent.preventDefault.bind(nativeEvent)
|
||||
: emptyFunction;
|
||||
const stopImmediatePropagation =
|
||||
typeof nativeEvent.stopImmediatePropagation === 'function'
|
||||
? nativeEvent.stopImmediatePropagation.bind(nativeEvent)
|
||||
: emptyFunction;
|
||||
const stopPropagation =
|
||||
typeof nativeEvent.stopPropagation === 'function'
|
||||
? nativeEvent.stopPropagation.bind(nativeEvent)
|
||||
: emptyFunction;
|
||||
|
||||
return {
|
||||
_normalized: true,
|
||||
bubbles: nativeEvent.bubbles,
|
||||
cancelable: nativeEvent.cancelable,
|
||||
changedTouches: touches,
|
||||
defaultPrevented: nativeEvent.defaultPrevented,
|
||||
identifier: touches[0].identifier,
|
||||
get locationX() {
|
||||
return touches[0].locationX;
|
||||
},
|
||||
get locationY() {
|
||||
return touches[0].locationY;
|
||||
},
|
||||
pageX: nativeEvent.pageX,
|
||||
pageY: nativeEvent.pageY,
|
||||
preventDefault,
|
||||
stopImmediatePropagation,
|
||||
stopPropagation,
|
||||
target: nativeEvent.target,
|
||||
timestamp: touches[0].timestamp,
|
||||
touches: nativeEvent.type === 'mouseup' ? emptyArray : touches,
|
||||
type: nativeEvent.type,
|
||||
which: nativeEvent.which
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: how to best handle keyboard events?
|
||||
function normalizeNativeEvent(nativeEvent: Object) {
|
||||
if (!nativeEvent || nativeEvent._normalized) {
|
||||
return nativeEvent;
|
||||
}
|
||||
const eventType = nativeEvent.type || '';
|
||||
const mouse = eventType.indexOf('mouse') >= 0;
|
||||
if (mouse) {
|
||||
return normalizeMouseEvent(nativeEvent);
|
||||
} else {
|
||||
return normalizeTouchEvent(nativeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
export default normalizeNativeEvent;
|
||||
Reference in New Issue
Block a user