From 53b12614cfdb6e19356d3d3d42b07b44612c25c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 8 Feb 2023 18:02:45 +0000 Subject: [PATCH 1/3] feat: Add smartInsertDelete prop to TextInput component --- .../Components/TextInput/RCTTextInputViewConfig.js | 1 + .../Libraries/Components/TextInput/TextInput.d.ts | 8 ++++++++ .../Libraries/Components/TextInput/TextInput.flow.js | 10 ++++++++++ .../Libraries/Components/TextInput/TextInput.js | 10 ++++++++++ packages/react-native/Libraries/Text/RCTConvert+Text.h | 1 + packages/react-native/Libraries/Text/RCTConvert+Text.m | 7 +++++++ .../Text/TextInput/RCTBaseTextInputViewManager.m | 1 + .../TextInput/RCTTextInputComponentView.mm | 5 +++++ .../ComponentViews/TextInput/RCTTextInputUtils.h | 2 ++ .../ComponentViews/TextInput/RCTTextInputUtils.mm | 7 +++++++ .../renderer/components/iostextinput/primitives.h | 9 +++++++++ .../components/iostextinput/propsConversions.h | 6 ++++++ 12 files changed, 67 insertions(+) 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..8b8b0694374d48 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,11 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & _backedTextInputView.passwordRules = RCTUITextInputPasswordRulesFromString(newTextInputProps.traits.passwordRules); } + if (newTextInputProps.traits.smartInsertDelete != oldTextInputProps.traits.smartInsertDelete) { + _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..86123984f068e4 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm @@ -43,6 +43,7 @@ void RCTCopyBackedTextInput( toTextInput.secureTextEntry = fromTextInput.secureTextEntry; toTextInput.keyboardType = fromTextInput.keyboardType; toTextInput.textContentType = fromTextInput.textContentType; + toTextInput.smartInsertDeleteType = fromTextInput.smartInsertDeleteType; toTextInput.passwordRules = fromTextInput.passwordRules; @@ -226,3 +227,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; } From 09cfb30616eeb876c85547cb529ca87c9e52bad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 9 Feb 2023 13:36:39 +0000 Subject: [PATCH 2/3] feat: add iOS version checks to `smartInsertDelete` prop --- .../ComponentViews/TextInput/RCTTextInputComponentView.mm | 6 ++++-- .../Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) 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 8b8b0694374d48..17a38ca7858d07 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -182,8 +182,10 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & } if (newTextInputProps.traits.smartInsertDelete != oldTextInputProps.traits.smartInsertDelete) { - _backedTextInputView.smartInsertDeleteType = - RCTUITextSmartInsertDeleteTypeFromOptionalBool(newTextInputProps.traits.smartInsertDelete); + if (@available(iOS 11.0, *)) { + _backedTextInputView.smartInsertDeleteType = + RCTUITextSmartInsertDeleteTypeFromOptionalBool(newTextInputProps.traits.smartInsertDelete); + } } // Traits `blurOnSubmit`, `clearTextOnFocus`, and `selectTextOnFocus` were omitted intentionally here 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 86123984f068e4..3f11a17203c0d2 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm @@ -43,7 +43,10 @@ void RCTCopyBackedTextInput( toTextInput.secureTextEntry = fromTextInput.secureTextEntry; toTextInput.keyboardType = fromTextInput.keyboardType; toTextInput.textContentType = fromTextInput.textContentType; - toTextInput.smartInsertDeleteType = fromTextInput.smartInsertDeleteType; + + if (@available(iOS 11.0, *)) { + toTextInput.smartInsertDeleteType = fromTextInput.smartInsertDeleteType; + } toTextInput.passwordRules = fromTextInput.passwordRules; From 8fc43312b0b94ad3446c65e1b962f6ff20510e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 9 Feb 2023 14:11:43 +0000 Subject: [PATCH 3/3] test: add tests for `smartInsertDelete` prop --- .../TextInput/TextInputExample.ios.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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);