Skip to content

Fix handling of keyDown/keyUp events by TextInput #1345

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,8 @@ function InternalTextInput(props: Props): React.Node {
onChangeSync={useOnChangeSync === true ? _onChangeSync : null}
onContentSizeChange={props.onContentSizeChange}
onFocus={_onFocus}
onKeyDown={props.onKeyDown} // TODO(macOS GH#774)
onKeyUp={props.onKeyUp} // TODO(macOS GH#774)
onScroll={_onScroll}
onSelectionChange={_onSelectionChange}
onSelectionChangeShouldSetResponder={emptyFunctionThatReturnsTrue}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ exports[`TextInput tests should render as expected: should deep render when mock
onChangeSync={null}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
Expand Down Expand Up @@ -44,8 +42,6 @@ exports[`TextInput tests should render as expected: should deep render when not
onChangeSync={null}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
Expand Down
13 changes: 13 additions & 0 deletions Libraries/Text/TextInput/Multiline/RCTUITextView.m
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,19 @@ - (void)deleteBackward {
[super 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]) {
[super keyDown:event];
}
}

- (void)keyUp:(NSEvent *)event {
if ([self.textInputDelegate textInputShouldHandleKeyEvent:event]) {
[super keyUp:event];
}
}
#endif // ]TODO(OSS Candidate ISS#2710739)

- (void)_updatePlaceholder
Expand Down
3 changes: 2 additions & 1 deletion Libraries/Text/TextInput/RCTBackedTextInputDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ NS_ASSUME_NONNULL_BEGIN
- (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)
#if TARGET_OS_OSX // [TODO(macOS GH#774)
- (BOOL)textInputShouldHandleDeleteForward:(id<RCTBackedTextInputViewProtocol>)sender; // Return `YES` to have the deleteForward event handled normally. Return `NO` to disallow it and handle it yourself.

- (BOOL)textInputShouldHandleKeyEvent:(NSEvent *)event; // Return `YES` to have the key event handled normally. Return `NO` to disallow it and handle it yourself.
- (BOOL)hasValidKeyDownOrValidKeyUp:(NSString *)key;
- (void)textInputDidCancel; // Handle `Escape` key press.
#endif // ]TODO(macOS GH#774)

Expand Down
8 changes: 6 additions & 2 deletions Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doComman
//escape
} else if (commandSelector == @selector(cancelOperation:)) {
[textInputDelegate textInputDidCancel];
[[_backedTextInputView window] makeFirstResponder:nil];
if (![textInputDelegate hasValidKeyDownOrValidKeyUp:@"Escape"]) {
[[_backedTextInputView window] makeFirstResponder:nil];
}
commandHandled = YES;
}

Expand Down Expand Up @@ -421,7 +423,9 @@ - (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
//escape
} else if (commandSelector == @selector(cancelOperation:)) {
[textInputDelegate textInputDidCancel];
[_backedTextInputView.window makeFirstResponder:nil];
if (![textInputDelegate hasValidKeyDownOrValidKeyUp:@"Escape"]) {
[[_backedTextInputView window] makeFirstResponder:nil];
}
commandHandled = YES;

}
Expand Down
11 changes: 9 additions & 2 deletions Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
{
RCTAssertParam(bridge);

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

RCT_NOT_IMPLEMENTED(- (instancetype)init)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)decoder)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)

- (RCTUIView<RCTBackedTextInputViewProtocol> *)backedTextInputView // TODO(macOS ISS#3536887)
{
Expand Down Expand Up @@ -595,6 +594,10 @@ - (BOOL)textInputShouldHandleDeleteForward:(__unused id)sender {
return YES;
}

- (BOOL)hasValidKeyDownOrValidKeyUp:(NSString *)key {
return [self.validKeysDown containsObject:key] || [self.validKeysUp containsObject:key];
}

- (void)textInputDidCancel {
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress
reactTag:self.reactTag
Expand All @@ -603,6 +606,10 @@ - (void)textInputDidCancel {
eventCount:_nativeEventCount];
[self textInputDidEndEditing];
}

- (BOOL)textInputShouldHandleKeyEvent:(NSEvent *)event {
return ![self handleKeyboardEvent:event];
}
#endif // ]TODO(macOS GH#774)

- (void)updateLocalData
Expand Down
17 changes: 17 additions & 0 deletions Libraries/Text/TextInput/Singleline/RCTUITextField.m
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,17 @@ - (BOOL)becomeFirstResponder
}
return isFirstResponder;
}

- (BOOL)performKeyEquivalent:(NSEvent *)event
{
// The currentEditor is NSText for historical reasons, but documented to be NSTextView.
NSTextView *currentEditor = (NSTextView *)self.currentEditor;
// The currentEditor is non-nil when focused and hasMarkedText means an IME is open.
if (currentEditor && !currentEditor.hasMarkedText && ![self.textInputDelegate textInputShouldHandleKeyEvent:event]) {
return YES; // Don't send currentEditor the keydown event.
}
return [super performKeyEquivalent:event];
}
#endif // ]TODO(macOS GH#774)

#if !TARGET_OS_OSX // TODO(macOS GH#774)
Expand Down Expand Up @@ -570,6 +581,12 @@ - (void)deleteBackward {
[super deleteBackward];
}
}
#else
- (void)keyUp:(NSEvent *)event {
if ([self.textInputDelegate textInputShouldHandleKeyEvent:event]) {
[super keyUp:event];
}
}
#endif // ]TODO(OSS Candidate ISS#2710739)

@end
Original file line number Diff line number Diff line change
Expand Up @@ -13,82 +13,108 @@
const React = require('react');
const ReactNative = require('react-native');
import {Platform} from 'react-native';
const {Button, PlatformColor, StyleSheet, Text, View} = ReactNative;
const {Button, PlatformColor, StyleSheet, Text, TextInput, View} = ReactNative;

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

type State = {
eventStream: string,
characters: string,
};

class KeyEventExample extends React.Component<{}, State> {
state: State = {
eventStream: '',
characters: '',
};

onKeyDownEvent: (e: KeyEvent) => void = (e: KeyEvent) => {
console.log('received view key down event\n', e.nativeEvent.key);
this.setState({characters: e.nativeEvent.key});
this.setState(prevState => ({
eventStream:
prevState.eventStream + '\nKey Down: ' + prevState.characters,
}));
function KeyEventExample(): React.Node {
const [log, setLog] = React.useState([]);
const appendLog = (line: string) => {
const limit = 12;
let newLog = log.slice(0, limit - 1);
newLog.unshift(line);
setLog(newLog);
};

onKeyUpEvent: (e: KeyEvent) => void = (e: KeyEvent) => {
console.log('received key up event\n', e.nativeEvent.key);
this.setState({characters: e.nativeEvent.key});
this.setState(prevState => ({
eventStream: prevState.eventStream + '\nKey Up: ' + prevState.characters,
}));
};

render() {
return (
return (
<View style={{padding: 10}}>
<Text>
Key events are called when a component detects a key press.To tab
between views on macOS: Enable System Preferences / Keyboard / Shortcuts
> Use keyboard navigation to move focus between controls.
</Text>
<View>
<Text>Key events are called when a component detects a key press.</Text>
<View>
{Platform.OS === 'macos' ? (
{Platform.OS === 'macos' ? (
<>
<Text style={styles.title}>View</Text>
<Text style={styles.text}>
validKeysDown: [g, Escape, Enter, ArrowLeft]{'\n'}
validKeysUp: [c, d]
</Text>
<View
focusable={true}
validKeysDown={['g', 'Tab', 'Escape', 'Enter', 'ArrowLeft']}
onKeyDown={this.onKeyDownEvent}
style={styles.row}
validKeysDown={['g', 'Escape', 'Enter', 'ArrowLeft']}
onKeyDown={e => appendLog('Key Down:' + e.nativeEvent.key)}
validKeysUp={['c', 'd']}
onKeyUp={this.onKeyUpEvent}>
<Button
title={'Test button'}
onKeyDown={this.onKeyDownEvent}
validKeysUp={['j', 'k', 'l']}
onKeyUp={this.onKeyUpEvent}
onPress={() => {}}
/>
</View>
) : null}
<Text>{'Events: ' + this.state.eventStream + '\n\n'}</Text>
</View>
onKeyUp={e => appendLog('Key Up:' + e.nativeEvent.key)}></View>
<Text style={styles.title}>TextInput</Text>
<Text style={styles.text}>
validKeysDown: [ArrowRight, ArrowDown]{'\n'}
validKeysUp: [Escape, Enter]
</Text>
<TextInput
blurOnSubmit={false}
placeholder={'Singleline textInput'}
multiline={false}
focusable={true}
style={styles.row}
validKeysDown={['ArrowRight', 'ArrowDown']}
onKeyDown={e => appendLog('Key Down:' + e.nativeEvent.key)}
validKeysUp={['Escape', 'Enter']}
onKeyUp={e => appendLog('Key Up:' + e.nativeEvent.key)}
/>
<TextInput
placeholder={'Multiline textInput'}
multiline={true}
focusable={true}
style={styles.row}
validKeysDown={['ArrowRight', 'ArrowDown']}
onKeyDown={e => appendLog('Key Down:' + e.nativeEvent.key)}
validKeysUp={['Escape', 'Enter']}
onKeyUp={e => appendLog('Key Up:' + e.nativeEvent.key)}
/>
<Text style={styles.text}>
validKeysDown: []{'\n'}
validKeysUp: []
</Text>
<TextInput
blurOnSubmit={false}
placeholder={'Singleline textInput'}
multiline={false}
focusable={true}
style={styles.row}
/>
<TextInput
placeholder={'Multiline textInput'}
multiline={true}
focusable={true}
style={styles.row}
/>
</>
) : null}
<Text>{'Events:\n' + log.join('\n')}</Text>
</View>
);
}
</View>
);
}

const styles = StyleSheet.create({
textInput: {
...Platform.select({
macos: {
color: PlatformColor('textColor'),
backgroundColor: PlatformColor('textBackgroundColor'),
borderColor: PlatformColor('gridColor'),
},
default: {
borderColor: '#0f0f0f',
},
}),
borderWidth: StyleSheet.hairlineWidth,
flex: 1,
fontSize: 13,
padding: 4,
row: {
height: 36,
marginTop: 8,
marginBottom: 8,
backgroundColor: 'grey',
padding: 10,
},
title: {
fontSize: 14,
paddingTop: 12,
paddingBottom: 8,
},
text: {
fontSize: 12,
paddingBottom: 4,
},
});

Expand Down