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

[iOS only] [v1.12+] onFocus event is only called on first focus on TextInput #456

Closed
christophemenager opened this issue May 30, 2024 · 31 comments · Fixed by #642
Closed
Assignees
Labels
🐛 bug Something isn't working focused input 📝 Anything about focused input functionality 🍎 iOS iOS specific

Comments

@christophemenager
Copy link

Describe the bug

  • Only on ios, onFocus is only called once, and not if text input is re-focused
  • Same happens for the onBlurevent
  • This regression has been introduced in 1.12, 1.11 is working fine
  • No issue on Android

To Reproduce
Steps to reproduce the behavior:

  1. Add a TextInput centered in a View on iOS (react-native 0.73.8)
  2. Click on the input, onFocus is called
  3. Click outside, onBlur is called
  4. Click on the input again: nothing happens (onFocus is NOT called)

Expected behavior
onFocus and onBlur events should be called everytime the input is focused or blurred

Smartphone (please complete the following information):

  • Device: iPhone X
  • OS: 15.x
  • RN version: 0.73.8
  • RN architecture: old
  • JS engine: Hermes
  • Library version: 1.12.1

Additional context
This regression has been introduced in 1.12, 1.11 is working fine

@kirillzyusko kirillzyusko added 🐛 bug Something isn't working 🍎 iOS iOS specific labels May 30, 2024
@kirillzyusko
Copy link
Owner

Hey @christophemenager

Can you post a minimal reproduction code, please? I've used this code:

export default function AwareScrollView({ navigation }: Props) {
  return (
    <>
      <ScrollView style={{flex: 1,}} contentContainerStyle={{paddingLeft: 100, flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "red"}}>
        <TextInput
          key={1}
          placeholder={`TextInput#${1}`}
          keyboardType={1 % 2 === 0 ? "numeric" : "default"}
          multiline={false}
          onFocus={() => console.log("onFocus")}
          onBlur={() => console.log("onBlur")}
          style={{borderWidth: 2, borderColor: "black"}}
        />
      </ScrollView>
    </>
  );
}

But it always calls onFocus/onBlur whenever I press on input/press outside:

 LOG  onFocus
 LOG  onBlur
 LOG  onFocus
 LOG  onBlur
 LOG  onFocus
 LOG  onBlur
 LOG  onFocus
 LOG  onBlur

I tested in RN 0.74 - do you think it makes a difference?

@kirillzyusko
Copy link
Owner

I also tested RN 0.73.4 (old architecture) - still everything works okay 🤷‍♂️ Guess something wrong with my code that I'm using 🤔

@kirillzyusko
Copy link
Owner

kirillzyusko commented Jun 6, 2024

@christophemenager I've also tested on iPhone 6s (iOS 15.8) and I can not reproduce this problem in example app (I used Toolbar screen as example).

May I ask you to test my example app and maybe perform some code modifications to show how it can be reproduced?

The issue seems to be quite critical, so I want to fix it ASAP, but I can not reproduce it 😔

Please, don't ignore me 🙏

@christophemenager
Copy link
Author

Sorry I am a bit busy this week, can I get back to you next Monday? I will try to give you a minimal repro code :)

@kirillzyusko
Copy link
Owner

Sorry I am a bit busy this week, can I get back to you next Monday?

Yeah, sure, take your time 👍

I will try to give you a minimal repro code :)

Thank you ❤️

@christophemenager
Copy link
Author

Hi there! Sorry for the delay.

I tried to reproduce in an isolated react-native environment but I could not.

I guess it must be very specific to my setup, so I will close this issue and reopen it if I find a way to reproduce it properly.

@kirillzyusko
Copy link
Owner

@christophemenager yes, feel free to re-open this 👍

In 1.12 version I started to inject my own delegate to intercept some events that are available only on delegate level. So I guess in some scenarios delegate substitution doesn't work properly, so it would be good if you could figure out and provide a repro 🙏

@bikowalczyk
Copy link

I'm also experiencing this issue, but only when multiline={true}. After the first onBlur no events (onFocus, onBlur, onTextChange) are called from the TextInput, although you can see its value changing. I was using a workaround of forcing the input to rerender (by changing its key) when the keyboard was dissmissed and it worked, but wasn't stable.

As mentioned above reverting to 1.11.7 fixes the issue!

@kirillzyusko
Copy link
Owner

@bikowalczyk do you think it would be possible to create a simple reproduction repository?

I think it's a very critical issue but I really can't reproduce it in my example project 🤷‍♂️ Staying on 1.11.7 long time is kind of not very good way, because you'll miss a lot of great improvements.

So if you can help me and prepare a reproduction example I would highly appreciate you ❤️

@christophemenager
Copy link
Author

Just for info, I had to stay on 1.11.7 for my project, upgrading to 1.12 introduces this issue. I will try to spend more time reproducing this issue in an isolated environment, but I am not super confident that I will find a way...

The issue is still here in latest version.

@kirillzyusko
Copy link
Owner

@christophemenager yeah, the algorithm for substituting the delegate hasn't been changed since 1.12.0 version, so the issue most likely will be in newest versions as well.

Maybe you are using specific props for TextInput? I had a problem when layout couldn't be properly detected if devs were using contextMenuHidden property - maybe you are also using a combination of properties that leads to that result?

@christophemenager
Copy link
Author

christophemenager commented Oct 8, 2024

@kirillzyusko I had a bit more time to dig into this issue and here is what I found:

  • It's not linked to some weird props, I reduced the number of props to its minimum and I still have the issue of onFocus (and onBlur) not being triggered after re-focus (they are triggered only once)
  • The problem occurs only on iOS
  • The problem was introduced in version 1.11.7 1.12.0 and is still present in the latest version 1.14.0
  • It's only reproduced on iOS 15 (and maybe lower ?) but not on iOS 16, 17 or 18
  • The problem occurs as soon as the library is added, to the project no matter if the provider is enabled or not on a given screen (I would have expect the problem to disappear if I disable the KeyBoardProvider)

I used react-native 0.75.3 for this investigation, and an iphone 13

So I guess you can easily reproduce this issue on your side following those steps:

  • iPhone emulator with ios 15
  • react-native 0.75.3
  • expo 51
  • TextInput from react-native, with only onFocus

I tried to repro on expo snacks but I could not find a way to select an ios version so it's not useful in this case !

Please let me know if I could help, but I guess the important thing here is that it's only on "old" versions of iOS, that's why it has never been reported a lot

@kirillzyusko kirillzyusko changed the title [iOS only] [v1.12] onFocus event is only called on first focus on TextInput [iOS only] [v1.12+] onFocus event is only called on first focus on TextInput Oct 8, 2024
@kirillzyusko kirillzyusko added the focused input 📝 Anything about focused input functionality label Oct 8, 2024
@kirillzyusko
Copy link
Owner

@christophemenager I think I did everything properly:

Screen.Recording.2024-10-08.at.22.37.29.mov

Can you show a video how it looks in your case?

The code is:

import "react-native-gesture-handler";

import * as React from "react";
import { ActivityIndicator, StatusBar, StyleSheet, TextInput } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";
import {
  SafeAreaProvider,
  initialWindowMetrics,
} from "react-native-safe-area-context";

const styles = StyleSheet.create({
  root: {
    flex: 1,
  },
});

export default function App() {
  return (
    <SafeAreaProvider initialMetrics={initialWindowMetrics}>
      <GestureHandlerRootView style={styles.root}>
        <KeyboardProvider statusBarTranslucent>
          <TextInput onFocus={() => console.log("onFocus")} style={{ width: 200, height: 50, backgroundColor: "yellow", marginTop: 50 }} />
        </KeyboardProvider>
      </GestureHandlerRootView>
    </SafeAreaProvider>
  );
}

Setup that I used:

  • iPhone 13 Pro (iOS 15.5);
  • react-native 0.75.3
  • it's not Expo project (it's bare RN)
  • TextInput from react-native, with only onFocus and style

The problem was introduced in version 1.11.7

Are you sure it was introduced in 1.11.7 and not 1.12.0? Because the issue has a different title 🙈

Any ideas what is the difference between our setups and why it doesn't work on your machine, but works on mine? The only difference that I see is that you are using Expo and I'm using pure RN project. Do you think it can be a key difference?

@christophemenager
Copy link
Author

@kirillzyusko thanks for your quick reply.

My mistake, this issue was introduced in 1.12.0, I edited my last comment.

I will keep digging and send you more info later today.

@kirillzyusko
Copy link
Owner

kirillzyusko commented Oct 9, 2024

@christophemenager thank you! Hope we eventually can figure out what's exactly happening there 👀

By the way - I also have a physical device with iOS 15.8 will try to test today and see if I can reproduce the problem there!

@christophemenager
Copy link
Author

It seems I managed to repro it on a simple expo example! 🎉

CleanShot.2024-10-09.at.09.55.10.mp4
  "dependencies": {
    "@expo/vector-icons": "^14.0.2",
    "@react-navigation/native": "^6.0.2",
    "expo": "~51.0.28",
    "expo-constants": "~16.0.2",
    "expo-font": "~12.0.9",
    "expo-linking": "~6.3.1",
    "expo-router": "~3.5.23",
    "expo-splash-screen": "~0.27.5",
    "expo-status-bar": "~1.12.1",
    "expo-system-ui": "~3.0.7",
    "expo-web-browser": "~13.0.3",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-native": "0.74.5",
    "react-native-gesture-handler": "~2.16.1",
    "react-native-keyboard-controller": "^1.14.0",
    "react-native-library": "^1.0.25",
    "react-native-reanimated": "~3.10.1",
    "react-native-safe-area-context": "4.10.5",
    "react-native-screens": "3.31.1",
    "react-native-web": "~0.19.10"
  },

Extra infos:

  • Iphone 13, ios 15.2
  • Something I discovered while playing with my app on emulator: the problem only occurs when the keyboard opens. On emulator, if you don't trigger (CMD+K) the keyboard on focus, then the problem is not visible

@kirillzyusko
Copy link
Owner

@christophemenager would you mind to publish reproduction example? And maybe you have a discord or any other messenger so that we can chat with you (I think it'll be more productive)?

if you don't trigger (CMD+K) the keyboard on focus, then the problem is not visible

So it's visible only if you trigger a keyboard via CMD+K, right? Can you say when exactly you press these keys, i. e. set focus, keyboard is not shown and then press CMD+K (according to the video)?

Have you tried to reproduce it on a real device?

@kirillzyusko
Copy link
Owner

I could reproduce it only one time - was super happy, but when I modified code the issue is not reproducible anymore 🤯

My brain is 🤯

@christophemenager
Copy link
Author

@christophemenager would you mind to publish reproduction example? And maybe you have a discord or any other messenger so that we can chat with you (I think it'll be more productive)?

Yes and yes, but not this week, I will get back to you next week :)

So it's visible only if you trigger a keyboard via CMD+K, right? Can you say when exactly you press these keys, i. e. set focus, keyboard is not shown and then press CMD+K (according to the video)?

On the video record, If I remember well, I am not pressing CMD+K, I only click on the input. I made other tests in other conditions and I noticed that I could re-focus without any issue while the keyboard do not open. As soon as it opens, the onFocus is not called again. Maybe it's a coincidence, but I thought it could be a hint for you to know where to start :)

@kirillzyusko
Copy link
Owner

On the video record, If I remember well, I am not pressing CMD+K, I only click on the input. I made other tests in other conditions and I noticed that I could re-focus without any issue while the keyboard do not open. As soon as it opens, the onFocus is not called again. Maybe it's a coincidence, but I thought it could be a hint for you to know where to start :)

Well, will try to do more testing, but at the moment it doesn't give me any hints 🙃

@christophemenager
Copy link
Author

@kirillzyusko I will try to push a repro repo today :)

@christophemenager
Copy link
Author

@kirillzyusko well I tried to reproduce again this morning and... I failed 🤦 Trying around 50 times I managed to reproduce only one time, but I could not find how :/

I tried to tackle this issue from the other way: trying to simplify my project (where I can systematically reproduce) until I solve the issue so I could find what is exactly is causing the issue in my current project setup.
I cleaned every component and had up to having a simple textInput in a simple react-native scrollView, but I still had the issue.

My feeling is that it could come from an other dependency installed in my project that messes up with yours, but I am really not sure.

The only thing I know for sure is that in my project, I reproduced on iOS 15.2 but not on 16,17 and 18.

As

  • we both tried to reproduce but could not manage
  • the issue seems pretty specific to my setup and limited to "old" ios versions

I suggest we close the issue for now and wait for more inputs if others have the same problem.

@kirillzyusko
Copy link
Owner

My feeling is that it could come from an other dependency installed in my project that messes up with yours, but I am really not sure.

@christophemenager would you mind sharing your package.json here? Maybe I can spot a problematic dependency 🤔

@christophemenager
Copy link
Author

Sure, here it is:

  "dependencies": {
    "@faker-js/faker": "9.0.3",
    "@gorhom/bottom-sheet": "4.6.4",
    "@react-native-community/blur": "4.4.1",
    "@react-native-community/geolocation": "3.4.0",
    "@react-native-community/netinfo": "11.4.1",
    "@react-navigation/bottom-tabs": "6.6.1",
    "@react-navigation/native": "6.1.18",
    "@react-navigation/native-stack": "6.11.0",
    "@sentry/react-native": "5.33.2",
    "@shopify/flash-list": "1.7.1",
    "@shopify/react-native-skia": "1.4.2",
    "@shopify/restyle": "2.4.4",
    "@supabase/supabase-js": "2.45.4",
    "@tanstack/query-sync-storage-persister": "5.59.0",
    "@tanstack/react-query": "5.59.3",
    "@tanstack/react-query-persist-client": "5.59.3",
    "app-icon-badge": "0.0.15",
    "date-fns": "4.1.0",
    "expo": "51.0.37",
    "expo-application": "5.9.1",
    "expo-av": "14.0.7",
    "expo-battery": "8.0.1",
    "expo-clipboard": "6.0.3",
    "expo-constants": "16.0.2",
    "expo-dev-client": "4.0.28",
    "expo-device": "6.0.2",
    "expo-font": "12.0.10",
    "expo-haptics": "13.0.1",
    "expo-image": "1.13.0",
    "expo-keep-awake": "13.0.2",
    "expo-linear-gradient": "13.0.2",
    "expo-localization": "15.0.3",
    "expo-location": "17.0.1",
    "expo-sensors": "13.0.9",
    "expo-speech": "12.0.2",
    "expo-speech-recognition": "0.2.22",
    "expo-splash-screen": "0.27.6",
    "expo-status-bar": "1.12.1",
    "expo-store-review": "7.0.2",
    "expo-system-ui": "3.0.7",
    "fuse.js": "7.0.0",
    "geolib": "3.3.4",
    "i18next": "23.15.2",
    "lodash.groupby": "4.6.0",
    "mixpanel-react-native": "2.4.1",
    "moti": "0.29.0",
    "p-limit": "6.1.0",
    "react": "18.3.1",
    "react-hook-form": "7.53.0",
    "react-i18next": "15.0.2",
    "react-native": "0.75.4",
    "react-native-android-location-enabler": "2.0.1",
    "react-native-awesome-gallery": "0.4.3",
    "react-native-blob-util": "0.19.11",
    "react-native-gesture-handler": "2.20.0",
    "react-native-get-random-values": "1.11.0",
    "react-native-keyboard-controller": "1.14.0",
    "react-native-map-link": "3.6.1",
    "react-native-maps": "1.18.0",
    "react-native-mmkv": "2.12.2",
    "react-native-modal": "13.0.1",
    "react-native-navigation-bar-color": "2.0.2",
    "react-native-pager-view": "6.4.1",
    "react-native-permissions": "4.1.5",
    "react-native-reanimated": "3.15.4",
    "react-native-reanimated-carousel": "3.5.1",
    "react-native-render-html": "6.3.4",
    "react-native-safe-area-context": "4.11.0",
    "react-native-screens": "3.34.0",
    "react-native-svg": "15.7.1",
    "react-native-tab-view": "3.5.2",
    "react-native-vision-camera": "4.5.3",
    "react-native-volume-manager": "1.10.0",
    "react-native-webview": "13.12.3",
    "react-native-worklets-core": "1.3.3",
    "react-native-zoom-toolkit": "3.1.0",
    "react-query-kit": "3.3.0",
    "rive-react-native": "7.3.0",
    "semver": "7.6.3",
    "uuid": "10.0.0",
    "zod": "3.23.8",
    "zustand": "4.5.5"
  },
  "comments": {
    "react-native-get-random-values": "Needed by uuid",
    "react-native-pager-view": "Needed by react-native-tab-view"
  },
  "devDependencies": {
    "@babel/core": "7.25.7",
    "@commitlint/cli": "19.5.0",
    "@oclif/core": "4.0.27",
    "@react-native/eslint-config": "0.75.4",
    "@tanstack/eslint-plugin-query": "5.59.2",
    "@testing-library/jest-dom": "6.5.0",
    "@testing-library/jest-native": "5.4.3",
    "@testing-library/react-native": "12.7.2",
    "@types/jest": "29.5.13",
    "@types/lodash.groupby": "4.6.9",
    "@types/react": "18.3.11",
    "@types/react-test-renderer": "18.3.0",
    "@types/uuid": "10.0.0",
    "@types/xml2js": "0.4.14",
    "@typescript-eslint/eslint-plugin": "7.18.0",
    "@typescript-eslint/parser": "7.18.0",
    "babel-plugin-module-resolver": "5.0.2",
    "blurhash": "2.0.5",
    "cross-env": "7.0.3",
    "deepmerge": "4.3.1",
    "dotenv": "16.4.5",
    "eslint": "8.57.0",
    "eslint-config-prettier": "9.1.0",
    "eslint-plugin-i18n-json": "4.0.0",
    "eslint-plugin-i18next": "6.1.0",
    "eslint-plugin-jest": "28.8.3",
    "eslint-plugin-prettier": "5.2.1",
    "eslint-plugin-simple-import-sort": "12.1.1",
    "eslint-plugin-unicorn": "55.0.0",
    "eslint-plugin-unused-imports": "4.1.4",
    "husky": "9.1.6",
    "i18n-unused": "0.16.0",
    "jest": "29.7.0",
    "jest-environment-jsdom": "29.7.0",
    "jest-expo": "51.0.4",
    "jest-junit": "16.0.0",
    "knip": "5.33.2",
    "lint-staged": "15.2.10",
    "metro-babel-register": "0.80.12",
    "np": "10.0.7",
    "prettier": "3.3.3",
    "react-test-renderer": "18.3.1",
    "sharp": "0.33.5",
    "supabase": "1.200.3",
    "ts-jest": "29.2.5",
    "ts-node": "10.9.2",
    "typescript": "5.6.3",
    "xml2js": "0.6.2"
  }

@kirillzyusko
Copy link
Owner

@christophemenager okay, I don't see anything suspicious 🙈 If you can create an empty project, add all these dependencies and the issue will be reproducible - let me know and I'll have a look on a reproduction example.

As you suggested - I'll close the issue, but if you (or anyone else) find anything new to share, feel free to do it here and I'll be happy to re-open that issue!

@kirillzyusko
Copy link
Owner

@christophemenager I don't give up. Just a random thought came to my mind - maybe RN substitute delegate on its own level? In this case when I try to substitute delegate back most likely the reference (to the delegate that I hold) becomes null and I substitute delegate back with null and thus all events stop working 🤷‍♂️

Can you try to use this code in your project:

  private func substituteDelegateBack(_ input: UIResponder?) {
    if let textField = input as? UITextField {
      if (textField.delegate is KCTextInputCompositeDelegate) {
        textField.delegate = delegate.activeDelegate as? UITextFieldDelegate
      }
    } else if let textView = input as? UITextView {
       if (textView.delegate is KCTextInputCompositeDelegate) {
          (textView as? RCTUITextView)?.setForceDelegate(delegate.activeDelegate as? UITextViewDelegate)
       }
    }
  }

And test the scenario that 100% reproducible in your project?

@christophemenager
Copy link
Author

@kirillzyusko ahah such a warrior fighting again this bug, I admire that ;) Sure, can you just give me the path of the file where I should add this piece of code?

@kirillzyusko
Copy link
Owner

@christophemenager sure, it's located here:

private func substituteDelegateBack(_ input: UIResponder?) {
if let textField = input as? UITextField {
textField.delegate = delegate.activeDelegate as? UITextFieldDelegate
} else if let textView = input as? UITextView {
(textView as? RCTUITextView)?.setForceDelegate(delegate.activeDelegate as? UITextViewDelegate)
}
}

You need to replace this function with a code that I provided 🙂

@christophemenager
Copy link
Author

@kirillzyusko good news, it fixes the issue !! 🎉

Before After
https://github.com/user-attachments/assets/a12dcf98-b3ea-4d6f-9c02-c0d132c4e3ac https://github.com/user-attachments/assets/06b1ed57-aa11-4e13-b11d-8233aa5a0bdf

We can't see it but on the "before" video I am pressing multiple times on the first inputs but they are not getting focused.

I made sure to revert the change and I reproduced the issue again so I confirm this piece of codes you gave me fixes the issue !

@kirillzyusko
Copy link
Owner

Amazing @christophemenager ! 🎉 I'll submit a PR with a fix today then and will publish a fix under 1.14.2 version 👀

May I also ask you to test this variant of the code?

  private func substituteDelegateBack(_ input: UIResponder?) {
    if let textField = input as? UITextField, let oldDelegate = delegate.activeDelegate as? UITextFieldDelegate {
      textField.delegate = oldDelegate
    } else if let textView = input as? UITextView, let oldDelegate = delegate.activeDelegate as? UITextViewDelegate {
      (textView as? RCTUITextView)?.setForceDelegate(oldDelegate)
    }
  }

Just want to understand better what's going wrong internally to apply a better suitable fix 🙃

@kirillzyusko kirillzyusko reopened this Oct 16, 2024
@christophemenager
Copy link
Author

May I also ask you to test this variant of the code?

It fixes the issue too :)

kirillzyusko added a commit that referenced this issue Oct 17, 2024
## 📜 Description

Fixes a problem when all events such as `onFocus`/`onBlur`/etc. will
stop working after keyboard dismissal.

## 💡 Motivation and Context

It looks like a delegate in original `TextInput` can be somehow
substituted and we are loosing a pointer to original delegate. If on
keyboard-hide event we'll substitute delegate with `nil` - we will make
all events, such as `onFocus`, `onBlur` etc. not coming to JS.

So the most obvious decision is to check, if we actually have a delegate
and do a substitution only in this case.

Of course a rhetorical question arises - if we had original delegate,
then injected our delegate, then it was substituted with a new one ->
turns out that our events will not come, right? Right. But:
- I don't know how it's possible - I checked RN sources and I don't see
a way how delegate can be injected after component creation;
- for now it's still safer to miss events in `keyboard-controller`
rather than disable all events at framework level;
- I can not reliably reproduce this in empty project and the issue is
reproducible on relatively old devices (iO 15.2).

So yeah, the problem is somewhere deeper, but I don't have an access to
the code so can't debug what exactly happens there. So for now it'll be
safer not to ruin the RN framework and merge this fix 🙃

Closes
#456

## 📢 Changelog

<!-- High level overview of important changes -->
<!-- For example: fixed status bar manipulation; added new types
declarations; -->
<!-- If your changes don't affect one of platform/language below - then
remove this platform/language -->

### iOS

- check previous delegate presence when substitute delegate back;

## 🤔 How Has This Been Tested?

Tested manually on iOS 15.2

## 📸 Screenshots (if appropriate):

|Before|After|
|---|---|
|<video
src="https://github.com/user-attachments/assets/a12dcf98-b3ea-4d6f-9c02-c0d132c4e3ac">|<video
src="https://github.com/user-attachments/assets/06b1ed57-aa11-4e13-b11d-8233aa5a0bdf">|

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🐛 bug Something isn't working focused input 📝 Anything about focused input functionality 🍎 iOS iOS specific
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants