diff --git a/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/Libraries/Components/TextInput/RCTTextInputViewConfig.js index 6af9cd331c1830..d49c7a494e0b60 100644 --- a/Libraries/Components/TextInput/RCTTextInputViewConfig.js +++ b/Libraries/Components/TextInput/RCTTextInputViewConfig.js @@ -150,6 +150,7 @@ const RCTTextInputViewConfig = { showSoftInputOnFocus: true, autoFocus: true, lineBreakStrategyIOS: true, + smartInsertDelete: true, ...ConditionallyIgnoredEventHandlers({ onChange: true, onSelectionChange: true, diff --git a/Libraries/Components/TextInput/TextInput.d.ts b/Libraries/Components/TextInput/TextInput.d.ts index 5bc4318f5bf3f6..0b40b29146397f 100644 --- a/Libraries/Components/TextInput/TextInput.d.ts +++ b/Libraries/Components/TextInput/TextInput.d.ts @@ -272,6 +272,14 @@ export interface TextInputIOSProps { * If false, scrolling of the text view will be disabled. The default value is true. Only works with multiline={true} */ scrollEnabled?: boolean | undefined; + + /** + * If `false`, the iOS system will not insert an extra space after a paste operation + * neither delete one or two spaces after a cut or delete operation. + * + * The default value is `true`. + */ + smartInsertDelete?: boolean | undefined; } /** diff --git a/Libraries/Components/TextInput/TextInput.flow.js b/Libraries/Components/TextInput/TextInput.flow.js index 57259190f1a449..a37e9ef03e85d8 100644 --- a/Libraries/Components/TextInput/TextInput.flow.js +++ b/Libraries/Components/TextInput/TextInput.flow.js @@ -325,6 +325,16 @@ type IOSProps = $ReadOnly<{| * @platform ios */ lineBreakStrategyIOS?: ?('none' | 'standard' | 'hangul-word' | 'push-out'), + + /** + * If `false`, the iOS system will not insert an extra space after a paste operation + * neither delete one or two spaces after a cut or delete operation. + * + * The default value is `true`. + * + * @platform ios + */ + smartInsertDelete?: ?boolean, |}>; type AndroidProps = $ReadOnly<{| diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 386e6b87c8822e..6ccd8cde8720e7 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -358,6 +358,16 @@ type IOSProps = $ReadOnly<{| * @platform ios */ lineBreakStrategyIOS?: ?('none' | 'standard' | 'hangul-word' | 'push-out'), + + /** + * If `false`, the iOS system will not insert an extra space after a paste operation + * neither delete one or two spaces after a cut or delete operation. + * + * The default value is `true`. + * + * @platform ios + */ + smartInsertDelete?: ?boolean, |}>; type AndroidProps = $ReadOnly<{| diff --git a/Libraries/Text/RCTConvert+Text.h b/Libraries/Text/RCTConvert+Text.h index b7c411a2a69366..4425cc2ccfa8e3 100644 --- a/Libraries/Text/RCTConvert+Text.h +++ b/Libraries/Text/RCTConvert+Text.h @@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN + (UITextAutocorrectionType)UITextAutocorrectionType:(nullable id)json; + (UITextSpellCheckingType)UITextSpellCheckingType:(nullable id)json; + (RCTTextTransform)RCTTextTransform:(nullable id)json; ++ (UITextSmartInsertDeleteType)UITextSmartInsertDeleteType:(nullable id)json; @end diff --git a/Libraries/Text/RCTConvert+Text.m b/Libraries/Text/RCTConvert+Text.m index aa6e5e30a59082..3ab3cc656d0b63 100644 --- a/Libraries/Text/RCTConvert+Text.m +++ b/Libraries/Text/RCTConvert+Text.m @@ -34,4 +34,11 @@ + (UITextSpellCheckingType)UITextSpellCheckingType:(id)json RCTTextTransformUndefined, integerValue) ++ (UITextSmartInsertDeleteType)UITextSmartInsertDeleteType:(id)json +{ + return json == nil ? UITextSmartInsertDeleteTypeDefault + : [RCTConvert BOOL:json] ? UITextSmartInsertDeleteTypeYes + : UITextSmartInsertDeleteTypeNo; +} + @end diff --git a/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m b/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m index 47da2cefee5926..a19b55569e8d71 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m @@ -47,6 +47,7 @@ @implementation RCTBaseTextInputViewManager { RCT_REMAP_VIEW_PROPERTY(clearButtonMode, backedTextInputView.clearButtonMode, UITextFieldViewMode) RCT_REMAP_VIEW_PROPERTY(scrollEnabled, backedTextInputView.scrollEnabled, BOOL) RCT_REMAP_VIEW_PROPERTY(secureTextEntry, backedTextInputView.secureTextEntry, BOOL) +RCT_REMAP_VIEW_PROPERTY(smartInsertDelete, backedTextInputView.smartInsertDeleteType, UITextSmartInsertDeleteType) RCT_EXPORT_VIEW_PROPERTY(autoFocus, BOOL) RCT_EXPORT_VIEW_PROPERTY(submitBehavior, NSString) RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL) diff --git a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index c7c0f1d1a7937b..3d8b7f7b06f824 100644 --- a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -185,6 +185,21 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & } } + /* + * When updating component's props, we compare if the new value is different from the old one. + * If it is different, we update the native view with the new value. + * + * `RCTUITextSmartInsertDeleteTypeFromOptionalBool` is used to convert the boolean value coming + * from JS side to the appropriate `UITextSmartInsertDeleteType` value, that is required for + * this `smartInsertDeleteType` attribute. + */ + if (newTextInputProps.traits.smartInsertDelete != oldTextInputProps.traits.smartInsertDelete) { + if (@available(iOS 11.0, *)) { + _backedTextInputView.smartInsertDeleteType = + RCTUITextSmartInsertDeleteTypeFromOptionalBool(newTextInputProps.traits.smartInsertDelete); + } + } + // Traits `blurOnSubmit`, `clearTextOnFocus`, and `selectTextOnFocus` were omitted intentially here // because they are being checked on-demand. diff --git a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h index ffaac133609248..6f775385a5a312 100644 --- a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h +++ b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h @@ -41,4 +41,7 @@ UITextContentType RCTUITextContentTypeFromString(std::string const &contentType) API_AVAILABLE(ios(12.0)) UITextInputPasswordRules *RCTUITextInputPasswordRulesFromString(std::string const &passwordRules); +API_AVAILABLE(ios(11.0)) +UITextSmartInsertDeleteType RCTUITextSmartInsertDeleteTypeFromOptionalBool(std::optional smartInsertDelete); + NS_ASSUME_NONNULL_END diff --git a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm index 99f46819af9d09..41343e1c75db22 100644 --- a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm +++ b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm @@ -44,6 +44,10 @@ void RCTCopyBackedTextInput( toTextInput.keyboardType = fromTextInput.keyboardType; toTextInput.textContentType = fromTextInput.textContentType; + if (@available(iOS 11.0, *)) { + toTextInput.smartInsertDeleteType = fromTextInput.smartInsertDeleteType; + } + if (@available(iOS 12.0, *)) { toTextInput.passwordRules = fromTextInput.passwordRules; } @@ -232,3 +236,10 @@ UITextContentType RCTUITextContentTypeFromString(std::string const &contentType) { return [UITextInputPasswordRules passwordRulesWithDescriptor:RCTNSStringFromStringNilIfEmpty(passwordRules)]; } + +API_AVAILABLE(ios(11.0)) +UITextSmartInsertDeleteType RCTUITextSmartInsertDeleteTypeFromOptionalBool(std::optional smartInsertDelete) +{ + return smartInsertDelete.has_value() ? (*smartInsertDelete ? UITextSmartInsertDeleteTypeYes : UITextSmartInsertDeleteTypeNo) + : UITextSmartInsertDeleteTypeDefault; +} diff --git a/ReactCommon/react/renderer/components/textinput/iostextinput/primitives.h b/ReactCommon/react/renderer/components/textinput/iostextinput/primitives.h index bbb56d26d9a418..bfcf0f45127e54 100644 --- a/ReactCommon/react/renderer/components/textinput/iostextinput/primitives.h +++ b/ReactCommon/react/renderer/components/textinput/iostextinput/primitives.h @@ -223,6 +223,15 @@ class TextInputTraits final { * Default value: `` (no rules). */ std::string passwordRules{}; + + /* + * If `false`, the iOS system will not insert an extra space after a paste operation + * neither delete one or two spaces after a cut or delete operation. + * iOS-only (inherently iOS-specific) + * Can be empty (`null` in JavaScript) which means `default`. + * Default value: `empty` (`null`). + */ + std::optional smartInsertDelete{}; }; } // namespace react diff --git a/ReactCommon/react/renderer/components/textinput/iostextinput/propsConversions.h b/ReactCommon/react/renderer/components/textinput/iostextinput/propsConversions.h index 15dc0004596da0..fc03b469997f26 100644 --- a/ReactCommon/react/renderer/components/textinput/iostextinput/propsConversions.h +++ b/ReactCommon/react/renderer/components/textinput/iostextinput/propsConversions.h @@ -141,6 +141,12 @@ static TextInputTraits convertRawProp( "passwordRules", sourceTraits.passwordRules, defaultTraits.passwordRules); + traits.smartInsertDelete = convertRawProp( + context, + rawProps, + "smartInsertDelete", + sourceTraits.smartInsertDelete, + defaultTraits.smartInsertDelete); return traits; } diff --git a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js index 7cff25b37adae0..31721ed17ef22d 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js @@ -901,4 +901,23 @@ exports.examples = ([ ); }, }, + { + title: 'iOS autoformatting behaviors', + render: function (): React.Node { + return ( + + + + + + + + + ); + }, + }, ]: Array);