diff --git a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js index 6f693295320eb4..88d3cc8fe756e5 100644 --- a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +++ b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js @@ -156,6 +156,7 @@ const RCTTextInputViewConfig = { showSoftInputOnFocus: true, autoFocus: true, lineBreakStrategyIOS: true, + smartInsertDelete: true, ...ConditionallyIgnoredEventHandlers({ onChange: true, onSelectionChange: true, diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts index 8badb2a9d39de5..97fe9371065419 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts @@ -291,6 +291,14 @@ export interface TextInputIOSProps { | 'hangul-word' | 'push-out' | 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/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js index 7ed4579d4d87c8..be702737024815 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -295,6 +295,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/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index 67ad18c0c19b42..657145ef2ad781 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -339,6 +339,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/packages/react-native/Libraries/Text/RCTConvert+Text.h b/packages/react-native/Libraries/Text/RCTConvert+Text.h index b7c411a2a69366..4425cc2ccfa8e3 100644 --- a/packages/react-native/Libraries/Text/RCTConvert+Text.h +++ b/packages/react-native/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/packages/react-native/Libraries/Text/RCTConvert+Text.m b/packages/react-native/Libraries/Text/RCTConvert+Text.m index aa6e5e30a59082..3ab3cc656d0b63 100644 --- a/packages/react-native/Libraries/Text/RCTConvert+Text.m +++ b/packages/react-native/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/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m index 47da2cefee5926..a19b55569e8d71 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m +++ b/packages/react-native/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/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index 9e41f9d3b5f20b..17a38ca7858d07 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -181,6 +181,13 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & _backedTextInputView.passwordRules = RCTUITextInputPasswordRulesFromString(newTextInputProps.traits.passwordRules); } + 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 intentionally here // because they are being checked on-demand. diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h index 2a89399964a4d5..70c55719262be0 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h @@ -39,4 +39,6 @@ UITextContentType RCTUITextContentTypeFromString(std::string const &contentType) UITextInputPasswordRules *RCTUITextInputPasswordRulesFromString(std::string const &passwordRules); +UITextSmartInsertDeleteType RCTUITextSmartInsertDeleteTypeFromOptionalBool(std::optional smartInsertDelete); + NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm index 532c29e59e3bdd..3f11a17203c0d2 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm +++ b/packages/react-native/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; + } + toTextInput.passwordRules = fromTextInput.passwordRules; [toTextInput setSelectedTextRange:fromTextInput.selectedTextRange notifyDelegate:NO]; @@ -226,3 +230,9 @@ UITextContentType RCTUITextContentTypeFromString(std::string const &contentType) { return [UITextInputPasswordRules passwordRulesWithDescriptor:RCTNSStringFromStringNilIfEmpty(passwordRules)]; } + +UITextSmartInsertDeleteType RCTUITextSmartInsertDeleteTypeFromOptionalBool(std::optional smartInsertDelete) +{ + return smartInsertDelete.has_value() ? (*smartInsertDelete ? UITextSmartInsertDeleteTypeYes : UITextSmartInsertDeleteTypeNo) + : UITextSmartInsertDeleteTypeDefault; +} diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/primitives.h index 9fadc3d23ed959..5bb5e6dd015b45 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/primitives.h @@ -222,6 +222,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 facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/propsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/propsConversions.h index f498ed385191fe..885d9e50d11c80 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/propsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/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 b0563e0d2f384d..9daea74eb4b796 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js @@ -906,4 +906,23 @@ exports.examples = ([ ); }, }, + { + title: 'iOS autoformatting behaviors', + render: function (): React.Node { + return ( + + + + + + + + + ); + }, + }, ]: Array);