@samitouri / QOS-React-2 / commits / 836d1c5d0c

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\\"}