Skip to content

Commit

Permalink
Allow multiline TextInputs be submittable without blurring (#33653)
Browse files Browse the repository at this point in the history
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

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/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: #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
  • Loading branch information
Tony Du authored and facebook-github-bot committed Jul 22, 2022
1 parent ccdf9ac commit 1e3cb91
Show file tree
Hide file tree
Showing 22 changed files with 351 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
*
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion Libraries/Components/TextInput/RCTTextInputViewConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')},
Expand Down
77 changes: 63 additions & 14 deletions Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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`.
*
Expand Down Expand Up @@ -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:
*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ exports[`TextInput tests should render as expected: should deep render when mock
<RCTSinglelineTextInputView
accessible={true}
allowFontScaling={true}
blurOnSubmit={true}
focusable={true}
forwardedRef={null}
mostRecentEventCount={0}
Expand All @@ -24,6 +23,7 @@ exports[`TextInput tests should render as expected: should deep render when mock
onStartShouldSetResponder={[Function]}
rejectResponderTermination={true}
selection={null}
submitBehavior="blurAndSubmit"
text=""
underlineColorAndroid="transparent"
/>
Expand All @@ -33,7 +33,6 @@ exports[`TextInput tests should render as expected: should deep render when not
<RCTSinglelineTextInputView
accessible={true}
allowFontScaling={true}
blurOnSubmit={true}
focusable={true}
forwardedRef={null}
mostRecentEventCount={0}
Expand All @@ -53,6 +52,7 @@ exports[`TextInput tests should render as expected: should deep render when not
onStartShouldSetResponder={[Function]}
rejectResponderTermination={true}
selection={null}
submitBehavior="blurAndSubmit"
text=""
underlineColorAndroid="transparent"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ @implementation RCTMultilineTextInputView
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super initWithBridge:bridge]) {
// `blurOnSubmit` defaults to `false` for <TextInput multiline={true}> by design.
self.blurOnSubmit = NO;

_backedTextInputView = [[RCTUITextView alloc] initWithFrame:self.bounds];
_backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_backedTextInputView.textInputDelegate = self;
Expand Down
4 changes: 3 additions & 1 deletion Libraries/Text/TextInput/RCTBackedTextInputDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
8 changes: 7 additions & 1 deletion Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ - (BOOL)textField:(__unused UITextField *)textField shouldChangeCharactersInRang

- (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];
}

Expand Down Expand Up @@ -209,10 +211,14 @@ - (BOOL)textView:(__unused UITextView *)textView shouldChangeTextInRange:(NSRang
{
// 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;
}
}

Expand Down
2 changes: 1 addition & 1 deletion Libraries/Text/TextInput/RCTBaseTextInputView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
31 changes: 19 additions & 12 deletions Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line number Diff line number Diff line change
Expand Up @@ -350,20 +350,27 @@ - (void)textInputDidEndEditing
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
Expand Down
2 changes: 1 addition & 1 deletion Libraries/Text/TextInput/RCTBaseTextInputViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ @implementation RCTBaseTextInputViewManager
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ @implementation RCTSinglelineTextInputView
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super initWithBridge:bridge]) {
// `blurOnSubmit` defaults to `true` for <TextInput multiline={false}> by design.
self.blurOnSubmit = YES;
// `submitBehavior` defaults to `"blurAndSubmit"` for <TextInput multiline={false}> by design.
self.submitBehavior = @"blurAndSubmit";

_backedTextInputView = [[RCTUITextField alloc] initWithFrame:self.bounds];
_backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,20 +300,25 @@ - (void)textInputDidEndEditing
}
}

- (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<TextInputEventEmitter const>(_eventEmitter)->onSubmitEditing([self _textInputMetrics]);
}
return shouldSubmit;
}

auto const &props = *std::static_pointer_cast<TextInputProps const>(_props);
return props.traits.blurOnSubmit;
- (BOOL)textInputShouldReturn
{
return [self getSubmitBehavior] == SubmitBehavior::BlurAndSubmit;
}

- (void)textInputDidReturn
Expand Down Expand Up @@ -644,6 +649,19 @@ - (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldTe
}
}

- (SubmitBehavior)getSubmitBehavior
{
auto const &props = *std::static_pointer_cast<TextInputProps const>(_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<RCTComponentViewProtocol> RCTTextInputCls(void)
Expand Down
Loading

0 comments on commit 1e3cb91

Please sign in to comment.