Browse Source

[v2] Refactor preview api (#3708)

* Refactor peek and pop

* Rollback some XCode stuff

* Added tests for touchablePreview event

* Also fixing searchbarcancelpressed event

* Making sure tests work
Birkir Rafn Guðjónsson 6 years ago
parent
commit
b63d178c35

+ 32
- 0
docs/docs/animations.md View File

32
     }
32
     }
33
   }
33
   }
34
 });
34
 });
35
+```
36
+
37
+## Peek and Pop (iOS 11.4+)
38
+
39
+react-native-navigation supports the [Peek and pop](
40
+https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/Adopting3DTouchOniPhone/#//apple_ref/doc/uid/TP40016543-CH1-SW3) feature in iOS 11.4 and newer.
41
+
42
+This works by passing a ref a componentent you would want to transform into a peek view. We have included a handly component to handle all the touches and ref for you.
43
+
44
+```jsx
45
+const handlePress ({ reactTag }) => {
46
+  Navigation.push(this.props.componentId, {
47
+    component {
48
+      name: 'previewed.screen',
49
+      options: {
50
+        preview: {
51
+          reactTag,
52
+        },
53
+      },
54
+    },
55
+  });
56
+};
57
+
58
+const Button = (
59
+  <Navigation.TouchablePreview
60
+    touchableComponent={TouchableHighlight}
61
+    onPress={handlePress}
62
+    onPressIn={handlePress}
63
+  >
64
+    <Text>My button</Text>
65
+  </Navigation.TouchablePreview>
66
+);
35
 ```
67
 ```

+ 16
- 0
docs/docs/events.md View File

166
   }
166
   }
167
 }
167
 }
168
 ```
168
 ```
169
+
170
+## previewCompleted (iOS 11.4+ only)
171
+Called when preview peek is completed
172
+
173
+```js
174
+class MyComponent extends Component {
175
+  constructor(props) {
176
+    super(props);
177
+    Navigation.events().bindComponent(this);
178
+  }
179
+
180
+  previewCompleted({ previewComponentId }) {
181
+
182
+  }
183
+}
184
+```

+ 1
- 1
docs/docs/styling.md View File

143
     interceptTouchOutside: true
143
     interceptTouchOutside: true
144
   },
144
   },
145
   preview: {
145
   preview: {
146
-    elementId: 'PreviewId',
146
+    reactTag: 0, // result from findNodeHandle(ref)
147
     width: 100,
147
     width: 100,
148
     height: 100,
148
     height: 100,
149
     commit: false,
149
     commit: false,

+ 20
- 6
lib/ios/RNNCommandsHandler.m View File

89
 	RNNRootViewController *newVc = (RNNRootViewController *)[_controllerFactory createLayoutAndSaveToStore:layout];
89
 	RNNRootViewController *newVc = (RNNRootViewController *)[_controllerFactory createLayoutAndSaveToStore:layout];
90
 	UIViewController *fromVC = [_store findComponentForId:componentId];
90
 	UIViewController *fromVC = [_store findComponentForId:componentId];
91
 	
91
 	
92
-	if (newVc.options.preview.elementId) {
92
+	if ([newVc.options.preview.reactTag floatValue] > 0) {
93
 		UIViewController* vc = [_store findComponentForId:componentId];
93
 		UIViewController* vc = [_store findComponentForId:componentId];
94
 		
94
 		
95
 		if([vc isKindOfClass:[RNNRootViewController class]]) {
95
 		if([vc isKindOfClass:[RNNRootViewController class]]) {
96
 			RNNRootViewController* rootVc = (RNNRootViewController*)vc;
96
 			RNNRootViewController* rootVc = (RNNRootViewController*)vc;
97
 			rootVc.previewController = newVc;
97
 			rootVc.previewController = newVc;
98
-			
99
-			RNNElementFinder* elementFinder = [[RNNElementFinder alloc] initWithFromVC:vc];
100
-			RNNElementView* elementView = [elementFinder findElementForId:newVc.options.preview.elementId];
98
+
99
+      rootVc.previewCallback = ^(UIViewController *vcc) {
100
+				RNNRootViewController* rvc  = (RNNRootViewController*)vcc;
101
+				[self->_eventEmitter sendOnPreviewCompleted:componentId previewComponentId:newVc.componentId];
102
+				if ([newVc.options.preview.commit floatValue] > 0) {
103
+					[CATransaction begin];
104
+					[CATransaction setCompletionBlock:^{
105
+						[self->_eventEmitter sendOnNavigationCommandCompletion:push params:@{@"componentId": componentId}];
106
+						completion();
107
+					}];
108
+					[rvc.navigationController pushViewController:newVc animated:YES];
109
+					[CATransaction commit];
110
+				}
111
+			};
101
 			
112
 			
102
 			CGSize size = CGSizeMake(rootVc.view.frame.size.width, rootVc.view.frame.size.height);
113
 			CGSize size = CGSizeMake(rootVc.view.frame.size.width, rootVc.view.frame.size.height);
103
 			
114
 			
112
 			if (newVc.options.preview.width || newVc.options.preview.height) {
123
 			if (newVc.options.preview.width || newVc.options.preview.height) {
113
 				newVc.preferredContentSize = size;
124
 				newVc.preferredContentSize = size;
114
 			}
125
 			}
115
-			
116
-			[rootVc registerForPreviewingWithDelegate:(id)rootVc sourceView:elementView];
126
+      
127
+			RCTExecuteOnMainQueue(^{
128
+				UIView *view = [[ReactNativeNavigation getBridge].uiManager viewForReactTag:newVc.options.preview.reactTag];
129
+				[rootVc registerForPreviewingWithDelegate:(id)rootVc sourceView:view];
130
+			});
117
 		}
131
 		}
118
 	} else {
132
 	} else {
119
 		id animationDelegate = (newVc.options.animations.push.hasCustomAnimation || newVc.isCustomTransitioned) ? newVc : nil;
133
 		id animationDelegate = (newVc.options.animations.push.hasCustomAnimation || newVc.isCustomTransitioned) ? newVc : nil;

+ 3
- 0
lib/ios/RNNEventEmitter.h View File

22
 
22
 
23
 -(void)sendOnSearchBarCancelPressed:(NSString *)componentId;
23
 -(void)sendOnSearchBarCancelPressed:(NSString *)componentId;
24
 
24
 
25
+-(void)sendOnPreviewCompleted:(NSString *)componentId previewComponentId:(NSString *)previewComponentId;
26
+
25
 - (void)sendModalsDismissedEvent:(NSString *)componentId numberOfModalsDismissed:(NSNumber *)modalsDismissed;
27
 - (void)sendModalsDismissedEvent:(NSString *)componentId numberOfModalsDismissed:(NSNumber *)modalsDismissed;
26
 
28
 
29
+
27
 @end
30
 @end

+ 10
- 1
lib/ios/RNNEventEmitter.m View File

17
 static NSString* const ModalDismissed	        = @"RNN.ModalDismissed";
17
 static NSString* const ModalDismissed	        = @"RNN.ModalDismissed";
18
 static NSString* const SearchBarUpdated 		= @"RNN.SearchBarUpdated";
18
 static NSString* const SearchBarUpdated 		= @"RNN.SearchBarUpdated";
19
 static NSString* const SearchBarCancelPressed 	= @"RNN.SearchBarCancelPressed";
19
 static NSString* const SearchBarCancelPressed 	= @"RNN.SearchBarCancelPressed";
20
+static NSString* const PreviewCompleted         = @"RNN.PreviewCompleted";
20
 
21
 
21
 -(NSArray<NSString *> *)supportedEvents {
22
 -(NSArray<NSString *> *)supportedEvents {
22
 	return @[AppLaunched,
23
 	return @[AppLaunched,
27
 			 NavigationButtonPressed,
28
 			 NavigationButtonPressed,
28
 			 ModalDismissed,
29
 			 ModalDismissed,
29
 			 SearchBarUpdated,
30
 			 SearchBarUpdated,
30
-			 SearchBarCancelPressed];
31
+			 SearchBarCancelPressed,
32
+			 PreviewCompleted];
31
 }
33
 }
32
 
34
 
33
 # pragma mark public
35
 # pragma mark public
92
 											}];
94
 											}];
93
 }
95
 }
94
 
96
 
97
+- (void)sendOnPreviewCompleted:(NSString *)componentId previewComponentId:(NSString *)previewComponentId {
98
+	[self send:PreviewCompleted body:@{
99
+											 @"componentId": componentId,
100
+											 @"previewComponentId": previewComponentId
101
+                       }];
102
+}
103
+
95
 - (void)sendModalsDismissedEvent:(NSString *)componentId numberOfModalsDismissed:(NSNumber *)modalsDismissed {
104
 - (void)sendModalsDismissedEvent:(NSString *)componentId numberOfModalsDismissed:(NSNumber *)modalsDismissed {
96
 	[self send:ModalDismissed body:@{
105
 	[self send:ModalDismissed body:@{
97
 											 @"componentId": componentId,
106
 											 @"componentId": componentId,

+ 1
- 1
lib/ios/RNNPreviewOptions.h View File

2
 
2
 
3
 @interface RNNPreviewOptions : RNNOptions
3
 @interface RNNPreviewOptions : RNNOptions
4
 
4
 
5
-@property (nonatomic, strong) NSString* elementId;
5
+@property (nonatomic, strong) NSNumber* reactTag;
6
 @property (nonatomic, strong) NSNumber* width;
6
 @property (nonatomic, strong) NSNumber* width;
7
 @property (nonatomic, strong) NSNumber* height;
7
 @property (nonatomic, strong) NSNumber* height;
8
 @property (nonatomic, strong) NSNumber* commit;
8
 @property (nonatomic, strong) NSNumber* commit;

+ 2
- 1
lib/ios/RNNRootViewController.h View File

8
 #import "RNNUIBarButtonItem.h"
8
 #import "RNNUIBarButtonItem.h"
9
 
9
 
10
 typedef void (^RNNReactViewReadyCompletionBlock)(void);
10
 typedef void (^RNNReactViewReadyCompletionBlock)(void);
11
+typedef void (^PreviewCallback)(UIViewController *vc);
11
 
12
 
12
 @interface RNNRootViewController : UIViewController	<UIViewControllerPreviewingDelegate, UISearchResultsUpdating, UISearchBarDelegate, UINavigationControllerDelegate, UISplitViewControllerDelegate>
13
 @interface RNNRootViewController : UIViewController	<UIViewControllerPreviewingDelegate, UISearchResultsUpdating, UISearchBarDelegate, UINavigationControllerDelegate, UISplitViewControllerDelegate>
13
 
14
 
17
 @property (nonatomic) id<RNNRootViewCreator> creator;
18
 @property (nonatomic) id<RNNRootViewCreator> creator;
18
 @property (nonatomic, strong) RNNAnimator* animator;
19
 @property (nonatomic, strong) RNNAnimator* animator;
19
 @property (nonatomic, strong) UIViewController* previewController;
20
 @property (nonatomic, strong) UIViewController* previewController;
20
-
21
+@property (nonatomic, copy) PreviewCallback previewCallback;
21
 
22
 
22
 - (instancetype)initWithName:(NSString*)name
23
 - (instancetype)initWithName:(NSString*)name
23
 				 withOptions:(RNNNavigationOptions*)options
24
 				 withOptions:(RNNNavigationOptions*)options

+ 4
- 17
lib/ios/RNNRootViewController.m View File

23
 
23
 
24
 @implementation RNNRootViewController
24
 @implementation RNNRootViewController
25
 
25
 
26
+@synthesize previewCallback;
27
+
26
 -(instancetype)initWithName:(NSString*)name
28
 -(instancetype)initWithName:(NSString*)name
27
 				withOptions:(RNNNavigationOptions*)options
29
 				withOptions:(RNNNavigationOptions*)options
28
 			withComponentId:(NSString*)componentId
30
 			withComponentId:(NSString*)componentId
301
 }
303
 }
302
 
304
 
303
 - (UIViewController *)previewingContext:(id<UIViewControllerPreviewing>)previewingContext viewControllerForLocation:(CGPoint)location{
305
 - (UIViewController *)previewingContext:(id<UIViewControllerPreviewing>)previewingContext viewControllerForLocation:(CGPoint)location{
304
-	if (self.previewController) {
305
-		//		RNNRootViewController * vc = (RNNRootViewController*) self.previewController;
306
-		//		[_eventEmitter sendOnNavigationEvent:@"previewContext" params:@{
307
-		//																		@"previewComponentId": vc.componentId,
308
-		//																		@"componentId": self.componentId
309
-		//																		}];
310
-	}
311
 	return self.previewController;
306
 	return self.previewController;
312
 }
307
 }
313
 
308
 
314
 
309
 
315
 - (void)previewingContext:(id<UIViewControllerPreviewing>)previewingContext commitViewController:(UIViewController *)viewControllerToCommit {
310
 - (void)previewingContext:(id<UIViewControllerPreviewing>)previewingContext commitViewController:(UIViewController *)viewControllerToCommit {
316
-	RNNRootViewController * vc = (RNNRootViewController*) self.previewController;
317
-	//	NSDictionary * params = @{
318
-	//							  @"previewComponentId": vc.componentId,
319
-	//							  @"componentId": self.componentId
320
-	//							  };
321
-	if (vc.options.preview.commit) {
322
-		//		[_eventEmitter sendOnNavigationEvent:@"previewCommit" params:params];
323
-		[self.navigationController pushViewController:vc animated:false];
324
-	} else {
325
-		//		[_eventEmitter sendOnNavigationEvent:@"previewDismissed" params:params];
311
+	if (self.previewCallback) {
312
+		self.previewCallback(self);
326
 	}
313
 	}
327
 }
314
 }
328
 
315
 

+ 1
- 0
lib/ios/ReactNativeNavigation.h View File

1
 #import <Foundation/Foundation.h>
1
 #import <Foundation/Foundation.h>
2
 #import <UIKit/UIKit.h>
2
 #import <UIKit/UIKit.h>
3
 #import <React/RCTBridge.h>
3
 #import <React/RCTBridge.h>
4
+#import <React/RCTUIManager.h>
4
 #import "RNNBridgeManagerDelegate.h"
5
 #import "RNNBridgeManagerDelegate.h"
5
 
6
 
6
 typedef UIViewController * (^RNNExternalViewCreator)(NSDictionary* props, RCTBridge* bridge);
7
 typedef UIViewController * (^RNNExternalViewCreator)(NSDictionary* props, RCTBridge* bridge);

+ 3
- 0
lib/src/Navigation.ts View File

13
 import { Constants } from './adapters/Constants';
13
 import { Constants } from './adapters/Constants';
14
 import { ComponentType } from 'react';
14
 import { ComponentType } from 'react';
15
 import { ComponentEventsObserver } from './events/ComponentEventsObserver';
15
 import { ComponentEventsObserver } from './events/ComponentEventsObserver';
16
+import { TouchablePreview } from './adapters/TouchablePreview';
16
 import { LayoutRoot, Layout } from './interfaces/Layout';
17
 import { LayoutRoot, Layout } from './interfaces/Layout';
17
 import { Options } from './interfaces/Options';
18
 import { Options } from './interfaces/Options';
18
 
19
 
19
 export class Navigation {
20
 export class Navigation {
20
   public readonly Element: React.ComponentType<{ elementId: any; resizeMode?: any; }>;
21
   public readonly Element: React.ComponentType<{ elementId: any; resizeMode?: any; }>;
22
+  public readonly TouchablePreview: React.ComponentType<any>;
21
   public readonly store: Store;
23
   public readonly store: Store;
22
   private readonly nativeEventsReceiver: NativeEventsReceiver;
24
   private readonly nativeEventsReceiver: NativeEventsReceiver;
23
   private readonly uniqueIdProvider: UniqueIdProvider;
25
   private readonly uniqueIdProvider: UniqueIdProvider;
32
 
34
 
33
   constructor() {
35
   constructor() {
34
     this.Element = Element;
36
     this.Element = Element;
37
+    this.TouchablePreview = TouchablePreview;
35
     this.store = new Store();
38
     this.store = new Store();
36
     this.nativeEventsReceiver = new NativeEventsReceiver();
39
     this.nativeEventsReceiver = new NativeEventsReceiver();
37
     this.uniqueIdProvider = new UniqueIdProvider();
40
     this.uniqueIdProvider = new UniqueIdProvider();

+ 5
- 0
lib/src/adapters/NativeEventsReceiver.ts View File

6
   NavigationButtonPressedEvent,
6
   NavigationButtonPressedEvent,
7
   SearchBarUpdatedEvent,
7
   SearchBarUpdatedEvent,
8
   SearchBarCancelPressedEvent,
8
   SearchBarCancelPressedEvent,
9
+  PreviewCompletedEvent,
9
   ModalDismissedEvent
10
   ModalDismissedEvent
10
 } from '../interfaces/ComponentEvents';
11
 } from '../interfaces/ComponentEvents';
11
 import { CommandCompletedEvent, BottomTabSelectedEvent } from '../interfaces/Events';
12
 import { CommandCompletedEvent, BottomTabSelectedEvent } from '../interfaces/Events';
54
     return this.emitter.addListener('RNN.SearchBarCancelPressed', callback);
55
     return this.emitter.addListener('RNN.SearchBarCancelPressed', callback);
55
   }
56
   }
56
 
57
 
58
+  public registerPreviewCompletedListener(callback: (event: PreviewCompletedEvent) => void): EventSubscription {
59
+    return this.emitter.addListener('RNN.PreviewCompleted', callback);
60
+  }
61
+
57
   public registerCommandCompletedListener(callback: (data: CommandCompletedEvent) => void): EventSubscription {
62
   public registerCommandCompletedListener(callback: (data: CommandCompletedEvent) => void): EventSubscription {
58
     return this.emitter.addListener('RNN.CommandCompleted', callback);
63
     return this.emitter.addListener('RNN.CommandCompleted', callback);
59
   }
64
   }

+ 144
- 0
lib/src/adapters/TouchablePreview.tsx View File

1
+import * as React from 'react';
2
+import * as PropTypes from 'prop-types';
3
+import {
4
+  View,
5
+  Platform,
6
+  findNodeHandle,
7
+  TouchableOpacity,
8
+  TouchableHighlight,
9
+  TouchableNativeFeedback,
10
+  TouchableWithoutFeedback,
11
+  GestureResponderEvent,
12
+  NativeTouchEvent,
13
+  NativeSyntheticEvent,
14
+} from 'react-native';
15
+
16
+// Polyfill GestureResponderEvent type with additional `force` property (iOS)
17
+interface NativeTouchEventWithForce extends NativeTouchEvent { force: number; }
18
+interface GestureResponderEventWithForce extends NativeSyntheticEvent<NativeTouchEventWithForce> {}
19
+
20
+export interface Props {
21
+  children: React.ReactNode;
22
+  touchableComponent?: TouchableHighlight | TouchableOpacity | TouchableNativeFeedback | TouchableWithoutFeedback | React.ReactNode;
23
+  onPress?: () => void;
24
+  onPressIn?: (reactTag?) => void;
25
+  onPeekIn?: () => void;
26
+  onPeekOut?: () => void;
27
+}
28
+
29
+const PREVIEW_DELAY = 350;
30
+const PREVIEW_MIN_FORCE = 0.1;
31
+const PREVIEW_TIMEOUT = 1250;
32
+
33
+export class TouchablePreview extends React.PureComponent<Props, any> {
34
+
35
+  static propTypes = {
36
+    children: PropTypes.node,
37
+    touchableComponent: PropTypes.func,
38
+    onPress: PropTypes.func,
39
+    onPressIn: PropTypes.func,
40
+    onPeekIn: PropTypes.func,
41
+    onPeekOut: PropTypes.func,
42
+  };
43
+
44
+  static defaultProps = {
45
+    touchableComponent: TouchableWithoutFeedback,
46
+  };
47
+
48
+  static peeking = false;
49
+
50
+  private ref: React.Component<any> | null = null;
51
+  private timeout: number | undefined;
52
+  private ts: number = 0;
53
+
54
+  onRef = (ref: React.Component<any>) => {
55
+    this.ref = ref;
56
+  }
57
+
58
+  onPress = () => {
59
+    const { onPress } = this.props;
60
+
61
+    if (typeof onPress !== 'function' || TouchablePreview.peeking) {
62
+      return;
63
+    }
64
+
65
+    return onPress();
66
+  }
67
+
68
+  onPressIn = () => {
69
+    if (Platform.OS === 'ios') {
70
+      const { onPressIn } = this.props;
71
+
72
+      if (!onPressIn) {
73
+        return;
74
+      }
75
+
76
+      const reactTag = findNodeHandle(this.ref);
77
+
78
+      return onPressIn({ reactTag });
79
+    }
80
+
81
+    // Other platforms don't support 3D Touch Preview API
82
+    return null;
83
+  }
84
+
85
+  onTouchStart = (event: GestureResponderEvent) => {
86
+    // Store a timstamp of the initial touch start
87
+    this.ts = event.nativeEvent.timestamp;
88
+  }
89
+
90
+  onTouchMove = (event: GestureResponderEventWithForce) => {
91
+    clearTimeout(this.timeout);
92
+    const { force, timestamp } = event.nativeEvent;
93
+    const diff = (timestamp - this.ts);
94
+
95
+    if (force > PREVIEW_MIN_FORCE && diff > PREVIEW_DELAY) {
96
+      TouchablePreview.peeking = true;
97
+
98
+      if (typeof this.props.onPeekIn === 'function') {
99
+        this.props.onPeekIn();
100
+      }
101
+    }
102
+
103
+    this.timeout = setTimeout(this.onTouchEnd, PREVIEW_TIMEOUT);
104
+  }
105
+
106
+  onTouchEnd = () => {
107
+    clearTimeout(this.timeout);
108
+    TouchablePreview.peeking = false;
109
+
110
+    if (typeof this.props.onPeekOut === 'function') {
111
+      this.props.onPeekOut();
112
+    }
113
+  }
114
+
115
+  render() {
116
+    const { children, touchableComponent, onPress, onPressIn, ...props } = this.props;
117
+
118
+    // Default to TouchableWithoutFeedback for iOS if set to TouchableNativeFeedback
119
+    const Touchable = (
120
+      Platform.OS === 'ios' && touchableComponent instanceof TouchableNativeFeedback
121
+        ? TouchableWithoutFeedback
122
+        : touchableComponent
123
+    ) as typeof TouchableWithoutFeedback;
124
+
125
+    // Wrap component with Touchable for handling platform touches
126
+    // and a single react View for detecting force and timing.
127
+    return (
128
+      <Touchable
129
+        ref={this.onRef}
130
+        onPress={this.onPress}
131
+        onPressIn={this.onPressIn}
132
+        {...props}
133
+      >
134
+        <View
135
+          onTouchStart={this.onTouchStart}
136
+          onTouchMove={this.onTouchMove as (event: GestureResponderEvent) => void}
137
+          onTouchEnd={this.onTouchEnd}
138
+        >
139
+          {children}
140
+        </View>
141
+      </Touchable>
142
+    );
143
+  }
144
+}

+ 11
- 0
lib/src/events/ComponentEventsObserver.test.tsx View File

13
   const navigationButtonPressedFn = jest.fn();
13
   const navigationButtonPressedFn = jest.fn();
14
   const searchBarUpdatedFn = jest.fn();
14
   const searchBarUpdatedFn = jest.fn();
15
   const searchBarCancelPressedFn = jest.fn();
15
   const searchBarCancelPressedFn = jest.fn();
16
+  const previewCompletedFn = jest.fn();
16
   const modalDismissedFn = jest.fn();
17
   const modalDismissedFn = jest.fn();
17
   let subscription;
18
   let subscription;
18
 
19
 
60
       searchBarCancelPressedFn(event);
61
       searchBarCancelPressedFn(event);
61
     }
62
     }
62
 
63
 
64
+    previewCompleted(event) {
65
+      previewCompletedFn(event);
66
+    }
67
+
63
     render() {
68
     render() {
64
       return 'Hello';
69
       return 'Hello';
65
     }
70
     }
106
     expect(searchBarCancelPressedFn).toHaveBeenCalledTimes(1);
111
     expect(searchBarCancelPressedFn).toHaveBeenCalledTimes(1);
107
     expect(searchBarCancelPressedFn).toHaveBeenCalledWith({ componentId: 'myCompId' });
112
     expect(searchBarCancelPressedFn).toHaveBeenCalledWith({ componentId: 'myCompId' });
108
 
113
 
114
+    uut.notifyPreviewCompleted({ componentId: 'myCompId' });
115
+    expect(previewCompletedFn).toHaveBeenCalledTimes(1);
116
+    expect(previewCompletedFn).toHaveBeenCalledWith({ componentId: 'myCompId' });
117
+
109
     tree.unmount();
118
     tree.unmount();
110
     expect(willUnmountFn).toHaveBeenCalledTimes(1);
119
     expect(willUnmountFn).toHaveBeenCalledTimes(1);
111
   });
120
   });
181
     expect(mockEventsReceiver.registerNavigationButtonPressedListener).not.toHaveBeenCalled();
190
     expect(mockEventsReceiver.registerNavigationButtonPressedListener).not.toHaveBeenCalled();
182
     expect(mockEventsReceiver.registerSearchBarUpdatedListener).not.toHaveBeenCalled();
191
     expect(mockEventsReceiver.registerSearchBarUpdatedListener).not.toHaveBeenCalled();
183
     expect(mockEventsReceiver.registerSearchBarCancelPressedListener).not.toHaveBeenCalled();
192
     expect(mockEventsReceiver.registerSearchBarCancelPressedListener).not.toHaveBeenCalled();
193
+    expect(mockEventsReceiver.registerPreviewCompletedListener).not.toHaveBeenCalled();
184
     uut.registerOnceForAllComponentEvents();
194
     uut.registerOnceForAllComponentEvents();
185
     uut.registerOnceForAllComponentEvents();
195
     uut.registerOnceForAllComponentEvents();
186
     uut.registerOnceForAllComponentEvents();
196
     uut.registerOnceForAllComponentEvents();
190
     expect(mockEventsReceiver.registerNavigationButtonPressedListener).toHaveBeenCalledTimes(1);
200
     expect(mockEventsReceiver.registerNavigationButtonPressedListener).toHaveBeenCalledTimes(1);
191
     expect(mockEventsReceiver.registerSearchBarUpdatedListener).toHaveBeenCalledTimes(1);
201
     expect(mockEventsReceiver.registerSearchBarUpdatedListener).toHaveBeenCalledTimes(1);
192
     expect(mockEventsReceiver.registerSearchBarCancelPressedListener).toHaveBeenCalledTimes(1);
202
     expect(mockEventsReceiver.registerSearchBarCancelPressedListener).toHaveBeenCalledTimes(1);
203
+    expect(mockEventsReceiver.registerPreviewCompletedListener).toHaveBeenCalledTimes(1);
193
   });
204
   });
194
 
205
 
195
   it(`warn when button event is not getting handled`, () => {
206
   it(`warn when button event is not getting handled`, () => {

+ 7
- 0
lib/src/events/ComponentEventsObserver.ts View File

7
   SearchBarUpdatedEvent,
7
   SearchBarUpdatedEvent,
8
   SearchBarCancelPressedEvent,
8
   SearchBarCancelPressedEvent,
9
   ComponentEvent,
9
   ComponentEvent,
10
+  PreviewCompletedEvent,
10
   ModalDismissedEvent
11
   ModalDismissedEvent
11
 } from '../interfaces/ComponentEvents';
12
 } from '../interfaces/ComponentEvents';
12
 import { NativeEventsReceiver } from '../adapters/NativeEventsReceiver';
13
 import { NativeEventsReceiver } from '../adapters/NativeEventsReceiver';
22
     this.notifyModalDismissed = this.notifyModalDismissed.bind(this);
23
     this.notifyModalDismissed = this.notifyModalDismissed.bind(this);
23
     this.notifySearchBarUpdated = this.notifySearchBarUpdated.bind(this);
24
     this.notifySearchBarUpdated = this.notifySearchBarUpdated.bind(this);
24
     this.notifySearchBarCancelPressed = this.notifySearchBarCancelPressed.bind(this);
25
     this.notifySearchBarCancelPressed = this.notifySearchBarCancelPressed.bind(this);
26
+    this.notifyPreviewCompleted = this.notifyPreviewCompleted.bind(this);
25
   }
27
   }
26
 
28
 
27
   public registerOnceForAllComponentEvents() {
29
   public registerOnceForAllComponentEvents() {
33
     this.nativeEventsReceiver.registerModalDismissedListener(this.notifyModalDismissed);
35
     this.nativeEventsReceiver.registerModalDismissedListener(this.notifyModalDismissed);
34
     this.nativeEventsReceiver.registerSearchBarUpdatedListener(this.notifySearchBarUpdated);
36
     this.nativeEventsReceiver.registerSearchBarUpdatedListener(this.notifySearchBarUpdated);
35
     this.nativeEventsReceiver.registerSearchBarCancelPressedListener(this.notifySearchBarCancelPressed);
37
     this.nativeEventsReceiver.registerSearchBarCancelPressedListener(this.notifySearchBarCancelPressed);
38
+    this.nativeEventsReceiver.registerPreviewCompletedListener(this.notifyPreviewCompleted);
36
   }
39
   }
37
 
40
 
38
   public bindComponent(component: React.Component<any>): EventSubscription {
41
   public bindComponent(component: React.Component<any>): EventSubscription {
81
     this.triggerOnAllListenersByComponentId(event, 'searchBarCancelPressed');
84
     this.triggerOnAllListenersByComponentId(event, 'searchBarCancelPressed');
82
   }
85
   }
83
 
86
 
87
+  notifyPreviewCompleted(event: PreviewCompletedEvent) {
88
+    this.triggerOnAllListenersByComponentId(event, 'previewCompleted');
89
+  }
90
+
84
   private triggerOnAllListenersByComponentId(event: ComponentEvent, method: string) {
91
   private triggerOnAllListenersByComponentId(event: ComponentEvent, method: string) {
85
     let listenersTriggered = 0;
92
     let listenersTriggered = 0;
86
     _.forEach(this.listeners[event.componentId], (component) => {
93
     _.forEach(this.listeners[event.componentId], (component) => {

+ 7
- 0
lib/src/events/EventsRegistry.test.tsx View File

81
     expect(mockNativeEventsReceiver.registerSearchBarCancelPressedListener).toHaveBeenCalledWith(cb);
81
     expect(mockNativeEventsReceiver.registerSearchBarCancelPressedListener).toHaveBeenCalledWith(cb);
82
   });
82
   });
83
 
83
 
84
+  it('delegates previewCompleted to nativeEventsReceiver', () => {
85
+    const cb = jest.fn();
86
+    uut.registerPreviewCompletedListener(cb);
87
+    expect(mockNativeEventsReceiver.registerPreviewCompletedListener).toHaveBeenCalledTimes(1);
88
+    expect(mockNativeEventsReceiver.registerPreviewCompletedListener).toHaveBeenCalledWith(cb);
89
+  });
90
+
84
   it('delegates registerCommandListener to commandObserver', () => {
91
   it('delegates registerCommandListener to commandObserver', () => {
85
     const cb = jest.fn();
92
     const cb = jest.fn();
86
     const result = uut.registerCommandListener(cb);
93
     const result = uut.registerCommandListener(cb);

+ 5
- 0
lib/src/events/EventsRegistry.ts View File

8
   NavigationButtonPressedEvent,
8
   NavigationButtonPressedEvent,
9
   SearchBarUpdatedEvent,
9
   SearchBarUpdatedEvent,
10
   SearchBarCancelPressedEvent,
10
   SearchBarCancelPressedEvent,
11
+  PreviewCompletedEvent,
11
   ModalDismissedEvent
12
   ModalDismissedEvent
12
 } from '../interfaces/ComponentEvents';
13
 } from '../interfaces/ComponentEvents';
13
 import { CommandCompletedEvent, BottomTabSelectedEvent } from '../interfaces/Events';
14
 import { CommandCompletedEvent, BottomTabSelectedEvent } from '../interfaces/Events';
51
     return this.nativeEventsReceiver.registerSearchBarCancelPressedListener(callback);
52
     return this.nativeEventsReceiver.registerSearchBarCancelPressedListener(callback);
52
   }
53
   }
53
 
54
 
55
+  public registerPreviewCompletedListener(callback: (event: PreviewCompletedEvent) => void): EventSubscription {
56
+    return this.nativeEventsReceiver.registerPreviewCompletedListener(callback);
57
+  }
58
+
54
   public registerCommandListener(callback: (name: string, params: any) => void): EventSubscription {
59
   public registerCommandListener(callback: (name: string, params: any) => void): EventSubscription {
55
     return this.commandsObserver.register(callback);
60
     return this.commandsObserver.register(callback);
56
   }
61
   }

+ 5
- 0
lib/src/interfaces/ComponentEvents.ts View File

26
 export interface SearchBarCancelPressedEvent extends ComponentEvent {
26
 export interface SearchBarCancelPressedEvent extends ComponentEvent {
27
   componentName?: string;
27
   componentName?: string;
28
 }
28
 }
29
+
30
+export interface PreviewCompletedEvent extends ComponentEvent {
31
+  componentName?: string;
32
+  previewComponentId?: string;
33
+}

+ 7
- 1
playground/src/screens/Button.js View File

2
 const { Component } = require('react');
2
 const { Component } = require('react');
3
 const PropTypes = require('prop-types');
3
 const PropTypes = require('prop-types');
4
 const { Platform, ColorPropType, StyleSheet, TouchableNativeFeedback, TouchableOpacity, View, Text } = require('react-native');
4
 const { Platform, ColorPropType, StyleSheet, TouchableNativeFeedback, TouchableOpacity, View, Text } = require('react-native');
5
+const { Navigation } = require('react-native-navigation');
5
 
6
 
6
 class Button extends Component {
7
 class Button extends Component {
7
 
8
 
47
 
48
 
48
     const formattedTitle =
49
     const formattedTitle =
49
       Platform.OS === 'android' ? title.toUpperCase() : title;
50
       Platform.OS === 'android' ? title.toUpperCase() : title;
50
-    const Touchable =
51
+    let Touchable =
51
       Platform.OS === 'android' ? TouchableNativeFeedback : TouchableOpacity;
52
       Platform.OS === 'android' ? TouchableNativeFeedback : TouchableOpacity;
53
+
54
+    if (typeof onPressIn === 'function') {
55
+      Touchable = Navigation.TouchablePreview;
56
+    }
57
+
52
     return (
58
     return (
53
       <Touchable
59
       <Touchable
54
         accessibilityComponentType='button'
60
         accessibilityComponentType='button'

+ 3
- 7
playground/src/screens/PushedScreen.js View File

83
         <Text testID={testIDs.PUSHED_SCREEN_HEADER} style={styles.h1}>{`Pushed Screen`}</Text>
83
         <Text testID={testIDs.PUSHED_SCREEN_HEADER} style={styles.h1}>{`Pushed Screen`}</Text>
84
         <Text style={styles.h2}>{`Stack Position: ${stackPosition}`}</Text>
84
         <Text style={styles.h2}>{`Stack Position: ${stackPosition}`}</Text>
85
         <Button title='Push' testID={testIDs.PUSH_BUTTON} onPress={this.onClickPush} />
85
         <Button title='Push' testID={testIDs.PUSH_BUTTON} onPress={this.onClickPush} />
86
-        {Platform.OS === 'ios' && (
87
-          <Navigation.Element elementId='PreviewElement'>
88
-            <Button testID={testIDs.SHOW_PREVIEW_BUTTON} onPress={this.onClickPush} onPressIn={this.onClickShowPreview} title='Push Preview' />
89
-          </Navigation.Element>
90
-        )}
86
+        {Platform.OS === 'ios' && <Button testID={testIDs.SHOW_PREVIEW_BUTTON} onPress={this.onClickPush} onPressIn={this.onClickShowPreview} title='Push Preview' />}
91
         <Button title='Pop' testID={testIDs.POP_BUTTON} onPress={this.onClickPop} />
87
         <Button title='Pop' testID={testIDs.POP_BUTTON} onPress={this.onClickPop} />
92
         <Button title='Pop Previous' testID={testIDs.POP_PREVIOUS_BUTTON} onPress={this.onClickPopPrevious} />
88
         <Button title='Pop Previous' testID={testIDs.POP_PREVIOUS_BUTTON} onPress={this.onClickPopPrevious} />
93
         <Button title='Pop To Root' testID={testIDs.POP_TO_ROOT} onPress={this.onClickPopToRoot} />
89
         <Button title='Pop To Root' testID={testIDs.POP_TO_ROOT} onPress={this.onClickPopToRoot} />
99
     );
95
     );
100
   }
96
   }
101
 
97
 
102
-  onClickShowPreview = async () => {
98
+  onClickShowPreview = async ({ reactTag }) => {
103
     await Navigation.push(this.props.componentId, {
99
     await Navigation.push(this.props.componentId, {
104
       component: {
100
       component: {
105
         name: 'navigation.playground.PushedScreen',
101
         name: 'navigation.playground.PushedScreen',
119
             }
115
             }
120
           },
116
           },
121
           preview: {
117
           preview: {
122
-            elementId: 'PreviewElement',
118
+            reactTag,
123
             height: 400,
119
             height: 400,
124
             commit: true,
120
             commit: true,
125
             actions: [{
121
             actions: [{

+ 5
- 9
playground/src/screens/WelcomeScreen.js View File

41
           <Button title='Push Lifecycle Screen' testID={testIDs.PUSH_LIFECYCLE_BUTTON} onPress={this.onClickLifecycleScreen} />
41
           <Button title='Push Lifecycle Screen' testID={testIDs.PUSH_LIFECYCLE_BUTTON} onPress={this.onClickLifecycleScreen} />
42
           <Button title='Static Lifecycle Events' testID={testIDs.PUSH_STATIC_LIFECYCLE_BUTTON} onPress={this.onClickShowStaticLifecycleOverlay} />
42
           <Button title='Static Lifecycle Events' testID={testIDs.PUSH_STATIC_LIFECYCLE_BUTTON} onPress={this.onClickShowStaticLifecycleOverlay} />
43
           <Button title='Push' testID={testIDs.PUSH_BUTTON} onPress={this.onClickPush} />
43
           <Button title='Push' testID={testIDs.PUSH_BUTTON} onPress={this.onClickPush} />
44
-          {Platform.OS === 'ios' && (
45
-            <Navigation.Element elementId='PreviewElement'>
46
-              <Button testID={testIDs.SHOW_PREVIEW_BUTTON} onPressIn={this.onClickShowPreview} title='Push Preview' />
47
-            </Navigation.Element>
48
-          )}
44
+          {Platform.OS === 'ios' && <Button testID={testIDs.SHOW_PREVIEW_BUTTON} onPress={this.onClickPush} onPressIn={this.onClickShowPreview} title='Push Preview' />}
49
           <Button title='Push Options Screen' testID={testIDs.PUSH_OPTIONS_BUTTON} onPress={this.onClickPushOptionsScreen} />
45
           <Button title='Push Options Screen' testID={testIDs.PUSH_OPTIONS_BUTTON} onPress={this.onClickPushOptionsScreen} />
50
           <Button title='Push External Component' testID={testIDs.PUSH_EXTERNAL_COMPONENT_BUTTON} onPress={this.onClickPushExternalComponent} />
46
           <Button title='Push External Component' testID={testIDs.PUSH_EXTERNAL_COMPONENT_BUTTON} onPress={this.onClickPushExternalComponent} />
51
           {Platform.OS === 'android' && <Button title='Push Top Tabs screen' testID={testIDs.PUSH_TOP_TABS_BUTTON} onPress={this.onClickPushTopTabsScreen} />}
47
           {Platform.OS === 'android' && <Button title='Push Top Tabs screen' testID={testIDs.PUSH_TOP_TABS_BUTTON} onPress={this.onClickPushTopTabsScreen} />}
334
     undefined();
330
     undefined();
335
   }
331
   }
336
 
332
 
337
-  onClickShowPreview = async () => {
333
+  onClickShowPreview = async ({ reactTag }) => {
338
     await Navigation.push(this.props.componentId, {
334
     await Navigation.push(this.props.componentId, {
339
       component: {
335
       component: {
340
         name: 'navigation.playground.PushedScreen',
336
         name: 'navigation.playground.PushedScreen',
344
               enable: false
340
               enable: false
345
             }
341
             }
346
           },
342
           },
347
-          preview: {
348
-            elementId: 'PreviewElement',
343
+          preview: reactTag ? {
344
+            reactTag,
349
             height: 300,
345
             height: 300,
350
             commit: true,
346
             commit: true,
351
             actions: [{
347
             actions: [{
360
                 style: 'destructive'
356
                 style: 'destructive'
361
               }]
357
               }]
362
             }]
358
             }]
363
-          }
359
+          } : undefined,
364
         }
360
         }
365
       }
361
       }
366
     });
362
     });