ReactNativeBridgeEventPlugin supports lazily-registered event types (#10679)
* ReactNativeBridgeEventPlugin supports lazily-registered event types This accompanies changes on the native side to now specify which event types each view manager supports as part of its view config. Ultimately the goal is to lazily initialize view managers on the native side as well. * Bubbling and direct attributes are optional on view config This should help ease transition for pre-existing JS-only components.
Brian Vaughn committed
Sep 14, 2017 at 14:55 UTC
836d1c5d0c4c2b1f2f69ee4bf5b9051ff1191331
7 files changed
+166
-358
scripts/rollup/shims/react-native/ReactNativeBridgeEventPlugin.js
new
+19
@@ -0,0 +1,19 @@
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 ReactNativeBridgeEventPlugin
10
+ */
11
+
12
+'use strict';
13
+
14
+const {
15
+ __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
16
+} = require('ReactNative');
17
+
18
+module.exports =
19
+ __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactNativeBridgeEventPlugin;
src/renderers/native/ReactNativeBridgeEventPlugin.js
+51
-22
@@ -11,28 +11,17 @@
11
*/
12
'use strict';
13
14
-var EventPropagators = require('EventPropagators');
15
-var SyntheticEvent = require('SyntheticEvent');
16
-var ReactNativeEventTypes = require('ReactNativeEventTypes');
17
-var invariant = require('fbjs/lib/invariant');
14
+const EventPropagators = require('EventPropagators');
15
+const SyntheticEvent = require('SyntheticEvent');
16
+const invariant = require('fbjs/lib/invariant');
17
19
-var customBubblingEventTypes = ReactNativeEventTypes.customBubblingEventTypes;
20
-var customDirectEventTypes = ReactNativeEventTypes.customDirectEventTypes;
18
+const customBubblingEventTypes = {};
19
+const customDirectEventTypes = {};
20
22
-if (__DEV__) {
23
- var warning = require('fbjs/lib/warning');
21
+import type {ReactNativeBaseComponentViewConfig} from 'ReactNativeTypes';
22
25
- for (var directTypeName in customDirectEventTypes) {
26
- warning(
27
- !customBubblingEventTypes[directTypeName],
28
- 'Event cannot be both direct and bubbling: %s',
29
- directTypeName,
30
- );
31
- }
32
-}
33
-
34
-var ReactNativeBridgeEventPlugin = {
35
- eventTypes: {...customBubblingEventTypes, ...customDirectEventTypes},
23
+const ReactNativeBridgeEventPlugin = {
24
+ eventTypes: {},
25
26
/**
27
* @see {EventPluginHub.extractEvents}
@@ -43,14 +32,14 @@ var ReactNativeBridgeEventPlugin = {
32
nativeEvent: Event,
33
nativeEventTarget: Object,
34
): ?Object {
46
- var bubbleDispatchConfig = customBubblingEventTypes[topLevelType];
47
- var directDispatchConfig = customDirectEventTypes[topLevelType];
35
+ const bubbleDispatchConfig = customBubblingEventTypes[topLevelType];
36
+ const directDispatchConfig = customDirectEventTypes[topLevelType];
37
invariant(
38
bubbleDispatchConfig || directDispatchConfig,
39
'Unsupported top level event type "%s" dispatched',
40
topLevelType,
41
);
53
- var event = SyntheticEvent.getPooled(
42
+ const event = SyntheticEvent.getPooled(
43
bubbleDispatchConfig || directDispatchConfig,
44
targetInst,
45
nativeEvent,
@@ -65,6 +54,46 @@ var ReactNativeBridgeEventPlugin = {
54
}
55
return event;
56
},
57
+
58
+ processEventTypes: function(
59
+ viewConfig: ReactNativeBaseComponentViewConfig,
60
+ ): void {
61
+ const {bubblingEventTypes, directEventTypes} = viewConfig;
62
+
63
+ if (__DEV__) {
64
+ if (bubblingEventTypes != null && directEventTypes != null) {
65
+ for (const topLevelType in directEventTypes) {
66
+ invariant(
67
+ bubblingEventTypes[topLevelType] == null,
68
+ 'Event cannot be both direct and bubbling: %s',
69
+ topLevelType,
70
+ );
71
+ }
72
+ }
73
+ }
74
+
75
+ if (bubblingEventTypes != null) {
76
+ for (const topLevelType in bubblingEventTypes) {
77
+ if (customBubblingEventTypes[topLevelType] == null) {
78
+ ReactNativeBridgeEventPlugin.eventTypes[
79
+ topLevelType
80
+ ] = customBubblingEventTypes[topLevelType] =
81
+ bubblingEventTypes[topLevelType];
82
+ }
83
+ }
84
+ }
85
+
86
+ if (directEventTypes != null) {
87
+ for (const topLevelType in directEventTypes) {
88
+ if (customDirectEventTypes[topLevelType] == null) {
89
+ ReactNativeBridgeEventPlugin.eventTypes[
90
+ topLevelType
91
+ ] = customDirectEventTypes[topLevelType] =
92
+ directEventTypes[topLevelType];
93
+ }
94
+ }
95
+ }
96
+ },
97
};
98
99
module.exports = ReactNativeBridgeEventPlugin;
src/renderers/native/ReactNativeEventTypes.js
deleted
-319
@@ -1,319 +0,0 @@
1
-/**
2
- * Copyright (c) 2015-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 ReactNativeEventTypes
10
- * @flow
11
- */
12
-'use strict';
13
-
14
-const Platform = require('Platform');
15
-
16
-const emptyObject = require('fbjs/lib/emptyObject');
17
-
18
-const COMMON_BUBBLING_EVENT_TYPES = {
19
- topBlur: {
20
- phasedRegistrationNames: {
21
- captured: 'onBlurCapture',
22
- bubbled: 'onBlur',
23
- },
24
- },
25
- topChange: {
26
- phasedRegistrationNames: {
27
- captured: 'onChangeCapture',
28
- bubbled: 'onChange',
29
- },
30
- },
31
- topEndEditing: {
32
- phasedRegistrationNames: {
33
- captured: 'onEndEditingCapture',
34
- bubbled: 'onEndEditing',
35
- },
36
- },
37
- topFocus: {
38
- phasedRegistrationNames: {
39
- captured: 'onFocusCapture',
40
- bubbled: 'onFocus',
41
- },
42
- },
43
- topSubmitEditing: {
44
- phasedRegistrationNames: {
45
- captured: 'onSubmitEditingCapture',
46
- bubbled: 'onSubmitEditing',
47
- },
48
- },
49
- topTouchEnd: {
50
- phasedRegistrationNames: {
51
- captured: 'onTouchEndCapture',
52
- bubbled: 'onTouchEnd',
53
- },
54
- },
55
- topTouchMove: {
56
- phasedRegistrationNames: {
57
- captured: 'onTouchMoveCapture',
58
- bubbled: 'onTouchMove',
59
- },
60
- },
61
- topTouchStart: {
62
- phasedRegistrationNames: {
63
- captured: 'onTouchStartCapture',
64
- bubbled: 'onTouchStart',
65
- },
66
- },
67
-};
68
-
69
-const COMMON_DIRECT_EVENT_TYPES = {
70
- topError: {
71
- registrationName: 'onError',
72
- },
73
- topLayout: {
74
- registrationName: 'onLayout',
75
- },
76
- topLoad: {
77
- registrationName: 'onLoad',
78
- },
79
- topLoadEnd: {
80
- registrationName: 'onLoadEnd',
81
- },
82
- topLoadStart: {
83
- registrationName: 'onLoadStart',
84
- },
85
- topLoadingError: {
86
- registrationName: 'onLoadingError',
87
- },
88
- topLoadingFinish: {
89
- registrationName: 'onLoadingFinish',
90
- },
91
- topLoadingStart: {
92
- registrationName: 'onLoadingStart',
93
- },
94
- topMessage: {
95
- registrationName: 'onMessage',
96
- },
97
- topMomentumScrollBegin: {
98
- registrationName: 'onMomentumScrollBegin',
99
- },
100
- topMomentumScrollEnd: {
101
- registrationName: 'onMomentumScrollEnd',
102
- },
103
- topRefresh: {
104
- registrationName: 'onRefresh',
105
- },
106
- topScroll: {
107
- registrationName: 'onScroll',
108
- },
109
- topScrollAnimationEnd: {
110
- registrationName: 'onScrollAnimationEnd',
111
- },
112
- topScrollBeginDrag: {
113
- registrationName: 'onScrollBeginDrag',
114
- },
115
- topScrollEndDrag: {
116
- registrationName: 'onScrollEndDrag',
117
- },
118
- topSelectionChange: {
119
- registrationName: 'onSelectionChange',
120
- },
121
- topShow: {
122
- registrationName: 'onShow',
123
- },
124
-};
125
-
126
-const ANDROID_BUBBLING_EVENT_TYPES = {
127
- ...COMMON_BUBBLING_EVENT_TYPES,
128
- topSelect: {
129
- phasedRegistrationNames: {
130
- bubbled: 'onSelect',
131
- captured: 'onSelectCapture',
132
- },
133
- },
134
- topTextInput: {
135
- phasedRegistrationNames: {
136
- bubbled: 'onTextInput',
137
- captured: 'onTextInputCapture',
138
- },
139
- },
140
-};
141
-
142
-const ANDROID_DIRECT_EVENT_TYPES = {
143
- ...COMMON_DIRECT_EVENT_TYPES,
144
- topContentSizeChange: {
145
- registrationName: 'onContentSizeChange',
146
- },
147
- topDrawerClosed: {
148
- registrationName: 'onDrawerClose',
149
- },
150
- topDrawerOpened: {
151
- registrationName: 'onDrawerOpen',
152
- },
153
- topDrawerSlide: {
154
- registrationName: 'onDrawerSlide',
155
- },
156
- topDrawerStateChanged: {
157
- registrationName: 'onDrawerStateChanged',
158
- },
159
- topPageScroll: {
160
- registrationName: 'onPageScroll',
161
- },
162
- topPageScrollStateChanged: {
163
- registrationName: 'onPageScrollStateChanged',
164
- },
165
- topPageSelected: {
166
- registrationName: 'onPageSelected',
167
- },
168
- topRequestClose: {
169
- registrationName: 'onRequestClose',
170
- },
171
- topSlidingComplete: {
172
- registrationName: 'onSlidingComplete',
173
- },
174
- topVideoProgress: {
175
- registrationName: 'onProgress',
176
- },
177
- topVideoSizeDetected: {
178
- registrationName: 'onVideoSizeDetected',
179
- },
180
- topVideoStateChange: {
181
- registrationName: 'onStateChange',
182
- },
183
- topZoom: {
184
- registrationName: 'onZoom',
185
- },
186
-};
187
-
188
-const IOS_BUBBLING_EVENT_TYPES = {
189
- ...COMMON_BUBBLING_EVENT_TYPES,
190
- topAnnotationBlur: {
191
- phasedRegistrationNames: {
192
- captured: 'onAnnotationBlurCapture',
193
- bubbled: 'onAnnotationBlur',
194
- },
195
- },
196
- topAnnotationDragStateChange: {
197
- phasedRegistrationNames: {
198
- captured: 'onAnnotationDragStateChangeCapture',
199
- bubbled: 'onAnnotationDragStateChange',
200
- },
201
- },
202
- topAnnotationFocus: {
203
- phasedRegistrationNames: {
204
- captured: 'onAnnotationFocusCapture',
205
- bubbled: 'onAnnotationFocus',
206
- },
207
- },
208
- topContentSizeChange: {
209
- phasedRegistrationNames: {
210
- captured: 'onContentSizeChangeCapture',
211
- bubbled: 'onContentSizeChange',
212
- },
213
- },
214
- topKeyPress: {
215
- phasedRegistrationNames: {
216
- captured: 'onKeyPressCapture',
217
- bubbled: 'onKeyPress',
218
- },
219
- },
220
- topLeftButtonPress: {
221
- phasedRegistrationNames: {
222
- captured: 'onLeftButtonPressCapture',
223
- bubbled: 'onLeftButtonPress',
224
- },
225
- },
226
- topNavigationComplete: {
227
- phasedRegistrationNames: {
228
- captured: 'onNavigationCompleteCapture',
229
- bubbled: 'onNavigationComplete',
230
- },
231
- },
232
- topPress: {
233
- phasedRegistrationNames: {
234
- captured: 'onPressCapture',
235
- bubbled: 'onPress',
236
- },
237
- },
238
- topRightButtonPress: {
239
- phasedRegistrationNames: {
240
- captured: 'onRightButtonPressCapture',
241
- bubbled: 'onRightButtonPress',
242
- },
243
- },
244
- topSlidingComplete: {
245
- phasedRegistrationNames: {
246
- captured: 'onSlidingCompleteCapture',
247
- bubbled: 'onSlidingComplete',
248
- },
249
- },
250
- topTouchCancel: {
251
- phasedRegistrationNames: {
252
- captured: 'onTouchCancelCapture',
253
- bubbled: 'onTouchCancel',
254
- },
255
- },
256
- topValueChange: {
257
- phasedRegistrationNames: {
258
- captured: 'onValueChangeCapture',
259
- bubbled: 'onValueChange',
260
- },
261
- },
262
-};
263
-
264
-const IOS_DIRECT_EVENT_TYPES = {
265
- ...COMMON_DIRECT_EVENT_TYPES,
266
- topAccessibilityTap: {
267
- registrationName: 'onAccessibilityTap',
268
- },
269
- topMagicTap: {
270
- registrationName: 'onMagicTap',
271
- },
272
- topNavigationProgress: {
273
- registrationName: 'onNavigationProgress',
274
- },
275
- topOrientationChange: {
276
- registrationName: 'onOrientationChange',
277
- },
278
- topPartialLoad: {
279
- registrationName: 'onPartialLoad',
280
- },
281
- topProgress: {
282
- registrationName: 'onProgress',
283
- },
284
- topShouldStartLoadWithRequest: {
285
- registrationName: 'onShouldStartLoadWithRequest',
286
- },
287
- topSnapshotReady: {
288
- registrationName: 'onSnapshotReady',
289
- },
290
- topStateChange: {
291
- registrationName: 'onStateChange',
292
- },
293
- topTextInput: {
294
- registrationName: 'onTextInput',
295
- },
296
- topTextLayout: {
297
- registrationName: 'onTextLayout',
298
- },
299
-};
300
-
301
-let ReactNativeEventTypes;
302
-if (Platform.OS === 'ios') {
303
- ReactNativeEventTypes = {
304
- customBubblingEventTypes: IOS_BUBBLING_EVENT_TYPES,
305
- customDirectEventTypes: IOS_DIRECT_EVENT_TYPES,
306
- };
307
-} else if (Platform.OS === 'android') {
308
- ReactNativeEventTypes = {
309
- customBubblingEventTypes: ANDROID_BUBBLING_EVENT_TYPES,
310
- customDirectEventTypes: ANDROID_DIRECT_EVENT_TYPES,
311
- };
312
-} else {
313
- ReactNativeEventTypes = {
314
- customBubblingEventTypes: emptyObject,
315
- customDirectEventTypes: emptyObject,
316
- };
317
-}
318
-
319
-module.exports = ReactNativeEventTypes;
src/renderers/native/ReactNativeFiberEntry.js
+1
@@ -96,6 +96,7 @@ const ReactNativeFiber: ReactNativeType = {
96
NativeMethodsMixin: require('NativeMethodsMixin'),
97
98
// Used by react-native-github/Libraries/ components
99
+ ReactNativeBridgeEventPlugin: require('ReactNativeBridgeEventPlugin'), // requireNativeComponent
100
ReactGlobalSharedState: require('ReactGlobalSharedState'), // Systrace
101
ReactNativeComponentTree: require('ReactNativeComponentTree'), // InspectorUtils, ScrollResponder
102
ReactNativePropRegistry: require('ReactNativePropRegistry'), // flattenStyle, Stylesheet
src/renderers/native/ReactNativeTypes.js
+18
@@ -34,9 +34,22 @@ export type MeasureLayoutOnSuccessCallback = (
34
height: number,
35
) => void;
36
37
+type BubblingEventType = {
38
+ phasedRegistrationNames: {
39
+ captured: string,
40
+ bubbled: string,
41
+ },
42
+};
43
+
44
+type DirectEventType = {
45
+ registrationName: string,
46
+};
47
+
48
export type ReactNativeBaseComponentViewConfig = {
49
validAttributes: Object,
50
uiViewClassName: string,
51
+ bubblingEventTypes?: {[topLevelType: string]: BubblingEventType},
52
+ directEventTypes?: {[topLevelType: string]: DirectEventType},
53
propTypes?: Object,
54
};
55
@@ -59,12 +72,17 @@ export type NativeMethodsMixinType = {
72
setNativeProps(nativeProps: Object): void,
73
};
74
75
+type ReactNativeBridgeEventPlugin = {
76
+ processEventTypes(viewConfig: ReactNativeBaseComponentViewConfig): void,
77
+};
78
+
79
type SecretInternalsType = {
80
NativeMethodsMixin: NativeMethodsMixinType,
81
createReactNativeComponentClass(
82
name: string,
83
callback: ViewConfigGetter,
84
): any,
85
+ ReactNativeBridgeEventPlugin: ReactNativeBridgeEventPlugin,
86
ReactNativeComponentTree: any,
87
ReactNativePropRegistry: any,
88
// TODO (bvaughn) Decide which additional types to expose here?
src/renderers/native/__tests__/ReactNativeEvents-test.js
+72
-17
@@ -15,10 +15,55 @@ var PropTypes;
15
var RCTEventEmitter;
16
var React;
17
var ReactNative;
18
+var ReactNativeBridgeEventPlugin;
19
var ResponderEventPlugin;
20
var UIManager;
21
var createReactNativeComponentClass;
22
23
+// Parallels requireNativeComponent() in that it lazily constructs a view config,
24
+// And registers view manager event types with ReactNativeBridgeEventPlugin.
25
+const fakeRequireNativeComponent = (uiViewClassName, validAttributes) => {
26
+ const getViewConfig = () => {
27
+ const viewConfig = {
28
+ uiViewClassName,
29
+ validAttributes,
30
+ bubblingEventTypes: {
31
+ topTouchCancel: {
32
+ phasedRegistrationNames: {
33
+ bubbled: 'onTouchCancel',
34
+ captured: 'onTouchCancelCapture',
35
+ },
36
+ },
37
+ topTouchEnd: {
38
+ phasedRegistrationNames: {
39
+ bubbled: 'onTouchEnd',
40
+ captured: 'onTouchEndCapture',
41
+ },
42
+ },
43
+ topTouchMove: {
44
+ phasedRegistrationNames: {
45
+ bubbled: 'onTouchMove',
46
+ captured: 'onTouchMoveCapture',
47
+ },
48
+ },
49
+ topTouchStart: {
50
+ phasedRegistrationNames: {
51
+ bubbled: 'onTouchStart',
52
+ captured: 'onTouchStartCapture',
53
+ },
54
+ },
55
+ },
56
+ directEventTypes: {},
57
+ };
58
+
59
+ ReactNativeBridgeEventPlugin.processEventTypes(viewConfig);
60
+
61
+ return viewConfig;
62
+ };
63
+
64
+ return createReactNativeComponentClass(uiViewClassName, getViewConfig);
65
+};
66
+
67
beforeEach(() => {
68
jest.resetModules();
69
@@ -26,18 +71,37 @@ beforeEach(() => {
71
RCTEventEmitter = require('RCTEventEmitter');
72
React = require('react');
73
ReactNative = require('react-native');
74
+ ReactNativeBridgeEventPlugin = require('ReactNativeBridgeEventPlugin');
75
ResponderEventPlugin = require('ResponderEventPlugin');
76
UIManager = require('UIManager');
77
createReactNativeComponentClass = require('createReactNativeComponentClass');
78
});
79
80
+it('fails if unknown/unsupported event types are dispatched', () => {
81
+ expect(RCTEventEmitter.register.mock.calls.length).toBe(1);
82
+ var EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
83
+ var View = fakeRequireNativeComponent('View', {});
84
+
85
+ ReactNative.render(<View onUnspecifiedEvent={() => {}} />, 1);
86
+
87
+ expect(UIManager.__dumpHierarchyForJestTestsOnly()).toMatchSnapshot();
88
+ expect(UIManager.createView.mock.calls.length).toBe(1);
89
+
90
+ const target = UIManager.createView.mock.calls[0][0];
91
+
92
+ expect(() => {
93
+ EventEmitter.receiveTouches(
94
+ 'unspecifiedEvent',
95
+ [{target, identifier: 17}],
96
+ [0],
97
+ );
98
+ }).toThrow('Unsupported top level event type "unspecifiedEvent" dispatched');
99
+});
100
+
101
it('handles events', () => {
102
expect(RCTEventEmitter.register.mock.calls.length).toBe(1);
103
var EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
37
- var View = createReactNativeComponentClass('View', () => ({
38
- validAttributes: {foo: true},
39
- uiViewClassName: 'View',
40
- }));
104
+ var View = fakeRequireNativeComponent('View', {foo: true});
105
106
var log = [];
107
ReactNative.render(
@@ -93,11 +157,7 @@ it('handles events', () => {
157
it('handles events on text nodes', () => {
158
expect(RCTEventEmitter.register.mock.calls.length).toBe(1);
159
var EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
96
-
97
- var Text = createReactNativeComponentClass('Text', () => ({
98
- validAttributes: {foo: true},
99
- uiViewClassName: 'Text',
100
- }));
160
+ var Text = fakeRequireNativeComponent('Text', {});
161
162
class ContextHack extends React.Component {
163
static childContextTypes = {isInAParentText: PropTypes.bool};
@@ -179,10 +239,7 @@ it('handles events on text nodes', () => {
239
240
it('handles when a responder is unmounted while a touch sequence is in progress', () => {
241
var EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
182
- var View = createReactNativeComponentClass('View', () => ({
183
- validAttributes: {id: true},
184
- uiViewClassName: 'View',
185
- }));
242
+ var View = fakeRequireNativeComponent('View', {id: true});
243
244
function getViewById(id) {
245
return UIManager.createView.mock.calls.find(
@@ -273,10 +330,8 @@ it('handles when a responder is unmounted while a touch sequence is in progress'
330
331
it('handles events without target', () => {
332
var EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
276
- var View = createReactNativeComponentClass('View', () => ({
277
- validAttributes: {id: true},
278
- uiViewClassName: 'View',
279
- }));
333
+
334
+ var View = fakeRequireNativeComponent('View', {id: true});
335
336
function getViewById(id) {
337
return UIManager.createView.mock.calls.find(
src/renderers/native/__tests__/__snapshots__/ReactNativeEvents-test.js.snap
+5
@@ -1,5 +1,10 @@
1
// Jest Snapshot v1, https://goo.gl/fbAQLP
2
3
+exports[`fails if unknown/unsupported event types are dispatched 1`] = `
4
+"<native root> {}
5
+ View null"
6
+`;
7
+
8
exports[`handles events 1`] = `
9
"<native root> {}
10
View {\\"foo\\":\\"outer\\"}