Просмотр исходного кода

[v2] add onSearchBarCancelPressed Event (#3390)

Summary: This adds a new event `onSearchBarCancelPressed` that fires
when the user presses the cancel button on the search bar in the
navigation bar. `onSearchBarUpdated` does indirectly fire when the user
presses cancel, as the query will be reset to an empty string, but it is
not possible to distinguish those events from the user simply resetting
the query. My use case for this is to have a view that appears on top of
the normal content of the view controller when the search bar is
focused.

Test Plan: ran `npm run test-js` and confirmed added JS unit tests
passed.

I also tested this with my app, and confirmed the
`onSearchBarCancelPressed` method was called when I pressed the cancel
button.
Ben Hiller 6 лет назад
Родитель
Сommit
87d1be7469

+ 10
- 0
docs/docs/events.md Просмотреть файл

@@ -103,3 +103,13 @@ class MyComponent extends Component {
103 103
   }
104 104
 }
105 105
 ```
106
+
107
+## onSearchBarCancelPressed (iOS 11+ only)
108
+Called when the cancel button on the SearchBar from NavigationBar gets pressed.
109
+```js
110
+class MyComponent extends Component {
111
+  onSearchBarCancelPressed() {
112
+
113
+  }
114
+}
115
+```

+ 2
- 0
lib/ios/RNNEventEmitter.h Просмотреть файл

@@ -22,4 +22,6 @@
22 22
 
23 23
 -(void)sendOnSearchBarUpdated:(NSString *)componentId text:(NSString*)text isFocused:(BOOL)isFocused;
24 24
 
25
+-(void)sendOnSearchBarCancelPressed:(NSString *)componentId;
26
+
25 27
 @end

+ 6
- 0
lib/ios/RNNEventEmitter.m Просмотреть файл

@@ -62,6 +62,12 @@ static NSString* const navigationEvent	= @"RNN.nativeEvent";
62 62
 												  @"isFocused": @(isFocused)}}];
63 63
 }
64 64
 
65
+- (void)sendOnSearchBarCancelPressed:(NSString *)componentId {
66
+	[self send:navigationEvent body:@{@"name": @"searchBarCancelPressed",
67
+									  @"params": @{
68
+											  @"componentId": componentId}}];
69
+}
70
+
65 71
 - (void)addListener:(NSString *)eventName {
66 72
 	[super addListener:eventName];
67 73
 	if ([eventName isEqualToString:onAppLaunched]) {

+ 1
- 1
lib/ios/RNNRootViewController.h Просмотреть файл

@@ -13,7 +13,7 @@
13 13
 
14 14
 typedef void (^RNNReactViewReadyCompletionBlock)(void);
15 15
 
16
-@interface RNNRootViewController : UIViewController	<RNNRootViewProtocol, UIViewControllerPreviewingDelegate, UISearchResultsUpdating>
16
+@interface RNNRootViewController : UIViewController	<RNNRootViewProtocol, UIViewControllerPreviewingDelegate, UISearchResultsUpdating, UISearchBarDelegate>
17 17
 
18 18
 @property (nonatomic, strong) RNNNavigationOptions* options;
19 19
 @property (nonatomic, strong) RNNEventEmitter *eventEmitter;

+ 4
- 0
lib/ios/RNNRootViewController.m Просмотреть файл

@@ -105,6 +105,10 @@
105 105
 									isFocused:searchController.searchBar.isFirstResponder];
106 106
 }
107 107
 
108
+- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
109
+	[self.eventEmitter sendOnSearchBarCancelPressed:self.componentId];
110
+}
111
+
108 112
 - (void)viewDidLoad {
109 113
 	[super viewDidLoad];
110 114
 }

+ 1
- 0
lib/ios/RNNTopBarOptions.m Просмотреть файл

@@ -39,6 +39,7 @@ extern const NSInteger BLUR_TOPBAR_TAG;
39 39
 			if ([viewController conformsToProtocol:@protocol(UISearchResultsUpdating)]) {
40 40
 				[search setSearchResultsUpdater:((UIViewController <UISearchResultsUpdating> *) viewController)];
41 41
 			}
42
+			search.searchBar.delegate = (id<UISearchBarDelegate>)viewController;
42 43
 			if (self.searchBarPlaceholder) {
43 44
 				search.searchBar.placeholder = self.searchBarPlaceholder;
44 45
 			}

+ 14
- 0
lib/src/components/ComponentWrapper.test.tsx Просмотреть файл

@@ -156,6 +156,7 @@ describe('ComponentWrapper', () => {
156 156
     const componentDidDisappearCallback = jest.fn();
157 157
     const onNavigationButtonPressedCallback = jest.fn();
158 158
     const onSearchBarCallback = jest.fn();
159
+    const onSearchBarCancelCallback = jest.fn();
159 160
 
160 161
     class MyLifecycleComponent extends MyComponent {
161 162
       componentDidAppear() {
@@ -173,6 +174,10 @@ describe('ComponentWrapper', () => {
173 174
       onSearchBarUpdated() {
174 175
         onSearchBarCallback();
175 176
       }
177
+
178
+      onSearchBarCancelPressed() {
179
+        onSearchBarCancelCallback();
180
+      }
176 181
     }
177 182
 
178 183
     it('componentDidAppear, componentDidDisappear and onNavigationButtonPressed are optional', () => {
@@ -182,6 +187,7 @@ describe('ComponentWrapper', () => {
182 187
       expect(() => tree.getInstance()!.componentDidDisappear()).not.toThrow();
183 188
       expect(() => tree.getInstance()!.onNavigationButtonPressed()).not.toThrow();
184 189
       expect(() => tree.getInstance()!.onSearchBarUpdated()).not.toThrow();
190
+      expect(() => tree.getInstance()!.onSearchBarCancelPressed()).not.toThrow();
185 191
     });
186 192
 
187 193
     it('calls componentDidAppear on OriginalComponent', () => {
@@ -215,5 +221,13 @@ describe('ComponentWrapper', () => {
215 221
       tree.getInstance()!.onSearchBarUpdated();
216 222
       expect(onSearchBarCallback).toHaveBeenCalledTimes(1);
217 223
     });
224
+
225
+    it('calls onSearchBarCancelPressed on OriginalComponent', () => {
226
+      const NavigationComponent = ComponentWrapper.wrap(componentName, MyLifecycleComponent, store);
227
+      const tree = renderer.create(<NavigationComponent componentId={'component1'} />);
228
+      expect(onSearchBarCancelCallback).toHaveBeenCalledTimes(0);
229
+      tree.getInstance()!.onSearchBarCancelPressed();
230
+      expect(onSearchBarCancelCallback).toHaveBeenCalledTimes(1);
231
+    });
218 232
   });
219 233
 });

+ 6
- 0
lib/src/components/ComponentWrapper.tsx Просмотреть файл

@@ -58,6 +58,12 @@ export class ComponentWrapper {
58 58
         }
59 59
       }
60 60
 
61
+      onSearchBarCancelPressed() {
62
+        if (this.originalComponentRef.onSearchBarCancelPressed) {
63
+          this.originalComponentRef.onSearchBarCancelPressed();
64
+        }
65
+      }
66
+
61 67
       render() {
62 68
         return (
63 69
           <OriginalComponentClass

+ 24
- 1
lib/src/events/ComponentEventsObserver.test.ts Просмотреть файл

@@ -19,7 +19,8 @@ describe(`ComponentEventsObserver`, () => {
19 19
       componentDidAppear: jest.fn(),
20 20
       componentDidDisappear: jest.fn(),
21 21
       onNavigationButtonPressed: jest.fn(),
22
-      onSearchBarUpdated: jest.fn()
22
+      onSearchBarUpdated: jest.fn(),
23
+      onSearchBarCancelPressed: jest.fn()
23 24
     };
24 25
 
25 26
     store = new Store();
@@ -44,6 +45,7 @@ describe(`ComponentEventsObserver`, () => {
44 45
     expect(mockComponentRef.componentDidDisappear).toHaveBeenCalledTimes(0);
45 46
     expect(mockComponentRef.onNavigationButtonPressed).toHaveBeenCalledTimes(0);
46 47
     expect(mockComponentRef.onSearchBarUpdated).toHaveBeenCalledTimes(0);
48
+    expect(mockComponentRef.onSearchBarCancelPressed).toHaveBeenCalledTimes(0);
47 49
     uut.registerForAllComponents();
48 50
     eventRegistry.registerComponentDidAppearListener.mock.calls[0][0](refId);
49 51
     eventRegistry.registerComponentDidDisappearListener.mock.calls[0][0](refId);
@@ -52,6 +54,7 @@ describe(`ComponentEventsObserver`, () => {
52 54
     expect(mockComponentRef.componentDidDisappear).toHaveBeenCalledTimes(1);
53 55
     expect(mockComponentRef.onNavigationButtonPressed).toHaveBeenCalledTimes(0);
54 56
     expect(mockComponentRef.onSearchBarUpdated).toHaveBeenCalledTimes(0);
57
+    expect(mockComponentRef.onSearchBarCancelPressed).toHaveBeenCalledTimes(0);
55 58
   });
56 59
 
57 60
   it('bubbles onNavigationButtonPressed to component by id', () => {
@@ -95,6 +98,26 @@ describe(`ComponentEventsObserver`, () => {
95 98
     expect(mockComponentRef.onSearchBarUpdated).toHaveBeenCalledWith('query', true);
96 99
   });
97 100
 
101
+  it('bubbles onSearchBarCancelPressed to component by id', () => {
102
+    const params = {
103
+      componentId: refId,
104
+    };
105
+    expect(mockComponentRef.onSearchBarCancelPressed).toHaveBeenCalledTimes(0);
106
+    uut.registerForAllComponents();
107
+
108
+    eventRegistry.registerNativeEventListener.mock.calls[0][0]('buttonPressed', params);
109
+    expect(mockComponentRef.onSearchBarCancelPressed).toHaveBeenCalledTimes(0);
110
+
111
+    eventRegistry.registerNativeEventListener.mock.calls[0][0]('searchBarCancelPressed', params);
112
+    expect(mockComponentRef.onSearchBarCancelPressed).toHaveBeenCalledTimes(1);
113
+
114
+    const paramsForUnexisted = {
115
+      componentId: 'NOT_EXISTED',
116
+    };
117
+    eventRegistry.registerNativeEventListener.mock.calls[0][0]('searchBarCancelPressed', paramsForUnexisted);
118
+    expect(mockComponentRef.onSearchBarCancelPressed).toHaveBeenCalledTimes(1);
119
+  });
120
+
98 121
   it('defensive unknown id', () => {
99 122
     uut.registerForAllComponents();
100 123
     expect(() => {

+ 7
- 0
lib/src/events/ComponentEventsObserver.ts Просмотреть файл

@@ -3,6 +3,7 @@ import { Store } from '../components/Store';
3 3
 
4 4
 const BUTTON_PRESSED_EVENT_NAME = 'buttonPressed';
5 5
 const ON_SEARCH_BAR_UPDATED = 'searchBarUpdated';
6
+const ON_SEARCH_BAR_CANCEL_PRESSED = 'searchBarCancelPressed';
6 7
 
7 8
 export class ComponentEventsObserver {
8 9
   constructor(private eventsRegistry: EventsRegistry, private store: Store) {
@@ -44,5 +45,11 @@ export class ComponentEventsObserver {
44 45
         componentRef.onSearchBarUpdated(params.text, params.isFocused);
45 46
       }
46 47
     }
48
+    if (name === ON_SEARCH_BAR_CANCEL_PRESSED) {
49
+      const componentRef = this.store.getRefForId(params.componentId);
50
+      if (componentRef && componentRef.onSearchBarCancelPressed) {
51
+        componentRef.onSearchBarCancelPressed();
52
+      }
53
+    }
47 54
   }
48 55
 }