Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions ios/RNNComponentPresenter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ - (void)applyOptions:(RNNNavigationOptions *)options {
tintColor:[options.topBar.searchBar.tintColor
withDefault:nil]
cancelText:[withDefault.topBar.searchBar.cancelText
withDefault:nil]];
withDefault:nil]
placement:[withDefault.topBar.searchBar.placement
withDefault:SearchBarPlacementStacked]];
}

[_topBarTitlePresenter applyOptions:withDefault.topBar];
Expand Down Expand Up @@ -145,7 +147,9 @@ - (void)mergeOptions:(RNNNavigationOptions *)mergeOptions
tintColor:[mergeOptions.topBar.searchBar.tintColor
withDefault:nil]
cancelText:[withDefault.topBar.searchBar.cancelText
withDefault:nil]];
withDefault:nil]
placement:[withDefault.topBar.searchBar.placement
withDefault:SearchBarPlacementStacked]];
} else {
[viewController setSearchBarVisible:NO];
}
Expand Down
2 changes: 2 additions & 0 deletions ios/RNNSearchBarOptions.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import "RNNOptions.h"
#import "RNNSearchBarPlacement.h"

@interface RNNSearchBarOptions : RNNOptions

Expand All @@ -11,5 +12,6 @@
@property(nonatomic, strong) Color *tintColor;
@property(nonatomic, strong) Text *placeholder;
@property(nonatomic, strong) Text *cancelText;
@property(nonatomic, strong) RNNSearchBarPlacement *placement;

@end
5 changes: 5 additions & 0 deletions ios/RNNSearchBarOptions.mm
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ - (instancetype)initWithDict:(NSDictionary *)dict {
self.tintColor = [ColorParser parse:dict key:@"tintColor"];
self.placeholder = [TextParser parse:dict key:@"placeholder"];
self.cancelText = [TextParser parse:dict key:@"cancelText"];
self.placement = (RNNSearchBarPlacement *)[EnumParser parse:dict
key:@"placement"
ofClass:RNNSearchBarPlacement.class];
return self;
}

Expand All @@ -36,6 +39,8 @@ - (void)mergeOptions:(RNNSearchBarOptions *)options {
self.placeholder = options.placeholder;
if (options.cancelText.hasValue)
self.cancelText = options.cancelText;
if (options.placement.hasValue)
self.placement = options.placement;
}

@end
14 changes: 14 additions & 0 deletions ios/RNNSearchBarPlacement.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#import "Enum.h"

typedef NS_ENUM(NSInteger, SearchBarPlacement) {
SearchBarPlacementStacked = 0,
SearchBarPlacementIntegrated
};

@interface RNNSearchBarPlacement: Enum

- (SearchBarPlacement)get;

- (SearchBarPlacement)withDefault:(SearchBarPlacement)defaultValue;

@end
17 changes: 17 additions & 0 deletions ios/RNNSearchBarPlacement.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#import "RNNSearchBarPlacement.h"
#import <React/RCTConvert.h>

@implementation RNNSearchBarPlacement

- (SearchBarPlacement)convertString:(NSString *)string {
return [self.class SearchBarPlacement:string];
}

RCT_ENUM_CONVERTER(SearchBarPlacement, (@{
@"stacked" : @(SearchBarPlacementStacked),
@"integrated" : @(SearchBarPlacementIntegrated)
}),
SearchBarPlacementStacked, integerValue)

@end

4 changes: 3 additions & 1 deletion ios/UIViewController+RNNOptions.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#import "RNNSearchBarPlacement.h"
#import <UIKit/UIKit.h>

@class RNNBottomTabOptions;
Expand All @@ -15,7 +16,8 @@
obscuresBackgroundDuringPresentation:(BOOL)obscuresBackgroundDuringPresentation
backgroundColor:(nullable UIColor *)backgroundColor
tintColor:(nullable UIColor *)tintColor
cancelText:(NSString *_Nullable)cancelText;
cancelText:(NSString *_Nullable)cancelText
placement:(SearchBarPlacement)placement;

- (void)setSearchBarHiddenWhenScrolling:(BOOL)searchBarHidden;

Expand Down
38 changes: 32 additions & 6 deletions ios/UIViewController+RNNOptions.mm
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ - (void)setSearchBarWithOptions:(NSString *)placeholder
obscuresBackgroundDuringPresentation:(BOOL)obscuresBackgroundDuringPresentation
backgroundColor:(nullable UIColor *)backgroundColor
tintColor:(nullable UIColor *)tintColor
cancelText:(NSString *)cancelText {
cancelText:(NSString *)cancelText
placement:(SearchBarPlacement)placement {
if (!self.navigationItem.searchController) {
UISearchController *search =
[[UISearchController alloc] initWithSearchResultsController:nil];
Expand All @@ -52,11 +53,16 @@ - (void)setSearchBarWithOptions:(NSString *)placeholder
search.searchBar.searchTextField.backgroundColor = backgroundColor;
}

if (focus) {
dispatch_async(dispatch_get_main_queue(), ^{
self.navigationItem.searchController.active = true;
[self.navigationItem.searchController.searchBar becomeFirstResponder];
});
if (@available(iOS 26.0, *)) {
if (placement == SearchBarPlacementIntegrated) {
if (focus) {
self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementIntegrated;
} else {
self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementIntegratedButton;
}
} else {
self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementStacked;
}
}

self.navigationItem.searchController = search;
Expand All @@ -65,6 +71,26 @@ - (void)setSearchBarWithOptions:(NSString *)placeholder
// Fixes #3450, otherwise, UIKit will infer the presentation context to
// be the root most view controller
self.definesPresentationContext = YES;

if (focus) {
dispatch_async(dispatch_get_main_queue(), ^{
self.navigationItem.searchController.active = true;
[self.navigationItem.searchController.searchBar becomeFirstResponder];
});
}
} else {
// Update placement on existing searchController (iOS 26+)
if (@available(iOS 26.0, *)) {
if (placement == SearchBarPlacementIntegrated) {
if (focus) {
self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementIntegrated;
} else {
self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementIntegratedButton;
}
} else {
self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementStacked;
}
}
}
}

Expand Down
45 changes: 42 additions & 3 deletions playground/e2e/SearchBar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ describe.e2e(':ios: SearchBar', () => {
await elementById(TestIDs.HIDE_SEARCH_BAR_BTN).tap();
await expect(elementByTraits(['searchField'])).toBeNotVisible();
});

it('find magnifying button in integrated placement and tap it (iOS 26+)', async () => {
try {
await expect(elementById(TestIDs.TOGGLE_PLACEMENT_BTN)).toExist();
} catch (e) {
console.log('Skipping - requires iOS 26+');
return;
}
await elementById(TestIDs.TOGGLE_PLACEMENT_BTN).tap();
await elementById(TestIDs.SHOW_SEARCH_BAR_BTN).tap();
const searchButton = element(
by.type('_UIButtonBarButton').and(by.label('Search')).withAncestor(by.type('UINavigationBar'))
);
await expect(searchButton).toBeVisible();
await searchButton.tap();
await expect(elementByTraits(['searchField'])).toBeVisible();
});
});

describe.e2e(':ios: SearchBar Modal', () => {
Expand All @@ -35,7 +52,29 @@ describe.e2e(':ios: SearchBar Modal', () => {
it('searching then exiting works', async () => {
await elementById(TestIDs.SHOW_SEARCH_BAR_BTN).tap();
await elementByTraits(['searchField']).replaceText('foo');
await elementById(TestIDs.DISMISS_MODAL_TOPBAR_BTN).tap();
await expect(elementById(TestIDs.OPTIONS_TAB)).toBeVisible();
try {
await expect(elementById(TestIDs.TOGGLE_PLACEMENT_BTN)).toExist();
} catch (e) {
await elementById(TestIDs.DISMISS_MODAL_TOPBAR_BTN).tap();
await expect(elementById(TestIDs.OPTIONS_TAB)).toBeVisible();
}
});
});

it('find magnifying button in integrated placement and tap it (iOS 26+)', async () => {
try {
await expect(elementById(TestIDs.TOGGLE_PLACEMENT_BTN)).toExist();
} catch (e) {
console.log('Skipping - requires iOS 26+');
return;
}
await elementById(TestIDs.TOGGLE_PLACEMENT_BTN).tap();
await elementById(TestIDs.SHOW_SEARCH_BAR_BTN).tap();
const searchButton = element(
by.type('UISearchBarTextField').withAncestor(by.type('_UIFloatingBarContainerView'))
);
await expect(searchButton).toExist();
await expect(element(by.type('_UISearchBarFieldEditor'))).not.toExist();
await searchButton.tap();
await expect(element(by.type('_UISearchBarFieldEditor'))).toExist();
});
});
33 changes: 31 additions & 2 deletions playground/src/screens/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import React from 'react';
import { Platform } from 'react-native';
import { NavigationProps } from 'react-native-navigation';

import Root from '../components/Root';
import Button from '../components/Button';
import Navigation from '../services/Navigation';
import testIDs from '../testIDs';

const { HIDE_TOP_BAR_BTN, SHOW_TOP_BAR_BTN, SHOW_SEARCH_BAR_BTN, HIDE_SEARCH_BAR_BTN, TOP_BAR } =
const { HIDE_TOP_BAR_BTN, SHOW_TOP_BAR_BTN, SHOW_SEARCH_BAR_BTN, HIDE_SEARCH_BAR_BTN, TOP_BAR, TOGGLE_PLACEMENT_BTN } =
testIDs;

interface Props extends NavigationProps {}
interface Props extends NavigationProps { }

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

state = {
isAndroidNavigationBarVisible: true,
placement: 'stacked' as 'stacked' | 'integrated',
};

render() {
Expand All @@ -35,6 +37,13 @@ export default class SearchBar extends React.Component<Props> {
<Button label="Show TopBar" testID={SHOW_TOP_BAR_BTN} onPress={this.showTopBar} />
<Button label="Hide SearchBar" testID={HIDE_SEARCH_BAR_BTN} onPress={this.hideSearchBar} />
<Button label="Show SearchBar" testID={SHOW_SEARCH_BAR_BTN} onPress={this.showSearchBar} />
{parseInt(String(Platform.Version), 10) >= 26 && (
<Button
label={`Toggle Placement (${this.state.placement})`}
testID={TOGGLE_PLACEMENT_BTN}
onPress={this.togglePlacement}
/>
)}
</Root>
);
}
Expand Down Expand Up @@ -67,7 +76,27 @@ export default class SearchBar extends React.Component<Props> {
topBar: {
searchBar: {
visible: true,
placement: this.state.placement,
},
},
});

togglePlacement = () => {
const newPlacement = this.state.placement === 'stacked' ? 'integrated' : 'stacked';
this.setState({ placement: newPlacement });
Navigation.mergeOptions(this, {
topBar: {
searchBar: {
visible: false,
},
},
});
Navigation.mergeOptions(this, {
topBar: {
searchBar: {
placement: newPlacement,
},
},
});
};
}
33 changes: 31 additions & 2 deletions playground/src/screens/SearchBarModal.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React from 'react';
import { Platform } from 'react-native';
import { NavigationProps } from 'react-native-navigation';
import Button from '../components/Button';
import Root from '../components/Root';
import Navigation from '../services/Navigation';
import testIDs from '../testIDs';

const { SHOW_SEARCH_BAR_BTN, HIDE_SEARCH_BAR_BTN, TOP_BAR } = testIDs;
const { SHOW_SEARCH_BAR_BTN, HIDE_SEARCH_BAR_BTN, TOP_BAR, TOGGLE_PLACEMENT_BTN } = testIDs;

interface Props extends NavigationProps {}
interface Props extends NavigationProps { }

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

state = {
isAndroidNavigationBarVisible: true,
placement: 'stacked' as 'stacked' | 'integrated',
};

render() {
Expand All @@ -33,6 +35,13 @@ export default class SearchBarModal extends React.Component<Props> {
{/* <Button label="Show TopBar" testID={SHOW_TOP_BAR_BTN} onPress={this.showTopBar} /> */}
<Button label="Hide SearchBar" testID={HIDE_SEARCH_BAR_BTN} onPress={this.hideSearchBar} />
<Button label="Show SearchBar" testID={SHOW_SEARCH_BAR_BTN} onPress={this.showSearchBar} />
{parseInt(String(Platform.Version), 10) >= 26 && (
<Button
label={`Toggle Placement (${this.state.placement})`}
testID={TOGGLE_PLACEMENT_BTN}
onPress={this.togglePlacement}
/>
)}
</Root>
);
}
Expand All @@ -51,7 +60,27 @@ export default class SearchBarModal extends React.Component<Props> {
topBar: {
searchBar: {
visible: true,
placement: this.state.placement,
},
},
});

togglePlacement = () => {
const newPlacement = this.state.placement === 'stacked' ? 'integrated' : 'stacked';
this.setState({ placement: newPlacement });
Navigation.mergeOptions(this, {
topBar: {
searchBar: {
visible: false,
},
},
});
Navigation.mergeOptions(this, {
topBar: {
searchBar: {
placement: newPlacement,
},
},
});
};
}
1 change: 1 addition & 0 deletions playground/src/testIDs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ const testIDs = {
TOGGLE_REACT_DECLARED_MODAL: 'TOGGLE_REACT_DECLARED_MODAL',
REPLACE_TAB_TEST_ID: 'REPLACE_TAB_TEST_ID',
REPLACED_TAB: 'REPLACED_TAB',
TOGGLE_PLACEMENT_BTN: 'TOGGLE_PLACEMENT_BTN',
MOUNTED_SCREENS_TEXT: 'MOUNTED_SCREENS_TEXT',

GOTO_TOPBAR_TITLE_TEST: 'GOTO_TOPBAR_TITLE_TEST',
Expand Down
Loading