From f3b8d4976f8608c2cda1f071923f14b6d4538967 Mon Sep 17 00:00:00 2001 From: Shuhao Zhang Date: Fri, 4 Jun 2021 13:37:24 -0700 Subject: [PATCH] Bug fix: content is reset when emoji is entered at the max length Summary: When maxLength is defined in , if the last character at max length is an emoji, the content of the input is cleared: {F620865178} {F620865237} Related Github issues: https://github.com/facebook/react-native/issues/10929 https://github.com/facebook/react-native/issues/10964 ## Root cause: When NSString was created, unicode characters were 16-bit long, so Objective-C considers every unicode character as 16-bit. However, unicode was later extended to more than 16bit, for example, emojis, which causes NSString substring method cuts off at the wrong position. Example: ``` NSString *s = @"abc{emoji:1f601}"; NSInteger len = s.length; //length is 5 (as {emoji:1f601} occupies two 16-bit characters) NSString *s3 = [s substringToIndex: 3]; //s3 is "abc" NSString *s4 = [s substringToIndex: 4]; //s4 is null! ``` If string s, "abc{emoji:1f601}", is entered in , which has max length 4, it will truncate the string to the first 4 characters, "cutting" the emoji in half which causes encoding error and returns null. The text input is cleared. ## Solution: If the character at max length is longer than 16-bit, truncate the character BEFORE it instead. In the previous example, truncate till index 3 instead of 4. The end result will be "abc" and the emoji is dropped. ## Changelog: [iOS] [Fixed] - content is reset when emoji is entered at the max length Reviewed By: p-sun Differential Revision: D28821909 fbshipit-source-id: 4720d864970b554160ed5388f65b352ce95a6199 --- Libraries/Text/TextInput/RCTBaseTextInputView.m | 8 ++++++++ .../TextInput/RCTTextInputComponentView.mm | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.m b/Libraries/Text/TextInput/RCTBaseTextInputView.m index d619e5b5640a14..9965f9ca2972c0 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -397,6 +397,14 @@ - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range if (text.length > allowedLength) { // If we typed/pasted more than one character, limit the text inputted. if (text.length > 1) { + if (allowedLength > 0) { + //make sure unicode characters that are longer than 16 bits (such as emojis) are not cut off + NSRange cutOffCharacterRange = [text rangeOfComposedCharacterSequenceAtIndex: allowedLength - 1]; + if (cutOffCharacterRange.location + cutOffCharacterRange.length > allowedLength) { + //the character at the length limit takes more than 16bits, truncation should end at the character before + allowedLength = cutOffCharacterRange.location; + } + } // Truncate the input string so the result is exactly maxLength NSString *limitedString = [text substringToIndex:allowedLength]; NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy]; diff --git a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index e9e59a9235aa78..0018c787236ee1 100644 --- a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -335,6 +335,15 @@ - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range if (props.maxLength) { NSInteger allowedLength = props.maxLength - _backedTextInputView.attributedText.string.length + range.length; + if (allowedLength > 0 && text.length > allowedLength) { + // make sure unicode characters that are longer than 16 bits (such as emojis) are not cut off + NSRange cutOffCharacterRange = [text rangeOfComposedCharacterSequenceAtIndex:allowedLength - 1]; + if (cutOffCharacterRange.location + cutOffCharacterRange.length > allowedLength) { + // the character at the length limit takes more than 16bits, truncation should end at the character before + allowedLength = cutOffCharacterRange.location; + } + } + if (allowedLength <= 0) { return nil; }