diff --git a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js index 88d3cc8fe756e5..664d37d5f76802 100644 --- a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +++ b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js @@ -144,6 +144,8 @@ const RCTTextInputViewConfig = { placeholder: true, autoCorrect: true, multiline: true, + numberOfLines: true, + maximumNumberOfLines: true, textContentType: true, maxLength: true, autoCapitalize: true, diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts index 91d64ce7965f33..0f230b5cf4198f 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts @@ -374,12 +374,6 @@ export interface TextInputAndroidProps { */ inlineImagePadding?: number | undefined; - /** - * Sets the number of lines for a TextInput. - * Use it with multiline set to true to be able to fill the lines. - */ - numberOfLines?: number | undefined; - /** * Sets the return key to the label. Use it instead of `returnKeyType`. * @platform android @@ -695,11 +689,29 @@ export interface TextInputProps */ maxLength?: number | undefined; + /** + * Sets the maximum number of lines for a TextInput. + * Use it with multiline set to true to be able to fill the lines. + */ + maxNumberOfLines?: number | undefined; + /** * If true, the text input can be multiple lines. The default value is false. */ multiline?: boolean | undefined; + /** + * Sets the number of lines for a TextInput. + * Use it with multiline set to true to be able to fill the lines. + */ + numberOfLines?: number | undefined; + + /** + * Sets the number of rows for a TextInput. + * Use it with multiline set to true to be able to fill the lines. + */ + rows?: number | undefined; + /** * Callback that is called when the text input is blurred */ diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js index 9adbfe9f6f190b..06bfe51f1ff748 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -366,26 +366,12 @@ type AndroidProps = $ReadOnly<{| */ inlineImagePadding?: ?number, - /** - * Sets the number of lines for a `TextInput`. Use it with multiline set to - * `true` to be able to fill the lines. - * @platform android - */ - numberOfLines?: ?number, - /** * Sets the return key to the label. Use it instead of `returnKeyType`. * @platform android */ returnKeyLabel?: ?string, - /** - * Sets the number of rows for a `TextInput`. Use it with multiline set to - * `true` to be able to fill the lines. - * @platform android - */ - rows?: ?number, - /** * When `false`, it will prevent the soft keyboard from showing when the field is focused. * Defaults to `true`. @@ -665,6 +651,12 @@ export type Props = $ReadOnly<{| */ keyboardType?: ?KeyboardType, + /** + * Sets the maximum number of lines for a `TextInput`. Use it with multiline set to + * `true` to be able to fill the lines. + */ + maxNumberOfLines?: ?number, + /** * Specifies largest possible scale a font can reach when `allowFontScaling` is enabled. * Possible values: @@ -686,6 +678,12 @@ export type Props = $ReadOnly<{| */ multiline?: ?boolean, + /** + * Sets the number of lines for a `TextInput`. Use it with multiline set to + * `true` to be able to fill the lines. + */ + numberOfLines?: ?number, + /** * Callback that is called when the text input is blurred. */ @@ -847,6 +845,12 @@ export type Props = $ReadOnly<{| */ returnKeyType?: ?ReturnKeyType, + /** + * Sets the number of rows for a `TextInput`. Use it with multiline set to + * `true` to be able to fill the lines. + */ + rows?: ?number, + /** * If `true`, the text input obscures the text entered so that sensitive text * like passwords stay secure. The default value is `false`. Does not work with 'multiline={true}'. diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index b8d86021e17105..1365d847a55047 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -413,7 +413,6 @@ type AndroidProps = $ReadOnly<{| /** * Sets the number of lines for a `TextInput`. Use it with multiline set to * `true` to be able to fill the lines. - * @platform android */ numberOfLines?: ?number, @@ -426,10 +425,14 @@ type AndroidProps = $ReadOnly<{| /** * Sets the number of rows for a `TextInput`. Use it with multiline set to * `true` to be able to fill the lines. - * @platform android */ rows?: ?number, + /** + * Sets the maximum number of lines the TextInput can have. + */ + maxNumberOfLines?: ?number, + /** * When `false`, it will prevent the soft keyboard from showing when the field is focused. * Defaults to `true`. @@ -1102,6 +1105,9 @@ function InternalTextInput(props: Props): React.Node { accessibilityState, id, tabIndex, + rows, + numberOfLines, + maxNumberOfLines, selection: propsSelection, ...otherProps } = props; @@ -1458,6 +1464,8 @@ function InternalTextInput(props: Props): React.Node { focusable={tabIndex !== undefined ? !tabIndex : focusable} mostRecentEventCount={mostRecentEventCount} nativeID={id ?? props.nativeID} + numberOfLines={props.rows ?? props.numberOfLines} + maximumNumberOfLines={maxNumberOfLines} onBlur={_onBlur} onKeyPressSync={props.unstable_onKeyPressSync} onChange={_onChange} @@ -1513,6 +1521,7 @@ function InternalTextInput(props: Props): React.Node { mostRecentEventCount={mostRecentEventCount} nativeID={id ?? props.nativeID} numberOfLines={props.rows ?? props.numberOfLines} + maximumNumberOfLines={maxNumberOfLines} onBlur={_onBlur} onChange={_onChange} onFocus={_onFocus} diff --git a/packages/react-native/Libraries/Text/Text.js b/packages/react-native/Libraries/Text/Text.js index 0736e987f266c3..053c4e00de5dbb 100644 --- a/packages/react-native/Libraries/Text/Text.js +++ b/packages/react-native/Libraries/Text/Text.js @@ -17,7 +17,11 @@ import flattenStyle from '../StyleSheet/flattenStyle'; import processColor from '../StyleSheet/processColor'; import Platform from '../Utilities/Platform'; import TextAncestor from './TextAncestor'; -import {NativeText, NativeVirtualText} from './TextNativeComponent'; +import { + CONTAINS_MAX_NUMBER_OF_LINES_RENAME, + NativeText, + NativeVirtualText, +} from './TextNativeComponent'; import * as React from 'react'; import {useContext, useMemo, useState} from 'react'; @@ -56,6 +60,7 @@ const Text: React.AbstractComponent< onStartShouldSetResponder, pressRetentionOffset, suppressHighlighting, + numberOfLines, ...restProps } = props; @@ -195,14 +200,29 @@ const Text: React.AbstractComponent< } } - let numberOfLines = restProps.numberOfLines; + let numberOfLinesValue = numberOfLines; if (numberOfLines != null && !(numberOfLines >= 0)) { console.error( `'numberOfLines' in must be a non-negative number, received: ${numberOfLines}. The value will be set to 0.`, ); - numberOfLines = 0; + numberOfLinesValue = 0; } + const numberOfLinesProps = useMemo((): { + maximumNumberOfLines?: ?number, + numberOfLines?: ?number, + } => { + if (CONTAINS_MAX_NUMBER_OF_LINES_RENAME) { + return { + maximumNumberOfLines: numberOfLinesValue, + }; + } else { + return { + numberOfLines: numberOfLinesValue, + }; + } + }, [numberOfLinesValue]); + const hasTextAncestor = useContext(TextAncestor); const _accessible = Platform.select({ @@ -241,7 +261,6 @@ const Text: React.AbstractComponent< isHighlighted={isHighlighted} isPressable={isPressable} nativeID={id ?? nativeID} - numberOfLines={numberOfLines} ref={forwardedRef} selectable={_selectable} selectionColor={selectionColor} @@ -252,6 +271,7 @@ const Text: React.AbstractComponent< #import +#import +#import @implementation RCTMultilineTextInputViewManager @@ -17,8 +19,21 @@ - (UIView *)view return [[RCTMultilineTextInputView alloc] initWithBridge:self.bridge]; } +- (RCTShadowView *)shadowView +{ + RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[super shadowView]; + + shadowView.maximumNumberOfLines = 0; + shadowView.exactNumberOfLines = 0; + + return shadowView; +} + #pragma mark - Multiline (aka TextView) specific properties RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.dataDetectorTypes, UIDataDetectorTypes) +RCT_EXPORT_SHADOW_PROPERTY(maximumNumberOfLines, NSInteger) +RCT_REMAP_SHADOW_PROPERTY(numberOfLines, exactNumberOfLines, NSInteger) + @end diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h index 8f4cf7eb1b5f47..6238ebc600927f 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h @@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy, nullable) NSString *text; @property (nonatomic, copy, nullable) NSString *placeholder; @property (nonatomic, assign) NSInteger maximumNumberOfLines; +@property (nonatomic, assign) NSInteger exactNumberOfLines; @property (nonatomic, copy, nullable) RCTDirectEventBlock onContentSizeChange; - (void)uiManagerWillPerformMounting; diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm index 04d2446f86d9b3..9d777436763f24 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm @@ -218,7 +218,22 @@ - (NSAttributedString *)measurableAttributedText - (CGSize)sizeThatFitsMinimumSize:(CGSize)minimumSize maximumSize:(CGSize)maximumSize { - NSAttributedString *attributedText = [self measurableAttributedText]; + NSMutableAttributedString *attributedText = [[self measurableAttributedText] mutableCopy]; + + /* + * The block below is responsible for setting the exact height of the view in lines + * Unfortunatelly, iOS doesn't export any easy way to do it. So we set maximumNumberOfLines + * prop and then add random lines at the front. However, they are only used for layout + * so they are not visible on the screen. + */ + if (self.exactNumberOfLines) { + NSMutableString *newLines = [NSMutableString stringWithCapacity:self.exactNumberOfLines]; + for (NSUInteger i = 0UL; i < self.exactNumberOfLines; ++i) { + [newLines appendString:@"\n"]; + } + [attributedText insertAttributedString:[[NSAttributedString alloc] initWithString:newLines attributes:self.textAttributes.effectiveTextAttributes] atIndex:0]; + _maximumNumberOfLines = self.exactNumberOfLines; + } if (!_textStorage) { _textContainer = [NSTextContainer new]; diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.mm b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.mm index 413ac42238783a..56d039ce2518f9 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.mm +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.mm @@ -19,6 +19,7 @@ - (RCTShadowView *)shadowView RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[super shadowView]; shadowView.maximumNumberOfLines = 1; + shadowView.exactNumberOfLines = 0; return shadowView; } diff --git a/packages/react-native/Libraries/Text/TextNativeComponent.js b/packages/react-native/Libraries/Text/TextNativeComponent.js index 0d5990455b5be0..3216e4340cc92e 100644 --- a/packages/react-native/Libraries/Text/TextNativeComponent.js +++ b/packages/react-native/Libraries/Text/TextNativeComponent.js @@ -9,6 +9,7 @@ */ import {createViewConfig} from '../NativeComponent/ViewConfig'; +import getNativeComponentAttributes from '../ReactNative/getNativeComponentAttributes'; import UIManager from '../ReactNative/UIManager'; import createReactNativeComponentClass from '../Renderer/shims/createReactNativeComponentClass'; import {type HostComponent} from '../Renderer/shims/ReactNativeTypes'; @@ -18,6 +19,7 @@ import {type TextProps} from './TextProps'; type NativeTextProps = $ReadOnly<{ ...TextProps, + maximumNumberOfLines?: ?number, isHighlighted?: ?boolean, selectionColor?: ?ProcessedColorValue, onClick?: ?(event: PressEvent) => mixed, @@ -31,7 +33,7 @@ const textViewConfig = { validAttributes: { isHighlighted: true, isPressable: true, - numberOfLines: true, + maximumNumberOfLines: true, ellipsizeMode: true, allowFontScaling: true, dynamicTypeRamp: true, @@ -73,6 +75,12 @@ export const NativeText: HostComponent = createViewConfig(textViewConfig), ): any); +const jestIsDefined = typeof jest !== 'undefined'; +export const CONTAINS_MAX_NUMBER_OF_LINES_RENAME: boolean = jestIsDefined + ? true + : getNativeComponentAttributes('RCTText')?.NativeProps + ?.maximumNumberOfLines === 'number'; + export const NativeVirtualText: HostComponent = !global.RN$Bridgeless && !UIManager.hasViewManagerConfig('RCTVirtualText') ? NativeText diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp b/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp index 2994acac6122a0..fff0d5ebb10adc 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp @@ -16,6 +16,7 @@ namespace facebook::react { bool ParagraphAttributes::operator==(const ParagraphAttributes &rhs) const { return std::tie( + numberOfLines, maximumNumberOfLines, ellipsizeMode, textBreakStrategy, @@ -23,6 +24,7 @@ bool ParagraphAttributes::operator==(const ParagraphAttributes &rhs) const { includeFontPadding, android_hyphenationFrequency) == std::tie( + rhs.numberOfLines, rhs.maximumNumberOfLines, rhs.ellipsizeMode, rhs.textBreakStrategy, @@ -42,6 +44,7 @@ bool ParagraphAttributes::operator!=(const ParagraphAttributes &rhs) const { #if RN_DEBUG_STRING_CONVERTIBLE SharedDebugStringConvertibleList ParagraphAttributes::getDebugProps() const { return { + debugStringConvertibleItem("numberOfLines", numberOfLines), debugStringConvertibleItem("maximumNumberOfLines", maximumNumberOfLines), debugStringConvertibleItem("ellipsizeMode", ellipsizeMode), debugStringConvertibleItem("textBreakStrategy", textBreakStrategy), diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h index 510454d366e312..0f355aff3821de 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h @@ -29,6 +29,11 @@ class ParagraphAttributes : public DebugStringConvertible { public: #pragma mark - Fields + /* + * Number of lines which paragraph takes. + */ + int numberOfLines{}; + /* * Maximum number of lines which paragraph can take. * Zero value represents "no limit". @@ -90,6 +95,7 @@ struct hash { const facebook::react::ParagraphAttributes &attributes) const { return folly::hash::hash_combine( 0, + attributes.numberOfLines, attributes.maximumNumberOfLines, attributes.ellipsizeMode, attributes.textBreakStrategy, diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index 0d54c7e774935d..578f4a401f8dc2 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -691,12 +691,18 @@ inline ParagraphAttributes convertRawProp( ParagraphAttributes const &defaultParagraphAttributes) { auto paragraphAttributes = ParagraphAttributes{}; - paragraphAttributes.maximumNumberOfLines = convertRawProp( + paragraphAttributes.numberOfLines = convertRawProp( context, rawProps, "numberOfLines", - sourceParagraphAttributes.maximumNumberOfLines, - defaultParagraphAttributes.maximumNumberOfLines); + sourceParagraphAttributes.numberOfLines, + defaultParagraphAttributes.numberOfLines); + paragraphAttributes.maximumNumberOfLines = convertRawProp( + context, + rawProps, + "maximumNumberOfLines", + sourceParagraphAttributes.maximumNumberOfLines, + defaultParagraphAttributes.maximumNumberOfLines); paragraphAttributes.ellipsizeMode = convertRawProp( context, rawProps, @@ -769,6 +775,7 @@ inline std::string toString(AttributedString::Range const &range) { inline folly::dynamic toDynamic( const ParagraphAttributes ¶graphAttributes) { auto values = folly::dynamic::object(); + values("numberOfLines", paragraphAttributes.numberOfLines); values("maximumNumberOfLines", paragraphAttributes.maximumNumberOfLines); values("ellipsizeMode", toString(paragraphAttributes.ellipsizeMode)); values("textBreakStrategy", toString(paragraphAttributes.textBreakStrategy)); @@ -977,6 +984,7 @@ constexpr static MapBuffer::Key PA_KEY_TEXT_BREAK_STRATEGY = 2; constexpr static MapBuffer::Key PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3; constexpr static MapBuffer::Key PA_KEY_INCLUDE_FONT_PADDING = 4; constexpr static MapBuffer::Key PA_KEY_HYPHENATION_FREQUENCY = 5; +constexpr static MapBuffer::Key PA_KEY_NUMBER_OF_LINES = 6; inline MapBuffer toMapBuffer(const ParagraphAttributes ¶graphAttributes) { auto builder = MapBufferBuilder(); @@ -994,6 +1002,8 @@ inline MapBuffer toMapBuffer(const ParagraphAttributes ¶graphAttributes) { builder.putString( PA_KEY_HYPHENATION_FREQUENCY, toString(paragraphAttributes.android_hyphenationFrequency)); + builder.putInt( + PA_KEY_NUMBER_OF_LINES, paragraphAttributes.numberOfLines); return builder.build(); } diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp index 07ed9defdb3c08..f4259f8e0c0867 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp @@ -56,6 +56,10 @@ AndroidTextInputProps::AndroidTextInputProps( "numberOfLines", sourceProps.numberOfLines, {0})), + maximumNumberOfLines(CoreFeatures::enablePropIteratorSetter? sourceProps.maximumNumberOfLines : convertRawProp(context, rawProps, + "maximumNumberOfLines", + sourceProps.maximumNumberOfLines, + {0})), disableFullscreenUI(CoreFeatures::enablePropIteratorSetter? sourceProps.disableFullscreenUI : convertRawProp(context, rawProps, "disableFullscreenUI", sourceProps.disableFullscreenUI, @@ -281,6 +285,12 @@ void AndroidTextInputProps::setProp( value, paragraphAttributes, maximumNumberOfLines, + "maximumNumberOfLines"); + REBUILD_FIELD_SWITCH_CASE( + paDefaults, + value, + paragraphAttributes, + numberOfLines, "numberOfLines"); REBUILD_FIELD_SWITCH_CASE( paDefaults, value, paragraphAttributes, ellipsizeMode, "ellipsizeMode"); @@ -323,6 +333,7 @@ void AndroidTextInputProps::setProp( } switch (hash) { + RAW_SET_PROP_SWITCH_CASE_BASIC(maximumNumberOfLines); RAW_SET_PROP_SWITCH_CASE_BASIC(autoComplete); RAW_SET_PROP_SWITCH_CASE_BASIC(returnKeyLabel); RAW_SET_PROP_SWITCH_CASE_BASIC(numberOfLines); @@ -422,6 +433,7 @@ void AndroidTextInputProps::setProp( // TODO T53300085: support this in codegen; this was hand-written folly::dynamic AndroidTextInputProps::getDynamic() const { folly::dynamic props = folly::dynamic::object(); + props["maximumNumberOfLines"] = maximumNumberOfLines; props["autoComplete"] = autoComplete; props["returnKeyLabel"] = returnKeyLabel; props["numberOfLines"] = numberOfLines; diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h index 6c2996d99e07a6..0c9e9522f30793 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h @@ -83,6 +83,7 @@ class AndroidTextInputProps final : public ViewProps, public BaseTextProps { std::string autoComplete{}; std::string returnKeyLabel{}; int numberOfLines{0}; + int maximumNumberOfLines{0}; bool disableFullscreenUI{false}; std::string textBreakStrategy{}; SharedColor underlineColorAndroid{}; diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index 368c3341055804..3c51d4f2901c69 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -244,26 +244,50 @@ - (void)getRectWithAttributedString:(AttributedString)attributedString #pragma mark - Private -- (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)attributedString +- (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)inputAttributedString paragraphAttributes:(ParagraphAttributes)paragraphAttributes size:(CGSize)size { - NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size]; - textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. - textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0 - ? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode) - : NSLineBreakByClipping; - textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines; + NSMutableAttributedString *attributedString = [ inputAttributedString mutableCopy]; + + /* + * The block below is responsible for setting the exact height of the view in lines + * Unfortunatelly, iOS doesn't export any easy way to do it. So we set maximumNumberOfLines + * prop and then add random lines at the front. However, they are only used for layout + * so they are not visible on the screen. This method is used for drawing only for Paragraph component + * but we set exact height in lines only on TextInput that doesn't use it. + */ + if (paragraphAttributes.numberOfLines) { + paragraphAttributes.maximumNumberOfLines = paragraphAttributes.numberOfLines; + NSMutableString *newLines = [NSMutableString stringWithCapacity: paragraphAttributes.numberOfLines]; + for (NSUInteger i = 0UL; i < paragraphAttributes.numberOfLines; ++i) { + // K is added on purpose. New line seems to be not enough for NTtextContainer + [newLines appendString:@"K\n"]; + } + NSDictionary * attributesOfFirstCharacter = [inputAttributedString attributesAtIndex:0 effectiveRange:NULL]; + + [attributedString insertAttributedString:[[NSAttributedString alloc] initWithString:newLines attributes:attributesOfFirstCharacter] atIndex:0]; + } + + NSTextContainer *textContainer = [NSTextContainer new]; + NSLayoutManager *layoutManager = [NSLayoutManager new]; layoutManager.usesFontLeading = NO; [layoutManager addTextContainer:textContainer]; - - NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; - + NSTextStorage *textStorage = [NSTextStorage new]; [textStorage addLayoutManager:layoutManager]; + textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. + textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0 + ? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode) + : NSLineBreakByClipping; + textContainer.size = size; + textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines; + + [textStorage replaceCharactersInRange:(NSRange){0, textStorage.length} withAttributedString:attributedString]; + if (paragraphAttributes.adjustsFontSizeToFit) { CGFloat minimumFontSize = !isnan(paragraphAttributes.minimumFontSize) ? paragraphAttributes.minimumFontSize : 4.0; CGFloat maximumFontSize = !isnan(paragraphAttributes.maximumFontSize) ? paragraphAttributes.maximumFontSize : 96.0; diff --git a/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js b/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js index 5d89b2cdac838b..899638686049c4 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js @@ -376,36 +376,6 @@ const examples: Array = [ ); }, }, - { - title: 'Fixed number of lines', - platform: 'android', - render: function (): React.Node { - return ( - - - - - - - ); - }, - }, { title: 'Auto-expanding', render: function (): React.Node { diff --git a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js index c05948b0639800..1c88d35d9f11d4 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js @@ -91,6 +91,12 @@ const styles = StyleSheet.create({ right: -5, bottom: -5, }, + textInputLines: { + borderWidth: 1, + borderColor: 'black', + padding: 0, + textAlignVertical: Platform.OS === 'android' ? 'top' : undefined, + }, }); class WithLabel extends React.Component<$FlowFixMeProps> { @@ -1116,4 +1122,50 @@ module.exports = ([ name: 'textStyles', render: () => , }, + { + title: 'Height in rows/lines', + name: 'rows', + render: function (): React.Node { + return ( + + + + + + + + + ); + }, + }, ]: Array);