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

fix: Change name of focus and blur events to searchFocus and searchBlur #2154

Merged
merged 9 commits into from
Jun 12, 2024

Conversation

tboba
Copy link
Member

@tboba tboba commented May 21, 2024

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

8mb.video-rrF-K1N8IW5v.mp4

After

8mb.video-CCQ-5AnoEr2J.mp4

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.

Enhanced test
/* 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>
  );
}

Checklist

Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

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

Thanks for taking care of this 🙌🏻 Left important remarks below.

ios/RNSSearchBar.h Show resolved Hide resolved
src/types.tsx Outdated
Comment on lines 717 to 736
/**
* Since the search bar is a component that is extended from View on iOS,
* we can't use onFocus and onBlur events directly there (as of the event naming conflicts).
* To omit any breaking changes, we're handling this type to rename onFocus and onBlur events
* to onSearchFocus and onSearchBlur inside the native component of the search bar.
*/
export type RenamedSearchBarProps = Rename<
SearchBarProps,
{ onFocus: 'onSearchFocus'; onBlur: 'onSearchBlur' }
>;

/**
* Helper type, used to rename certain keys in the interface, given from object.
*/
type Rename<
T,
R extends {
[K in keyof R]: K extends keyof T ? PropertyKey : 'Error: key not in T';
}
> = { [P in keyof T as P extends keyof R ? R[P] : P]: T[P] };
Copy link
Member

Choose a reason for hiding this comment

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

I appreciate the idea here (and like it btw.), however do we need this? If w/o this fix setting these options would crash application anyway I believe we can safely treat this as a required fix and not a breaking change, thus removing the need for such workaround.

This comment was marked as off-topic.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I require an assistance of @WoLewicki there, as I was speaking with him about this change 😁 but when I was wondering about it, I was thinking that it would also require us to introduce the breaking change inside the react-navigation, and also there will be time for introducing such breaking changes (very soon 🤭). What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

We don't want to change the user's API for sure so the SearchBarProps must stay the same but NativeSearchBar should take its props from the spec inside fabric directory so we are sure we are sending the proper ones to the native side. Or am I missing something?

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree with you Wojtek and I would totally go into using Spec as the type only for codegen components, instead of reusing types from types.tsx, but tbh I'd leave this task for a separate PR. What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

But then you wouldn't have to add this RenamedSearchBarProps type at all yeah? But if you want, you can apply it in the follow-up PR. I'd also change all props to have prefix of onSearch... to keep the convention and not stumble to some similar issue later.

Copy link
Member

Choose a reason for hiding this comment

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

So the conclusion would be to:

  1. leave types in types.tsx intact (so the public API of our lib)
  2. Make NativeSearchBar use prop types that do come from Fabric spec, not from the types.tsx
  3. in SearchBar component hijack these event-related props: onBlur & onFocus and pass them to as values to onSearchBlur & onSearchFocus props on NativeSearchBar respectively

This way we avoid the need for this typescript hacks.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hi @WoLewicki @kkafar, I've just added a change that doesn't impact types file and does a little bit tricky stuff inside the SearchBar, but I think this is fine 😄. Could you tell me what do you think about it? 299ce6a

Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

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

It is likely I do not see something, but I still don't get why do we need such workarounds.

My impression is that we can have SearchBarProps (from types & native-stack/types unchanged, and just use different types on NativeSearchBar (do we actually need this proxy component instead of just SearchBarNativeComponent?) while bridging it as it is currently done in SearchBar (forwarding props with changed names).

@tboba
Copy link
Member Author

tboba commented May 27, 2024

@kkafar We unfortunately cannot edit types from NativeSearchBar, since Spec allows you to only have given set of types. Because of that, you cannot mix the type as something like onFocus?: DirectEventHandler<SearchBarEvent> | (e: NativeSyntheticEvent<TargetedEvent>) => void | null and you need to do pretty tricky stuff to mix these types somewhere else (like I do, in SearchBar.tsx).

@kkafar
Copy link
Member

kkafar commented Jun 8, 2024

Let's look into this issue on Monday, cause I want this PR landed before I do a release.

@tboba
Copy link
Member Author

tboba commented Jun 8, 2024

@kkafar sure 👍

@tboba tboba force-pushed the @tboba/fix-onfocus-onblur-errors-ios branch from 840dd3e to 2ea8296 Compare June 10, 2024 14:07
@tboba tboba mentioned this pull request Jun 10, 2024
8 tasks
@tboba tboba requested review from WoLewicki and kkafar June 10, 2024 14:13
Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

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

Final remarks before approving

react-navigation Outdated
Copy link
Member

Choose a reason for hiding this comment

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

What are these changes to react navigation? Are they here on purpose?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I've just bumped this submodule to main. Do you think we should keep these changes?

src/types.tsx Outdated Show resolved Hide resolved
Comment on lines +90 to +98
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>}
Copy link
Member

Choose a reason for hiding this comment

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

It's not pretty, but it's better than breaking changes in public types 🎉

Copy link
Member

Choose a reason for hiding this comment

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

Seems sad that it cannot infer one from another. Is it the same in all our libs and specs that we have to cast the type from codegen?

Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

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

Ok, so last thing is: what about those changes to HeaderConfig and readme in react-navigation, why do we have them here?

@tboba
Copy link
Member Author

tboba commented Jun 11, 2024

@kkafar it's just a bump of main branch on submodule. Do you think we should keep it?

@kkafar
Copy link
Member

kkafar commented Jun 11, 2024

@tboba, it seems that these do no introduce any changes in business logic, so it's ok. But for the future let's keep that separated please.

@kkafar
Copy link
Member

kkafar commented Jun 11, 2024

So, if you have tested this in action, let's merge this please

ja1ns pushed a commit to WiseOwlTech/react-native-screens that referenced this pull request Oct 9, 2024
…ur (software-mansion#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bridgeless] [RN 0.74] Event cannot be both direct and bubbling: topFocus
3 participants