diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts index b2ae497e8e04a6..ea941f9124ab53 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts @@ -583,6 +583,20 @@ export interface TextInputProps */ caretHidden?: boolean | undefined; + /** + * Allows to adjust caret height. + * The default value is 0, which means the height of the caret will be calculated automatically + * @platform ios + */ + caretHeight?: number | undefined; + + /** + * Allows to adjust caret position relative to the Y axis + * The default value is 0. + * @platform ios + */ + caretYOffset?: number | undefined; + /** * If true, context menu is hidden. The default value is false. */ diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js index cc3c4e7528bd0e..6b6c0e58320bfe 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -557,6 +557,20 @@ export type Props = $ReadOnly<{| */ caretHidden?: ?boolean, + /** + * Allows to adjust caret height. + * The default value is 0, which means the height of the caret will be calculated automatically + * @platform ios + */ + caretYOffset?: ?number, + + /** + * Allows to adjust caret position relative to the Y axis + * The default value is 0. + * @platform ios + */ + caretYHeight?: ?number, + /* * If `true`, contextMenuHidden is hidden. The default value is `false`. */ diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index 1ef9045bf5ae02..c30044fabf1b3a 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -376,6 +376,20 @@ type IOSProps = $ReadOnly<{| * @platform ios */ smartInsertDelete?: ?boolean, + + /** + * Allows to adjust caret height. + * The default value is 0, which means the height of the caret will be calculated automatically + * @platform ios + */ + caretYOffset?: ?number, + + /** + * Allows to adjust caret position relative to the Y axis + * The default value is 0. + * @platform ios + */ + caretHeight?: ?number, |}>; type AndroidProps = $ReadOnly<{| diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h index 205f9943262add..94b32923d7d9f7 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h @@ -35,6 +35,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) UITextFieldViewMode clearButtonMode; @property (nonatomic, assign) BOOL caretHidden; +@property (nonatomic, assign) CGFloat caretYOffset; +@property (nonatomic, assign) CGFloat caretHeight; @property (nonatomic, strong, nullable) NSString *inputAccessoryViewID; diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.m b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.m index 037a38276aee86..df1982c579a789 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.m +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.m @@ -13,6 +13,41 @@ #import #import +// the UITextSelectionRect subclass needs to be created because the original version is not writable +@interface CustomTextSelectionRect : UITextSelectionRect + +@property (nonatomic) CGRect _rect; +@property (nonatomic) NSWritingDirection _writingDirection; +@property (nonatomic) BOOL _containsStart; // Returns YES if the rect contains the start of the selection. +@property (nonatomic) BOOL _containsEnd; // Returns YES if the rect contains the end of the selection. +@property (nonatomic) BOOL _isVertical; // Returns YES if the rect is for vertically oriented text. + +@end + +@implementation CustomTextSelectionRect + +- (CGRect)rect { + return __rect; +} + +- (NSWritingDirection)writingDirection { + return __writingDirection; +} + +- (BOOL)containsStart { + return __containsStart; +} + +- (BOOL)containsEnd { + return __containsEnd; +} + +- (BOOL)isVertical { + return __isVertical; +} + +@end + @implementation RCTUITextView { UILabel *_placeholderView; UITextView *_detachedTextView; @@ -307,11 +342,44 @@ - (void)_updatePlaceholder - (CGRect)caretRectForPosition:(UITextPosition *)position { + CGRect originalRect = [super caretRectForPosition:position]; + if (_caretHidden) { return CGRectZero; } - return [super caretRectForPosition:position]; + if(_caretYOffset != 0) { + originalRect.origin.y += _caretYOffset; + } + + if(_caretHeight != 0) { + originalRect.size.height = _caretHeight; + } + + return originalRect; +} + +- (NSArray *)selectionRectsForRange:(UITextRange *)range { + NSArray *superRects = [super selectionRectsForRange:range]; + if(_caretYOffset != 0 && _caretHeight != 0) { + NSMutableArray *customTextSelectionRects = [NSMutableArray array]; + + for (UITextSelectionRect *rect in superRects) { + CustomTextSelectionRect *customTextRect = [[CustomTextSelectionRect alloc] init]; + + customTextRect._rect = CGRectMake(rect.rect.origin.x, rect.rect.origin.y + _caretYOffset, rect.rect.size.width, _caretHeight); + customTextRect._writingDirection = rect.writingDirection; + customTextRect._containsStart = rect.containsStart; + customTextRect._containsEnd = rect.containsEnd; + customTextRect._isVertical = rect.isVertical; + [customTextSelectionRects addObject:customTextRect]; + } + + return customTextSelectionRects; + + } + return superRects; + } #pragma mark - Utility Methods diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h index a8719ecd4d0165..2bd9581f8b2007 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h +++ b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h @@ -28,6 +28,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL contextMenuHidden; @property (nonatomic, assign, getter=isEditable) BOOL editable; @property (nonatomic, assign) BOOL caretHidden; +@property (nonatomic, assign) CGFloat caretYOffset; +@property (nonatomic, assign) CGFloat caretHeight; @property (nonatomic, assign) BOOL enablesReturnKeyAutomatically; @property (nonatomic, assign) UITextFieldViewMode clearButtonMode; @property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled; diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m index a19b55569e8d71..d6915ee37335ce 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.m @@ -44,6 +44,8 @@ @implementation RCTBaseTextInputViewManager { RCT_REMAP_VIEW_PROPERTY(selectionColor, backedTextInputView.tintColor, UIColor) RCT_REMAP_VIEW_PROPERTY(spellCheck, backedTextInputView.spellCheckingType, UITextSpellCheckingType) RCT_REMAP_VIEW_PROPERTY(caretHidden, backedTextInputView.caretHidden, BOOL) +RCT_REMAP_VIEW_PROPERTY(caretYOffset, backedTextInputView.caretYOffset, CGFloat) +RCT_REMAP_VIEW_PROPERTY(caretHeight, backedTextInputView.caretHeight, CGFloat) RCT_REMAP_VIEW_PROPERTY(clearButtonMode, backedTextInputView.clearButtonMode, UITextFieldViewMode) RCT_REMAP_VIEW_PROPERTY(scrollEnabled, backedTextInputView.scrollEnabled, BOOL) RCT_REMAP_VIEW_PROPERTY(secureTextEntry, backedTextInputView.secureTextEntry, BOOL) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index 9441efdb4492ca..7047e46bbba07f 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -152,6 +152,14 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & _backedTextInputView.caretHidden = newTextInputProps.traits.caretHidden; } + if(newTextInputProps.traits.caretYOffset != oldTextInputProps.traits.caretYOffset) { + _backedTextInputView.caretYOffset = newTextInputProps.traits.caretYOffset; + } + + if(newTextInputProps.traits.caretHeight != oldTextInputProps.traits.caretHeight) { + _backedTextInputView.caretHeight = newTextInputProps.traits.caretHeight; + } + if (newTextInputProps.traits.clearButtonMode != oldTextInputProps.traits.clearButtonMode) { _backedTextInputView.clearButtonMode = RCTUITextFieldViewModeFromTextInputAccessoryVisibilityMode(newTextInputProps.traits.clearButtonMode); diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm index 0cdb5642147973..c3685dc788f49c 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm @@ -38,6 +38,8 @@ void RCTCopyBackedTextInput( toTextInput.keyboardAppearance = fromTextInput.keyboardAppearance; toTextInput.spellCheckingType = fromTextInput.spellCheckingType; toTextInput.caretHidden = fromTextInput.caretHidden; + toTextInput.caretYOffset = fromTextInput.caretYOffset; + toTextInput.caretHeight = fromTextInput.caretHeight; toTextInput.clearButtonMode = fromTextInput.clearButtonMode; toTextInput.scrollEnabled = fromTextInput.scrollEnabled; toTextInput.secureTextEntry = fromTextInput.secureTextEntry; diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/primitives.h index bfcf0f45127e54..f0c50d96bd0736 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/primitives.h @@ -156,6 +156,18 @@ class TextInputTraits final { */ bool caretHidden{false}; + /* + * iOS-only (inherently iOS-specific) + * Default value: 0 with a default font. + */ + int caretHeight{0}; + + /* + * iOS-only (inherently iOS-specific) + * Default value: 0 means that the caret offset will have the default value + */ + int caretYOffset{0}; + /* * Controls the visibility of a `Clean` button. * iOS-only (implemented only on iOS for now) diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/propsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/propsConversions.h index bed8a6e21cf8a0..dcf00c45a36c2d 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/propsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/propsConversions.h @@ -76,6 +76,18 @@ static TextInputTraits convertRawProp( "caretHidden", sourceTraits.caretHidden, defaultTraits.caretHidden); + traits.caretYOffset = convertRawProp( + context, + rawProps, + "caretYOffset", + sourceTraits.caretYOffset, + defaultTraits.caretYOffset); + traits.caretHeight= convertRawProp( + context, + rawProps, + "caretHeight", + sourceTraits.caretHeight, + defaultTraits.caretHeight); traits.clearButtonMode = convertRawProp( context, rawProps,