-
Notifications
You must be signed in to change notification settings - Fork 24.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fabric: The basic implementation of <TextInput> for iOS
Summary: This is the partial implementation of Fabric-compatible <TextInput> component on iOS. All features are supported besides those: * `focus()`, `blur()`, `clear()` imperative calls; * Controlled TextInput as the whole feature in general; * Controlling selection from JavaScript side; * `autoFocus` prop; * KeyboardAccessoryView. Changelog: [Internal] Reviewed By: JoshuaGross Differential Revision: D17400907 fbshipit-source-id: 0ccd0e0923293e5f504d5fae7b7ba9f048f7d259
- Loading branch information
1 parent
4155796
commit 8219db9
Showing
3 changed files
with
368 additions
and
0 deletions.
There are no files selected for viewing
21 changes: 21 additions & 0 deletions
21
React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.h
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/* | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
#import <UIKit/UIKit.h> | ||
|
||
#import <React/RCTViewComponentView.h> | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
/** | ||
* UIView class for <TextInput> component. | ||
*/ | ||
@interface RCTTextInputComponentView : RCTViewComponentView | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END |
345 changes: 345 additions & 0 deletions
345
React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,345 @@ | ||
/* | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
#import "RCTTextInputComponentView.h" | ||
|
||
#import <react/components/iostextinput/TextInputComponentDescriptor.h> | ||
#import <react/graphics/Geometry.h> | ||
#import <react/textlayoutmanager/RCTAttributedTextUtils.h> | ||
#import <react/textlayoutmanager/TextLayoutManager.h> | ||
|
||
#import <React/RCTBackedTextInputViewProtocol.h> | ||
#import <React/RCTUITextField.h> | ||
#import <React/RCTUITextView.h> | ||
|
||
#import "RCTConversions.h" | ||
#import "RCTTextInputUtils.h" | ||
|
||
using namespace facebook::react; | ||
|
||
@interface RCTTextInputComponentView () <RCTBackedTextInputDelegate> | ||
@end | ||
|
||
@implementation RCTTextInputComponentView { | ||
TextInputShadowNode::ConcreteState::Shared _state; | ||
UIView<RCTBackedTextInputViewProtocol> *_backedTextInputView; | ||
size_t _stateRevision; | ||
} | ||
|
||
- (instancetype)initWithFrame:(CGRect)frame | ||
{ | ||
if (self = [super initWithFrame:frame]) { | ||
static const auto defaultProps = std::make_shared<TextInputProps const>(); | ||
_props = defaultProps; | ||
auto &props = *defaultProps; | ||
|
||
_backedTextInputView = props.traits.multiline ? [[RCTUITextView alloc] init] : [[RCTUITextField alloc] init]; | ||
_backedTextInputView.frame = self.bounds; | ||
_backedTextInputView.textInputDelegate = self; | ||
[self addSubview:_backedTextInputView]; | ||
} | ||
|
||
return self; | ||
} | ||
|
||
#pragma mark - RCTComponentViewProtocol | ||
|
||
+ (ComponentDescriptorProvider)componentDescriptorProvider | ||
{ | ||
return concreteComponentDescriptorProvider<TextInputComponentDescriptor>(); | ||
} | ||
|
||
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps | ||
{ | ||
auto const &oldTextInputProps = *std::static_pointer_cast<TextInputProps const>(_props); | ||
auto const &newTextInputProps = *std::static_pointer_cast<TextInputProps const>(props); | ||
|
||
// Traits: | ||
if (newTextInputProps.traits.multiline != oldTextInputProps.traits.multiline) { | ||
[self _setMultiline:newTextInputProps.traits.multiline]; | ||
} | ||
|
||
if (newTextInputProps.traits.autocapitalizationType != oldTextInputProps.traits.autocapitalizationType) { | ||
_backedTextInputView.autocapitalizationType = | ||
RCTUITextAutocapitalizationTypeFromAutocapitalizationType(newTextInputProps.traits.autocapitalizationType); | ||
} | ||
|
||
if (newTextInputProps.traits.autoCorrect != oldTextInputProps.traits.autoCorrect) { | ||
_backedTextInputView.autocorrectionType = | ||
RCTUITextAutocorrectionTypeFromOptionalBool(newTextInputProps.traits.autoCorrect); | ||
} | ||
|
||
if (newTextInputProps.traits.contextMenuHidden != oldTextInputProps.traits.contextMenuHidden) { | ||
_backedTextInputView.contextMenuHidden = newTextInputProps.traits.contextMenuHidden; | ||
} | ||
|
||
if (newTextInputProps.traits.editable != oldTextInputProps.traits.editable) { | ||
_backedTextInputView.editable = newTextInputProps.traits.editable; | ||
} | ||
|
||
if (newTextInputProps.traits.enablesReturnKeyAutomatically != | ||
oldTextInputProps.traits.enablesReturnKeyAutomatically) { | ||
_backedTextInputView.enablesReturnKeyAutomatically = newTextInputProps.traits.enablesReturnKeyAutomatically; | ||
} | ||
|
||
if (newTextInputProps.traits.keyboardAppearance != oldTextInputProps.traits.keyboardAppearance) { | ||
_backedTextInputView.keyboardAppearance = | ||
RCTUIKeyboardAppearanceFromKeyboardAppearance(newTextInputProps.traits.keyboardAppearance); | ||
} | ||
|
||
if (newTextInputProps.traits.spellCheck != oldTextInputProps.traits.spellCheck) { | ||
_backedTextInputView.spellCheckingType = | ||
RCTUITextSpellCheckingTypeFromOptionalBool(newTextInputProps.traits.spellCheck); | ||
} | ||
|
||
if (newTextInputProps.traits.caretHidden != oldTextInputProps.traits.caretHidden) { | ||
_backedTextInputView.caretHidden = newTextInputProps.traits.caretHidden; | ||
} | ||
|
||
if (newTextInputProps.traits.clearButtonMode != oldTextInputProps.traits.clearButtonMode) { | ||
_backedTextInputView.clearButtonMode = | ||
RCTUITextFieldViewModeFromTextInputAccessoryVisibilityMode(newTextInputProps.traits.clearButtonMode); | ||
} | ||
|
||
if (newTextInputProps.traits.scrollEnabled != oldTextInputProps.traits.scrollEnabled) { | ||
_backedTextInputView.scrollEnabled = newTextInputProps.traits.scrollEnabled; | ||
} | ||
|
||
if (newTextInputProps.traits.secureTextEntry != oldTextInputProps.traits.secureTextEntry) { | ||
_backedTextInputView.secureTextEntry = newTextInputProps.traits.secureTextEntry; | ||
} | ||
|
||
if (newTextInputProps.traits.keyboardType != oldTextInputProps.traits.keyboardType) { | ||
_backedTextInputView.keyboardType = RCTUIKeyboardTypeFromKeyboardType(newTextInputProps.traits.keyboardType); | ||
} | ||
|
||
if (newTextInputProps.traits.returnKeyType != oldTextInputProps.traits.returnKeyType) { | ||
_backedTextInputView.returnKeyType = RCTUIReturnKeyTypeFromReturnKeyType(newTextInputProps.traits.returnKeyType); | ||
} | ||
|
||
if (newTextInputProps.traits.textContentType != oldTextInputProps.traits.textContentType) { | ||
if (@available(iOS 10.0, *)) { | ||
_backedTextInputView.textContentType = RCTUITextContentTypeFromString(newTextInputProps.traits.textContentType); | ||
} | ||
} | ||
|
||
if (newTextInputProps.traits.passwordRules != oldTextInputProps.traits.passwordRules) { | ||
if (@available(iOS 12.0, *)) { | ||
_backedTextInputView.passwordRules = | ||
RCTUITextInputPasswordRulesFromString(newTextInputProps.traits.passwordRules); | ||
} | ||
} | ||
|
||
// Traits `blurOnSubmit`, `clearTextOnFocus`, and `selectTextOnFocus` were omitted intentially here | ||
// because they are being checked on-demand. | ||
|
||
// Other props: | ||
if (newTextInputProps.placeholder != oldTextInputProps.placeholder) { | ||
_backedTextInputView.placeholder = RCTNSStringFromString(newTextInputProps.placeholder); | ||
} | ||
|
||
if (newTextInputProps.textAttributes != oldTextInputProps.textAttributes) { | ||
_backedTextInputView.defaultTextAttributes = | ||
RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes()); | ||
} | ||
|
||
if (newTextInputProps.selectionColor != oldTextInputProps.selectionColor) { | ||
_backedTextInputView.tintColor = RCTUIColorFromSharedColor(newTextInputProps.selectionColor); | ||
} | ||
|
||
[super updateProps:props oldProps:oldProps]; | ||
} | ||
|
||
- (void)updateState:(State::Shared const &)state oldState:(State::Shared const &)oldState | ||
{ | ||
_state = std::static_pointer_cast<TextInputShadowNode::ConcreteState const>(state); | ||
|
||
if (!_state) { | ||
assert(false && "State is `null` for <TextInput> component."); | ||
_backedTextInputView.attributedText = nil; | ||
return; | ||
} | ||
|
||
auto data = _state->getData(); | ||
|
||
if (data.revision != _stateRevision) { | ||
_stateRevision = data.revision; | ||
_backedTextInputView.attributedText = RCTNSAttributedStringFromAttributedStringBox(data.attributedStringBox); | ||
} | ||
} | ||
|
||
- (void)updateLayoutMetrics:(LayoutMetrics const &)layoutMetrics | ||
oldLayoutMetrics:(LayoutMetrics const &)oldLayoutMetrics | ||
{ | ||
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; | ||
|
||
_backedTextInputView.frame = | ||
UIEdgeInsetsInsetRect(self.bounds, RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.borderWidth)); | ||
_backedTextInputView.textContainerInset = | ||
RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.contentInsets - layoutMetrics.borderWidth); | ||
} | ||
|
||
- (void)prepareForRecycle | ||
{ | ||
[super prepareForRecycle]; | ||
_backedTextInputView.attributedText = [[NSAttributedString alloc] init]; | ||
_state.reset(); | ||
_stateRevision = 0; | ||
} | ||
|
||
#pragma mark - RCTComponentViewProtocol | ||
|
||
- (void)_setMultiline:(BOOL)multiline | ||
{ | ||
[_backedTextInputView removeFromSuperview]; | ||
UIView<RCTBackedTextInputViewProtocol> *backedTextInputView = | ||
multiline ? [[RCTUITextView alloc] init] : [[RCTUITextField alloc] init]; | ||
backedTextInputView.frame = _backedTextInputView.frame; | ||
RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView); | ||
_backedTextInputView = backedTextInputView; | ||
[self addSubview:_backedTextInputView]; | ||
} | ||
|
||
#pragma mark - RCTBackedTextInputDelegate | ||
|
||
- (BOOL)textInputShouldBeginEditing | ||
{ | ||
return YES; | ||
} | ||
|
||
- (void)textInputDidBeginEditing | ||
{ | ||
auto const &props = *std::static_pointer_cast<TextInputProps const>(_props); | ||
|
||
if (props.traits.clearTextOnFocus) { | ||
_backedTextInputView.attributedText = [NSAttributedString new]; | ||
[self textInputDidChange]; | ||
} | ||
|
||
if (props.traits.selectTextOnFocus) { | ||
[_backedTextInputView selectAll:nil]; | ||
[self textInputDidChangeSelection]; | ||
} | ||
|
||
if (_eventEmitter) { | ||
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onFocus([self _textInputMetrics]); | ||
} | ||
} | ||
|
||
- (BOOL)textInputShouldEndEditing | ||
{ | ||
return YES; | ||
} | ||
|
||
- (void)textInputDidEndEditing | ||
{ | ||
if (_eventEmitter) { | ||
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onEndEditing([self _textInputMetrics]); | ||
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onBlur([self _textInputMetrics]); | ||
} | ||
} | ||
|
||
- (BOOL)textInputShouldReturn | ||
{ | ||
// We send `submit` event here, in `textInputShouldReturn` | ||
// (not in `textInputDidReturn)`, because of semantic of the event: | ||
// `onSubmitEditing` is called when "Submit" button | ||
// (the blue key on onscreen keyboard) did pressed | ||
// (no connection to any specific "submitting" process). | ||
|
||
if (_eventEmitter) { | ||
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onSubmitEditing([self _textInputMetrics]); | ||
} | ||
|
||
auto const &props = *std::static_pointer_cast<TextInputProps const>(_props); | ||
return props.traits.blurOnSubmit; | ||
} | ||
|
||
- (void)textInputDidReturn | ||
{ | ||
// Does nothing. | ||
} | ||
|
||
- (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range | ||
{ | ||
if (!_backedTextInputView.textWasPasted) { | ||
if (_eventEmitter) { | ||
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onKeyPress([self _textInputMetrics]); | ||
} | ||
} | ||
|
||
auto const &props = *std::static_pointer_cast<TextInputProps const>(_props); | ||
if (props.maxLength) { | ||
NSInteger allowedLength = props.maxLength - _backedTextInputView.attributedText.string.length + range.length; | ||
|
||
if (allowedLength <= 0) { | ||
return nil; | ||
} | ||
|
||
return allowedLength > text.length ? text : [text substringToIndex:allowedLength]; | ||
} | ||
|
||
return text; | ||
} | ||
|
||
- (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text | ||
{ | ||
return YES; | ||
} | ||
|
||
- (void)textInputDidChange | ||
{ | ||
[self _updateState]; | ||
|
||
if (_eventEmitter) { | ||
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onChange([self _textInputMetrics]); | ||
} | ||
} | ||
|
||
- (void)textInputDidChangeSelection | ||
{ | ||
if (_eventEmitter) { | ||
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onSelectionChange([self _textInputMetrics]); | ||
} | ||
} | ||
|
||
#pragma mark - Other | ||
|
||
- (TextInputMetrics)_textInputMetrics | ||
{ | ||
TextInputMetrics metrics; | ||
metrics.text = RCTStringFromNSString(_backedTextInputView.attributedText.string); | ||
metrics.selectionRange = [self _selectionRange]; | ||
return metrics; | ||
} | ||
|
||
- (void)_updateState | ||
{ | ||
NSAttributedString *attributedString = _backedTextInputView.attributedText; | ||
|
||
if (!_state) { | ||
return; | ||
} | ||
|
||
auto data = _state->getData(); | ||
data.revision++; | ||
_stateRevision = data.revision; | ||
data.attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(attributedString); | ||
_state->updateState(std::move(data), EventPriority::SynchronousUnbatched); | ||
} | ||
|
||
- (AttributedString::Range)_selectionRange | ||
{ | ||
UITextRange *selectedTextRange = _backedTextInputView.selectedTextRange; | ||
NSInteger start = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument | ||
toPosition:selectedTextRange.start]; | ||
NSInteger end = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument | ||
toPosition:selectedTextRange.end]; | ||
return AttributedString::Range{(int)start, (int)(end - start)}; | ||
} | ||
|
||
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters