Skip to content

Commit

Permalink
fix: Change name of focus and blur events to searchFocus and searchBl…
Browse files Browse the repository at this point in the history
…ur (#2154)

## Description

Right now, while trying to use search bar on new architecture, iOS
throws an error, that `topFocus` event cannot both direct and bubbling.
That's mainly because `RNSSearchBar` component extends from `View`,
which already has `topFocus` event registered.
This PR changes the naming of this event (along with `topBlur`, to keep
compliance of both behaviours) to `topSearchFocus` and `topSearchBlur`.

Fixes react-navigation/react-navigation#11928.

## Changes

- Changed topFocus and topBlur event to topSearchFocus and
topSearchBlur,
- Added type that renames onFocus and onBlur props inside SearchBar
native component (to don't introduce breaking change).

## Screenshots / GIFs

### Before


https://github.com/software-mansion/react-native-screens/assets/23281839/492a5e96-feaa-4107-ab3f-e0a3e1237fc3

### After


https://github.com/software-mansion/react-native-screens/assets/23281839/2cbce030-3844-43e4-bf3b-582ca82c5603

## Test code and steps to reproduce

You can test `Test758.tsx` test, along with Example -> Search bar to
test behaviour of the events on search bar. Eventually, you can use
snippet below, that has been enhanced by adding console.logs on every
search bar event.

<details>
<summary>Enhanced test</summary>

```tsx
/* eslint-disable react-hooks/exhaustive-deps */
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-navigation/native-stack';
import { SearchBarProps } from 'react-native-screens';

const AppStack = createNativeStackNavigator();

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

function First({ navigation }: NativeStackScreenProps<ParamListBase>) {
  React.useLayoutEffect(() => {
    navigation.setOptions({
      headerSearchBarOptions: searchBarOptions,
    });
  }, [navigation]);

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

  const searchBarOptions: SearchBarProps = {
    barTintColor: 'powderblue',
    tintColor: 'red',
    textColor: 'red',
    hideWhenScrolling: true,
    obscureBackground: false,
    hideNavigationBar: false,
    autoCapitalize: 'sentences',
    placeholder: 'Some text',
    cancelButtonText: 'Some text',
    onChangeText: (e: NativeSyntheticEvent<{ text: string }>) => {
      setSearch(e.nativeEvent.text);
      console.warn('Search text:', 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'),
    onOpen: () => console.warn('onOpen event'),
    onClose: () => console.warn('onClose 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')}
      />
      {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>
  );
}
```

</details>

## Checklist

- [X] Included code example that can be used to test this change
- [X] Updated TS types
- [X] Updated documentation: <!-- For adding new props to native-stack
-->
- [X]
https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx
- [X] Ensured that CI passes

---------

Co-authored-by: Kacper Kafara <kacper.kafara@swmansion.com>
  • Loading branch information
tboba and kkafar authored Jun 12, 2024
1 parent 818a323 commit e74d9aa
Show file tree
Hide file tree
Showing 8 changed files with 51 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,13 @@ class SearchBarManager : ViewGroupManager<SearchBarView>(), RNSSearchBarManagerI
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any>? {
return MapBuilder.of(
SearchBarBlurEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onBlur"),
MapBuilder.of("registrationName", "onSearchBlur"),
SearchBarChangeTextEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onChangeText"),
SearchBarCloseEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onClose"),
SearchBarFocusEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onFocus"),
MapBuilder.of("registrationName", "onSearchFocus"),
SearchBarOpenEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onOpen"),
SearchBarSearchButtonPressEvent.EVENT_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ class SearchBarBlurEvent(surfaceId: Int, viewId: Int) : Event<SearchBarBlurEvent
override fun getEventData(): WritableMap? = Arguments.createMap()

companion object {
const val EVENT_NAME = "topBlur"
const val EVENT_NAME = "topSearchBlur"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ class SearchBarFocusEvent(surfaceId: Int, viewId: Int) : Event<SearchBarFocusEve
override fun getEventData(): WritableMap? = Arguments.createMap()

companion object {
const val EVENT_NAME = "topFocus"
const val EVENT_NAME = "topSearchFocus"
}
}
10 changes: 5 additions & 5 deletions ios/RNSSearchBar.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@

#ifdef RCT_NEW_ARCH_ENABLED
#else
@property (nonatomic, copy) RCTBubblingEventBlock onChangeText;
@property (nonatomic, copy) RCTBubblingEventBlock onCancelButtonPress;
@property (nonatomic, copy) RCTBubblingEventBlock onSearchButtonPress;
@property (nonatomic, copy) RCTBubblingEventBlock onFocus;
@property (nonatomic, copy) RCTBubblingEventBlock onBlur;
@property (nonatomic, copy) RCTDirectEventBlock onChangeText;
@property (nonatomic, copy) RCTDirectEventBlock onCancelButtonPress;
@property (nonatomic, copy) RCTDirectEventBlock onSearchButtonPress;
@property (nonatomic, copy) RCTDirectEventBlock onSearchFocus;
@property (nonatomic, copy) RCTDirectEventBlock onSearchBlur;
#endif

@end
Expand Down
22 changes: 11 additions & 11 deletions ios/RNSSearchBar.mm
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ - (void)emitOnFocusEvent
#ifdef RCT_NEW_ARCH_ENABLED
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSSearchBarEventEmitter>(_eventEmitter)
->onFocus(react::RNSSearchBarEventEmitter::OnFocus{});
->onSearchFocus(react::RNSSearchBarEventEmitter::OnSearchFocus{});
}
#else
if (self.onFocus) {
self.onFocus(@{});
if (self.onSearchFocus) {
self.onSearchFocus(@{});
}
#endif
}
Expand All @@ -81,11 +81,11 @@ - (void)emitOnBlurEvent
#ifdef RCT_NEW_ARCH_ENABLED
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSSearchBarEventEmitter>(_eventEmitter)
->onBlur(react::RNSSearchBarEventEmitter::OnBlur{});
->onSearchBlur(react::RNSSearchBarEventEmitter::OnSearchBlur{});
}
#else
if (self.onBlur) {
self.onBlur(@{});
if (self.onSearchBlur) {
self.onSearchBlur(@{});
}
#endif
}
Expand Down Expand Up @@ -414,11 +414,11 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(cancelButtonText, NSString)
RCT_EXPORT_VIEW_PROPERTY(placement, RNSSearchBarPlacement)

RCT_EXPORT_VIEW_PROPERTY(onChangeText, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onCancelButtonPress, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSearchButtonPress, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onFocus, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onBlur, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onChangeText, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onCancelButtonPress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSearchButtonPress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSearchFocus, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSearchBlur, RCTDirectEventBlock)

#ifndef RCT_NEW_ARCH_ENABLED

Expand Down
27 changes: 24 additions & 3 deletions src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@ import { View } from 'react-native';
// Native components
import SearchBarNativeComponent, {
Commands as SearchBarNativeCommands,
NativeProps as SearchBarNativeProps,
SearchBarEvent,
SearchButtonPressedEvent,
ChangeTextEvent,
} from '../fabric/SearchBarNativeComponent';
import { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';

export const NativeSearchBar: React.ComponentType<SearchBarProps> &
export const NativeSearchBar: React.ComponentType<
SearchBarNativeProps & { ref?: React.RefObject<SearchBarCommands> }
> &
typeof NativeSearchBarCommands =
SearchBarNativeComponent as unknown as React.ComponentType<SearchBarProps> &
SearchBarNativeComponent as unknown as React.ComponentType<SearchBarNativeProps> &
SearchBarCommandsType;
export const NativeSearchBarCommands: SearchBarCommandsType =
SearchBarNativeCommands as SearchBarCommandsType;
Expand Down Expand Up @@ -76,7 +83,21 @@ function SearchBar(props: SearchBarProps, ref: React.Ref<SearchBarCommands>) {
return View as unknown as React.ReactNode;
}

return <NativeSearchBar {...props} ref={searchBarRef} />;
return (
<NativeSearchBar
ref={searchBarRef}
{...props}
onSearchFocus={props.onFocus as DirectEventHandler<SearchBarEvent>}
onSearchBlur={props.onBlur as DirectEventHandler<SearchBarEvent>}
onSearchButtonPress={
props.onSearchButtonPress as DirectEventHandler<SearchButtonPressedEvent>
}
onCancelButtonPress={
props.onCancelButtonPress as DirectEventHandler<SearchBarEvent>
}
onChangeText={props.onChangeText as DirectEventHandler<ChangeTextEvent>}
/>
);
}

export default React.forwardRef<SearchBarCommands, SearchBarProps>(SearchBar);
12 changes: 6 additions & 6 deletions src/fabric/SearchBarNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,23 @@ import type {
} from 'react-native/Libraries/Types/CodegenTypes';
import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';

type SearchBarEvent = Readonly<{}>;
export type SearchBarEvent = Readonly<{}>;

type SearchButtonPressedEvent = Readonly<{
export type SearchButtonPressedEvent = Readonly<{
text?: string;
}>;

type ChangeTextEvent = Readonly<{
export type ChangeTextEvent = Readonly<{
text?: string;
}>;

type SearchBarPlacement = 'automatic' | 'inline' | 'stacked';

type AutoCapitalizeType = 'none' | 'words' | 'sentences' | 'characters';

interface NativeProps extends ViewProps {
onFocus?: DirectEventHandler<SearchBarEvent> | null;
onBlur?: DirectEventHandler<SearchBarEvent> | null;
export interface NativeProps extends ViewProps {
onSearchFocus?: DirectEventHandler<SearchBarEvent> | null;
onSearchBlur?: DirectEventHandler<SearchBarEvent> | null;
onSearchButtonPress?: DirectEventHandler<SearchButtonPressedEvent> | null;
onCancelButtonPress?: DirectEventHandler<SearchBarEvent> | null;
onChangeText?: DirectEventHandler<ChangeTextEvent> | null;
Expand Down

0 comments on commit e74d9aa

Please sign in to comment.