Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API for imperative actions on RNSSearchBar #1523

Closed
wants to merge 1 commit into from
Closed
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
21 changes: 19 additions & 2 deletions Example/src/screens/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useLayoutEffect, useState } from 'react';
import React, { useLayoutEffect, useRef, useState } from 'react';
import { I18nManager, ScrollView, Text, StyleSheet } from 'react-native';
import { SearchBarProps } from 'react-native-screens';
import { SearchBarRef, SearchBarProps } from 'react-native-screens';
import {
createNativeStackNavigator,
NativeStackNavigationProp,
Expand Down Expand Up @@ -49,10 +49,12 @@ const MainScreen = ({ navigation }: MainScreenProps): JSX.Element => {
'sentences'
);
const [inputType, setInputType] = useState<InputType>('text');
const searchBarRef = useRef<SearchBarRef>(null);

useLayoutEffect(() => {
navigation.setOptions({
searchBar: {
ref: searchBarRef,
barTintColor,
hintTextColor,
headerIconColor,
Expand Down Expand Up @@ -173,6 +175,21 @@ const MainScreen = ({ navigation }: MainScreenProps): JSX.Element => {
value={shouldShowHintSearchIcon}
onValueChange={setShouldShowHintSearchIcon}
/>
<Text style={styles.heading}>Imperative actions</Text>
<Button onPress={() => searchBarRef.current?.blur()} title="Blur" />
<Button onPress={() => searchBarRef.current?.focus()} title="Focus" />
<Button
onPress={() => searchBarRef.current?.clearText()}
title="Clear Text"
/>
<Button
onPress={() => searchBarRef.current?.toggleCancelButton(true)}
title="Show cancel"
/>
<Button
onPress={() => searchBarRef.current?.toggleCancelButton(false)}
title="Hide cancel"
/>
<Text style={styles.heading}>Other</Text>
<Button
onPress={() => navigation.navigate('Search')}
Expand Down
113 changes: 113 additions & 0 deletions TestsExample/src/Test1097.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import * as React from 'react';
import {Button, NativeSyntheticEvent, ScrollView} from 'react-native';
import {
NavigationContainer,
NavigationProp,
ParamListBase,
} from '@react-navigation/native';
import {
createNativeStackNavigator,
NativeStackScreenProps,
} from 'react-native-screens/native-stack';
import {SearchBarProps} from 'react-native-screens';

const AppStack = createNativeStackNavigator();

export default function App(): JSX.Element {
return (
<NavigationContainer>
<AppStack.Navigator
screenOptions={{
headerLargeTitle: true,
headerTranslucent: true,
}}>
<AppStack.Screen name="First" component={First} />
<AppStack.Screen name="Second" component={Second} />
</AppStack.Navigator>
</NavigationContainer>
);
}

function First({navigation}: NativeStackScreenProps<ParamListBase>) {
const searchBarRef = React.useRef();

React.useEffect(() => {
navigation.setOptions({
searchBar: searchBarOptions,
});
}, [navigation]);

const [search, setSearch] = React.useState('');

const searchBarOptions: SearchBarProps = {
// @ts-ignore
ref: searchBarRef,
barTintColor: 'powderblue',
hideWhenScrolling: true,
obscureBackground: false,
hideNavigationBar: false,
autoCapitalize: 'sentences',
placeholder: 'Some text',
onChangeText: (e: NativeSyntheticEvent<{text: string}>) =>
setSearch(e.nativeEvent.text),
onCancelButtonPress: () => console.warn('Cancel button pressed'),
onSearchButtonPress: () => console.warn('Search button pressed'),
onFocus: () => console.warn('onFocus event'),
onBlur: () => console.warn('onBlur event'),
};

const items = [
'Apples',
'Pie',
'Juice',
'Cake',
'Nuggets',
'Some',
'Other',
'Stuff',
'To',
'Fill',
'The',
'Scrolling',
'Space',
];

return (
<ScrollView
contentInsetAdjustmentBehavior="automatic"
keyboardDismissMode="on-drag">
<Button
title="Tap me for second screen"
onPress={() => navigation.navigate('Second')}
/>
<Button
title="Tap me for ref"
onPress={() => (searchBarRef.current as any).focus()}
/>
{items
.filter(
(item) => item.toLowerCase().indexOf(search.toLowerCase()) !== -1,
)
.map((item) => (
<Button
title={item}
key={item}
onPress={() => {
console.warn(`${item} clicked`);
}}
/>
))}
</ScrollView>
);
}

function Second({navigation}: {navigation: NavigationProp<ParamListBase>}) {
return (
<ScrollView contentInsetAdjustmentBehavior="automatic">
<Button
title="Tap me for first screen"
onPress={() => navigation.navigate('First')}
/>
</ScrollView>
);
}
8 changes: 8 additions & 0 deletions guides/GUIDE_FOR_LIBRARY_AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,14 @@ To render a search bar use `ScreenStackHeaderSearchBarView` with `<SearchBar>` c
- `hintTextColor` - The search hint text color. (Android only)
- `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.

Allowed imperative actions on search bar are:

- `focus` - Function to focus on search bar. (iOS only)
- `blur` - Function to remove focus from search bar. (iOS only)
- `clearText` - Function to clear text in search bar. (iOS only)
- `toggleCancelButton` - Function toggle cancel button display near search bar. (iOS only)

Below is a list of properties that can be set with `ScreenStackHeaderConfig` component:

Expand Down
36 changes: 36 additions & 0 deletions ios/RNSSearchBar.mm
Original file line number Diff line number Diff line change
Expand Up @@ -348,4 +348,40 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(onFocus, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onBlur, RCTBubblingEventBlock)

RCT_EXPORT_METHOD(focus : (NSNumber *_Nonnull)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
RNSSearchBar *searchBar = viewRegistry[reactTag];
UISearchController *searchBarController = searchBar.controller;
[searchBarController.searchBar becomeFirstResponder];
}];
}

RCT_EXPORT_METHOD(blur : (NSNumber *_Nonnull)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
RNSSearchBar *searchBar = viewRegistry[reactTag];
UISearchController *searchBarController = searchBar.controller;
[searchBarController.searchBar resignFirstResponder];
}];
}

RCT_EXPORT_METHOD(clearText : (NSNumber *_Nonnull)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
RNSSearchBar *searchBar = viewRegistry[reactTag];
UISearchController *searchBarController = searchBar.controller;
[searchBarController.searchBar setText:@""];
}];
}

RCT_EXPORT_METHOD(toggleCancelButton : (NSNumber *_Nonnull)reactTag flag : (BOOL *)flag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
RNSSearchBar *searchBar = viewRegistry[reactTag];
UISearchController *searchBarController = searchBar.controller;
[searchBarController.searchBar setShowsCancelButton:flag ? YES : NO animated:YES];
}];
}

@end
22 changes: 22 additions & 0 deletions native-stack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,28 @@ The search and close icon color shown in the header. (Android only)

Show the search hint icon when search bar is focused. (Android only)

#### `ref`

A React ref to imperatively modify search bar.

#### Imperative actions

##### `focus` (iOS only)

Focus on search bar.

##### `blur` (iOS only)

Remove focus from search bar.

##### `clearText` (iOS only)

Clear text in search bar.

##### `toggleCancelButton` (iOS only)

Toggle cancel button display near search bar.

### Events

The navigator can [emit events](https://reactnavigation.org/docs/navigation-events) on certain actions. Supported events are:
Expand Down
52 changes: 41 additions & 11 deletions src/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import {
Animated,
findNodeHandle,
NativeModules,
Image,
ImageProps,
Platform,
Expand Down Expand Up @@ -83,7 +85,12 @@ let NativeScreenStackHeaderSubview: React.ComponentType<React.PropsWithChildren<
ViewProps & { type?: HeaderSubviewTypes }
>>;
let AnimatedNativeScreen: React.ComponentType<ScreenProps>;
let NativeSearchBar: React.ComponentType<SearchBarProps>;
let NativeSearchBar: React.ComponentType<SearchBarProps> & {
focus: (reactTag: number | null) => void;
blur: (reactTag: number | null) => void;
clearText: (reactTag: number | null) => void;
toggleCancelButton: (reactTag: number | null, flag: boolean) => void;
};
let NativeFullWindowOverlay: React.ComponentType<View>;

const ScreensNativeModules = {
Expand Down Expand Up @@ -353,6 +360,38 @@ const ScreenStackHeaderBackButtonImage = (props: ImageProps): JSX.Element => (
</ScreensNativeModules.NativeScreenStackHeaderSubview>
);

class SearchBar extends React.Component<SearchBarProps> {
blur() {
return NativeModules.RNSSearchBarManager.blur(findNodeHandle(this));
}

focus() {
return NativeModules.RNSSearchBarManager.focus(findNodeHandle(this));
}

toggleCancelButton(flag: boolean) {
return NativeModules.RNSSearchBarManager.toggleCancelButton(
findNodeHandle(this),
flag
);
}

clearText() {
return NativeModules.RNSSearchBarManager.clearText(findNodeHandle(this));
}

render() {
if (!isSearchBarAvailableForCurrentPlatform) {
console.warn(
'Importing SearchBar is only valid on iOS and Android devices.'
);
return View;
}

return <ScreensNativeModules.NativeSearchBar {...this.props} />;
}
}

const ScreenStackHeaderRightView = (
props: React.PropsWithChildren<ViewProps>
): JSX.Element => (
Expand Down Expand Up @@ -428,6 +467,7 @@ module.exports = {
ScreenContext,
ScreenStack,
InnerScreen,
SearchBar,

get NativeScreen() {
return ScreensNativeModules.NativeScreen;
Expand All @@ -447,16 +487,6 @@ module.exports = {
get ScreenStackHeaderSubview() {
return ScreensNativeModules.NativeScreenStackHeaderSubview;
},
get SearchBar() {
if (!isSearchBarAvailableForCurrentPlatform) {
console.warn(
'Importing SearchBar is only valid on iOS and Android devices.'
);
return View;
}

return ScreensNativeModules.NativeSearchBar;
},
get FullWindowOverlay() {
if (Platform.OS !== 'ios') {
console.warn('Importing FullWindowOverlay is only valid on iOS devices.');
Expand Down
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const ScreenStackHeaderCenterView = (
): JSX.Element => <View {...props} />;

export const ScreenStackHeaderSearchBarView = (
props: React.PropsWithChildren<SearchBarProps>
props: React.PropsWithChildren<Omit<SearchBarProps, 'ref'>>
): JSX.Element => <View {...props} />;

export const ScreenStackHeaderConfig: React.ComponentType<ScreenStackHeaderConfigProps> = View;
Expand Down
11 changes: 11 additions & 0 deletions src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ import {
TextInputFocusEventData,
} from 'react-native';

type SearchBarRefObject = React.RefObject<{
focus: () => void;
blur: () => void;
clearText: () => void;
toggleCancelButton: (flag: boolean) => void;
}>;

export type SearchBarRef = NonNullable<SearchBarRefObject['current']>;

export type StackPresentationTypes =
| 'push'
| 'modal'
Expand Down Expand Up @@ -430,6 +439,8 @@ export interface ScreenStackHeaderConfigProps extends ViewProps {
}

export interface SearchBarProps {
ref?: React.RefObject<SearchBarRef>;

/**
* The auto-capitalization behavior
*/
Expand Down