Skip to content

Commit bed7214

Browse files
authored
iOS 26 searchbar option added. (#8183)
* initial commit * added focus logic * SearchBar enhancement e2e test and documentation * test update for ios 26 only
1 parent 94ccaec commit bed7214

13 files changed

+225
-44
lines changed

ios/RNNComponentPresenter.mm

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ - (void)applyOptions:(RNNNavigationOptions *)options {
8181
tintColor:[options.topBar.searchBar.tintColor
8282
withDefault:nil]
8383
cancelText:[withDefault.topBar.searchBar.cancelText
84-
withDefault:nil]];
84+
withDefault:nil]
85+
placement:[withDefault.topBar.searchBar.placement
86+
withDefault:SearchBarPlacementStacked]];
8587
}
8688

8789
[_topBarTitlePresenter applyOptions:withDefault.topBar];
@@ -145,7 +147,9 @@ - (void)mergeOptions:(RNNNavigationOptions *)mergeOptions
145147
tintColor:[mergeOptions.topBar.searchBar.tintColor
146148
withDefault:nil]
147149
cancelText:[withDefault.topBar.searchBar.cancelText
148-
withDefault:nil]];
150+
withDefault:nil]
151+
placement:[withDefault.topBar.searchBar.placement
152+
withDefault:SearchBarPlacementStacked]];
149153
} else {
150154
[viewController setSearchBarVisible:NO];
151155
}

ios/RNNSearchBarOptions.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#import "RNNOptions.h"
2+
#import "RNNSearchBarPlacement.h"
23

34
@interface RNNSearchBarOptions : RNNOptions
45

@@ -11,5 +12,6 @@
1112
@property(nonatomic, strong) Color *tintColor;
1213
@property(nonatomic, strong) Text *placeholder;
1314
@property(nonatomic, strong) Text *cancelText;
15+
@property(nonatomic, strong) RNNSearchBarPlacement *placement;
1416

1517
@end

ios/RNNSearchBarOptions.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ - (instancetype)initWithDict:(NSDictionary *)dict {
1414
self.tintColor = [ColorParser parse:dict key:@"tintColor"];
1515
self.placeholder = [TextParser parse:dict key:@"placeholder"];
1616
self.cancelText = [TextParser parse:dict key:@"cancelText"];
17+
self.placement = (RNNSearchBarPlacement *)[EnumParser parse:dict
18+
key:@"placement"
19+
ofClass:RNNSearchBarPlacement.class];
1720
return self;
1821
}
1922

@@ -36,6 +39,8 @@ - (void)mergeOptions:(RNNSearchBarOptions *)options {
3639
self.placeholder = options.placeholder;
3740
if (options.cancelText.hasValue)
3841
self.cancelText = options.cancelText;
42+
if (options.placement.hasValue)
43+
self.placement = options.placement;
3944
}
4045

4146
@end

ios/RNNSearchBarPlacement.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#import "Enum.h"
2+
3+
typedef NS_ENUM(NSInteger, SearchBarPlacement) {
4+
SearchBarPlacementStacked = 0,
5+
SearchBarPlacementIntegrated
6+
};
7+
8+
@interface RNNSearchBarPlacement: Enum
9+
10+
- (SearchBarPlacement)get;
11+
12+
- (SearchBarPlacement)withDefault:(SearchBarPlacement)defaultValue;
13+
14+
@end

ios/RNNSearchBarPlacement.mm

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#import "RNNSearchBarPlacement.h"
2+
#import <React/RCTConvert.h>
3+
4+
@implementation RNNSearchBarPlacement
5+
6+
- (SearchBarPlacement)convertString:(NSString *)string {
7+
return [self.class SearchBarPlacement:string];
8+
}
9+
10+
RCT_ENUM_CONVERTER(SearchBarPlacement, (@{
11+
@"stacked" : @(SearchBarPlacementStacked),
12+
@"integrated" : @(SearchBarPlacementIntegrated)
13+
}),
14+
SearchBarPlacementStacked, integerValue)
15+
16+
@end
17+

ios/UIViewController+RNNOptions.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#import "RNNSearchBarPlacement.h"
12
#import <UIKit/UIKit.h>
23

34
@class RNNBottomTabOptions;
@@ -15,7 +16,8 @@
1516
obscuresBackgroundDuringPresentation:(BOOL)obscuresBackgroundDuringPresentation
1617
backgroundColor:(nullable UIColor *)backgroundColor
1718
tintColor:(nullable UIColor *)tintColor
18-
cancelText:(NSString *_Nullable)cancelText;
19+
cancelText:(NSString *_Nullable)cancelText
20+
placement:(SearchBarPlacement)placement;
1921

2022
- (void)setSearchBarHiddenWhenScrolling:(BOOL)searchBarHidden;
2123

ios/UIViewController+RNNOptions.mm

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ - (void)setSearchBarWithOptions:(NSString *)placeholder
3030
obscuresBackgroundDuringPresentation:(BOOL)obscuresBackgroundDuringPresentation
3131
backgroundColor:(nullable UIColor *)backgroundColor
3232
tintColor:(nullable UIColor *)tintColor
33-
cancelText:(NSString *)cancelText {
33+
cancelText:(NSString *)cancelText
34+
placement:(SearchBarPlacement)placement {
3435
if (!self.navigationItem.searchController) {
3536
UISearchController *search =
3637
[[UISearchController alloc] initWithSearchResultsController:nil];
@@ -52,11 +53,16 @@ - (void)setSearchBarWithOptions:(NSString *)placeholder
5253
search.searchBar.searchTextField.backgroundColor = backgroundColor;
5354
}
5455

55-
if (focus) {
56-
dispatch_async(dispatch_get_main_queue(), ^{
57-
self.navigationItem.searchController.active = true;
58-
[self.navigationItem.searchController.searchBar becomeFirstResponder];
59-
});
56+
if (@available(iOS 26.0, *)) {
57+
if (placement == SearchBarPlacementIntegrated) {
58+
if (focus) {
59+
self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementIntegrated;
60+
} else {
61+
self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementIntegratedButton;
62+
}
63+
} else {
64+
self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementStacked;
65+
}
6066
}
6167

6268
self.navigationItem.searchController = search;
@@ -65,6 +71,26 @@ - (void)setSearchBarWithOptions:(NSString *)placeholder
6571
// Fixes #3450, otherwise, UIKit will infer the presentation context to
6672
// be the root most view controller
6773
self.definesPresentationContext = YES;
74+
75+
if (focus) {
76+
dispatch_async(dispatch_get_main_queue(), ^{
77+
self.navigationItem.searchController.active = true;
78+
[self.navigationItem.searchController.searchBar becomeFirstResponder];
79+
});
80+
}
81+
} else {
82+
// Update placement on existing searchController (iOS 26+)
83+
if (@available(iOS 26.0, *)) {
84+
if (placement == SearchBarPlacementIntegrated) {
85+
if (focus) {
86+
self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementIntegrated;
87+
} else {
88+
self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementIntegratedButton;
89+
}
90+
} else {
91+
self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementStacked;
92+
}
93+
}
6894
}
6995
}
7096

playground/e2e/SearchBar.test.js

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ describe.e2e(':ios: SearchBar', () => {
1616
await elementById(TestIDs.HIDE_SEARCH_BAR_BTN).tap();
1717
await expect(elementByTraits(['searchField'])).toBeNotVisible();
1818
});
19+
20+
it('find magnifying button in integrated placement and tap it (iOS 26+)', async () => {
21+
try {
22+
await expect(elementById(TestIDs.TOGGLE_PLACEMENT_BTN)).toExist();
23+
} catch (e) {
24+
console.log('Skipping - requires iOS 26+');
25+
return;
26+
}
27+
await elementById(TestIDs.TOGGLE_PLACEMENT_BTN).tap();
28+
await elementById(TestIDs.SHOW_SEARCH_BAR_BTN).tap();
29+
const searchButton = element(
30+
by.type('_UIButtonBarButton').and(by.label('Search')).withAncestor(by.type('UINavigationBar'))
31+
);
32+
await expect(searchButton).toBeVisible();
33+
await searchButton.tap();
34+
await expect(elementByTraits(['searchField'])).toBeVisible();
35+
});
1936
});
2037

2138
describe.e2e(':ios: SearchBar Modal', () => {
@@ -35,7 +52,29 @@ describe.e2e(':ios: SearchBar Modal', () => {
3552
it('searching then exiting works', async () => {
3653
await elementById(TestIDs.SHOW_SEARCH_BAR_BTN).tap();
3754
await elementByTraits(['searchField']).replaceText('foo');
38-
await elementById(TestIDs.DISMISS_MODAL_TOPBAR_BTN).tap();
39-
await expect(elementById(TestIDs.OPTIONS_TAB)).toBeVisible();
55+
try {
56+
await expect(elementById(TestIDs.TOGGLE_PLACEMENT_BTN)).toExist();
57+
} catch (e) {
58+
await elementById(TestIDs.DISMISS_MODAL_TOPBAR_BTN).tap();
59+
await expect(elementById(TestIDs.OPTIONS_TAB)).toBeVisible();
60+
}
4061
});
41-
});
62+
63+
it('find magnifying button in integrated placement and tap it (iOS 26+)', async () => {
64+
try {
65+
await expect(elementById(TestIDs.TOGGLE_PLACEMENT_BTN)).toExist();
66+
} catch (e) {
67+
console.log('Skipping - requires iOS 26+');
68+
return;
69+
}
70+
await elementById(TestIDs.TOGGLE_PLACEMENT_BTN).tap();
71+
await elementById(TestIDs.SHOW_SEARCH_BAR_BTN).tap();
72+
const searchButton = element(
73+
by.type('UISearchBarTextField').withAncestor(by.type('_UIFloatingBarContainerView'))
74+
);
75+
await expect(searchButton).toExist();
76+
await expect(element(by.type('_UISearchBarFieldEditor'))).not.toExist();
77+
await searchButton.tap();
78+
await expect(element(by.type('_UISearchBarFieldEditor'))).toExist();
79+
});
80+
});

playground/src/screens/SearchBar.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import React from 'react';
2+
import { Platform } from 'react-native';
23
import { NavigationProps } from 'react-native-navigation';
34

45
import Root from '../components/Root';
56
import Button from '../components/Button';
67
import Navigation from '../services/Navigation';
78
import testIDs from '../testIDs';
89

9-
const { HIDE_TOP_BAR_BTN, SHOW_TOP_BAR_BTN, SHOW_SEARCH_BAR_BTN, HIDE_SEARCH_BAR_BTN, TOP_BAR } =
10+
const { HIDE_TOP_BAR_BTN, SHOW_TOP_BAR_BTN, SHOW_SEARCH_BAR_BTN, HIDE_SEARCH_BAR_BTN, TOP_BAR, TOGGLE_PLACEMENT_BTN } =
1011
testIDs;
1112

12-
interface Props extends NavigationProps {}
13+
interface Props extends NavigationProps { }
1314

1415
export default class SearchBar extends React.Component<Props> {
1516
static options() {
@@ -26,6 +27,7 @@ export default class SearchBar extends React.Component<Props> {
2627

2728
state = {
2829
isAndroidNavigationBarVisible: true,
30+
placement: 'stacked' as 'stacked' | 'integrated',
2931
};
3032

3133
render() {
@@ -35,6 +37,13 @@ export default class SearchBar extends React.Component<Props> {
3537
<Button label="Show TopBar" testID={SHOW_TOP_BAR_BTN} onPress={this.showTopBar} />
3638
<Button label="Hide SearchBar" testID={HIDE_SEARCH_BAR_BTN} onPress={this.hideSearchBar} />
3739
<Button label="Show SearchBar" testID={SHOW_SEARCH_BAR_BTN} onPress={this.showSearchBar} />
40+
{parseInt(String(Platform.Version), 10) >= 26 && (
41+
<Button
42+
label={`Toggle Placement (${this.state.placement})`}
43+
testID={TOGGLE_PLACEMENT_BTN}
44+
onPress={this.togglePlacement}
45+
/>
46+
)}
3847
</Root>
3948
);
4049
}
@@ -67,7 +76,27 @@ export default class SearchBar extends React.Component<Props> {
6776
topBar: {
6877
searchBar: {
6978
visible: true,
79+
placement: this.state.placement,
7080
},
7181
},
7282
});
83+
84+
togglePlacement = () => {
85+
const newPlacement = this.state.placement === 'stacked' ? 'integrated' : 'stacked';
86+
this.setState({ placement: newPlacement });
87+
Navigation.mergeOptions(this, {
88+
topBar: {
89+
searchBar: {
90+
visible: false,
91+
},
92+
},
93+
});
94+
Navigation.mergeOptions(this, {
95+
topBar: {
96+
searchBar: {
97+
placement: newPlacement,
98+
},
99+
},
100+
});
101+
};
73102
}

playground/src/screens/SearchBarModal.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import React from 'react';
2+
import { Platform } from 'react-native';
23
import { NavigationProps } from 'react-native-navigation';
34
import Button from '../components/Button';
45
import Root from '../components/Root';
56
import Navigation from '../services/Navigation';
67
import testIDs from '../testIDs';
78

8-
const { SHOW_SEARCH_BAR_BTN, HIDE_SEARCH_BAR_BTN, TOP_BAR } = testIDs;
9+
const { SHOW_SEARCH_BAR_BTN, HIDE_SEARCH_BAR_BTN, TOP_BAR, TOGGLE_PLACEMENT_BTN } = testIDs;
910

10-
interface Props extends NavigationProps {}
11+
interface Props extends NavigationProps { }
1112

1213
export default class SearchBarModal extends React.Component<Props> {
1314
static options() {
@@ -24,6 +25,7 @@ export default class SearchBarModal extends React.Component<Props> {
2425

2526
state = {
2627
isAndroidNavigationBarVisible: true,
28+
placement: 'stacked' as 'stacked' | 'integrated',
2729
};
2830

2931
render() {
@@ -33,6 +35,13 @@ export default class SearchBarModal extends React.Component<Props> {
3335
{/* <Button label="Show TopBar" testID={SHOW_TOP_BAR_BTN} onPress={this.showTopBar} /> */}
3436
<Button label="Hide SearchBar" testID={HIDE_SEARCH_BAR_BTN} onPress={this.hideSearchBar} />
3537
<Button label="Show SearchBar" testID={SHOW_SEARCH_BAR_BTN} onPress={this.showSearchBar} />
38+
{parseInt(String(Platform.Version), 10) >= 26 && (
39+
<Button
40+
label={`Toggle Placement (${this.state.placement})`}
41+
testID={TOGGLE_PLACEMENT_BTN}
42+
onPress={this.togglePlacement}
43+
/>
44+
)}
3645
</Root>
3746
);
3847
}
@@ -51,7 +60,27 @@ export default class SearchBarModal extends React.Component<Props> {
5160
topBar: {
5261
searchBar: {
5362
visible: true,
63+
placement: this.state.placement,
5464
},
5565
},
5666
});
67+
68+
togglePlacement = () => {
69+
const newPlacement = this.state.placement === 'stacked' ? 'integrated' : 'stacked';
70+
this.setState({ placement: newPlacement });
71+
Navigation.mergeOptions(this, {
72+
topBar: {
73+
searchBar: {
74+
visible: false,
75+
},
76+
},
77+
});
78+
Navigation.mergeOptions(this, {
79+
topBar: {
80+
searchBar: {
81+
placement: newPlacement,
82+
},
83+
},
84+
});
85+
};
5786
}

0 commit comments

Comments
 (0)