Flow type event plugins (#7667)

* Type SimpleEventPlugin and TapEventPlugin

- Renamed file from 'ReactSynteticEvent' to 'ReactSyntheticEventType'
- Fills in the 'any' holes that were left in DispatchConfig type and the
  type annotations in EventPluginRegistry.
- Adds polymorphic PluginModule type and related types
- Uses hack to support indexable properties on 'Touch' type in
  TapEventPlugin

The issue in TapEventPlugin is that the code is accessing one of four
possible properties on the 'Touch' type native event using the bracket
accessor. Classes in Flow don't support using the bracket accessor,
unless you use a declaration and the syntax `[key: Type]: Type`.[1] The
downside of using that here is that we create a global type, which we
may not need in other files.

[1]: https://github.com/facebook/flow/issues/1323

Other options:
- Use looser typing or a '@FixMe' comment and open an issue with Flow to
  support indexing on regular classes.
- Rewrite TapEventPlugin to not use the bracket accessor on 'Touch'. I
  thought the current implementation was elegant and didn't want to
  change it. But we could do something like this:
```
 if (nativeEvent.pageX || nativeEvent.pageY) {
   return axis.page === 'pageX' ? nativeEvent.pageX : nativeEvent.pageY;
 } else {
   var clientAxis = axis.client === 'clientX' ? nativeEvent.clientX : nativeEvent.clientY;
   return nativeEvent[axis.client] + ViewportMetrics[axis.envScroll];
 }
```

(cherry picked from commit 7b2d9655da)
This commit is contained in:
Flarnie Marchan
2016-10-03 17:57:08 -07:00
committed by Paul O’Shannessy
parent 26d060797c
commit c1f0b4e9da
7 changed files with 192 additions and 61 deletions
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule SimpleEventPlugin
* @flow
*/
'use strict';
@@ -30,6 +31,14 @@ var emptyFunction = require('emptyFunction');
var getEventCharCode = require('getEventCharCode');
var invariant = require('invariant');
import type {TopLevelTypes} from 'EventConstants';
import type {
DispatchConfig,
ReactSyntheticEvent,
} from 'ReactSyntheticEventType';
import type {ReactInstance} from 'ReactInstanceType';
import type {PluginModule} from 'PluginModuleType';
/**
* Turns
* ['abort', ...]
@@ -48,8 +57,8 @@ var invariant = require('invariant');
* 'topAbort': { sameConfig }
* };
*/
var eventTypes = {};
var topLevelEventsToDispatchConfig = {};
var eventTypes: {[key: string]: DispatchConfig} = {};
var topLevelEventsToDispatchConfig: {[key: TopLevelTypes]: DispatchConfig} = {};
[
'abort',
'animationEnd',
@@ -131,22 +140,22 @@ var topLevelEventsToDispatchConfig = {};
var onClickListeners = {};
function getDictionaryKey(inst) {
function getDictionaryKey(inst: ReactInstance): string {
// Prevents V8 performance issue:
// https://github.com/facebook/react/pull/7232
return '.' + inst._rootNodeID;
}
var SimpleEventPlugin = {
var SimpleEventPlugin: PluginModule<MouseEvent> = {
eventTypes: eventTypes,
extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget
) {
topLevelType: TopLevelTypes,
targetInst: ReactInstance,
nativeEvent: MouseEvent,
nativeEventTarget: EventTarget,
): null | ReactSyntheticEvent {
var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
if (!dispatchConfig) {
return null;
@@ -268,7 +277,11 @@ var SimpleEventPlugin = {
return event;
},
didPutListener: function(inst, registrationName, listener) {
didPutListener: function(
inst: ReactInstance,
registrationName: string,
listener: () => void,
): void {
// Mobile Safari does not fire properly bubble click events on
// non-interactive elements, which means delegated click listeners do not
// fire. The workaround for this bug involves attaching an empty click
@@ -286,7 +299,10 @@ var SimpleEventPlugin = {
}
},
willDeleteListener: function(inst, registrationName) {
willDeleteListener: function(
inst: ReactInstance,
registrationName: string,
): void {
if (registrationName === 'onClick') {
var key = getDictionaryKey(inst);
onClickListeners[key].remove();
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule TapEventPlugin
* @flow
*/
'use strict';
@@ -20,19 +21,60 @@ var ViewportMetrics = require('ViewportMetrics');
var isStartish = EventPluginUtils.isStartish;
var isEndish = EventPluginUtils.isEndish;
import type {DispatchConfig} from 'ReactSyntheticEventType';
import type {PluginModule} from 'PluginModuleType';
import type {ReactInstance} from 'ReactInstanceType';
import type {TopLevelTypes} from 'EventConstants';
/**
* We are extending the Flow 'Touch' declaration to enable using bracket
* notation to access properties.
* Without this adjustment Flow throws
* "Indexable signature not found in Touch".
* See https://github.com/facebook/flow/issues/1323
*/
type TouchPropertyKey =
'clientX' |
'clientY' |
'pageX' |
'pageY';
declare class _Touch extends Touch {
[key: TouchPropertyKey]: number;
}
type AxisCoordinateData = {
page: TouchPropertyKey,
client: TouchPropertyKey,
envScroll: 'currentPageScrollLeft' | 'currentPageScrollTop',
};
type AxisType = {
x: AxisCoordinateData,
y: AxisCoordinateData,
};
type CoordinatesType = {
x: number,
y: number,
};
/**
* Number of pixels that are tolerated in between a `touchStart` and `touchEnd`
* in order to still be considered a 'tap' event.
*/
var tapMoveThreshold = 10;
var startCoords = {x: null, y: null};
var startCoords: CoordinatesType = {x: 0, y: 0};
var Axis = {
var Axis: AxisType = {
x: {page: 'pageX', client: 'clientX', envScroll: 'currentPageScrollLeft'},
y: {page: 'pageY', client: 'clientY', envScroll: 'currentPageScrollTop'},
};
function getAxisCoordOfEvent(axis, nativeEvent) {
function getAxisCoordOfEvent(
axis: AxisCoordinateData,
nativeEvent: _Touch,
): number {
var singleTouch = TouchEventUtils.extractSingleTouch(nativeEvent);
if (singleTouch) {
return singleTouch[axis.page];
@@ -42,7 +84,10 @@ function getAxisCoordOfEvent(axis, nativeEvent) {
nativeEvent[axis.client] + ViewportMetrics[axis.envScroll];
}
function getDistance(coords, nativeEvent) {
function getDistance(
coords: CoordinatesType,
nativeEvent: _Touch,
): number {
var pageX = getAxisCoordOfEvent(Axis.x, nativeEvent);
var pageY = getAxisCoordOfEvent(Axis.y, nativeEvent);
return Math.pow(
@@ -64,7 +109,7 @@ var dependencies = [
'topMouseUp',
].concat(touchEvents);
var eventTypes = {
var eventTypes: DispatchConfig = {
touchTap: {
phasedRegistrationNames: {
bubbled: 'onTouchTap',
@@ -78,17 +123,17 @@ var usedTouch = false;
var usedTouchTime = 0;
var TOUCH_DELAY = 1000;
var TapEventPlugin = {
var TapEventPlugin: PluginModule<_Touch> = {
tapMoveThreshold: tapMoveThreshold,
eventTypes: eventTypes,
extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget
topLevelType: TopLevelTypes,
targetInst: ReactInstance,
nativeEvent: _Touch,
nativeEventTarget: EventTarget,
) {
if (!isStartish(topLevelType) && !isEndish(topLevelType)) {
return null;
@@ -15,15 +15,15 @@
import type {
DispatchConfig,
ReactSyntheticEvent,
} from 'ReactSyntheticEvent';
} from 'ReactSyntheticEventType';
type PluginName = string;
import type {
AnyNativeEvent,
PluginName,
PluginModule,
} from 'PluginModuleType';
type PluginModule = {
eventTypes: any,
};
type NamesToPlugins = {[key: PluginName]: PluginModule};
type NamesToPlugins = {[key: PluginName]: PluginModule<AnyNativeEvent>};
type EventPluginOrder = null | Array<PluginName>;
@@ -94,7 +94,7 @@ function recomputePluginOrdering(): void {
*/
function publishEventForPlugin(
dispatchConfig: DispatchConfig,
pluginModule: PluginModule,
pluginModule: PluginModule<AnyNativeEvent>,
eventName: string,
): boolean {
invariant(
@@ -139,7 +139,7 @@ function publishEventForPlugin(
*/
function publishRegistrationName(
registrationName: string,
pluginModule: PluginModule,
pluginModule: PluginModule<AnyNativeEvent>,
eventName: string,
): void {
invariant(
@@ -267,22 +267,27 @@ var EventPluginRegistry = {
*/
getPluginModuleForEvent: function(
event: ReactSyntheticEvent,
): null | PluginModule {
): null | PluginModule<AnyNativeEvent> {
var dispatchConfig = event.dispatchConfig;
if (dispatchConfig.registrationName) {
return EventPluginRegistry.registrationNameModules[
dispatchConfig.registrationName
] || null;
}
for (var phase in dispatchConfig.phasedRegistrationNames) {
if (!dispatchConfig.phasedRegistrationNames.hasOwnProperty(phase)) {
continue;
}
var pluginModule = EventPluginRegistry.registrationNameModules[
dispatchConfig.phasedRegistrationNames[phase]
];
if (pluginModule) {
return pluginModule;
if (dispatchConfig.phasedRegistrationNames !== undefined) {
// pulling phasedRegistrationNames out of dispatchConfig helps Flow see
// that it is not undefined.
var {phasedRegistrationNames} = dispatchConfig;
for (var phase in phasedRegistrationNames) {
if (!phasedRegistrationNames.hasOwnProperty(phase)) {
continue;
}
var pluginModule = EventPluginRegistry.registrationNameModules[
phasedRegistrationNames[phase]
];
if (pluginModule) {
return pluginModule;
}
}
}
return null;
@@ -0,0 +1,49 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule PluginModuleType
* @flow
*/
'use strict';
import type {ReactInstance} from 'ReactInstanceType';
import type {
DispatchConfig,
ReactSyntheticEvent,
} from 'ReactSyntheticEventType';
type EventTypes = {[key: string]: DispatchConfig};
export type AnyNativeEvent =
Event |
KeyboardEvent |
MouseEvent |
Touch;
export type PluginName = string;
export type PluginModule<NativeEvent> = {
eventTypes: EventTypes,
extractEvents: (
topLevelType: string,
targetInst: ReactInstance,
nativeTarget: NativeEvent,
nativeEventTarget: EventTarget,
) => null | ReactSyntheticEvent,
didPutListener?: (
inst: ReactInstance,
registrationName: string,
listener: () => void,
) => void,
willDeleteListener?: (
inst: ReactInstance,
registrationName: string,
) => void,
tapMoveThreshold?: number,
};
@@ -1,21 +0,0 @@
/*
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* Flow type for SyntheticEvent class that includes private properties
*
* @providesModule ReactSyntheticEvent
* @flow
*/
'use strict';
export type DispatchConfig = any;
export class ReactSyntheticEvent extends SyntheticEvent {
dispatchConfig: DispatchConfig;
}
@@ -0,0 +1,36 @@
/*
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* Flow type for SyntheticEvent class that includes private properties
*
* @providesModule ReactSyntheticEventType
* @flow
*/
'use strict';
import type {ReactInstance} from 'ReactInstanceType';
export type DispatchConfig = {
dependencies: Array<string>,
phasedRegistrationNames?: {
bubbled: string,
captured: string,
},
registrationName?: string,
};
export type ReactSyntheticEvent = {
dispatchConfig: DispatchConfig;
getPooled: (
dispatchConfig: DispatchConfig,
targetInst: ReactInstance,
nativeTarget: Event,
nativeEventTarget: EventTarget,
) => ReactSyntheticEvent;
} & SyntheticEvent;
@@ -28,6 +28,7 @@ export type ReactInstance = {
detachRef: (ref: string) => void,
getName: () => string,
getPublicInstance: any,
_rootNodeID: number,
// instantiateReactComponent
_mountIndex: number,