diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 431b14d152997c..092f65a1f04a74 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -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} diff --git a/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap b/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap index 34529112981c16..874fae2b158a92 100644 --- a/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap +++ b/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap @@ -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]} @@ -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]} diff --git a/Libraries/Text/TextInput/Multiline/RCTUITextView.m b/Libraries/Text/TextInput/Multiline/RCTUITextView.m index 1d0cd32c5e3375..bbf7e2ff3d9bb6 100644 --- a/Libraries/Text/TextInput/Multiline/RCTUITextView.m +++ b/Libraries/Text/TextInput/Multiline/RCTUITextView.m @@ -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 diff --git a/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h b/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h index 8d085e0d4a692d..1d9de9c8b2e210 100644 --- a/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h +++ b/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h @@ -42,7 +42,8 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)textInputShouldHandleDeleteBackward:(id)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)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) diff --git a/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m b/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m index 8ac48110f4acd4..db88b5d70a5507 100644 --- a/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m +++ b/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m @@ -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; } @@ -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; } diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.m b/Libraries/Text/TextInput/RCTBaseTextInputView.m index eb5b1d38969efb..7cd272a99d393e 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -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; } @@ -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 *)backedTextInputView // TODO(macOS ISS#3536887) { @@ -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 @@ -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 diff --git a/Libraries/Text/TextInput/Singleline/RCTUITextField.m b/Libraries/Text/TextInput/Singleline/RCTUITextField.m index b312c88ce6ab18..23b7d1f8ea21e3 100644 --- a/Libraries/Text/TextInput/Singleline/RCTUITextField.m +++ b/Libraries/Text/TextInput/Singleline/RCTUITextField.m @@ -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) @@ -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 diff --git a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js index 18f27318b02458..ac21daad72cc14 100644 --- a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js +++ b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js @@ -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 ( + + + 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. + - Key events are called when a component detects a key press. - - {Platform.OS === 'macos' ? ( + {Platform.OS === 'macos' ? ( + <> + View + + validKeysDown: [g, Escape, Enter, ArrowLeft]{'\n'} + validKeysUp: [c, d] + appendLog('Key Down:' + e.nativeEvent.key)} validKeysUp={['c', 'd']} - onKeyUp={this.onKeyUpEvent}> -