Skip to content

Commit e9791d2

Browse files
christophpurrerappdenSaadnajmi
authored
Fix handling of keyDown/keyUp events by TextInput (#1345)
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. Co-authored-by: Scott Kyle <[email protected]> Co-authored-by: Saad Najmi <[email protected]>
1 parent 6876a33 commit e9791d2

File tree

8 files changed

+138
-72
lines changed

8 files changed

+138
-72
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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ 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-
45+
- (BOOL)textInputShouldHandleKeyEvent:(NSEvent *)event; // Return `YES` to have the key event handled normally. Return `NO` to disallow it and handle it yourself.
46+
- (BOOL)hasValidKeyDownOrValidKeyUp:(NSString *)key;
4647
- (void)textInputDidCancel; // Handle `Escape` key press.
4748
#endif // ]TODO(macOS GH#774)
4849

Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,9 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doComman
217217
//escape
218218
} else if (commandSelector == @selector(cancelOperation:)) {
219219
[textInputDelegate textInputDidCancel];
220-
[[_backedTextInputView window] makeFirstResponder:nil];
220+
if (![textInputDelegate hasValidKeyDownOrValidKeyUp:@"Escape"]) {
221+
[[_backedTextInputView window] makeFirstResponder:nil];
222+
}
221223
commandHandled = YES;
222224
}
223225

@@ -421,7 +423,9 @@ - (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
421423
//escape
422424
} else if (commandSelector == @selector(cancelOperation:)) {
423425
[textInputDelegate textInputDidCancel];
424-
[_backedTextInputView.window makeFirstResponder:nil];
426+
if (![textInputDelegate hasValidKeyDownOrValidKeyUp:@"Escape"]) {
427+
[[_backedTextInputView window] makeFirstResponder:nil];
428+
}
425429
commandHandled = YES;
426430

427431
}

Libraries/Text/TextInput/RCTBaseTextInputView.m

Lines changed: 9 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
{
@@ -595,6 +594,10 @@ - (BOOL)textInputShouldHandleDeleteForward:(__unused id)sender {
595594
return YES;
596595
}
597596

597+
- (BOOL)hasValidKeyDownOrValidKeyUp:(NSString *)key {
598+
return [self.validKeysDown containsObject:key] || [self.validKeysUp containsObject:key];
599+
}
600+
598601
- (void)textInputDidCancel {
599602
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress
600603
reactTag:self.reactTag
@@ -603,6 +606,10 @@ - (void)textInputDidCancel {
603606
eventCount:_nativeEventCount];
604607
[self textInputDidEndEditing];
605608
}
609+
610+
- (BOOL)textInputShouldHandleKeyEvent:(NSEvent *)event {
611+
return ![self handleKeyboardEvent:event];
612+
}
606613
#endif // ]TODO(macOS GH#774)
607614

608615
- (void)updateLocalData

Libraries/Text/TextInput/Singleline/RCTUITextField.m

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,17 @@ - (BOOL)becomeFirstResponder
489489
}
490490
return isFirstResponder;
491491
}
492+
493+
- (BOOL)performKeyEquivalent:(NSEvent *)event
494+
{
495+
// The currentEditor is NSText for historical reasons, but documented to be NSTextView.
496+
NSTextView *currentEditor = (NSTextView *)self.currentEditor;
497+
// The currentEditor is non-nil when focused and hasMarkedText means an IME is open.
498+
if (currentEditor && !currentEditor.hasMarkedText && ![self.textInputDelegate textInputShouldHandleKeyEvent:event]) {
499+
return YES; // Don't send currentEditor the keydown event.
500+
}
501+
return [super performKeyEquivalent:event];
502+
}
492503
#endif // ]TODO(macOS GH#774)
493504

494505
#if !TARGET_OS_OSX // TODO(macOS GH#774)
@@ -570,6 +581,12 @@ - (void)deleteBackward {
570581
[super deleteBackward];
571582
}
572583
}
584+
#else
585+
- (void)keyUp:(NSEvent *)event {
586+
if ([self.textInputDelegate textInputShouldHandleKeyEvent:event]) {
587+
[super keyUp:event];
588+
}
589+
}
573590
#endif // ]TODO(OSS Candidate ISS#2710739)
574591

575592
@end

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

Lines changed: 89 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -13,82 +13,108 @@
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

20-
type State = {
21-
eventStream: string,
22-
characters: string,
23-
};
24-
25-
class KeyEventExample extends React.Component<{}, State> {
26-
state: State = {
27-
eventStream: '',
28-
characters: '',
29-
};
30-
31-
onKeyDownEvent: (e: KeyEvent) => void = (e: KeyEvent) => {
32-
console.log('received view key down event\n', e.nativeEvent.key);
33-
this.setState({characters: e.nativeEvent.key});
34-
this.setState(prevState => ({
35-
eventStream:
36-
prevState.eventStream + '\nKey Down: ' + prevState.characters,
37-
}));
20+
function KeyEventExample(): React.Node {
21+
const [log, setLog] = React.useState([]);
22+
const appendLog = (line: string) => {
23+
const limit = 12;
24+
let newLog = log.slice(0, limit - 1);
25+
newLog.unshift(line);
26+
setLog(newLog);
3827
};
3928

40-
onKeyUpEvent: (e: KeyEvent) => void = (e: KeyEvent) => {
41-
console.log('received key up event\n', e.nativeEvent.key);
42-
this.setState({characters: e.nativeEvent.key});
43-
this.setState(prevState => ({
44-
eventStream: prevState.eventStream + '\nKey Up: ' + prevState.characters,
45-
}));
46-
};
47-
48-
render() {
49-
return (
29+
return (
30+
<View style={{padding: 10}}>
31+
<Text>
32+
Key events are called when a component detects a key press.To tab
33+
between views on macOS: Enable System Preferences / Keyboard / Shortcuts
34+
> Use keyboard navigation to move focus between controls.
35+
</Text>
5036
<View>
51-
<Text>Key events are called when a component detects a key press.</Text>
52-
<View>
53-
{Platform.OS === 'macos' ? (
37+
{Platform.OS === 'macos' ? (
38+
<>
39+
<Text style={styles.title}>View</Text>
40+
<Text style={styles.text}>
41+
validKeysDown: [g, Escape, Enter, ArrowLeft]{'\n'}
42+
validKeysUp: [c, d]
43+
</Text>
5444
<View
5545
focusable={true}
56-
validKeysDown={['g', 'Tab', 'Escape', 'Enter', 'ArrowLeft']}
57-
onKeyDown={this.onKeyDownEvent}
46+
style={styles.row}
47+
validKeysDown={['g', 'Escape', 'Enter', 'ArrowLeft']}
48+
onKeyDown={e => appendLog('Key Down:' + e.nativeEvent.key)}
5849
validKeysUp={['c', 'd']}
59-
onKeyUp={this.onKeyUpEvent}>
60-
<Button
61-
title={'Test button'}
62-
onKeyDown={this.onKeyDownEvent}
63-
validKeysUp={['j', 'k', 'l']}
64-
onKeyUp={this.onKeyUpEvent}
65-
onPress={() => {}}
66-
/>
67-
</View>
68-
) : null}
69-
<Text>{'Events: ' + this.state.eventStream + '\n\n'}</Text>
70-
</View>
50+
onKeyUp={e => appendLog('Key Up:' + e.nativeEvent.key)}></View>
51+
<Text style={styles.title}>TextInput</Text>
52+
<Text style={styles.text}>
53+
validKeysDown: [ArrowRight, ArrowDown]{'\n'}
54+
validKeysUp: [Escape, Enter]
55+
</Text>
56+
<TextInput
57+
blurOnSubmit={false}
58+
placeholder={'Singleline textInput'}
59+
multiline={false}
60+
focusable={true}
61+
style={styles.row}
62+
validKeysDown={['ArrowRight', 'ArrowDown']}
63+
onKeyDown={e => appendLog('Key Down:' + e.nativeEvent.key)}
64+
validKeysUp={['Escape', 'Enter']}
65+
onKeyUp={e => appendLog('Key Up:' + e.nativeEvent.key)}
66+
/>
67+
<TextInput
68+
placeholder={'Multiline textInput'}
69+
multiline={true}
70+
focusable={true}
71+
style={styles.row}
72+
validKeysDown={['ArrowRight', 'ArrowDown']}
73+
onKeyDown={e => appendLog('Key Down:' + e.nativeEvent.key)}
74+
validKeysUp={['Escape', 'Enter']}
75+
onKeyUp={e => appendLog('Key Up:' + e.nativeEvent.key)}
76+
/>
77+
<Text style={styles.text}>
78+
validKeysDown: []{'\n'}
79+
validKeysUp: []
80+
</Text>
81+
<TextInput
82+
blurOnSubmit={false}
83+
placeholder={'Singleline textInput'}
84+
multiline={false}
85+
focusable={true}
86+
style={styles.row}
87+
/>
88+
<TextInput
89+
placeholder={'Multiline textInput'}
90+
multiline={true}
91+
focusable={true}
92+
style={styles.row}
93+
/>
94+
</>
95+
) : null}
96+
<Text>{'Events:\n' + log.join('\n')}</Text>
7197
</View>
72-
);
73-
}
98+
</View>
99+
);
74100
}
75101

76102
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,
103+
row: {
104+
height: 36,
105+
marginTop: 8,
106+
marginBottom: 8,
107+
backgroundColor: 'grey',
108+
padding: 10,
109+
},
110+
title: {
111+
fontSize: 14,
112+
paddingTop: 12,
113+
paddingBottom: 8,
114+
},
115+
text: {
116+
fontSize: 12,
117+
paddingBottom: 4,
92118
},
93119
});
94120

0 commit comments

Comments
 (0)