@samitouri / QOS-React-2 / commits / 7b2d9655da

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]; } ```

Flarnie Marchan committed Sep 7, 2016 at 12:40 UTC 7b2d9655da218f8311d1dab4ab1142c35c3eef3b
7 files changed +192 -61
src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js
+27 -11
@@ -7,6 +7,7 @@
7 * of patent rights can be found in the PATENTS file in the same directory.
8 *
9 * @providesModule SimpleEventPlugin
10 + * @flow
11 */
12
13 'use strict';
@@ -30,6 +31,14 @@ var emptyFunction = require('emptyFunction');
31 var getEventCharCode = require('getEventCharCode');
32 var invariant = require('invariant');
33
34 +import type {TopLevelTypes} from 'EventConstants';
35 +import type {
36 + DispatchConfig,
37 + ReactSyntheticEvent,
38 +} from 'ReactSyntheticEventType';
39 +import type {ReactInstance} from 'ReactInstanceType';
40 +import type {PluginModule} from 'PluginModuleType';
41 +
42 /**
43 * Turns
44 * ['abort', ...]
@@ -48,8 +57,8 @@ var invariant = require('invariant');
57 * 'topAbort': { sameConfig }
58 * };
59 */
51 -var eventTypes = {};
52 -var topLevelEventsToDispatchConfig = {};
60 +var eventTypes: {[key: string]: DispatchConfig} = {};
61 +var topLevelEventsToDispatchConfig: {[key: TopLevelTypes]: DispatchConfig} = {};
62 [
63 'abort',
64 'animationEnd',
@@ -131,22 +140,22 @@ var topLevelEventsToDispatchConfig = {};
140
141 var onClickListeners = {};
142
134 -function getDictionaryKey(inst) {
143 +function getDictionaryKey(inst: ReactInstance): string {
144 // Prevents V8 performance issue:
145 // https://github.com/facebook/react/pull/7232
146 return '.' + inst._rootNodeID;
147 }
148
140 -var SimpleEventPlugin = {
149 +var SimpleEventPlugin: PluginModule<MouseEvent> = {
150
151 eventTypes: eventTypes,
152
153 extractEvents: function(
145 - topLevelType,
146 - targetInst,
147 - nativeEvent,
148 - nativeEventTarget
149 - ) {
154 + topLevelType: TopLevelTypes,
155 + targetInst: ReactInstance,
156 + nativeEvent: MouseEvent,
157 + nativeEventTarget: EventTarget,
158 + ): null | ReactSyntheticEvent {
159 var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
160 if (!dispatchConfig) {
161 return null;
@@ -268,7 +277,11 @@ var SimpleEventPlugin = {
277 return event;
278 },
279
271 - didPutListener: function(inst, registrationName, listener) {
280 + didPutListener: function(
281 + inst: ReactInstance,
282 + registrationName: string,
283 + listener: () => void,
284 + ): void {
285 // Mobile Safari does not fire properly bubble click events on
286 // non-interactive elements, which means delegated click listeners do not
287 // fire. The workaround for this bug involves attaching an empty click
@@ -286,7 +299,10 @@ var SimpleEventPlugin = {
299 }
300 },
301
289 - willDeleteListener: function(inst, registrationName) {
302 + willDeleteListener: function(
303 + inst: ReactInstance,
304 + registrationName: string,
305 + ): void {
306 if (registrationName === 'onClick') {
307 var key = getDictionaryKey(inst);
308 onClickListeners[key].remove();
src/renderers/dom/client/eventPlugins/TapEventPlugin.js
+55 -10
@@ -7,6 +7,7 @@
7 * of patent rights can be found in the PATENTS file in the same directory.
8 *
9 * @providesModule TapEventPlugin
10 + * @flow
11 */
12
13 'use strict';
@@ -20,19 +21,60 @@ var ViewportMetrics = require('ViewportMetrics');
21 var isStartish = EventPluginUtils.isStartish;
22 var isEndish = EventPluginUtils.isEndish;
23
24 +import type {DispatchConfig} from 'ReactSyntheticEventType';
25 +import type {PluginModule} from 'PluginModuleType';
26 +import type {ReactInstance} from 'ReactInstanceType';
27 +import type {TopLevelTypes} from 'EventConstants';
28 +
29 +/**
30 + * We are extending the Flow 'Touch' declaration to enable using bracket
31 + * notation to access properties.
32 + * Without this adjustment Flow throws
33 + * "Indexable signature not found in Touch".
34 + * See https://github.com/facebook/flow/issues/1323
35 + */
36 +type TouchPropertyKey =
37 + 'clientX' |
38 + 'clientY' |
39 + 'pageX' |
40 + 'pageY';
41 +
42 +declare class _Touch extends Touch {
43 + [key: TouchPropertyKey]: number;
44 +}
45 +
46 +type AxisCoordinateData = {
47 + page: TouchPropertyKey,
48 + client: TouchPropertyKey,
49 + envScroll: 'currentPageScrollLeft' | 'currentPageScrollTop',
50 +};
51 +
52 +type AxisType = {
53 + x: AxisCoordinateData,
54 + y: AxisCoordinateData,
55 +};
56 +
57 +type CoordinatesType = {
58 + x: number,
59 + y: number,
60 +};
61 +
62 /**
63 * Number of pixels that are tolerated in between a `touchStart` and `touchEnd`
64 * in order to still be considered a 'tap' event.
65 */
66 var tapMoveThreshold = 10;
28 -var startCoords = {x: null, y: null};
67 +var startCoords: CoordinatesType = {x: 0, y: 0};
68
30 -var Axis = {
69 +var Axis: AxisType = {
70 x: {page: 'pageX', client: 'clientX', envScroll: 'currentPageScrollLeft'},
71 y: {page: 'pageY', client: 'clientY', envScroll: 'currentPageScrollTop'},
72 };
73
35 -function getAxisCoordOfEvent(axis, nativeEvent) {
74 +function getAxisCoordOfEvent(
75 + axis: AxisCoordinateData,
76 + nativeEvent: _Touch,
77 +): number {
78 var singleTouch = TouchEventUtils.extractSingleTouch(nativeEvent);
79 if (singleTouch) {
80 return singleTouch[axis.page];
@@ -42,7 +84,10 @@ function getAxisCoordOfEvent(axis, nativeEvent) {
84 nativeEvent[axis.client] + ViewportMetrics[axis.envScroll];
85 }
86
45 -function getDistance(coords, nativeEvent) {
87 +function getDistance(
88 + coords: CoordinatesType,
89 + nativeEvent: _Touch,
90 +): number {
91 var pageX = getAxisCoordOfEvent(Axis.x, nativeEvent);
92 var pageY = getAxisCoordOfEvent(Axis.y, nativeEvent);
93 return Math.pow(
@@ -64,7 +109,7 @@ var dependencies = [
109 'topMouseUp',
110 ].concat(touchEvents);
111
67 -var eventTypes = {
112 +var eventTypes: DispatchConfig = {
113 touchTap: {
114 phasedRegistrationNames: {
115 bubbled: 'onTouchTap',
@@ -78,17 +123,17 @@ var usedTouch = false;
123 var usedTouchTime = 0;
124 var TOUCH_DELAY = 1000;
125
81 -var TapEventPlugin = {
126 +var TapEventPlugin: PluginModule<_Touch> = {
127
128 tapMoveThreshold: tapMoveThreshold,
129
130 eventTypes: eventTypes,
131
132 extractEvents: function(
88 - topLevelType,
89 - targetInst,
90 - nativeEvent,
91 - nativeEventTarget
133 + topLevelType: TopLevelTypes,
134 + targetInst: ReactInstance,
135 + nativeEvent: _Touch,
136 + nativeEventTarget: EventTarget,
137 ) {
138 if (!isStartish(topLevelType) && !isEndish(topLevelType)) {
139 return null;
src/renderers/shared/stack/event/EventPluginRegistry.js
+24 -19
@@ -15,15 +15,15 @@
15 import type {
16 DispatchConfig,
17 ReactSyntheticEvent,
18 -} from 'ReactSyntheticEvent';
18 +} from 'ReactSyntheticEventType';
19
20 -type PluginName = string;
21 -
22 -type PluginModule = {
23 - eventTypes: any,
24 -};
20 +import type {
21 + AnyNativeEvent,
22 + PluginName,
23 + PluginModule,
24 +} from 'PluginModuleType';
25
26 -type NamesToPlugins = {[key: PluginName]: PluginModule};
26 +type NamesToPlugins = {[key: PluginName]: PluginModule<AnyNativeEvent>};
27
28 type EventPluginOrder = null | Array<PluginName>;
29
@@ -94,7 +94,7 @@ function recomputePluginOrdering(): void {
94 */
95 function publishEventForPlugin(
96 dispatchConfig: DispatchConfig,
97 - pluginModule: PluginModule,
97 + pluginModule: PluginModule<AnyNativeEvent>,
98 eventName: string,
99 ): boolean {
100 invariant(
@@ -139,7 +139,7 @@ function publishEventForPlugin(
139 */
140 function publishRegistrationName(
141 registrationName: string,
142 - pluginModule: PluginModule,
142 + pluginModule: PluginModule<AnyNativeEvent>,
143 eventName: string,
144 ): void {
145 invariant(
@@ -267,22 +267,27 @@ var EventPluginRegistry = {
267 */
268 getPluginModuleForEvent: function(
269 event: ReactSyntheticEvent,
270 - ): null | PluginModule {
270 + ): null | PluginModule<AnyNativeEvent> {
271 var dispatchConfig = event.dispatchConfig;
272 if (dispatchConfig.registrationName) {
273 return EventPluginRegistry.registrationNameModules[
274 dispatchConfig.registrationName
275 ] || null;
276 }
277 - for (var phase in dispatchConfig.phasedRegistrationNames) {
278 - if (!dispatchConfig.phasedRegistrationNames.hasOwnProperty(phase)) {
279 - continue;
280 - }
281 - var pluginModule = EventPluginRegistry.registrationNameModules[
282 - dispatchConfig.phasedRegistrationNames[phase]
283 - ];
284 - if (pluginModule) {
285 - return pluginModule;
277 + if (dispatchConfig.phasedRegistrationNames !== undefined) {
278 + // pulling phasedRegistrationNames out of dispatchConfig helps Flow see
279 + // that it is not undefined.
280 + var {phasedRegistrationNames} = dispatchConfig;
281 + for (var phase in phasedRegistrationNames) {
282 + if (!phasedRegistrationNames.hasOwnProperty(phase)) {
283 + continue;
284 + }
285 + var pluginModule = EventPluginRegistry.registrationNameModules[
286 + phasedRegistrationNames[phase]
287 + ];
288 + if (pluginModule) {
289 + return pluginModule;
290 + }
291 }
292 }
293 return null;
src/renderers/shared/stack/event/PluginModuleType.js new
+49
@@ -0,0 +1,49 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule PluginModuleType
10 + * @flow
11 + */
12 +
13 +'use strict';
14 +
15 +import type {ReactInstance} from 'ReactInstanceType';
16 +import type {
17 + DispatchConfig,
18 + ReactSyntheticEvent,
19 +} from 'ReactSyntheticEventType';
20 +
21 +type EventTypes = {[key: string]: DispatchConfig};
22 +
23 +export type AnyNativeEvent =
24 + Event |
25 + KeyboardEvent |
26 + MouseEvent |
27 + Touch;
28 +
29 +export type PluginName = string;
30 +
31 +export type PluginModule<NativeEvent> = {
32 + eventTypes: EventTypes,
33 + extractEvents: (
34 + topLevelType: string,
35 + targetInst: ReactInstance,
36 + nativeTarget: NativeEvent,
37 + nativeEventTarget: EventTarget,
38 + ) => null | ReactSyntheticEvent,
39 + didPutListener?: (
40 + inst: ReactInstance,
41 + registrationName: string,
42 + listener: () => void,
43 + ) => void,
44 + willDeleteListener?: (
45 + inst: ReactInstance,
46 + registrationName: string,
47 + ) => void,
48 + tapMoveThreshold?: number,
49 +};
src/renderers/shared/stack/event/ReactSyntheticEvent.js deleted
-21
@@ -1,21 +0,0 @@
1 -/*
2 - * Copyright 2013-present, Facebook, Inc.
3 - * All rights reserved.
4 - *
5 - * This source code is licensed under the BSD-style license found in the
6 - * LICENSE file in the root directory of this source tree. An additional grant
7 - * of patent rights can be found in the PATENTS file in the same directory.
8 - *
9 - * Flow type for SyntheticEvent class that includes private properties
10 - *
11 - * @providesModule ReactSyntheticEvent
12 - * @flow
13 - */
14 -
15 -'use strict';
16 -
17 -export type DispatchConfig = any;
18 -
19 -export class ReactSyntheticEvent extends SyntheticEvent {
20 - dispatchConfig: DispatchConfig;
21 -}
src/renderers/shared/stack/event/ReactSyntheticEventType.js new
+36
@@ -0,0 +1,36 @@
1 +/*
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * Flow type for SyntheticEvent class that includes private properties
10 + *
11 + * @providesModule ReactSyntheticEventType
12 + * @flow
13 + */
14 +
15 +'use strict';
16 +
17 +import type {ReactInstance} from 'ReactInstanceType';
18 +
19 +export type DispatchConfig = {
20 + dependencies: Array<string>,
21 + phasedRegistrationNames?: {
22 + bubbled: string,
23 + captured: string,
24 + },
25 + registrationName?: string,
26 +};
27 +
28 +export type ReactSyntheticEvent = {
29 + dispatchConfig: DispatchConfig;
30 + getPooled: (
31 + dispatchConfig: DispatchConfig,
32 + targetInst: ReactInstance,
33 + nativeTarget: Event,
34 + nativeEventTarget: EventTarget,
35 + ) => ReactSyntheticEvent;
36 +} & SyntheticEvent;
src/renderers/shared/stack/reconciler/ReactInstanceType.js
+1
@@ -28,6 +28,7 @@ export type ReactInstance = {
28 detachRef: (ref: string) => void,
29 getName: () => string,
30 getPublicInstance: any,
31 + _rootNodeID: number,
32
33 // instantiateReactComponent
34 _mountIndex: number,