From 3f9ac1c142a293754f10876749eed5005468075b Mon Sep 17 00:00:00 2001 From: Alex Chiu Date: Fri, 4 Mar 2022 14:22:12 -0800 Subject: [PATCH] Adds declarative props to clear/submit MultilineTextInput This brings macOS parity with react-native-windows https://github.com/microsoft/react-native-windows/pull/7333 for MultilineTextInput fields See react-native-windows PR for desired feature set. We can't do it for SinglelineTextInput fields (at least not w/o hacks) as it does not support overriding the keyDown event :-( https://stackoverflow.com/a/6076492 --- Libraries/Components/TextInput/TextInput.js | 26 +++++++++++++ .../Text/TextInput/Multiline/RCTUITextView.m | 15 ++++++- .../TextInput/RCTBackedTextInputDelegate.h | 1 + .../RCTBackedTextInputDelegateAdapter.h | 6 +++ .../RCTBackedTextInputDelegateAdapter.m | 39 +++++++++++++++---- .../Text/TextInput/RCTBaseTextInputView.h | 4 +- .../Text/TextInput/RCTBaseTextInputView.m | 37 ++++++++++++++++++ .../TextInput/RCTBaseTextInputViewManager.m | 5 +++ .../TextInput/TextInputExample.ios.js | 39 +++++++++++++++++++ 9 files changed, 161 insertions(+), 11 deletions(-) diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 4264b5e04b96cf..8a426c0763ed22 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -353,6 +353,31 @@ type IOSProps = $ReadOnly<{| textContentType?: ?TextContentType, |}>; +// [TODO(macOS GH#774) +export type SubmitKeyEvent = $ReadOnly<{| + key: string, + altKey?: ?boolean, + ctrlKey?: ?boolean, + metaKey?: ?boolean, + shiftKey?: ?boolean, + functionKey?: ?boolean, +|}>; + +type MacOSProps = $ReadOnly<{| + /** + * If `true`, clears the text field synchronously before `onSubmitEditing` is emitted. + * @platform macos + */ + clearTextOnSubmit?: ?boolean, + + /** + * Configures keys that can be used to submit editing for the TextInput. Defaults to 'Enter' key. + * @platform macos + */ + submitKeyEvents?: ?$ReadOnlyArray, +|}>; +// ]TODO(macOS GH#774) + type AndroidProps = $ReadOnly<{| /** * Specifies autocomplete hints for the system, so it can provide autofill. On Android, the system will always attempt to offer autofill by using heuristics to identify the type of content. @@ -515,6 +540,7 @@ type AndroidProps = $ReadOnly<{| export type Props = $ReadOnly<{| ...$Diff>, ...IOSProps, + ...MacOSProps, ...AndroidProps, /** diff --git a/Libraries/Text/TextInput/Multiline/RCTUITextView.m b/Libraries/Text/TextInput/Multiline/RCTUITextView.m index 48780bc2045c14..4c3bc66b3eb678 100644 --- a/Libraries/Text/TextInput/Multiline/RCTUITextView.m +++ b/Libraries/Text/TextInput/Multiline/RCTUITextView.m @@ -551,9 +551,20 @@ - (void)deleteBackward { } #else - (void)keyDown:(NSEvent *)event { - // If hasMarkedText is true then an IME is open, so don't send event to JS. - if (self.hasMarkedText || [self.textInputDelegate textInputShouldHandleKeyEvent:event]) { + // If has marked text, handle by native and return + // Do this check before textInputShouldHandleKeyEvent as that one attempts to send the event to JS + if (self.hasMarkedText) { [super keyDown:event]; + return; + } + + // textInputShouldHandleKeyEvent represents if native should handle the event instead of JS. + // textInputShouldHandleKeyEvent also sends keyDown even to JS internally, so we only call this once + BOOL keyDownHandledByNative = [self.textInputDelegate textInputShouldHandleKeyEvent:event]; + + if (keyDownHandledByNative) { + [super keyDown:event]; + [self.textInputDelegate submitOnKeyDownIfNeeded:event]; } } diff --git a/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h b/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h index f836b9454a909a..30253c300c4266 100644 --- a/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h +++ b/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h @@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)automaticSpellingCorrectionDidChange:(BOOL)enabled; - (void)continuousSpellCheckingDidChange:(BOOL)enabled; - (void)grammarCheckingDidChange:(BOOL)enabled; +- (void)submitOnKeyDownIfNeeded:(NSEvent *)event; #endif // ]TODO(macOS GH#774) /* diff --git a/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h b/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h index b9c7e1d2375aca..1dd678d5c2d34e 100644 --- a/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h +++ b/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h @@ -9,6 +9,12 @@ NS_ASSUME_NONNULL_BEGIN +#if TARGET_OS_OSX // TODO(macOS GH#774) +@interface RCTBackedTextFieldDelegateAdapterUtility : NSObject ++ (BOOL)isShiftOrOptionKeyDown; +@end +#endif // ]TODO(macOS GH#774) + #pragma mark - RCTBackedTextFieldDelegateAdapter (for UITextField) @protocol RCTBackedTextInputViewProtocol; // TODO(OSS Candidate ISS#2710739) diff --git a/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m b/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m index 6853065a48d038..fa5280cbc4ed17 100644 --- a/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m +++ b/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m @@ -14,6 +14,18 @@ static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingContext; +#if TARGET_OS_OSX // [TODO(macOS GH#774) +@implementation RCTBackedTextFieldDelegateAdapterUtility ++ (BOOL)isShiftOrOptionKeyDown +{ + NSEvent* event = [NSApp currentEvent]; + BOOL isShiftKeyDown = (event.modifierFlags & NSEventModifierFlagShift) == NSEventModifierFlagShift; + BOOL isOptionKeyDown = (event.modifierFlags & NSEventModifierFlagOption) == NSEventModifierFlagOption; + return isShiftKeyDown || isOptionKeyDown; +} +@end +#endif // ]TODO(macOS GH#774) + @interface RCTBackedTextFieldDelegateAdapter () #if !TARGET_OS_OSX // [TODO(macOS GH#774) @@ -191,11 +203,17 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doComman BOOL commandHandled = NO; // enter/return if (commandSelector == @selector(insertNewline:) || commandSelector == @selector(insertNewlineIgnoringFieldEditor:)) { - [self textFieldDidEndEditingOnExit]; - if ([textInputDelegate textInputShouldReturn]) { - [[_backedTextInputView window] makeFirstResponder:nil]; + #if TARGET_OS_OSX // [TODO(macOS Candidate GH#774) + if (![RCTBackedTextFieldDelegateAdapterUtility isShiftOrOptionKeyDown]) { + #endif // ]TODO(macOS Candidate GH#774) + [self textFieldDidEndEditingOnExit]; + if ([[_backedTextInputView textInputDelegate] textInputShouldReturn]) { + [[_backedTextInputView window] makeFirstResponder:nil]; + } + commandHandled = YES; + #if TARGET_OS_OSX // [TODO(macOS Candidate GH#774) } - commandHandled = YES; + #endif // ]TODO(macOS Candidate GH#774) //backspace } else if (commandSelector == @selector(deleteBackward:)) { if (textInputDelegate != nil && ![textInputDelegate textInputShouldHandleDeleteBackward:_backedTextInputView]) { @@ -226,8 +244,7 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doComman [[_backedTextInputView window] makeFirstResponder:nil]; } commandHandled = YES; -} - + } return commandHandled; } @@ -415,10 +432,16 @@ - (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector id textInputDelegate = [_backedTextInputView textInputDelegate]; // enter/return if ((commandSelector == @selector(insertNewline:) || commandSelector == @selector(insertNewlineIgnoringFieldEditor:))) { - if (textInputDelegate.textInputShouldReturn) { - [_backedTextInputView.window makeFirstResponder:nil]; + #if TARGET_OS_OSX // [TODO(macOS GH#774) + if (![RCTBackedTextFieldDelegateAdapterUtility isShiftOrOptionKeyDown]) { + #endif // ]TODO(macOS GH#774) + if (textInputDelegate.textInputShouldReturn) { + [_backedTextInputView.window makeFirstResponder:nil]; + } commandHandled = YES; + #if TARGET_OS_OSX // [TODO(macOS GH#774) } + #endif //backspace } else if (commandSelector == @selector(deleteBackward:)) { commandHandled = textInputDelegate != nil && ![textInputDelegate textInputShouldHandleDeleteBackward:_backedTextInputView]; diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.h b/Libraries/Text/TextInput/RCTBaseTextInputView.h index 6cac4a9cab17ef..559f0e8695d0a3 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.h +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.h @@ -47,8 +47,10 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy, nullable) RCTBubblingEventBlock onAutoCorrectChange; @property (nonatomic, copy, nullable) RCTBubblingEventBlock onSpellCheckChange; @property (nonatomic, copy, nullable) RCTBubblingEventBlock onGrammarCheckChange; +@property (nonatomic, assign) BOOL clearTextOnSubmit; +@property (nonatomic, copy, nullable) RCTDirectEventBlock onSubmitEditing; +@property (nonatomic, copy) NSArray *submitKeyEvents; #endif // TODO(macOS GH#774) - @property (nonatomic, assign) NSInteger mostRecentEventCount; @property (nonatomic, assign, readonly) NSInteger nativeEventCount; @property (nonatomic, assign) BOOL autoFocus; diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.m b/Libraries/Text/TextInput/RCTBaseTextInputView.m index 6a9ded1dc218a1..135ab3127122ea 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -14,6 +14,7 @@ #import #import +#import // TODO(macOS GH#774) #import #import #import @@ -428,6 +429,42 @@ - (void)grammarCheckingDidChange:(BOOL)enabled _onGrammarCheckChange(@{@"enabled": [NSNumber numberWithBool:enabled]}); } } + +- (void)submitOnKeyDownIfNeeded:(NSEvent *)event +{ + NSDictionary *currentKeyboardEvent = [RCTViewKeyboardEvent bodyFromEvent:event]; + // Enter is the default clearTextOnSubmit key + BOOL shouldSubmit = NO; + if (!_submitKeyEvents) { + shouldSubmit = [currentKeyboardEvent[@"key"] isEqualToString:@"Enter"] + && ![currentKeyboardEvent[@"shiftKey"] boolValue]; // Default clearTextOnSubmit key + } else { + for (NSDictionary *submitKeyEvent in _submitKeyEvents) { + if ( + [submitKeyEvent[@"key"] isEqualToString:currentKeyboardEvent[@"key"]] && + [submitKeyEvent[@"altKey"] boolValue] == [currentKeyboardEvent[@"altKey"] boolValue] && + [submitKeyEvent[@"shiftKey"] boolValue] == [currentKeyboardEvent[@"shiftKey"] boolValue] && + [submitKeyEvent[@"ctrlKey"] boolValue]== [currentKeyboardEvent[@"ctrlKey"] boolValue] && + [submitKeyEvent[@"metaKey"] boolValue]== [currentKeyboardEvent[@"metaKey"] boolValue] && + [submitKeyEvent[@"functionKey"] boolValue]== [currentKeyboardEvent[@"functionKey"] boolValue] + ) { + shouldSubmit = YES; + break; + } + } + } + + if (shouldSubmit) { + if (_onSubmitEditing) { + _onSubmitEditing(@{}); + } + + if (_clearTextOnSubmit) { + self.backedTextInputView.attributedText = [NSAttributedString new]; + [self.backedTextInputView.textInputDelegate textInputDidChange]; + } + } +} #endif // ]TODO(macOS GH#774) - (BOOL)textInputShouldReturn diff --git a/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m b/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m index 97682e16784f2f..755b36c3af9797 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m @@ -71,6 +71,11 @@ @implementation RCTBaseTextInputViewManager RCT_EXPORT_VIEW_PROPERTY(onAutoCorrectChange, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onSpellCheckChange, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onGrammarCheckChange, RCTBubblingEventBlock); + +// Specifically for clearing text on enter key press +RCT_EXPORT_VIEW_PROPERTY(clearTextOnSubmit, BOOL); +RCT_EXPORT_VIEW_PROPERTY(onSubmitEditing, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(submitKeyEvents, NSArray); #endif // TODO(macOS GH#774) RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) diff --git a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js index 2573db35e23659..de5028d26f11c9 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js @@ -304,6 +304,9 @@ const styles = StyleSheet.create({ fontFamily: 'Cochin', height: 60, }, + singleLine: { + fontSize: 16, + }, singlelinePlaceholderStyles: { letterSpacing: 10, textAlign: 'center', @@ -945,6 +948,42 @@ if (Platform.OS === 'macos') { return ; }, }, + { + title: 'Clear text on submit - Multiline Textfield', + render: function (): React.Node { + return ( + + Default submit key (Enter): + + Custom submit key (Enter): - same as above + + Custom submit key (CMD + Enter): + + Custom submit key (Shift + Enter): + + + ); + }, + }, { title: 'onDragEnter, onDragLeave and onDrop - Single- & MultiLineTextInput',