From 6c7c845145e1da44ac5d965d49097eff1c11cc49 Mon Sep 17 00:00:00 2001 From: Dave Sibiski Date: Mon, 2 Nov 2015 09:13:41 -0800 Subject: [PATCH] Implements `onKeyPress` Summary: - When a key is pressed, it's `key value` is passed as an argument to the callback handler. - For `Enter` and `Backspace` keys, I'm using their `key value` as defined [here](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key#Key_values). As per JonasJonny & brentvatne's [suggestion](https://github.com/facebook/react-native/issues/1882#issuecomment-123485883). - Example ```javascript _handleKeyPress: function(e) { console.log(e.nativeEvent.key); }, render: function() { return ( ); } ``` - Implements [shouldChangeCharactersInRange](https://developer.apple.com/library/prerelease/ios/documentat Closes https://github.com/facebook/react-native/pull/2082 Reviewed By: javache Differential Revision: D2280460 Pulled By: nicklockwood fb-gh-sync-id: 1f824f80649043dc2520c089e2531d428d799405 --- Examples/UIExplorer/TextInputExample.ios.js | 8 ++++- Libraries/Components/TextInput/TextInput.js | 7 ++++ Libraries/Text/RCTTextField.h | 3 ++ Libraries/Text/RCTTextField.m | 22 ++++++++++++ Libraries/Text/RCTTextFieldManager.m | 15 ++++++++ Libraries/Text/RCTTextView.m | 40 ++++++++++++++++++--- React/Base/RCTEventDispatcher.h | 4 ++- React/Base/RCTEventDispatcher.m | 31 +++++++++++++--- React/Views/RCTViewManager.m | 1 + 9 files changed, 119 insertions(+), 12 deletions(-) diff --git a/Examples/UIExplorer/TextInputExample.ios.js b/Examples/UIExplorer/TextInputExample.ios.js index d51a95e33b..a4ae78e997 100644 --- a/Examples/UIExplorer/TextInputExample.ios.js +++ b/Examples/UIExplorer/TextInputExample.ios.js @@ -42,6 +42,7 @@ var TextEventsExample = React.createClass({ curText: '', prevText: '', prev2Text: '', + prev3Text: '', }; }, @@ -51,6 +52,7 @@ var TextEventsExample = React.createClass({ curText: text, prevText: state.curText, prev2Text: state.prevText, + prev3Text: state.prev2Text, }; }); }, @@ -73,12 +75,16 @@ var TextEventsExample = React.createClass({ onSubmitEditing={(event) => this.updateText( 'onSubmitEditing text: ' + event.nativeEvent.text )} + onKeyPress={(event) => { + this.updateText('onKeyPress key: ' + event.nativeEvent.key); + }} style={styles.default} /> {this.state.curText}{'\n'} (prev: {this.state.prevText}){'\n'} - (prev2: {this.state.prev2Text}) + (prev2: {this.state.prev2Text}){'\n'} + (prev3: {this.state.prev3Text}) ); diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 52dc552223..0b01f0ad86 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -217,6 +217,13 @@ var TextInput = React.createClass({ * Callback that is called when the text input's submit button is pressed. */ onSubmitEditing: PropTypes.func, + /** + * Callback that is called when a key is pressed. + * Pressed key value is passed as an argument to the callback handler. + * Fires before onChange callbacks. + * @platform ios + */ + onKeyPress: PropTypes.func, /** * Invoked on mount and layout changes with `{x, y, width, height}`. */ diff --git a/Libraries/Text/RCTTextField.h b/Libraries/Text/RCTTextField.h index 3aba72bba1..3fab22f245 100644 --- a/Libraries/Text/RCTTextField.h +++ b/Libraries/Text/RCTTextField.h @@ -20,8 +20,11 @@ @property (nonatomic, strong) UIColor *placeholderTextColor; @property (nonatomic, assign) NSInteger mostRecentEventCount; @property (nonatomic, strong) NSNumber *maxLength; +@property (nonatomic, assign) BOOL textWasPasted; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + - (void)textFieldDidChange; +- (void)sendKeyValueForString:(NSString *)string; @end diff --git a/Libraries/Text/RCTTextField.m b/Libraries/Text/RCTTextField.m index a75a268e91..2593f0137e 100644 --- a/Libraries/Text/RCTTextField.m +++ b/Libraries/Text/RCTTextField.m @@ -39,6 +39,23 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) +- (void)sendKeyValueForString:(NSString *)string +{ + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress + reactTag:self.reactTag + text:nil + key:string + eventCount:_nativeEventCount]; +} + +// This method is overriden for `onKeyPress`. The manager +// will not send a keyPress for text that was pasted. +- (void)paste:(id)sender +{ + _textWasPasted = YES; + [super paste:sender]; +} + - (void)setText:(NSString *)text { NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; @@ -134,6 +151,7 @@ static void RCTUpdatePlaceholder(RCTTextField *self) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange reactTag:self.reactTag text:self.text + key:nil eventCount:_nativeEventCount]; } @@ -142,6 +160,7 @@ static void RCTUpdatePlaceholder(RCTTextField *self) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd reactTag:self.reactTag text:self.text + key:nil eventCount:_nativeEventCount]; } - (void)textFieldSubmitEditing @@ -149,6 +168,7 @@ static void RCTUpdatePlaceholder(RCTTextField *self) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit reactTag:self.reactTag text:self.text + key:nil eventCount:_nativeEventCount]; } @@ -162,6 +182,7 @@ static void RCTUpdatePlaceholder(RCTTextField *self) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus reactTag:self.reactTag text:self.text + key:nil eventCount:_nativeEventCount]; } @@ -181,6 +202,7 @@ static void RCTUpdatePlaceholder(RCTTextField *self) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur reactTag:self.reactTag text:self.text + key:nil eventCount:_nativeEventCount]; } return result; diff --git a/Libraries/Text/RCTTextFieldManager.m b/Libraries/Text/RCTTextFieldManager.m index 8ce0b14305..d0fdcb0fef 100644 --- a/Libraries/Text/RCTTextFieldManager.m +++ b/Libraries/Text/RCTTextFieldManager.m @@ -31,6 +31,13 @@ RCT_EXPORT_MODULE() - (BOOL)textField:(RCTTextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { + // Only allow single keypresses for onKeyPress, pasted text will not be sent. + if (textField.textWasPasted) { + textField.textWasPasted = NO; + } else { + [textField sendKeyValueForString:string]; + } + if (textField.maxLength == nil || [string isEqualToString:@"\n"]) { // Make sure forms can be submitted via return return YES; } @@ -54,6 +61,14 @@ RCT_EXPORT_MODULE() } } +// This method allows us to detect a `Backspace` keyPress +// even when there is no more text in the TextField +- (BOOL)keyboardInputShouldDelete:(RCTTextField *)textField +{ + [self textField:textField shouldChangeCharactersInRange:NSMakeRange(0, 0) replacementString:@""]; + return YES; +} + RCT_EXPORT_VIEW_PROPERTY(caretHidden, BOOL) RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL) RCT_REMAP_VIEW_PROPERTY(editable, enabled, BOOL) diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 5e24e3a1c0..93962916ca 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -14,6 +14,22 @@ #import "RCTUtils.h" #import "UIView+React.h" +@interface RCTUITextView : UITextView + +@property (nonatomic, assign) BOOL textWasPasted; + +@end + +@implementation RCTUITextView + +- (void)paste:(id)sender +{ + _textWasPasted = YES; + [super paste:sender]; +} + +@end + @implementation RCTTextView { RCTEventDispatcher *_eventDispatcher; @@ -33,7 +49,7 @@ _eventDispatcher = eventDispatcher; _placeholderTextColor = [self defaultPlaceholderTextColor]; - _textView = [[UITextView alloc] initWithFrame:self.bounds]; + _textView = [[RCTUITextView alloc] initWithFrame:self.bounds]; _textView.backgroundColor = [UIColor clearColor]; _textView.scrollsToTop = NO; _textView.delegate = self; @@ -56,15 +72,15 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) // first focused. UIEdgeInsets adjustedFrameInset = UIEdgeInsetsZero; adjustedFrameInset.left = _contentInset.left - 5; - + UIEdgeInsets adjustedTextContainerInset = _contentInset; adjustedTextContainerInset.top += 5; adjustedTextContainerInset.left = 0; - + CGRect frame = UIEdgeInsetsInsetRect(self.bounds, adjustedFrameInset); _textView.frame = frame; _placeholderView.frame = frame; - + _textView.textContainerInset = adjustedTextContainerInset; _placeholderView.textContainerInset = adjustedTextContainerInset; } @@ -138,8 +154,18 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) return _textView.text; } -- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +- (BOOL)textView:(RCTUITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if (textView.textWasPasted) { + textView.textWasPasted = NO; + } else { + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress + reactTag:self.reactTag + text:nil + key:text + eventCount:_nativeEventCount]; + } + if (_maxLength == nil) { return YES; } @@ -215,6 +241,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus reactTag:self.reactTag text:textView.text + key:nil eventCount:_nativeEventCount]; } @@ -225,6 +252,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange reactTag:self.reactTag text:textView.text + key:nil eventCount:_nativeEventCount]; } @@ -234,6 +262,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd reactTag:self.reactTag text:textView.text + key:nil eventCount:_nativeEventCount]; } @@ -253,6 +282,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur reactTag:self.reactTag text:_textView.text + key:nil eventCount:_nativeEventCount]; } return result; diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h index 114d91586a..4cbd84fa29 100644 --- a/React/Base/RCTEventDispatcher.h +++ b/React/Base/RCTEventDispatcher.h @@ -16,7 +16,8 @@ typedef NS_ENUM(NSInteger, RCTTextEventType) { RCTTextEventTypeBlur, RCTTextEventTypeChange, RCTTextEventTypeSubmit, - RCTTextEventTypeEnd + RCTTextEventTypeEnd, + RCTTextEventTypeKeyPress }; typedef NS_ENUM(NSInteger, RCTScrollEventType) { @@ -95,6 +96,7 @@ RCT_EXTERN NSString *RCTNormalizeInputEventName(NSString *eventName); - (void)sendTextEventWithType:(RCTTextEventType)type reactTag:(NSNumber *)reactTag text:(NSString *)text + key:(NSString *)key eventCount:(NSInteger)eventCount; /** diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index 48f98e4575..39503f9abc 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -144,6 +144,7 @@ RCT_EXPORT_MODULE() - (void)sendTextEventWithType:(RCTTextEventType)type reactTag:(NSNumber *)reactTag text:(NSString *)text + key:(NSString *)key eventCount:(NSInteger)eventCount { static NSString *events[] = { @@ -152,16 +153,36 @@ RCT_EXPORT_MODULE() @"change", @"submitEditing", @"endEditing", + @"keyPress" }; - [self sendInputEventWithName:events[type] body:text ? @{ - @"text": text, - @"eventCount": @(eventCount), - @"target": reactTag - } : @{ + NSMutableDictionary *body = [[NSMutableDictionary alloc] initWithDictionary:@{ @"eventCount": @(eventCount), @"target": reactTag }]; + + if (text) { + body[@"text"] = text; + } + + if (key) { + if (key.length == 0) { + key = @"Backspace"; // backspace + } else { + switch ([key characterAtIndex:0]) { + case '\t': + key = @"Tab"; + break; + case '\n': + key = @"Enter"; + default: + break; + } + } + body[@"key"] = key; + } + + [self sendInputEventWithName:events[type] body:body]; } - (void)sendEvent:(id)event diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index c2cf93676b..2ad52943c5 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -80,6 +80,7 @@ RCT_EXPORT_MODULE() @"blur", @"submitEditing", @"endEditing", + @"keyPress", // Touch events @"touchStart",