Ver código fonte

Added modalAttemptedToDismiss event with tests and docs (#5832)

* Added modalAttemptedToDismiss event with tests and docs

* Typo on pageSheet modal button label

* Update ModalScreen.js

* Fixed button testID

* Revert label to fix test cases

Co-authored-by: Guy Carmeli <guyca@users.noreply.github.com>
Co-authored-by: Yogev Ben David <yogevbd@wix.com>
manicantic 4 anos atrás
pai
commit
87af42a56b

+ 16
- 0
docs/docs/events.md Ver arquivo

@@ -145,6 +145,22 @@ const modalDismissedListener = Navigation.events().registerModalDismissedListene
145 145
 modalDismissedListener.remove();
146 146
 ```
147 147
 
148
+## registerModalAttemptedToDismissListener(iOS 13+ only)
149
+Invoked only on iOS pageSheet modal when swipeToDismiss flag is set to true and modal swiped down to dismiss.
150
+
151
+```js
152
+// Subscribe
153
+const modalAttemptedToDismissListener = Navigation.events().registerModalAttemptedToDismissListener(({ componentId }) => {
154
+
155
+});
156
+...
157
+// Unsubscribe
158
+modalDismissedListener.remove();
159
+```
160
+|       Parameter         | Description |
161
+|:--------------------:|:-----|
162
+|**componentId** | Id of the modal tried to dismiss|
163
+
148 164
 ## registerScreenPoppedListener
149 165
 Invoked when screen is popped.
150 166
 

+ 4
- 0
lib/ios/RNNCommandsHandler.m Ver arquivo

@@ -354,6 +354,10 @@ static NSString* const setDefaultOptions	= @"setDefaultOptions";
354 354
 	[_eventEmitter sendModalsDismissedEvent:viewController.layoutInfo.componentId numberOfModalsDismissed:@(1)];
355 355
 }
356 356
 
357
+- (void)attemptedToDismissModal:(UIViewController *)viewController {
358
+    [_eventEmitter sendModalAttemptedToDismissEvent:viewController.layoutInfo.componentId];
359
+}
360
+
357 361
 - (void)dismissedMultipleModals:(NSArray *)viewControllers {
358 362
 	if (viewControllers && viewControllers.count) {
359 363
 		[_eventEmitter sendModalsDismissedEvent:((UIViewController *)viewControllers.lastObject).layoutInfo.componentId numberOfModalsDismissed:@(viewControllers.count)];

+ 2
- 0
lib/ios/RNNEventEmitter.h Ver arquivo

@@ -24,6 +24,8 @@
24 24
 
25 25
 - (void)sendModalsDismissedEvent:(NSString *)componentId numberOfModalsDismissed:(NSNumber *)modalsDismissed;
26 26
 
27
+- (void)sendModalAttemptedToDismissEvent:(NSString *)componentId;
28
+
27 29
 - (void)sendScreenPoppedEvent:(NSString *)componentId;
28 30
 
29 31
 

+ 9
- 1
lib/ios/RNNEventEmitter.m Ver arquivo

@@ -15,6 +15,7 @@ static NSString* const ComponentDidAppear		= @"RNN.ComponentDidAppear";
15 15
 static NSString* const ComponentDidDisappear	= @"RNN.ComponentDidDisappear";
16 16
 static NSString* const NavigationButtonPressed	= @"RNN.NavigationButtonPressed";
17 17
 static NSString* const ModalDismissed	        = @"RNN.ModalDismissed";
18
+static NSString* const ModalAttemptedToDismiss  = @"RNN.ModalAttemptedToDismiss";
18 19
 static NSString* const SearchBarUpdated 		= @"RNN.SearchBarUpdated";
19 20
 static NSString* const SearchBarCancelPressed 	= @"RNN.SearchBarCancelPressed";
20 21
 static NSString* const PreviewCompleted         = @"RNN.PreviewCompleted";
@@ -31,7 +32,8 @@ static NSString* const ScreenPopped             = @"RNN.ScreenPopped";
31 32
              SearchBarUpdated,
32 33
              SearchBarCancelPressed,
33 34
              PreviewCompleted,
34
-             ScreenPopped];
35
+             ScreenPopped,
36
+             ModalAttemptedToDismiss];
35 37
 }
36 38
 
37 39
 # pragma mark public
@@ -113,6 +115,12 @@ static NSString* const ScreenPopped             = @"RNN.ScreenPopped";
113 115
     }];
114 116
 }
115 117
 
118
+- (void)sendModalAttemptedToDismissEvent:(NSString *)componentId {
119
+    [self send:ModalAttemptedToDismiss body:@{
120
+        @"componentId": componentId,
121
+    }];
122
+}
123
+
116 124
 - (void)sendScreenPoppedEvent:(NSString *)componentId {
117 125
     [self send:ScreenPopped body:@{
118 126
         @"componentId": componentId

+ 1
- 0
lib/ios/RNNModalManager.h Ver arquivo

@@ -8,6 +8,7 @@ typedef void (^RNNTransitionRejectionBlock)(NSString *code, NSString *message, N
8 8
 @protocol RNNModalManagerDelegate <NSObject>
9 9
 
10 10
 - (void)dismissedModal:(UIViewController *)viewController;
11
+- (void)attemptedToDismissModal:(UIViewController *)viewController;
11 12
 - (void)dismissedMultipleModals:(NSArray *)viewControllers;
12 13
 
13 14
 @end

+ 4
- 0
lib/ios/RNNModalManager.m Ver arquivo

@@ -127,6 +127,10 @@
127 127
     [_delegate dismissedModal:presentationController.presentedViewController.presentedComponentViewController];
128 128
 }
129 129
 
130
+- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)presentationController {
131
+    [_delegate attemptedToDismissModal:presentationController.presentedViewController.presentedComponentViewController];
132
+}
133
+
130 134
 -(UIViewController*)topPresentedVC {
131 135
 	UIViewController *root = UIApplication.sharedApplication.delegate.window.rootViewController;
132 136
 	while(root.presentedViewController) {

+ 6
- 1
lib/src/adapters/NativeEventsReceiver.ts Ver arquivo

@@ -7,7 +7,8 @@ import {
7 7
   SearchBarCancelPressedEvent,
8 8
   PreviewCompletedEvent,
9 9
   ModalDismissedEvent,
10
-  ScreenPoppedEvent
10
+  ScreenPoppedEvent,
11
+  ModalAttemptedToDismissEvent
11 12
 } from '../interfaces/ComponentEvents';
12 13
 import { CommandCompletedEvent, BottomTabSelectedEvent } from '../interfaces/Events';
13 14
 
@@ -49,6 +50,10 @@ export class NativeEventsReceiver {
49 50
     return this.emitter.addListener('RNN.ModalDismissed', callback);
50 51
   }
51 52
 
53
+  public registerModalAttemptedToDismissListener(callback: (event: ModalAttemptedToDismissEvent) => void): EmitterSubscription {
54
+    return this.emitter.addListener('RNN.ModalAttemptedToDismiss', callback);
55
+  }
56
+
52 57
   public registerSearchBarUpdatedListener(callback: (event: SearchBarUpdatedEvent) => void): EmitterSubscription {
53 58
     return this.emitter.addListener('RNN.SearchBarUpdated', callback);
54 59
   }

+ 16
- 3
lib/src/events/ComponentEventsObserver.test.tsx Ver arquivo

@@ -18,6 +18,7 @@ describe('ComponentEventsObserver', () => {
18 18
   const searchBarCancelPressedFn = jest.fn();
19 19
   const previewCompletedFn = jest.fn();
20 20
   const modalDismissedFn = jest.fn();
21
+  const modalAttemptedToDismissFn = jest.fn();
21 22
   const screenPoppedFn = jest.fn();
22 23
   let subscription: EventSubscription;
23 24
   let uut: ComponentEventsObserver;
@@ -57,6 +58,10 @@ describe('ComponentEventsObserver', () => {
57 58
       modalDismissedFn(event);
58 59
     }
59 60
 
61
+    modalAttemptedToDismiss(event: any) {
62
+      modalAttemptedToDismissFn(event);
63
+    }
64
+
60 65
     searchBarUpdated(event: any) {
61 66
       searchBarUpdatedFn(event);
62 67
     }
@@ -108,6 +113,10 @@ describe('ComponentEventsObserver', () => {
108 113
       modalDismissedFn(event);
109 114
     }
110 115
 
116
+    modalAttemptedToDismiss(event: any) {
117
+      modalAttemptedToDismissFn(event);
118
+    }
119
+
111 120
     searchBarUpdated(event: any) {
112 121
       searchBarUpdatedFn(event);
113 122
     }
@@ -153,14 +162,14 @@ describe('ComponentEventsObserver', () => {
153 162
   });
154 163
 
155 164
   it(`bindComponent should use optional componentId if component has a componentId in props`, () => {
156
-    const tree = renderer.create(<UnboundScreen  componentId={'doNotUseThisId'} />);
165
+    const tree = renderer.create(<UnboundScreen componentId={'doNotUseThisId'} />);
157 166
     uut.bindComponent(tree.getInstance() as any, 'myCompId')
158 167
 
159 168
     expect(tree.toJSON()).toBeDefined();
160
-    
169
+
161 170
     uut.notifyComponentDidAppear({ componentId: 'dontUseThisId', componentName: 'doesnt matter', componentType: 'Component' });
162 171
     expect(didAppearFn).not.toHaveBeenCalled();
163
-    
172
+
164 173
 
165 174
     uut.notifyComponentDidAppear({ componentId: 'myCompId', componentName: 'doesnt matter', componentType: 'Component' });
166 175
     expect(didAppearFn).toHaveBeenCalledTimes(1);
@@ -188,6 +197,10 @@ describe('ComponentEventsObserver', () => {
188 197
     expect(modalDismissedFn).toHaveBeenCalledTimes(1);
189 198
     expect(modalDismissedFn).toHaveBeenLastCalledWith({ componentId: 'myCompId', modalsDismissed: 1 })
190 199
 
200
+    uut.notifyModalAttemptedToDismiss({ componentId: 'myCompId' });
201
+    expect(modalAttemptedToDismissFn).toHaveBeenCalledTimes(1);
202
+    expect(modalAttemptedToDismissFn).toHaveBeenLastCalledWith({ componentId: 'myCompId' })
203
+
191 204
     uut.notifySearchBarUpdated({ componentId: 'myCompId', text: 'theText', isFocused: true });
192 205
     expect(searchBarUpdatedFn).toHaveBeenCalledTimes(1);
193 206
     expect(searchBarUpdatedFn).toHaveBeenCalledWith({ componentId: 'myCompId', text: 'theText', isFocused: true });

+ 8
- 1
lib/src/events/ComponentEventsObserver.ts Ver arquivo

@@ -13,7 +13,8 @@ import {
13 13
   ComponentEvent,
14 14
   PreviewCompletedEvent,
15 15
   ModalDismissedEvent,
16
-  ScreenPoppedEvent
16
+  ScreenPoppedEvent,
17
+  ModalAttemptedToDismissEvent
17 18
 } from '../interfaces/ComponentEvents';
18 19
 import { NativeEventsReceiver } from '../adapters/NativeEventsReceiver';
19 20
 import { Store } from '../components/Store';
@@ -32,6 +33,7 @@ export class ComponentEventsObserver {
32 33
     this.notifyComponentDidDisappear = this.notifyComponentDidDisappear.bind(this);
33 34
     this.notifyNavigationButtonPressed = this.notifyNavigationButtonPressed.bind(this);
34 35
     this.notifyModalDismissed = this.notifyModalDismissed.bind(this);
36
+    this.notifyModalAttemptedToDismiss = this.notifyModalAttemptedToDismiss.bind(this);
35 37
     this.notifySearchBarUpdated = this.notifySearchBarUpdated.bind(this);
36 38
     this.notifySearchBarCancelPressed = this.notifySearchBarCancelPressed.bind(this);
37 39
     this.notifyPreviewCompleted = this.notifyPreviewCompleted.bind(this);
@@ -45,6 +47,7 @@ export class ComponentEventsObserver {
45 47
     this.nativeEventsReceiver.registerComponentDidDisappearListener(this.notifyComponentDidDisappear);
46 48
     this.nativeEventsReceiver.registerNavigationButtonPressedListener(this.notifyNavigationButtonPressed);
47 49
     this.nativeEventsReceiver.registerModalDismissedListener(this.notifyModalDismissed);
50
+    this.nativeEventsReceiver.registerModalAttemptedToDismissListener(this.notifyModalAttemptedToDismiss);
48 51
     this.nativeEventsReceiver.registerSearchBarUpdatedListener(this.notifySearchBarUpdated);
49 52
     this.nativeEventsReceiver.registerSearchBarCancelPressedListener(this.notifySearchBarCancelPressed);
50 53
     this.nativeEventsReceiver.registerPreviewCompletedListener(this.notifyPreviewCompleted);
@@ -87,6 +90,10 @@ export class ComponentEventsObserver {
87 90
     this.triggerOnAllListenersByComponentId(event, 'modalDismissed');
88 91
   }
89 92
 
93
+  notifyModalAttemptedToDismiss(event: ModalAttemptedToDismissEvent) {
94
+    this.triggerOnAllListenersByComponentId(event, 'modalAttemptedToDismiss');
95
+  }
96
+
90 97
   notifySearchBarUpdated(event: SearchBarUpdatedEvent) {
91 98
     this.triggerOnAllListenersByComponentId(event, 'searchBarUpdated');
92 99
   }

+ 7
- 0
lib/src/events/EventsRegistry.test.tsx Ver arquivo

@@ -68,6 +68,13 @@ describe('EventsRegistry', () => {
68 68
     expect(mockNativeEventsReceiver.registerModalDismissedListener).toHaveBeenCalledWith(cb);
69 69
   });
70 70
 
71
+  it('delegates modalAttemptedToDimiss to nativeEventsReceiver', () => {
72
+    const cb = jest.fn();
73
+    uut.registerModalAttemptedToDismissListener(cb);
74
+    expect(mockNativeEventsReceiver.registerModalAttemptedToDismissListener).toHaveBeenCalledTimes(1);
75
+    expect(mockNativeEventsReceiver.registerModalAttemptedToDismissListener).toHaveBeenCalledWith(cb);
76
+  });
77
+
71 78
   it('delegates searchBarUpdated to nativeEventsReceiver', () => {
72 79
     const cb = jest.fn();
73 80
     uut.registerSearchBarUpdatedListener(cb);

+ 6
- 1
lib/src/events/EventsRegistry.ts Ver arquivo

@@ -12,7 +12,8 @@ import {
12 12
   SearchBarCancelPressedEvent,
13 13
   PreviewCompletedEvent,
14 14
   ModalDismissedEvent,
15
-  ScreenPoppedEvent
15
+  ScreenPoppedEvent,
16
+  ModalAttemptedToDismissEvent
16 17
 } from '../interfaces/ComponentEvents';
17 18
 import { CommandCompletedEvent, BottomTabSelectedEvent } from '../interfaces/Events';
18 19
 
@@ -47,6 +48,10 @@ export class EventsRegistry {
47 48
     return this.nativeEventsReceiver.registerModalDismissedListener(callback);
48 49
   }
49 50
 
51
+  public registerModalAttemptedToDismissListener(callback: (event: ModalAttemptedToDismissEvent) => void): EmitterSubscription {
52
+    return this.nativeEventsReceiver.registerModalAttemptedToDismissListener(callback);
53
+  }
54
+
50 55
   public registerSearchBarUpdatedListener(callback: (event: SearchBarUpdatedEvent) => void): EmitterSubscription {
51 56
     return this.nativeEventsReceiver.registerSearchBarUpdatedListener(callback);
52 57
   }

+ 4
- 0
lib/src/interfaces/ComponentEvents.ts Ver arquivo

@@ -24,6 +24,10 @@ export interface ModalDismissedEvent extends ComponentEvent {
24 24
   modalsDismissed: number;
25 25
 }
26 26
 
27
+export interface ModalAttemptedToDismissEvent extends ComponentEvent {
28
+  componentId: string;
29
+}
30
+
27 31
 export interface SearchBarUpdatedEvent extends ComponentEvent {
28 32
   text: string;
29 33
   isFocused: boolean;

+ 1
- 0
playground/ios/NavigationTests/RNNCommandsHandlerTest.m Ver arquivo

@@ -103,6 +103,7 @@
103 103
 	[skipMethods addObject:@"readyToReceiveCommands"];
104 104
 	[skipMethods addObject:@".cxx_destruct"];
105 105
 	[skipMethods addObject:@"dismissedModal:"];
106
+	[skipMethods addObject:@"attemptedToDismissModal:"];
106 107
 	[skipMethods addObject:@"dismissedMultipleModals:"];
107 108
 	
108 109
 	NSMutableArray* result = [NSMutableArray new];

+ 12
- 0
playground/src/screens/ModalScreen.js Ver arquivo

@@ -34,6 +34,18 @@ class ModalScreen extends React.Component {
34 34
     };
35 35
   }
36 36
 
37
+  state = {
38
+    dimissCounter: 0,
39
+  }
40
+
41
+  componentDidMount() {
42
+    Navigation.events().bindComponent(this);
43
+  }
44
+
45
+  modalAttemptedToDismiss() {
46
+    return this.setState(state => ({ dimissCounter: state.dimissCounter + 1 }))
47
+  }
48
+
37 49
   render() {
38 50
     return (
39 51
       <Root componentId={this.props.componentId} footer={`Modal Stack Position: ${this.getModalPosition()}`}>

+ 13
- 3
playground/src/screens/NavigationScreen.js Ver arquivo

@@ -2,6 +2,7 @@ const React = require('react');
2 2
 const Root = require('../components/Root');
3 3
 const Button = require('../components/Button')
4 4
 const Navigation = require('./../services/Navigation');
5
+const { Platform } = require('react-native');
5 6
 const {
6 7
   NAVIGATION_TAB,
7 8
   MODAL_BTN,
@@ -9,11 +10,12 @@ const {
9 10
   EXTERNAL_COMP_BTN,
10 11
   SHOW_STATIC_EVENTS_SCREEN,
11 12
   SHOW_ORIENTATION_SCREEN,
12
-  SET_ROOT_BTN
13
+  SET_ROOT_BTN,
14
+  PAGE_SHEET_MODAL_BTN
13 15
 } = require('../testIDs');
14 16
 const Screens = require('./Screens');
15 17
 
16
-class NavigationScreen  extends React.Component {
18
+class NavigationScreen extends React.Component {
17 19
   static options() {
18 20
     return {
19 21
       topBar: {
@@ -34,6 +36,7 @@ class NavigationScreen  extends React.Component {
34 36
       <Root componentId={this.props.componentId}>
35 37
         <Button label='Set Root' testID={SET_ROOT_BTN} onPress={this.setRoot} />
36 38
         <Button label='Modal' testID={MODAL_BTN} onPress={this.showModal} />
39
+        {Platform.OS === 'ios' && <Button label='PageSheet modal' testID={PAGE_SHEET_MODAL_BTN} onPress={this.showPageSheetModal} />}
37 40
         <Button label='Overlay' testID={OVERLAY_BTN} onPress={this.showOverlay} />
38 41
         <Button label='External Component' testID={EXTERNAL_COMP_BTN} onPress={this.externalComponent} />
39 42
         <Button label='Static Events' testID={SHOW_STATIC_EVENTS_SCREEN} onPress={this.pushStaticEventsScreen} />
@@ -50,13 +53,20 @@ class NavigationScreen  extends React.Component {
50 53
 
51 54
   setRoot = () => Navigation.showModal(Screens.SetRoot);
52 55
   showModal = () => Navigation.showModal(Screens.Modal);
56
+
57
+  showPageSheetModal = () => Navigation.showModal(Screens.Modal, {
58
+    modalPresentationStyle: 'pageSheet',
59
+    modal: {
60
+      swipeToDismiss: false,
61
+    }
62
+  });
53 63
   showOverlay = () => Navigation.showModal(Screens.Overlay);
54 64
   externalComponent = () => Navigation.showModal(Screens.ExternalComponent);
55 65
   pushStaticEventsScreen = () => Navigation.showModal(Screens.EventsScreen)
56 66
   orientation = () => Navigation.showModal(Screens.Orientation);
57 67
   pushContextScreen = () => Navigation.push(this, Screens.ContextScreen);
58 68
   sharedElement = () => Navigation.showModal(Screens.CocktailsListScreen)
59
-  preview = ({reactTag}) => {
69
+  preview = ({ reactTag }) => {
60 70
     Navigation.push(this.props.componentId, {
61 71
       component: {
62 72
         name: Screens.Pushed,

+ 2
- 1
playground/src/testIDs.js Ver arquivo

@@ -12,6 +12,7 @@ module.exports = {
12 12
   OVERLAY_BTN: 'OVERLAY_BTN',
13 13
   SIDE_MENU_BTN: 'SIDE_MENU_BTN',
14 14
   MODAL_BTN: 'SHOW_MODAL_BUTTON',
15
+  PAGE_SHEET_MODAL_BTN: 'SHOW_PAGE_SHEET_MODAL_BUTTON',
15 16
   DISMISS_MODAL_BTN: 'DISMISS_MODAL_BUTTON',
16 17
   MODAL_SCREEN_HEADER: 'MODAL_SCREEN_HEADER',
17 18
   ALERT_BUTTON: 'ALERT_BUTTON',
@@ -129,7 +130,7 @@ module.exports = {
129 130
   SET_INTERCEPT_TOUCH: `SET_INTERCEPT_TOUCH`,
130 131
   PUSH_BOTTOM_TABS_BUTTON: `PUSH_BOTTOM_TABS_BUTTON`,
131 132
   SET_STACK_ROOT_BUTTON: `SET_STACK_ROOT_BUTTON`,
132
-  SET_ROOT:'SET_ROOT',
133
+  SET_ROOT: 'SET_ROOT',
133 134
   RESET_BUTTONS: 'RESET_BUTTONS',
134 135
   SHOW_LIFECYCLE_BTN: 'SHOW_LIFECYCLE_BTN',
135 136
   CHANGE_BUTTON_PROPS: 'CHANGE_BUTTON_PROPS',