Skip to content

Commit

Permalink
Adds declarative props to clear/submit MultilineTextInput
Browse files Browse the repository at this point in the history
This brings macOS parity with react-native-windows microsoft/react-native-windows#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
  • Loading branch information
Alex Chiu authored and christophpurrer committed Sep 12, 2022
1 parent 8f5b6f6 commit 3f9ac1c
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 11 deletions.
26 changes: 26 additions & 0 deletions Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<SubmitKeyEvent>,
|}>;
// ]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.
Expand Down Expand Up @@ -515,6 +540,7 @@ type AndroidProps = $ReadOnly<{|
export type Props = $ReadOnly<{|
...$Diff<ViewProps, $ReadOnly<{|style: ?ViewStyleProp|}>>,
...IOSProps,
...MacOSProps,
...AndroidProps,

/**
Expand Down
15 changes: 13 additions & 2 deletions Libraries/Text/TextInput/Multiline/RCTUITextView.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}

Expand Down
1 change: 1 addition & 0 deletions Libraries/Text/TextInput/RCTBackedTextInputDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)

/*
Expand Down
6 changes: 6 additions & 0 deletions Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 31 additions & 8 deletions Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
<UITextFieldDelegate>
Expand Down Expand Up @@ -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]) {
Expand Down Expand Up @@ -226,8 +244,7 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doComman
[[_backedTextInputView window] makeFirstResponder:nil];
}
commandHandled = YES;
}

}
return commandHandled;
}

Expand Down Expand Up @@ -415,10 +432,16 @@ - (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
id<RCTBackedTextInputDelegate> 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];
Expand Down
4 changes: 3 additions & 1 deletion Libraries/Text/TextInput/RCTBaseTextInputView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSDictionary *> *submitKeyEvents;
#endif // TODO(macOS GH#774)

@property (nonatomic, assign) NSInteger mostRecentEventCount;
@property (nonatomic, assign, readonly) NSInteger nativeEventCount;
@property (nonatomic, assign) BOOL autoFocus;
Expand Down
37 changes: 37 additions & 0 deletions Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#import <React/RCTUtils.h>
#import <React/UIView+React.h>

#import <React/RCTViewKeyboardEvent.h> // TODO(macOS GH#774)
#import <React/RCTInputAccessoryView.h>
#import <React/RCTInputAccessoryViewContent.h>
#import <React/RCTTextAttributes.h>
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions Libraries/Text/TextInput/RCTBaseTextInputViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSDictionary *>);
#endif // TODO(macOS GH#774)

RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
Expand Down
39 changes: 39 additions & 0 deletions packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ const styles = StyleSheet.create({
fontFamily: 'Cochin',
height: 60,
},
singleLine: {
fontSize: 16,
},
singlelinePlaceholderStyles: {
letterSpacing: 10,
textAlign: 'center',
Expand Down Expand Up @@ -945,6 +948,42 @@ if (Platform.OS === 'macos') {
return <AutoCorrectSpellCheckGrammarCheckCallbacks />;
},
},
{
title: 'Clear text on submit - Multiline Textfield',
render: function (): React.Node {
return (
<View>
<Text>Default submit key (Enter):</Text>
<TextInput
multiline={true}
clearTextOnSubmit={true}
style={styles.multiline}
/>
<Text>Custom submit key (Enter): - same as above</Text>
<TextInput
multiline={true}
clearTextOnSubmit={true}
style={styles.multiline}
submitKeyEvents={[{key: 'Enter'}]}
/>
<Text>Custom submit key (CMD + Enter):</Text>
<TextInput
multiline={true}
clearTextOnSubmit={true}
style={styles.multiline}
submitKeyEvents={[{key: 'Enter', metaKey: true}]}
/>
<Text>Custom submit key (Shift + Enter):</Text>
<TextInput
multiline={true}
clearTextOnSubmit={true}
style={styles.multiline}
submitKeyEvents={[{key: 'Enter', shiftKey: true}]}
/>
</View>
);
},
},
{
title:
'onDragEnter, onDragLeave and onDrop - Single- & MultiLineTextInput',
Expand Down

0 comments on commit 3f9ac1c

Please sign in to comment.