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
198 changes: 198 additions & 0 deletions apps/src/tests/issue-tests/Test4089.tsx
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search bar doesn't really support runtime changes for e.g. placement so we should probably have configuration on root screen and then apply the config on new pushed screen with the config.

Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import React, { useLayoutEffect, useState } from 'react';
import { Button, ScrollView, StyleSheet, Text, View } from 'react-native';
import type { SearchBarPlacement, SearchBarProps } from 'react-native-screens';

type StackParamList = {
Home: undefined;
SearchBar: undefined;
};

type PlaceholderMode = 'default' | 'custom' | 'empty';
type HintColorMode = 'default' | 'red' | 'blue';

type SearchBarConfig = {
placement: SearchBarPlacement;
placeholderMode: PlaceholderMode;
hintColorMode: HintColorMode;
allowToolbarIntegration: boolean;
};

const Stack = createNativeStackNavigator<StackParamList>();

const placements: SearchBarPlacement[] = [
'automatic',
'inline',
'stacked',
'integrated',
'integratedButton',
'integratedCentered',
];

const placeholderModes: PlaceholderMode[] = ['default', 'custom', 'empty'];
const hintColorModes: HintColorMode[] = ['default', 'red', 'blue'];

const defaultConfig: SearchBarConfig = {
placement: 'automatic',
placeholderMode: 'default',
hintColorMode: 'default',
allowToolbarIntegration: true,
};

function getNextValue<T>(values: readonly T[], currentValue: T): T {
const currentIndex = values.indexOf(currentValue);
return values[(currentIndex + 1) % values.length];
}

function getPlaceholder(mode: PlaceholderMode): SearchBarProps['placeholder'] {
switch (mode) {
case 'custom':
return 'Custom placeholder';
case 'empty':
return '';
default:
return undefined;
}
}

function getHintTextColor(
mode: HintColorMode,
): SearchBarProps['hintTextColor'] {
switch (mode) {
case 'red':
return 'red';
case 'blue':
return 'blue';
default:
return undefined;
}
}

function Home({ navigation }: NativeStackScreenProps<StackParamList, 'Home'>) {
return (
<View style={styles.centeredContainer}>
<Text style={styles.title}>Test4089</Text>
<Button
title="Open search bar test"
onPress={() => navigation.navigate('SearchBar')}
/>
</View>
);
}

function SearchBarScreen({
navigation,
}: NativeStackScreenProps<StackParamList, 'SearchBar'>) {
const [config, setConfig] = useState<SearchBarConfig>(defaultConfig);

useLayoutEffect(() => {
navigation.setOptions({
headerSearchBarOptions: {
placement: config.placement,
allowToolbarIntegration: config.allowToolbarIntegration,
hideWhenScrolling: false,
placeholder: getPlaceholder(config.placeholderMode),
hintTextColor: getHintTextColor(config.hintColorMode),
},
});
}, [config, navigation]);

return (
<ScrollView
contentInsetAdjustmentBehavior="automatic"
contentContainerStyle={styles.contentContainer}>
<Text style={styles.title}>Search bar options</Text>
<Text>Placement: {config.placement}</Text>
<Text>Placeholder: {config.placeholderMode}</Text>
<Text>Hint text color: {config.hintColorMode}</Text>
<Text>
Allow toolbar integration:{' '}
{config.allowToolbarIntegration ? 'true' : 'false'}
</Text>

<Button
title="Cycle placement"
onPress={() =>
setConfig(currentConfig => ({
...currentConfig,
placement: getNextValue(placements, currentConfig.placement),
}))
}
/>
<Button
title="Cycle placeholder"
onPress={() =>
setConfig(currentConfig => ({
...currentConfig,
placeholderMode: getNextValue(
placeholderModes,
currentConfig.placeholderMode,
),
}))
}
/>
<Button
title="Cycle hint color"
onPress={() =>
setConfig(currentConfig => ({
...currentConfig,
hintColorMode: getNextValue(
hintColorModes,
currentConfig.hintColorMode,
),
}))
}
/>
<Button
title="Toggle toolbar integration"
onPress={() =>
setConfig(currentConfig => ({
...currentConfig,
allowToolbarIntegration: !currentConfig.allowToolbarIntegration,
}))
}
/>
<Button
title="Restore default values"
onPress={() => setConfig(defaultConfig)}
/>
</ScrollView>
);
}

export default function App(): React.JSX.Element {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen
name="SearchBar"
component={SearchBarScreen}
options={{
headerLargeTitle: true,
title: 'Search bar',
}}
/>
</Stack.Navigator>
</NavigationContainer>
);
}

const styles = StyleSheet.create({
centeredContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 12,
},
contentContainer: {
padding: 16,
gap: 12,
},
title: {
fontSize: 20,
fontWeight: '600',
},
});
1 change: 1 addition & 0 deletions apps/src/tests/issue-tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export { default as Test3885 } from './Test3885';
export { default as Test3910 } from './Test3910';
export { default as Test4027 } from './Test4027';
export { default as Test4064 } from './Test4064';
export { default as Test4089 } from './Test4089';
export { default as TestScreenAnimation } from './TestScreenAnimation';
// The following test was meant to demo the "go back" gesture using Reanimated
// but the associated PR in react-navigation is currently put on hold
Expand Down
2 changes: 1 addition & 1 deletion guides/GUIDE_FOR_LIBRARY_AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ To render a search bar use `ScreenStackHeaderSearchBarView` with `<SearchBar>` c
- `placement` - Placement of the search bar in the navigation bar. (iOS only)
- `allowToolbarIntegration` - Indicates whether the system can place the search bar among other toolbar items on iPhone. (iOS only)
- `textColor` - The search field text color.
- `hintTextColor` - The search hint text color. (Android only)
- `hintTextColor` - The search hint text color.
- `headerIconColor` - The search and close icon color shown in the header. (Android only)
- `shouldShowHintSearchIcon` - Show the search hint icon when search bar is focused. (Android only)
- `ref` - A React ref to imperatively modify search bar.
Expand Down
1 change: 1 addition & 0 deletions ios/RNSScreenStackHeaderConfig.mm
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ + (void)updateViewController:(UIViewController *)vc
}
}
#endif /* Check for iOS 26.0 */
[searchBar updatePlaceholder];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we need this. We will be informed about any changes in updateProps right?

#endif /* !TARGET_OS_TV */
}
break;
Expand Down
2 changes: 2 additions & 0 deletions ios/RNSSearchBar.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

@property (nonatomic, retain) UISearchController *controller;

- (void)updatePlaceholder;

#if RNS_IPHONE_OS_VERSION_AVAILABLE(16_0) && !TARGET_OS_TV
- (UINavigationItemSearchBarPlacement)placementAsUINavigationItemSearchBarPlacement API_AVAILABLE(ios(16.0))
API_UNAVAILABLE(tvos, watchos);
Expand Down
88 changes: 86 additions & 2 deletions ios/RNSSearchBar.mm
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,19 @@
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED

@interface RNSSearchBar ()
- (void)setPlaceholder:(NSString *)placeholder;
- (void)setHintTextColor:(UIColor *)hintTextColor;
@end

@implementation RNSSearchBar {
__weak RCTBridge *_bridge;
UISearchController *_controller;
UIColor *_textColor;
UIColor *_hintTextColor;
NSString *_placeholder;
NSString *_defaultPlaceholder;
BOOL _didSetPlaceholder;

// We use those booleans to log a warning if user attempts to restore
// default behavior after setting explicit value for the prop.
Expand Down Expand Up @@ -66,6 +75,17 @@ - (void)initCommonProps

_controller.searchBar.delegate = self;

_didSetPlaceholder = NO;

#if !TARGET_OS_TV
_defaultPlaceholder = _controller.searchBar.placeholder;
if (_defaultPlaceholder == nil) {
_defaultPlaceholder = _controller.searchBar.searchTextField.placeholder;
}
#else
_defaultPlaceholder = _controller.searchBar.placeholder;
#endif

_isObscureBackgroundSet = NO;
_isHideNavigationBarSet = NO;

Expand Down Expand Up @@ -204,7 +224,48 @@ - (void)setAutoCapitalize:(UITextAutocapitalizationType)autoCapitalize

- (void)setPlaceholder:(NSString *)placeholder
{
[_controller.searchBar setPlaceholder:placeholder];
_placeholder = placeholder;
_didSetPlaceholder = YES;
}

- (void)setHintTextColor:(UIColor *)hintTextColor
{
_hintTextColor = hintTextColor;
}

- (void)updatePlaceholder
{
NSString *placeholder = _placeholder;

#if !TARGET_OS_TV
if (placeholder == nil) {
if (_didSetPlaceholder) {
placeholder = _defaultPlaceholder;
} else {
NSString *currentPlaceholder = _controller.searchBar.placeholder ?: _controller.searchBar.searchTextField.placeholder;
placeholder = currentPlaceholder ?: _defaultPlaceholder;
}
}

if (_hintTextColor != nil && placeholder != nil) {
[_controller.searchBar setPlaceholder:placeholder];
_controller.searchBar.searchTextField.attributedPlaceholder =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like it doesn't work on iOS 26 for new search bar at the bottom. We should investigate why & if we can't fix it, describe the limitations in the prop docs.

Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-05-25.at.11.24.44.mov

[[NSAttributedString alloc] initWithString:placeholder attributes:@{NSForegroundColorAttributeName : _hintTextColor}];
} else {
_controller.searchBar.searchTextField.attributedPlaceholder = nil;
if (placeholder != nil || _didSetPlaceholder) {
[_controller.searchBar setPlaceholder:placeholder];
}
}
#else
if (placeholder == nil) {
placeholder = _defaultPlaceholder;
}

if (placeholder != nil || _didSetPlaceholder) {
[_controller.searchBar setPlaceholder:placeholder];
}
#endif
}

- (void)setBarTintColor:(UIColor *)barTintColor
Expand Down Expand Up @@ -407,8 +468,11 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::
RNSOptionalBooleanFromRNSSearchBarHideNavigationBar:newScreenProps.hideNavigationBar]];
}

BOOL shouldUpdatePlaceholder = NO;

if (oldScreenProps.placeholder != newScreenProps.placeholder) {
[self setPlaceholder:RCTNSStringFromStringNilIfEmpty(newScreenProps.placeholder)];
shouldUpdatePlaceholder = YES;
}

#if !TARGET_OS_VISION
Expand All @@ -429,6 +493,15 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::
[self setTextColor:RCTUIColorFromSharedColor(newScreenProps.textColor)];
}

if (oldScreenProps.hintTextColor != newScreenProps.hintTextColor) {
[self setHintTextColor:RCTUIColorFromSharedColor(newScreenProps.hintTextColor)];
shouldUpdatePlaceholder = YES;
}

if (shouldUpdatePlaceholder) {
[self updatePlaceholder];
}

if (oldScreenProps.placement != newScreenProps.placement) {
self.placement = [RNSConvert RNSScreenSearchBarPlacementFromCppEquivalent:newScreenProps.placement];
}
Expand Down Expand Up @@ -508,10 +581,21 @@ - (UIView *)view
}
}

RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
RCT_CUSTOM_VIEW_PROPERTY(placeholder, NSString, RNSSearchBar)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've dropped support for Paper so these are not necessary. I think recently PR removing those landed on main. We should merge/rebase onto current main.

{
RNSSearchBar *searchBarView = static_cast<RNSSearchBar *>(view);
[searchBarView setPlaceholder:[RCTConvert NSString:json]];
[searchBarView updatePlaceholder];
}
RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(textColor, UIColor)
RCT_CUSTOM_VIEW_PROPERTY(hintTextColor, UIColor, RNSSearchBar)
{
RNSSearchBar *searchBarView = static_cast<RNSSearchBar *>(view);
[searchBarView setHintTextColor:[RCTConvert UIColor:json]];
[searchBarView updatePlaceholder];
}
RCT_EXPORT_VIEW_PROPERTY(cancelButtonText, NSString)
RCT_EXPORT_VIEW_PROPERTY(placement, RNSSearchBarPlacement)
RCT_EXPORT_VIEW_PROPERTY(allowToolbarIntegration, BOOL)
Expand Down
2 changes: 1 addition & 1 deletion src/fabric/SearchBarNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface NativeProps extends ViewProps {
obscureBackground?: CT.WithDefault<OptionalBoolean, 'undefined'>;
hideNavigationBar?: CT.WithDefault<OptionalBoolean, 'undefined'>;
cancelButtonText?: string | undefined;
hintTextColor?: ColorValue | undefined;
// TODO: implement these on iOS
barTintColor?: ColorValue | undefined;
tintColor?: ColorValue | undefined;
Expand All @@ -68,7 +69,6 @@ export interface NativeProps extends ViewProps {
inputType?: string | undefined;
onClose?: CT.DirectEventHandler<SearchBarEvent> | null | undefined;
onOpen?: CT.DirectEventHandler<SearchBarEvent> | null | undefined;
hintTextColor?: ColorValue | undefined;
headerIconColor?: ColorValue | undefined;
shouldShowHintSearchIcon?: CT.WithDefault<boolean, true>;
}
Expand Down
2 changes: 0 additions & 2 deletions src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1064,8 +1064,6 @@ export interface SearchBarProps {
textColor?: ColorValue | undefined;
/**
* The search hint text color
*
* @plaform android
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also remove (Android only) from GUIDE_FOR_LIBRARY_AUTHORS.md

*/
hintTextColor?: ColorValue | undefined;
/**
Expand Down