diff --git a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js index f91026796ce9..771ff2066867 100644 --- a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +++ b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js @@ -141,6 +141,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 80db8f0a6522..421ab9210694 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts @@ -377,12 +377,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 @@ -698,11 +692,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 0eb8f578d658..99dc3d593dae 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -369,26 +369,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`. @@ -668,6 +654,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: @@ -689,6 +681,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. */ @@ -855,6 +853,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 3e0d8bf7681d..2af41ff3a934 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -416,7 +416,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, @@ -429,10 +428,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`. @@ -1110,6 +1113,9 @@ function InternalTextInput(props: Props): React.Node { accessibilityState, id, tabIndex, + rows, + numberOfLines, + maxNumberOfLines, selection: propsSelection, ...otherProps } = props; @@ -1478,6 +1484,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} @@ -1533,6 +1541,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 d737cccbbf92..197641153dea 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({ @@ -251,7 +271,6 @@ const Text: React.AbstractComponent< isHighlighted={isHighlighted} isPressable={isPressable} nativeID={id ?? nativeID} - numberOfLines={numberOfLines} ref={forwardedRef} selectable={_selectable} selectionColor={selectionColor} @@ -262,6 +281,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 8f4cf7eb1b5f..6238ebc60092 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 1f06b79070aa..48172ce6e2ae 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 413ac4223878..56d039ce2518 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 0d5990455b5b..3216e4340cc9 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 f2317ba080f2..10f342c779b1 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 d73f8632b84f..1f85b229bc43 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". @@ -89,6 +94,7 @@ struct hash { size_t operator()( const facebook::react::ParagraphAttributes& attributes) const { return facebook::react::hash_combine( + 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 445e452e1f4d..5dba4e2652e7 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -692,12 +692,18 @@ inline ParagraphAttributes convertRawProp( const ParagraphAttributes& 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, @@ -770,6 +776,7 @@ inline std::string toString(const AttributedString::Range& range) { inline folly::dynamic toDynamic( const ParagraphAttributes& paragraphAttributes) { auto values = folly::dynamic::object(); + values("numberOfLines", paragraphAttributes.numberOfLines); values("maximumNumberOfLines", paragraphAttributes.maximumNumberOfLines); values("ellipsizeMode", toString(paragraphAttributes.ellipsizeMode)); values("textBreakStrategy", toString(paragraphAttributes.textBreakStrategy)); @@ -979,6 +986,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& paragraphAttributes) { auto builder = MapBufferBuilder(); @@ -996,6 +1004,8 @@ inline MapBuffer toMapBuffer(const ParagraphAttributes& paragraphAttributes) { 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 116284f1c6bc..531d8d4b5dcd 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 43cbb6859047..0bf63e798e2c 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 @@ -81,6 +81,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 f7d17f52dc37..e28691a3ec55 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 d0ad86615585..fee23c354899 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js @@ -374,36 +374,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 d03a0b9a33a2..24993559b4f9 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);