Skip to content

Commit 65bd3ff

Browse files
appdenchristophpurrer
authored andcommitted
Fix handling of keyDown/keyUp events by TextInput
This extends the ability to intercept `keyDown` and `keyUp` events to `TextInput`. We need this for the ability to insert newlines when holding shift in chat, along with support arrow up/down from the search input.
1 parent 8ed4340 commit 65bd3ff

File tree

8 files changed

+92
-39
lines changed

8 files changed

+92
-39
lines changed

Libraries/Components/TextInput/TextInput.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,8 @@ function InternalTextInput(props: Props): React.Node {
13331333
onChangeSync={useOnChangeSync === true ? _onChangeSync : null}
13341334
onContentSizeChange={props.onContentSizeChange}
13351335
onFocus={_onFocus}
1336+
onKeyDown={props.onKeyDown} // TODO(macOS GH#774)
1337+
onKeyUp={props.onKeyUp} // TODO(macOS GH#774)
13361338
onScroll={_onScroll}
13371339
onSelectionChange={_onSelectionChange}
13381340
onSelectionChangeShouldSetResponder={emptyFunctionThatReturnsTrue}

Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ exports[`TextInput tests should render as expected: should deep render when mock
1313
onChangeSync={null}
1414
onClick={[Function]}
1515
onFocus={[Function]}
16-
onKeyDown={[Function]}
17-
onKeyUp={[Function]}
1816
onResponderGrant={[Function]}
1917
onResponderMove={[Function]}
2018
onResponderRelease={[Function]}
@@ -44,8 +42,6 @@ exports[`TextInput tests should render as expected: should deep render when not
4442
onChangeSync={null}
4543
onClick={[Function]}
4644
onFocus={[Function]}
47-
onKeyDown={[Function]}
48-
onKeyUp={[Function]}
4945
onResponderGrant={[Function]}
5046
onResponderMove={[Function]}
5147
onResponderRelease={[Function]}

Libraries/Text/TextInput/Multiline/RCTUITextView.m

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,19 @@ - (void)deleteBackward {
514514
[super deleteBackward];
515515
}
516516
}
517+
#else
518+
- (void)keyDown:(NSEvent *)event {
519+
// If hasMarkedText is true then an IME is open, so don't send event to JS.
520+
if (self.hasMarkedText || [self.textInputDelegate textInputShouldHandleKeyEvent:event]) {
521+
[super keyDown:event];
522+
}
523+
}
524+
525+
- (void)keyUp:(NSEvent *)event {
526+
if ([self.textInputDelegate textInputShouldHandleKeyEvent:event]) {
527+
[super keyUp:event];
528+
}
529+
}
517530
#endif // ]TODO(OSS Candidate ISS#2710739)
518531

519532
- (void)_updatePlaceholder

Libraries/Text/TextInput/RCTBackedTextInputDelegate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ NS_ASSUME_NONNULL_BEGIN
4242
- (BOOL)textInputShouldHandleDeleteBackward:(id<RCTBackedTextInputViewProtocol>)sender; // Return `YES` to have the deleteBackward event handled normally. Return `NO` to disallow it and handle it yourself. TODO(OSS Candidate ISS#2710739)
4343
#if TARGET_OS_OSX // [TODO(macOS GH#774)
4444
- (BOOL)textInputShouldHandleDeleteForward:(id<RCTBackedTextInputViewProtocol>)sender; // Return `YES` to have the deleteForward event handled normally. Return `NO` to disallow it and handle it yourself.
45+
- (BOOL)textInputShouldHandleKeyEvent:(NSEvent *)event; // Return `YES` to have the key event handled normally. Return `NO` to disallow it and handle it yourself.
4546

4647
- (void)textInputDidCancel; // Handle `Escape` key press.
4748
#endif // ]TODO(macOS GH#774)

Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
#import <React/RCTBackedTextInputDelegateAdapter.h>
9+
#import <React/RCTBaseTextInputView.h> // TODO(macOS GH#774)
910
#import "RCTBackedTextInputViewProtocol.h" // TODO(OSS Candidate ISS#2710739)
1011
#import "RCTBackedTextInputDelegate.h" // TODO(OSS Candidate ISS#2710739)
1112
#import "../RCTTextUIKit.h" // TODO(macOS GH#774)
@@ -188,6 +189,9 @@ - (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor
188189
- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
189190
{
190191
id<RCTBackedTextInputDelegate> textInputDelegate = [_backedTextInputView textInputDelegate];
192+
RCTBaseTextInputView* textInputView = (RCTBaseTextInputView*)_backedTextInputView.superview;
193+
BOOL hasValidKeys = textInputView && (textInputView.validKeysUp.count || textInputView.validKeysDown.count);
194+
191195
BOOL commandHandled = NO;
192196
// enter/return
193197
if (commandSelector == @selector(insertNewline:) || commandSelector == @selector(insertNewlineIgnoringFieldEditor:)) {
@@ -217,9 +221,11 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doComman
217221
//escape
218222
} else if (commandSelector == @selector(cancelOperation:)) {
219223
[textInputDelegate textInputDidCancel];
220-
[[_backedTextInputView window] makeFirstResponder:nil];
224+
if (!hasValidKeys) {
225+
[_backedTextInputView.window makeFirstResponder:nil];
226+
}
221227
commandHandled = YES;
222-
}
228+
}
223229

224230
return commandHandled;
225231
}
@@ -406,6 +412,9 @@ - (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
406412
{
407413
BOOL commandHandled = NO;
408414
id<RCTBackedTextInputDelegate> textInputDelegate = [_backedTextInputView textInputDelegate];
415+
RCTBaseTextInputView* textInputView = (RCTBaseTextInputView*)_backedTextInputView.superview.superview.superview;
416+
BOOL hasValidKeys = textInputView && (textInputView.validKeysUp.count || textInputView.validKeysDown.count);
417+
409418
// enter/return
410419
if ((commandSelector == @selector(insertNewline:) || commandSelector == @selector(insertNewlineIgnoringFieldEditor:))) {
411420
if (textInputDelegate.textInputShouldReturn) {
@@ -421,9 +430,10 @@ - (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
421430
//escape
422431
} else if (commandSelector == @selector(cancelOperation:)) {
423432
[textInputDelegate textInputDidCancel];
424-
[_backedTextInputView.window makeFirstResponder:nil];
433+
if (!hasValidKeys) {
434+
[_backedTextInputView.window makeFirstResponder:nil];
435+
}
425436
commandHandled = YES;
426-
427437
}
428438

429439
return commandHandled;

Libraries/Text/TextInput/RCTBaseTextInputView.m

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
3232
{
3333
RCTAssertParam(bridge);
3434

35-
if (self = [super initWithFrame:CGRectZero]) {
35+
if (self = [super initWithEventDispatcher:bridge.eventDispatcher]) { // TODO(OSS Candidate GH#774)
3636
_bridge = bridge;
3737
_eventDispatcher = bridge.eventDispatcher;
3838
}
@@ -42,7 +42,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
4242

4343
RCT_NOT_IMPLEMENTED(- (instancetype)init)
4444
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)decoder)
45-
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
4645

4746
- (RCTUIView<RCTBackedTextInputViewProtocol> *)backedTextInputView // TODO(macOS ISS#3536887)
4847
{
@@ -597,6 +596,10 @@ - (void)textInputDidCancel {
597596
eventCount:_nativeEventCount];
598597
[self textInputDidEndEditing];
599598
}
599+
600+
- (BOOL)textInputShouldHandleKeyEvent:(NSEvent *)event {
601+
return ![self handleKeyboardEvent:event];
602+
}
600603
#endif // ]TODO(macOS GH#774)
601604

602605
- (void)updateLocalData

Libraries/Text/TextInput/Singleline/RCTUITextField.m

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,17 @@ - (BOOL)becomeFirstResponder
482482
}
483483
return isFirstResponder;
484484
}
485+
486+
- (BOOL)performKeyEquivalent:(NSEvent *)event
487+
{
488+
// The currentEditor is NSText for historical reasons, but documented to be NSTextView.
489+
NSTextView *currentEditor = (NSTextView *)self.currentEditor;
490+
// The currentEditor is non-nil when focused and hasMarkedText means an IME is open.
491+
if (currentEditor && !currentEditor.hasMarkedText && ![self.textInputDelegate textInputShouldHandleKeyEvent:event]) {
492+
return YES; // Don't send currentEditor the keydown event.
493+
}
494+
return [super performKeyEquivalent:event];
495+
}
485496
#endif // ]TODO(macOS GH#774)
486497

487498
#if !TARGET_OS_OSX // TODO(macOS GH#774)
@@ -563,6 +574,12 @@ - (void)deleteBackward {
563574
[super deleteBackward];
564575
}
565576
}
577+
#else
578+
- (void)keyUp:(NSEvent *)event {
579+
if ([self.textInputDelegate textInputShouldHandleKeyEvent:event]) {
580+
[super keyUp:event];
581+
}
582+
}
566583
#endif // ]TODO(OSS Candidate ISS#2710739)
567584

568585
@end

packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
const React = require('react');
1414
const ReactNative = require('react-native');
1515
import {Platform} from 'react-native';
16-
const {Button, PlatformColor, StyleSheet, Text, View} = ReactNative;
16+
const {Button, PlatformColor, StyleSheet, Text, TextInput, View} = ReactNative;
1717

1818
import type {KeyEvent} from 'react-native/Libraries/Types/CoreEventTypes';
1919

@@ -47,24 +47,44 @@ class KeyEventExample extends React.Component<{}, State> {
4747

4848
render() {
4949
return (
50-
<View>
51-
<Text>Key events are called when a component detects a key press.</Text>
50+
<View style={{padding: 10}}>
51+
<Text>
52+
Key events are called when a component detects a key press. To tab
53+
between views: Enable System Preferences / Keyboard / Shortcuts > Use
54+
keyboard navigation to move focus between controls.
55+
</Text>
5256
<View>
5357
{Platform.OS === 'macos' ? (
54-
<View
55-
focusable={true}
56-
validKeysDown={['g', 'Tab', 'Escape', 'Enter', 'ArrowLeft']}
57-
onKeyDown={this.onKeyDownEvent}
58-
validKeysUp={['c', 'd']}
59-
onKeyUp={this.onKeyUpEvent}>
60-
<Button
61-
title={'Test button'}
58+
<>
59+
<View
60+
focusable={true}
61+
style={styles.row}
62+
validKeysDown={['g', 'Escape', 'Enter', 'ArrowLeft']}
6263
onKeyDown={this.onKeyDownEvent}
63-
validKeysUp={['j', 'k', 'l']}
64+
validKeysUp={['c', 'd']}
65+
onKeyUp={this.onKeyUpEvent}></View>
66+
<TextInput
67+
blurOnSubmit={false}
68+
placeholder={'Singleline textInput'}
69+
multiline={false}
70+
focusable={true}
71+
style={styles.row}
72+
validKeysDown={['ArrowRight', 'ArrowDown']}
73+
onKeyDown={this.onKeyDownEvent}
74+
validKeysUp={['Escape', 'Enter']}
75+
onKeyUp={this.onKeyUpEvent}
76+
/>
77+
<TextInput
78+
placeholder={'Multiline textInput'}
79+
multiline={true}
80+
focusable={true}
81+
style={styles.row}
82+
validKeysDown={['ArrowLeft', 'ArrowUp']}
83+
onKeyDown={this.onKeyDownEvent}
84+
validKeysUp={['Escape', 'Enter']}
6485
onKeyUp={this.onKeyUpEvent}
65-
onPress={() => {}}
6686
/>
67-
</View>
87+
</>
6888
) : null}
6989
<Text>{'Events: ' + this.state.eventStream + '\n\n'}</Text>
7090
</View>
@@ -74,21 +94,12 @@ class KeyEventExample extends React.Component<{}, State> {
7494
}
7595

7696
const styles = StyleSheet.create({
77-
textInput: {
78-
...Platform.select({
79-
macos: {
80-
color: PlatformColor('textColor'),
81-
backgroundColor: PlatformColor('textBackgroundColor'),
82-
borderColor: PlatformColor('gridColor'),
83-
},
84-
default: {
85-
borderColor: '#0f0f0f',
86-
},
87-
}),
88-
borderWidth: StyleSheet.hairlineWidth,
89-
flex: 1,
90-
fontSize: 13,
91-
padding: 4,
97+
row: {
98+
height: 40,
99+
marginTop: 10,
100+
marginBottom: 10,
101+
backgroundColor: 'grey',
102+
padding: 10,
92103
},
93104
});
94105

0 commit comments

Comments
 (0)