diff --git a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js index f91026796ce9cb..16b0024b785ba1 100644 --- a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +++ b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js @@ -124,6 +124,8 @@ const RCTTextInputViewConfig = { editable: true, inputAccessoryViewID: true, caretHidden: true, + caretHeight: true, + caretYOffset: true, enablesReturnKeyAutomatically: true, placeholderTextColor: { process: require('../../StyleSheet/processColor').default, diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts index 7234cbb3c325c5..94300e36782661 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts @@ -668,6 +668,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 638acd7c7925a2..8088e1606f9ee5 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -591,6 +591,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 7b522fe5dcb026..72a5ea347c36d1 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -365,6 +365,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.mm b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm index 582b49c1ef4f4b..f752f722db5a3f 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm @@ -13,6 +13,33 @@ #import #import +// subclass needs to be created as UITextSelectionRect is an abstract base class +@interface RCTTextSelectionRect : UITextSelectionRect + +@property (nonatomic, assign) CGRect rect; +@property (nonatomic, assign) NSWritingDirection writingDirection; +@property (nonatomic, assign) BOOL containsStart; // Returns YES if the rect contains the start of the selection. +@property (nonatomic, assign) BOOL containsEnd; // Returns YES if the rect contains the end of the selection. +@property (nonatomic, assign) BOOL isVertical; // Returns YES if the rect is for vertically oriented text. + +@end + +@implementation RCTTextSelectionRect { + CGRect _customRect; + NSWritingDirection _customWritingDirection; + BOOL _customContainsStart; + BOOL _customContainsEnd; + BOOL _customIsVertical; +} + +@synthesize rect = _customRect; +@synthesize writingDirection = _customWritingDirection; +@synthesize containsStart = _customContainsStart; +@synthesize containsEnd = _customContainsEnd; +@synthesize isVertical = _customIsVertical; + +@end + @implementation RCTUITextView { UILabel *_placeholderView; UITextView *_detachedTextView; @@ -307,11 +334,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) { + RCTTextSelectionRect *customTextRect = [[RCTTextSelectionRect 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.mm b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm index 6d3d18b11ebe80..47e16be69cb2e6 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm @@ -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/Libraries/Text/TextInput/Singleline/RCTUITextField.h b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h index 91f8eb087acf87..0687407393b1e1 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h @@ -22,6 +22,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) id textInputDelegate; @property (nonatomic, assign) BOOL caretHidden; +@property (nonatomic, assign) BOOL caretYOffset; +@property (nonatomic, assign) BOOL caretHeight; @property (nonatomic, assign) BOOL contextMenuHidden; @property (nonatomic, assign, readonly) BOOL textWasPasted; @property (nonatomic, assign, readonly) BOOL dictationRecognizing; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index de27ba219ed603..5fc6328813722a 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -2405,18 +2405,6 @@ declare export default HostComponent; " `; -exports[`public API should not change unintentionally Libraries/Components/TextInput/InputAccessoryView.js 1`] = ` -"type Props = $ReadOnly<{| - +children: React.Node, - nativeID?: ?string, - style?: ?ViewStyleProp, - backgroundColor?: ?ColorValue, -|}>; -declare const InputAccessoryView: React.AbstractComponent; -declare export default typeof InputAccessoryView; -" -`; - exports[`public API should not change unintentionally Libraries/Components/TextInput/RCTInputAccessoryViewNativeComponent.js 1`] = ` "export * from \\"../../../src/private/specs/components/RCTInputAccessoryViewNativeComponent\\"; declare export default typeof RCTInputAccessoryViewNativeComponent; 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 44e74da5cea06f..d90766d24d75c2 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -151,6 +151,14 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _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 d4b91c921b1114..15beb67af72f87 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/platform/ios/react/renderer/components/iostextinput/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/primitives.h index 664c0ccf0496ac..90ab92134b6f2e 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/primitives.h @@ -155,6 +155,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/platform/ios/react/renderer/components/iostextinput/propsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h index 109f5a1237be0a..2a722f75cd8096 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h @@ -75,6 +75,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,