From c1f0b4e9da9ec249aeaad9f5ea6c900b27f33c32 Mon Sep 17 00:00:00 2001 From: Flarnie Marchan Date: Wed, 7 Sep 2016 12:40:45 -0700 Subject: [PATCH] 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 7b2d9655da218f8311d1dab4ab1142c35c3eef3b) --- .../client/eventPlugins/SimpleEventPlugin.js | 38 +++++++---- .../dom/client/eventPlugins/TapEventPlugin.js | 65 ++++++++++++++++--- .../shared/stack/event/EventPluginRegistry.js | 43 ++++++------ .../shared/stack/event/PluginModuleType.js | 49 ++++++++++++++ .../shared/stack/event/ReactSyntheticEvent.js | 21 ------ .../stack/event/ReactSyntheticEventType.js | 36 ++++++++++ .../stack/reconciler/ReactInstanceType.js | 1 + 7 files changed, 192 insertions(+), 61 deletions(-) create mode 100644 src/renderers/shared/stack/event/PluginModuleType.js delete mode 100644 src/renderers/shared/stack/event/ReactSyntheticEvent.js create mode 100644 src/renderers/shared/stack/event/ReactSyntheticEventType.js diff --git a/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js b/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js index 304172ebfa..e17d72889d 100644 --- a/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js @@ -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 = { 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(); diff --git a/src/renderers/dom/client/eventPlugins/TapEventPlugin.js b/src/renderers/dom/client/eventPlugins/TapEventPlugin.js index fb6ccc5797..363ecc095b 100644 --- a/src/renderers/dom/client/eventPlugins/TapEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/TapEventPlugin.js @@ -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; diff --git a/src/renderers/shared/stack/event/EventPluginRegistry.js b/src/renderers/shared/stack/event/EventPluginRegistry.js index 6a33b71fd1..f44593eab8 100644 --- a/src/renderers/shared/stack/event/EventPluginRegistry.js +++ b/src/renderers/shared/stack/event/EventPluginRegistry.js @@ -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}; type EventPluginOrder = null | Array; @@ -94,7 +94,7 @@ function recomputePluginOrdering(): void { */ function publishEventForPlugin( dispatchConfig: DispatchConfig, - pluginModule: PluginModule, + pluginModule: PluginModule, eventName: string, ): boolean { invariant( @@ -139,7 +139,7 @@ function publishEventForPlugin( */ function publishRegistrationName( registrationName: string, - pluginModule: PluginModule, + pluginModule: PluginModule, eventName: string, ): void { invariant( @@ -267,22 +267,27 @@ var EventPluginRegistry = { */ getPluginModuleForEvent: function( event: ReactSyntheticEvent, - ): null | PluginModule { + ): null | PluginModule { 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; diff --git a/src/renderers/shared/stack/event/PluginModuleType.js b/src/renderers/shared/stack/event/PluginModuleType.js new file mode 100644 index 0000000000..7f22bacebc --- /dev/null +++ b/src/renderers/shared/stack/event/PluginModuleType.js @@ -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 = { + 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, +}; diff --git a/src/renderers/shared/stack/event/ReactSyntheticEvent.js b/src/renderers/shared/stack/event/ReactSyntheticEvent.js deleted file mode 100644 index 672e137da1..0000000000 --- a/src/renderers/shared/stack/event/ReactSyntheticEvent.js +++ /dev/null @@ -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; -} diff --git a/src/renderers/shared/stack/event/ReactSyntheticEventType.js b/src/renderers/shared/stack/event/ReactSyntheticEventType.js new file mode 100644 index 0000000000..b40c03fd94 --- /dev/null +++ b/src/renderers/shared/stack/event/ReactSyntheticEventType.js @@ -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, + phasedRegistrationNames?: { + bubbled: string, + captured: string, + }, + registrationName?: string, +}; + +export type ReactSyntheticEvent = { + dispatchConfig: DispatchConfig; + getPooled: ( + dispatchConfig: DispatchConfig, + targetInst: ReactInstance, + nativeTarget: Event, + nativeEventTarget: EventTarget, + ) => ReactSyntheticEvent; +} & SyntheticEvent; diff --git a/src/renderers/shared/stack/reconciler/ReactInstanceType.js b/src/renderers/shared/stack/reconciler/ReactInstanceType.js index 15f321b6cb..0336467cb9 100644 --- a/src/renderers/shared/stack/reconciler/ReactInstanceType.js +++ b/src/renderers/shared/stack/reconciler/ReactInstanceType.js @@ -28,6 +28,7 @@ export type ReactInstance = { detachRef: (ref: string) => void, getName: () => string, getPublicInstance: any, + _rootNodeID: number, // instantiateReactComponent _mountIndex: number,