From 1e3cb9170794afa03a3b4b15f75b711dace7a774 Mon Sep 17 00:00:00 2001 From: Tony Du Date: Fri, 22 Jul 2022 13:08:45 -0700 Subject: [PATCH] Allow multiline TextInputs be submittable without blurring (#33653) Summary: For multiline TextInputs, it's possible to send the submit event when pressing the return key only with `blurOnSubmit`. However, there's currently no way to do so without blurring the input and dismissing the keyboard. This problem is apparent when we want to use a TextInput to span multiple lines but still have it be submittable (but not blurrable), like one might want for a TODO list. ![multiline-momentary-blur](https://user-images.githubusercontent.com/22553678/163596940-aae779f5-4d2a-4425-8ed0-e4aa77b90699.gif) ## Changelog [General] [Added] - Add `returnKeyAction` prop to `TextInput` component [General] [Deprecated] - Remove usages of `blurOnSubmit` in native code and convert `blurOnSubmit` to `returnKeyAction` in the JavaScript conversion layer Pull Request resolved: https://github.com/facebook/react-native/pull/33653 Test Plan: Verified old usages of combinations of `blurOnSubmit` and `multiline` matched previous behavior and that the new `returnKeyAction` prop behaves as expected. | Android | iOS | | --- | -- | | ![android-returnkeyaction-test](https://user-images.githubusercontent.com/22553678/163597864-2e306f98-7b6e-4ddf-8a35-625d397d3dce.gif) | ![ios-returnkeyaction-test](https://user-images.githubusercontent.com/22553678/163598407-9e059f74-3549-4b46-8e03-c19bfaa6dd3d.gif) | With the changes, the TODO list example from before now looks like this: ![multiline-no-momentary-blur](https://user-images.githubusercontent.com/22553678/163598810-f3a71d62-5514-486e-bf6a-79169fe86378.gif) Reviewed By: yungsters Differential Revision: D35735249 Pulled By: makovkastar fbshipit-source-id: 1f2237a2a5e11dd141165d7568c91c9824bd6f25 --- .../AndroidTextInputNativeComponent.js | 29 +++++- .../TextInput/RCTTextInputViewConfig.js | 2 +- Libraries/Components/TextInput/TextInput.js | 77 +++++++++++++--- .../__snapshots__/TextInput-test.js.snap | 4 +- .../Multiline/RCTMultilineTextInputView.m | 3 - .../TextInput/RCTBackedTextInputDelegate.h | 4 +- .../RCTBackedTextInputDelegateAdapter.m | 8 +- .../Text/TextInput/RCTBaseTextInputView.h | 2 +- .../Text/TextInput/RCTBaseTextInputView.m | 31 ++++--- .../TextInput/RCTBaseTextInputViewManager.m | 2 +- .../Singleline/RCTSinglelineTextInputView.m | 4 +- .../TextInput/RCTTextInputComponentView.mm | 28 ++++-- .../react/tests/TextInputTestCase.java | 10 +-- .../react/views/textinput/ReactEditText.java | 54 ++++++++--- .../textinput/ReactTextInputManager.java | 44 ++++----- .../textinput/ReactTextInputPropertyTest.java | 2 +- .../AndroidTextInputProps.cpp | 10 +-- .../androidtextinput/AndroidTextInputProps.h | 2 +- .../textinput/iostextinput/conversions.h | 20 +++++ .../textinput/iostextinput/primitives.h | 12 ++- .../textinput/iostextinput/propsConversions.h | 8 +- .../TextInput/TextInputSharedExamples.js | 90 +++++++++++++++++++ 22 files changed, 351 insertions(+), 95 deletions(-) diff --git a/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js index e896a77f810..e12b1ad054c 100644 --- a/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +++ b/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js @@ -66,6 +66,8 @@ export type ReturnKeyType = | 'route' | 'yahoo'; +export type SubmitBehavior = 'submit' | 'blurAndSubmit' | 'newline'; + export type NativeProps = $ReadOnly<{| // This allows us to inherit everything from ViewProps except for style (see below) // This must be commented for Fabric codegen to work. @@ -520,9 +522,34 @@ export type NativeProps = $ReadOnly<{| * multiline fields. Note that for multiline fields, setting `blurOnSubmit` * to `true` means that pressing return will blur the field and trigger the * `onSubmitEditing` event instead of inserting a newline into the field. + * + * @deprecated + * Note that `submitBehavior` now takes the place of `blurOnSubmit` and will + * override any behavior defined by `blurOnSubmit`. + * @see submitBehavior */ blurOnSubmit?: ?boolean, + /** + * When the return key is pressed, + * + * For single line inputs: + * + * - `'newline`' defaults to `'blurAndSubmit'` + * - `undefined` defaults to `'blurAndSubmit'` + * + * For multiline inputs: + * + * - `'newline'` adds a newline + * - `undefined` defaults to `'newline'` + * + * For both single line and multiline inputs: + * + * - `'submit'` will only send a submit event and not blur the input + * - `'blurAndSubmit`' will both blur the input and send a submit event + */ + submitBehavior?: ?SubmitBehavior, + /** * Note that not all Text styles are supported, an incomplete list of what is not supported includes: * @@ -657,7 +684,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { process: require('../../StyleSheet/processColor'), }, textDecorationLine: true, - blurOnSubmit: true, + submitBehavior: true, textAlignVertical: true, fontStyle: true, textShadowOffset: true, diff --git a/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/Libraries/Components/TextInput/RCTTextInputViewConfig.js index 52134db4419..68ee77143d1 100644 --- a/Libraries/Components/TextInput/RCTTextInputViewConfig.js +++ b/Libraries/Components/TextInput/RCTTextInputViewConfig.js @@ -128,7 +128,7 @@ const RCTTextInputViewConfig = { keyboardType: true, selection: true, returnKeyType: true, - blurOnSubmit: true, + submitBehavior: true, mostRecentEventCount: true, scrollEnabled: true, selectionColor: {process: require('../../StyleSheet/processColor')}, diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 8fa117192fa..8e3296f5d2c 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -173,6 +173,8 @@ export type ReturnKeyType = | 'route' | 'yahoo'; +export type SubmitBehavior = 'submit' | 'blurAndSubmit' | 'newline'; + export type AutoCapitalize = 'none' | 'sentences' | 'words' | 'characters'; export type TextContentType = @@ -502,15 +504,6 @@ export type Props = $ReadOnly<{| */ allowFontScaling?: ?boolean, - /** - * If `true`, the text field will blur when submitted. - * The default value is true for single-line fields and false for - * multiline fields. Note that for multiline fields, setting `blurOnSubmit` - * to `true` means that pressing return will blur the field and trigger the - * `onSubmitEditing` event instead of inserting a newline into the field. - */ - blurOnSubmit?: ?boolean, - /** * If `true`, caret is hidden. The default value is `false`. * @@ -775,6 +768,40 @@ export type Props = $ReadOnly<{| */ selectTextOnFocus?: ?boolean, + /** + * If `true`, the text field will blur when submitted. + * The default value is true for single-line fields and false for + * multiline fields. Note that for multiline fields, setting `blurOnSubmit` + * to `true` means that pressing return will blur the field and trigger the + * `onSubmitEditing` event instead of inserting a newline into the field. + * + * @deprecated + * Note that `submitBehavior` now takes the place of `blurOnSubmit` and will + * override any behavior defined by `blurOnSubmit`. + * @see submitBehavior + */ + blurOnSubmit?: ?boolean, + + /** + * When the return key is pressed, + * + * For single line inputs: + * + * - `'newline`' defaults to `'blurAndSubmit'` + * - `undefined` defaults to `'blurAndSubmit'` + * + * For multiline inputs: + * + * - `'newline'` adds a newline + * - `undefined` defaults to `'newline'` + * + * For both single line and multiline inputs: + * + * - `'submit'` will only send a submit event and not blur the input + * - `'blurAndSubmit`' will both blur the input and send a submit event + */ + submitBehavior?: ?SubmitBehavior, + /** * Note that not all Text styles are supported, an incomplete list of what is not supported includes: * @@ -1185,9 +1212,31 @@ function InternalTextInput(props: Props): React.Node { let textInput = null; - // The default value for `blurOnSubmit` is true for single-line fields and - // false for multi-line fields. - const blurOnSubmit = props.blurOnSubmit ?? !props.multiline; + const multiline = props.multiline ?? false; + + let submitBehavior: SubmitBehavior; + if (props.submitBehavior != null) { + // `submitBehavior` is set explicitly + if (!multiline && props.submitBehavior === 'newline') { + // For single line text inputs, `'newline'` is not a valid option + submitBehavior = 'blurAndSubmit'; + } else { + submitBehavior = props.submitBehavior; + } + } else if (multiline) { + if (props.blurOnSubmit === true) { + submitBehavior = 'blurAndSubmit'; + } else { + submitBehavior = 'newline'; + } + } else { + // Single line + if (props.blurOnSubmit !== false) { + submitBehavior = 'blurAndSubmit'; + } else { + submitBehavior = 'submit'; + } + } const accessible = props.accessible !== false; const focusable = props.focusable !== false; @@ -1246,7 +1295,7 @@ function InternalTextInput(props: Props): React.Node { {...props} {...eventHandlers} accessible={accessible} - blurOnSubmit={blurOnSubmit} + submitBehavior={submitBehavior} caretHidden={caretHidden} dataDetectorTypes={props.dataDetectorTypes} focusable={focusable} @@ -1294,7 +1343,7 @@ function InternalTextInput(props: Props): React.Node { {...eventHandlers} accessible={accessible} autoCapitalize={autoCapitalize} - blurOnSubmit={blurOnSubmit} + submitBehavior={submitBehavior} caretHidden={caretHidden} children={children} disableFullscreenUI={props.disableFullscreenUI} diff --git a/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap b/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap index 874fae2b158..d83ac32fe9b 100644 --- a/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap +++ b/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap @@ -4,7 +4,6 @@ exports[`TextInput tests should render as expected: should deep render when mock @@ -33,7 +33,6 @@ exports[`TextInput tests should render as expected: should deep render when not diff --git a/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.m b/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.m index 0cbaea87b42..51d70e0c684 100644 --- a/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.m +++ b/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.m @@ -19,9 +19,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge { if (self = [super initWithBridge:bridge]) { - // `blurOnSubmit` defaults to `false` for by design. - self.blurOnSubmit = NO; - _backedTextInputView = [[RCTUITextView alloc] initWithFrame:self.bounds]; _backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _backedTextInputView.textInputDelegate = self; diff --git a/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h b/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h index c2a4362f028..0f073dce992 100644 --- a/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h +++ b/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h @@ -19,9 +19,11 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)textInputShouldEndEditing; // Return `YES` to allow editing to stop and to resign first responder status. `NO` to disallow the editing session to end. - (void)textInputDidEndEditing; // May be called if forced even if `textInputShouldEndEditing` returns `NO` (e.g. view removed from window) or `[textInput endEditing:YES]` called. -- (BOOL)textInputShouldReturn; // May be called right before `textInputShouldEndEditing` if "Return" button was pressed. +- (BOOL)textInputShouldReturn; // May be called right before `textInputShouldEndEditing` if "Return" button was pressed. Dismisses keyboard if true - (void)textInputDidReturn; +- (BOOL)textInputShouldSubmitOnReturn; // Checks whether to submit when return is pressed and emits an event if true. + /* * Called before any change in the TextInput. The delegate has the opportunity to change the replacement string or reject the change completely. * To change the replacement, return the changed version of the `text`. diff --git a/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m b/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m index c6c254ce5db..f46fa123241 100644 --- a/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m +++ b/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m @@ -100,6 +100,8 @@ static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingCo - (BOOL)textFieldShouldReturn:(__unused UITextField *)textField { + // Ignore the value of whether we submitted; just make sure the submit event is called if necessary. + [_backedTextInputView.textInputDelegate textInputShouldSubmitOnReturn]; return [_backedTextInputView.textInputDelegate textInputShouldReturn]; } @@ -209,10 +211,14 @@ static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingCo { // Custom implementation of `textInputShouldReturn` and `textInputDidReturn` pair for `UITextView`. if (!_backedTextInputView.textWasPasted && [text isEqualToString:@"\n"]) { - if ([_backedTextInputView.textInputDelegate textInputShouldReturn]) { + const BOOL shouldSubmit = [_backedTextInputView.textInputDelegate textInputShouldSubmitOnReturn]; + const BOOL shouldReturn = [_backedTextInputView.textInputDelegate textInputShouldReturn]; + if (shouldReturn) { [_backedTextInputView.textInputDelegate textInputDidReturn]; [_backedTextInputView endEditing:NO]; return NO; + } else if (shouldSubmit) { + return NO; } } diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.h b/Libraries/Text/TextInput/RCTBaseTextInputView.h index 5c6c5cfcfa4..967101da624 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.h +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.h @@ -42,7 +42,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) NSInteger mostRecentEventCount; @property (nonatomic, assign, readonly) NSInteger nativeEventCount; @property (nonatomic, assign) BOOL autoFocus; -@property (nonatomic, assign) BOOL blurOnSubmit; +@property (nonatomic, copy) NSString *submitBehavior; @property (nonatomic, assign) BOOL selectTextOnFocus; @property (nonatomic, assign) BOOL clearTextOnFocus; @property (nonatomic, assign) BOOL secureTextEntry; diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.m b/Libraries/Text/TextInput/RCTBaseTextInputView.m index a4924923f19..c719b562891 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -350,20 +350,27 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) eventCount:_nativeEventCount]; } +- (BOOL)textInputShouldSubmitOnReturn +{ + const BOOL shouldSubmit = [_submitBehavior isEqualToString:@"blurAndSubmit"] || [_submitBehavior isEqualToString:@"submit"]; + if (shouldSubmit) { + // We send `submit` event here, in `textInputShouldSubmit` + // (not in `textInputDidReturn)`, because of semantic of the event: + // `onSubmitEditing` is called when "Submit" button + // (the blue key on onscreen keyboard) did pressed + // (no connection to any specific "submitting" process). + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit + reactTag:self.reactTag + text:[self.backedTextInputView.attributedText.string copy] + key:nil + eventCount:_nativeEventCount]; + } + return shouldSubmit; +} + - (BOOL)textInputShouldReturn { - // We send `submit` event here, in `textInputShouldReturn` - // (not in `textInputDidReturn)`, because of semantic of the event: - // `onSubmitEditing` is called when "Submit" button - // (the blue key on onscreen keyboard) did pressed - // (no connection to any specific "submitting" process). - [_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit - reactTag:self.reactTag - text:[self.backedTextInputView.attributedText.string copy] - key:nil - eventCount:_nativeEventCount]; - - return _blurOnSubmit; + return [_submitBehavior isEqualToString:@"blurAndSubmit"]; } - (void)textInputDidReturn diff --git a/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m b/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m index b1ecf854330..fc59b088fe6 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m @@ -49,7 +49,7 @@ RCT_REMAP_VIEW_PROPERTY(clearButtonMode, backedTextInputView.clearButtonMode, UI RCT_REMAP_VIEW_PROPERTY(scrollEnabled, backedTextInputView.scrollEnabled, BOOL) RCT_REMAP_VIEW_PROPERTY(secureTextEntry, backedTextInputView.secureTextEntry, BOOL) RCT_EXPORT_VIEW_PROPERTY(autoFocus, BOOL) -RCT_EXPORT_VIEW_PROPERTY(blurOnSubmit, BOOL) +RCT_EXPORT_VIEW_PROPERTY(submitBehavior, NSString) RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL) RCT_EXPORT_VIEW_PROPERTY(keyboardType, UIKeyboardType) RCT_EXPORT_VIEW_PROPERTY(showSoftInputOnFocus, BOOL) diff --git a/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputView.m b/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputView.m index 51feea12f86..f2d6ee4c849 100644 --- a/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputView.m +++ b/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputView.m @@ -19,8 +19,8 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge { if (self = [super initWithBridge:bridge]) { - // `blurOnSubmit` defaults to `true` for by design. - self.blurOnSubmit = YES; + // `submitBehavior` defaults to `"blurAndSubmit"` for by design. + self.submitBehavior = @"blurAndSubmit"; _backedTextInputView = [[RCTUITextField alloc] initWithFrame:self.bounds]; _backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; diff --git a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index 3631fb45cbc..c7c0f1d1a79 100644 --- a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -300,20 +300,25 @@ using namespace facebook::react; } } -- (BOOL)textInputShouldReturn +- (BOOL)textInputShouldSubmitOnReturn { - // We send `submit` event here, in `textInputShouldReturn` + const SubmitBehavior submitBehavior = [self getSubmitBehavior]; + const BOOL shouldSubmit = submitBehavior == SubmitBehavior::Submit || submitBehavior == SubmitBehavior::BlurAndSubmit; + // We send `submit` event here, in `textInputShouldSubmitOnReturn` // (not in `textInputDidReturn)`, because of semantic of the event: // `onSubmitEditing` is called when "Submit" button // (the blue key on onscreen keyboard) did pressed // (no connection to any specific "submitting" process). - if (_eventEmitter) { + if (_eventEmitter && shouldSubmit) { std::static_pointer_cast(_eventEmitter)->onSubmitEditing([self _textInputMetrics]); } + return shouldSubmit; +} - auto const &props = *std::static_pointer_cast(_props); - return props.traits.blurOnSubmit; +- (BOOL)textInputShouldReturn +{ + return [self getSubmitBehavior] == SubmitBehavior::BlurAndSubmit; } - (void)textInputDidReturn @@ -644,6 +649,19 @@ using namespace facebook::react; } } +- (SubmitBehavior)getSubmitBehavior +{ + auto const &props = *std::static_pointer_cast(_props); + const SubmitBehavior submitBehaviorDefaultable = props.traits.submitBehavior; + + // We should always have a non-default `submitBehavior`, but in case we don't, set it based on multiline. + if (submitBehaviorDefaultable == SubmitBehavior::Default) { + return props.traits.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit; + } + + return submitBehaviorDefaultable; +} + @end Class RCTTextInputCls(void) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/TextInputTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/TextInputTestCase.java index b4b3aaa7b4b..f58bd22b301 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/TextInputTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/TextInputTestCase.java @@ -232,12 +232,12 @@ public class TextInputTestCase extends ReactAppInstrumentationTestCase { private void fireEditorActionAndCheckRecording( final ReactEditText reactEditText, final int actionId) throws Throwable { - fireEditorActionAndCheckRecording(reactEditText, actionId, true); - fireEditorActionAndCheckRecording(reactEditText, actionId, false); + fireEditorActionAndCheckRecording(reactEditText, actionId, "blurAndSubmit"); + fireEditorActionAndCheckRecording(reactEditText, actionId, "newline"); } private void fireEditorActionAndCheckRecording( - final ReactEditText reactEditText, final int actionId, final boolean blurOnSubmit) + final ReactEditText reactEditText, final int actionId, final String submitBehavior) throws Throwable { mRecordingModule.reset(); @@ -246,14 +246,14 @@ public class TextInputTestCase extends ReactAppInstrumentationTestCase { @Override public void run() { reactEditText.requestFocusFromJS(); - reactEditText.setBlurOnSubmit(blurOnSubmit); + reactEditText.setSubmitBehavior(submitBehavior); reactEditText.onEditorAction(actionId); } }); waitForBridgeAndUIIdle(); assertEquals(1, mRecordingModule.getCalls().size()); - assertEquals(!blurOnSubmit, reactEditText.isFocused()); + assertEquals(!submitBehavior.equals("blurAndSubmit"), reactEditText.isFocused()); } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index 4847b9b5d86..14863e49801 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -95,7 +95,7 @@ public class ReactEditText extends AppCompatEditText private @Nullable TextWatcherDelegator mTextWatcherDelegator; private int mStagedInputType; protected boolean mContainsImages; - private @Nullable Boolean mBlurOnSubmit; + private @Nullable String mSubmitBehavior = null; private boolean mDisableFullscreen; private @Nullable String mReturnKeyType; private @Nullable SelectionWatcher mSelectionWatcher; @@ -135,7 +135,6 @@ public class ReactEditText extends AppCompatEditText mDefaultGravityVertical = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; mNativeEventCount = 0; mIsSettingTextFromJS = false; - mBlurOnSubmit = null; mDisableFullscreen = false; mListeners = null; mTextWatcherDelegator = null; @@ -256,7 +255,7 @@ public class ReactEditText extends AppCompatEditText inputConnection, reactContext, this, mEventDispatcher); } - if (isMultiline() && getBlurOnSubmit()) { + if (isMultiline() && (shouldBlurOnReturn() || shouldSubmitOnReturn())) { // Remove IME_FLAG_NO_ENTER_ACTION to keep the original IME_OPTION outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION; } @@ -380,21 +379,52 @@ public class ReactEditText extends AppCompatEditText mSelectionWatcher = selectionWatcher; } - public void setBlurOnSubmit(@Nullable Boolean blurOnSubmit) { - mBlurOnSubmit = blurOnSubmit; - } - public void setOnKeyPress(boolean onKeyPress) { mOnKeyPress = onKeyPress; } - public boolean getBlurOnSubmit() { - if (mBlurOnSubmit == null) { - // Default blurOnSubmit - return isMultiline() ? false : true; + public boolean shouldBlurOnReturn() { + String submitBehavior = getSubmitBehavior(); + boolean shouldBlur; + + // Default shouldBlur + if (submitBehavior == null) { + if (!isMultiline()) { + shouldBlur = true; + } else { + shouldBlur = false; + } + } else { + shouldBlur = submitBehavior.equals("blurAndSubmit"); } - return mBlurOnSubmit; + return shouldBlur; + } + + public boolean shouldSubmitOnReturn() { + String submitBehavior = getSubmitBehavior(); + boolean shouldSubmit; + + // Default shouldSubmit + if (submitBehavior == null) { + if (!isMultiline()) { + shouldSubmit = true; + } else { + shouldSubmit = false; + } + } else { + shouldSubmit = submitBehavior.equals("submit") || submitBehavior.equals("blurAndSubmit"); + } + + return shouldSubmit; + } + + public String getSubmitBehavior() { + return mSubmitBehavior; + } + + public void setSubmitBehavior(String submitBehavior) { + mSubmitBehavior = submitBehavior; } public void setDisableFullscreenUI(boolean disableFullscreenUI) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index f3134f9cb1d..aac043da34f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -441,9 +441,9 @@ public class ReactTextInputManager extends BaseViewManager Clear focus; prevent default behaviour (return + // * shouldSubmit => Clear focus; prevent default behavior (return true); + // * shouldBlur => Submit; prevent default behavior (return true); + // * !shouldBlur && !shouldSubmit && isMultiline => Perform default behavior (return + // false); + // * !shouldBlur && !shouldSubmit && !isMultiline => Prevent default behavior (return // true); - // * blurOnSubmit && !isMultiline => Clear focus; prevent default behaviour (return - // true); - // * !blurOnSubmit && isMultiline => Perform default behaviour (return false); - // * !blurOnSubmit && !isMultiline => Prevent default behaviour (return true). - // Additionally we always generate a `submit` event. + if (shouldSubmit) { + EventDispatcher eventDispatcher = getEventDispatcher(reactContext, editText); + eventDispatcher.dispatchEvent( + new ReactTextInputSubmitEditingEvent( + reactContext.getSurfaceId(), + editText.getId(), + editText.getText().toString())); + } - EventDispatcher eventDispatcher = getEventDispatcher(reactContext, editText); - eventDispatcher.dispatchEvent( - new ReactTextInputSubmitEditingEvent( - reactContext.getSurfaceId(), - editText.getId(), - editText.getText().toString())); - - if (blurOnSubmit) { + if (shouldBlur) { editText.clearFocus(); } // Prevent default behavior except when we want it to insert a newline. - if (blurOnSubmit || !isMultiline) { + if (shouldBlur || shouldSubmit || !isMultiline) { return true; } - // If we've reached this point, it means that the TextInput has 'blurOnSubmit' set to - // false and 'multiline' set to true. But it's still possible to get IME_ACTION_NEXT + // If we've reached this point, it means that the TextInput has 'submitBehavior' set + // nullish and 'multiline' set to true. But it's still possible to get IME_ACTION_NEXT // and IME_ACTION_PREVIOUS here in case if 'disableFullscreenUI' is false and Android // decides to render this EditText in the full screen mode (when a phone has the // landscape orientation for example). The full screen EditText also renders an action diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java index a817b810d41..edf798ab86e 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java @@ -194,7 +194,7 @@ public class ReactTextInputPropertyTest { ReactEditText view = mManager.createViewInstance(mThemedContext); mManager.updateProperties(view, buildStyles("multiline", true)); - mManager.updateProperties(view, buildStyles("blurOnSubmit", true)); + mManager.updateProperties(view, buildStyles("submitBehavior", "blurAndSubmit")); EditorInfo editorInfo = new EditorInfo(); editorInfo.imeOptions = EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_ENTER_ACTION; diff --git a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp index becfec590be..b3950b9eacb 100644 --- a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp +++ b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp @@ -145,10 +145,10 @@ AndroidTextInputProps::AndroidTextInputProps( "selectTextOnFocus", sourceProps.selectTextOnFocus, {false})), - blurOnSubmit(convertRawProp(context, rawProps, - "blurOnSubmit", - sourceProps.blurOnSubmit, - {false})), + submitBehavior(convertRawProp(context, rawProps, + "submitBehavior", + sourceProps.submitBehavior, + {})), caretHidden(convertRawProp(context, rawProps, "caretHidden", sourceProps.caretHidden, @@ -298,7 +298,7 @@ folly::dynamic AndroidTextInputProps::getDynamic() const { props["value"] = value; props["defaultValue"] = defaultValue; props["selectTextOnFocus"] = selectTextOnFocus; - props["blurOnSubmit"] = blurOnSubmit; + props["submitBehavior"] = submitBehavior; props["caretHidden"] = caretHidden; props["contextMenuHidden"] = contextMenuHidden; props["textShadowColor"] = toAndroidRepr(textShadowColor); diff --git a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h index 6843425b190..e487bddd34a 100644 --- a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h +++ b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h @@ -143,7 +143,7 @@ class AndroidTextInputProps final : public ViewProps, public BaseTextProps { const std::string value{}; const std::string defaultValue{}; const bool selectTextOnFocus{false}; - const bool blurOnSubmit{false}; + const std::string submitBehavior{}; const bool caretHidden{false}; const bool contextMenuHidden{false}; const SharedColor textShadowColor{}; diff --git a/ReactCommon/react/renderer/components/textinput/iostextinput/conversions.h b/ReactCommon/react/renderer/components/textinput/iostextinput/conversions.h index 98e44fd7c07..f8b16d24add 100644 --- a/ReactCommon/react/renderer/components/textinput/iostextinput/conversions.h +++ b/ReactCommon/react/renderer/components/textinput/iostextinput/conversions.h @@ -126,6 +126,26 @@ inline void fromRawValue( abort(); } +inline void fromRawValue( + const PropsParserContext &context, + const RawValue &value, + SubmitBehavior &result) { + auto string = (std::string)value; + if (string == "newline") { + result = SubmitBehavior::Newline; + return; + } + if (string == "submit") { + result = SubmitBehavior::Submit; + return; + } + if (string == "blurAndSubmit") { + result = SubmitBehavior::BlurAndSubmit; + return; + } + abort(); +} + inline void fromRawValue( const PropsParserContext &context, const RawValue &value, diff --git a/ReactCommon/react/renderer/components/textinput/iostextinput/primitives.h b/ReactCommon/react/renderer/components/textinput/iostextinput/primitives.h index 1747a98bf4c..bbb56d26d9a 100644 --- a/ReactCommon/react/renderer/components/textinput/iostextinput/primitives.h +++ b/ReactCommon/react/renderer/components/textinput/iostextinput/primitives.h @@ -48,6 +48,14 @@ enum class ReturnKeyType { Continue, }; +// iOS & Android. +enum class SubmitBehavior { + Default, + Submit, + BlurAndSubmit, + Newline, +}; + // iOS-only enum class TextInputAccessoryVisibilityMode { Never, @@ -170,9 +178,9 @@ class TextInputTraits final { /* * iOS & Android - * Default value: `false`. + * Default value: `Default`. */ - bool blurOnSubmit{false}; + SubmitBehavior submitBehavior{SubmitBehavior::Default}; /* * iOS-only (implemented only on iOS for now) diff --git a/ReactCommon/react/renderer/components/textinput/iostextinput/propsConversions.h b/ReactCommon/react/renderer/components/textinput/iostextinput/propsConversions.h index 84c52eeace6..15dc0004596 100644 --- a/ReactCommon/react/renderer/components/textinput/iostextinput/propsConversions.h +++ b/ReactCommon/react/renderer/components/textinput/iostextinput/propsConversions.h @@ -93,12 +93,12 @@ static TextInputTraits convertRawProp( "secureTextEntry", sourceTraits.secureTextEntry, defaultTraits.secureTextEntry); - traits.blurOnSubmit = convertRawProp( + traits.submitBehavior = convertRawProp( context, rawProps, - "blurOnSubmit", - sourceTraits.blurOnSubmit, - defaultTraits.blurOnSubmit); + "submitBehavior", + sourceTraits.submitBehavior, + defaultTraits.submitBehavior); traits.clearTextOnFocus = convertRawProp( context, rawProps, diff --git a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js index 2e9a4d754dc..9b8e62126e5 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js @@ -248,6 +248,90 @@ class BlurOnSubmitExample extends React.Component<{...}> { } } +class SubmitBehaviorExample extends React.Component<{...}> { + ref1 = React.createRef(); + ref2 = React.createRef(); + ref3 = React.createRef(); + ref4 = React.createRef(); + ref5 = React.createRef(); + ref6 = React.createRef(); + ref7 = React.createRef(); + ref8 = React.createRef(); + ref9 = React.createRef(); + ref10 = React.createRef(); + ref11 = React.createRef(); + + render() { + return ( + + this.ref2.current?.focus()} + /> + this.ref3.current?.focus()} + /> + this.ref4.current?.focus()} + /> + this.ref5.current?.focus()} + /> + this.ref6.current?.focus()} + /> + this.ref7.current?.focus()} + /> + this.ref8.current?.focus()} + /> + this.ref9.current?.focus()} + /> + + + + + ); + } +} + class TextEventsExample extends React.Component<{...}, $FlowFixMeState> { state = { curText: '', @@ -620,6 +704,12 @@ module.exports = ([ return ; }, }, + { + title: 'Submit behavior', + render: function (): React.Element { + return ; + }, + }, { title: 'Event handling', render: function (): React.Element {