react-native icon indicating copy to clipboard operation
react-native copied to clipboard

[IOS] Text replacement by dot after 2 spaces

Open gedu opened this issue 2 years ago • 26 comments

Summary:

Pre-requisite:

  • Ensure that the option "." Shortcut" is enabled in Settings->Keyboard.

This PR addresses several issues:

When using TextInput in an uncontrolled manner, without using the value prop, after entering a letter or word and double-tapping the spacebar, it should now correctly add a "." at the end of the letter or word. When using TextInput in an uncontrolled manner, without using the value prop, after entering certain letters for text replacement, such as "omw" or "@@" (previously created), it now produces the expected replacements.

Motivation:

From Expensify app: https://github.com/Expensify/App/issues/17153 From discussion on: https://github.com/facebook/react-native/issues/27693

Changelog:

[IOS][FIXED] - For uncontrolled TextInput: Double tapping space should add "." and Text Replacement for special characters

Test Plan:

TextInput:

"." shortcut

Pre-requisite:

  • Ensure that the option "." Shortcut" is enabled in Settings->Keyboard.

  • Using the onChangeText and the value props to set new values:

    • Writing a letter: like E then tapping the space 2 times, you should see E.
    • Writing a word: like Test then tapping the space 2 times, you should see Test.

Text Replacement

Pre-requisite:

  • Create a new Text Replacement inside of Settings -> Keyboard, using symbols: like @@ -> [email protected]

  • Using the onChangeText and the value props to set new values:

    • Writing the symbol created: @@ should be selected and clicking space, the @@ should be replaced by [email protected]

https://github.com/facebook/react-native/assets/1676818/22941057-9ac5-49f3-b469-66880b71e596

gedu avatar Sep 11 '23 13:09 gedu

Hi @gedu!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at [email protected]. Thanks!

facebook-github-bot avatar Sep 11 '23 13:09 facebook-github-bot

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

facebook-github-bot avatar Sep 11 '23 15:09 facebook-github-bot

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

facebook-github-bot avatar Sep 11 '23 15:09 facebook-github-bot

Could you provide an integration test that demonstrates how this interacts with UIKit? This piece of code is quite complex, and already has to do quite a bit of complex state management (eg see _comingFromJS, _ignoreNextTextInputCall)

Not updating the attributed string may cause issues if the text completion causes the text input to have to grow over multiple lines.

javache avatar Sep 11 '23 16:09 javache

@javache Hey, yes I can, is there something already in place for TextInput?

gedu avatar Sep 11 '23 18:09 gedu

Unfortunately, I don't think we have any in place right now. You could look at adding them here: https://github.com/facebook/react-native/tree/main/packages/rn-tester/RNTesterIntegrationTests (but this may need some work to support the new architecture). You could also try an rn-tester-e2e test.

javache avatar Sep 12 '23 10:09 javache

I will try to work on this beginning next week

gedu avatar Sep 14 '23 09:09 gedu

I ran into some issues when updating main, next week I will start adding the test under rn-tester-e2e

gedu avatar Sep 22 '23 16:09 gedu

Ok could set the first test, and is working, will keep adding more, and will try to add as much as I can.

Testing RewriteExample

input: test space replace
output: test_space_replace
Screenshot 2023-09-29 at 17 13 25

gedu avatar Sep 29 '23 15:09 gedu

During my testing. I observed that whenever I attempted to set a text, the second and third characters were getting cropped.

https://github.com/facebook/react-native/assets/1676818/ff1da81f-6b43-467c-bbf0-cf51a3d56520

So then I applied my fix on the Native realm (I wanted to make sure that all was working before my fix, but I couldn't make it work)

https://github.com/facebook/react-native/assets/1676818/babecaf3-5fde-4229-b14a-879e64ef3790

And it seems that it is working after running many times the tests

Screenshot 2023-10-04 at 20 13 30

gedu avatar Oct 04 '23 18:10 gedu

I updated my local and seems there were many changes and I need to find a new way to fix this issue, given that seems that isn't working now

gedu avatar Oct 18 '23 05:10 gedu

Ok I could find the fix, seems that wasn't a big change after all, but only will work for fabric, but I'm double checking. Now I'm working on the testings

gedu avatar Dec 13 '23 09:12 gedu

The integration tests are working on Android, but on iOS getting the text from the Input isn't working. I can add text but later I can't get it back from the Input. Looking into that

gedu avatar Jan 02 '24 17:01 gedu

Platform Engine Arch Size (bytes) Diff
android hermes arm64-v8a 16,574,182 +4
android hermes armeabi-v7a n/a --
android hermes x86 n/a --
android hermes x86_64 n/a --
android jsc arm64-v8a 19,955,218 -1
android jsc armeabi-v7a n/a --
android jsc x86 n/a --
android jsc x86_64 n/a --

Base commit: e4708d661bf3d01ec857635f04a4aabf9d954a5e Branch: main

analysis-bot avatar Jan 04 '24 10:01 analysis-bot

@javache I added some tests. What do you think?

rn_test_ios rn_tests_android Screenshot 2024-01-03 at 21 14 19

gedu avatar Jan 04 '24 13:01 gedu

@javache friendly ping

gedu avatar Jan 17 '24 07:01 gedu

@javache did you have a chance to look at this?

gedu avatar Feb 15 '24 08:02 gedu

In addition to fixing #27693, @gedu has confirmed this will also fix #29572 and the duplicate issue #42792. Thanks for fixing these long standing issues!

mhoran avatar Mar 01 '24 21:03 mhoran

hey team - can you merge? would love to get this in my app

zwilderrr avatar Mar 11 '24 13:03 zwilderrr

@javache can you take a look at this or ping someone that can help us to review this PR?

gedu avatar Apr 05 '24 09:04 gedu

I will fix the conflict soon

gedu avatar Jun 07 '24 10:06 gedu

Thanks!!!

On Fri, Jun 7, 2024 at 6:11 AM edug @.***> wrote:

I will fix the conflict soon

— Reply to this email directly, view it on GitHub https://github.com/facebook/react-native/pull/39385#issuecomment-2154529253, or unsubscribe https://github.com/notifications/unsubscribe-auth/AFJCPNV4IOVRCIFZSEO3FW3ZGGBO3AVCNFSM6AAAAAA4TKBPT2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCNJUGUZDSMRVGM . You are receiving this because you commented.Message ID: @.***>

zwilderrr avatar Jun 07 '24 16:06 zwilderrr

Apologies for the delay here, and thank you for adding the test!!

Synchronizing state between JS and native is a really complex part of React Native and introducing new variables like _stateUpdated add additional complexity there. I'd love to see a more scoped fix with detailed explanation of how updateState ends-up becoming re-entrant.

The right fix is probably not in bailing out early inside updateState, but in whatever callback gets fired that triggers the re-entrant state update. This is already what _comingFromJS means ("delegate methods textInputDidChange and textInputDidChangeSelection will exit early"). Can we leverage that instead?

javache avatar Jun 19 '24 11:06 javache

@javache I found the e2e tests were removed, I will remove them from here too: https://github.com/facebook/react-native/pull/45032

I'd love to see a more scoped fix with detailed explanation of how updateState ends-up becoming re-entrant.

Yeah, I would have to dig again to remember the exact flow. The main issue is multiple calls and flow are happen almost at the same time and sometimes in different orders (depending if the change/update is for a controlled or uncontrolled TextInput) I will look into so I can refresh my memory

gedu avatar Jun 20 '24 13:06 gedu

@javache Looking a bit deeper, I found something in the _setAttributedString when using _textOf with value and onChangeText (controlled component). The if condition was false, so it was processing the text in another way. I saw that: newText was

A{
    NSBackgroundColor = "UIExtendedSRGBColorSpace 0 0 0 0";
    NSColor = "UIExtendedSRGBColorSpace 0 0 0 1";
    NSFont = "<UICTFont: 0x165124130> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 13.00pt";
}

oldText

A{
    NSBackgroundColor = "UIExtendedSRGBColorSpace 0 0 0 0";
    NSColor = "UIExtendedSRGBColorSpace 0 0 0 1";
    NSFont = "<UICTFont: 0x165124130> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 13.00pt";
    NSParagraphStyle = "Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 4, Tabs (\n    28L,\n    56L,\n    84L,\n    112L,\n    140L,\n    168L,\n    196L,\n    224L,\n    252L,\n    280L,\n    308L,\n    336L\n), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection 0, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 65535 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)'";
    NSShadow = "NSShadow {0, -1} color = {(null)}";
}

When not using value and onChangeText (uncontrolled component), both newText and oldText were the same. I noticed that RCTNSAttributedStringFromAttributedStringBox was making a copy of the attributes, done by RCTNSTextAttributesFromTextAttributes. It checks if the attributes exist; if they do, it copies them. In the case of the controlled component, those attributes were missing, so nothing was copied, and when _textOf checked, it failed. Here's what I did: After checking, all the attributes are set to default values before returning the copy:

 NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc]
        initWithString:string
            attributes:RCTNSTextAttributesFromTextAttributes(fragment.textAttributes)];
      // Apply default attributes if they're missing
    NSDictionary<NSAttributedStringKey, id> *defaultAttributes = RCTDefaultTextAttributes();
    for (NSAttributedStringKey key in defaultAttributes) {
      if (![mutableAttributedString attribute:key atIndex:0 effectiveRange:NULL]) {
        [mutableAttributedString addAttribute:key value:defaultAttributes[key] range:NSMakeRange(0, mutableAttributedString.length)];
      }
    }   
    return mutableAttributedString; 

That way it works. This way I don't have to add the new flag

What do you think?

https://github.com/facebook/react-native/assets/1676818/175047d2-306c-47d3-a310-da02f642c3ab

gedu avatar Jun 25 '24 15:06 gedu

@javache did you have time to look into the new proposal?

gedu avatar Jun 27 '24 11:06 gedu

@javache friendly ping

gedu avatar Jul 19 '24 07:07 gedu

Does the new proposal makes sense? Looks good?

gedu avatar Aug 09 '24 07:08 gedu

@javache can you take a look at my new proposal, I'm not using a new flag

gedu avatar Sep 23 '24 07:09 gedu

I'll dedicate time to fixing the conflicts and getting everything sorted

gedu avatar Oct 09 '24 07:10 gedu