diff --git a/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/Libraries/Components/TextInput/RCTTextInputViewConfig.js index 1962d095693e92..6af9cd331c1830 100644 --- a/Libraries/Components/TextInput/RCTTextInputViewConfig.js +++ b/Libraries/Components/TextInput/RCTTextInputViewConfig.js @@ -149,6 +149,7 @@ const RCTTextInputViewConfig = { clearTextOnFocus: true, showSoftInputOnFocus: true, autoFocus: true, + lineBreakStrategyIOS: true, ...ConditionallyIgnoredEventHandlers({ onChange: true, onSelectionChange: true, diff --git a/Libraries/Components/TextInput/TextInput.flow.js b/Libraries/Components/TextInput/TextInput.flow.js index 10a53f155c0739..57259190f1a449 100644 --- a/Libraries/Components/TextInput/TextInput.flow.js +++ b/Libraries/Components/TextInput/TextInput.flow.js @@ -319,6 +319,12 @@ type IOSProps = $ReadOnly<{| * @platform ios */ textContentType?: ?TextContentType, + + /** + * Set line break strategy on iOS. + * @platform ios + */ + lineBreakStrategyIOS?: ?('none' | 'standard' | 'hangul-word' | 'push-out'), |}>; type AndroidProps = $ReadOnly<{| diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index be6cab34a348c3..fa45a7b441e8fb 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -352,6 +352,12 @@ type IOSProps = $ReadOnly<{| * @platform ios */ textContentType?: ?TextContentType, + + /** + * Set line break strategy on iOS. + * @platform ios + */ + lineBreakStrategyIOS?: ?('none' | 'standard' | 'hangul-word' | 'push-out'), |}>; type AndroidProps = $ReadOnly<{| diff --git a/Libraries/Text/BaseText/RCTBaseTextViewManager.m b/Libraries/Text/BaseText/RCTBaseTextViewManager.m index fee8647bab155f..059f561c2f549e 100644 --- a/Libraries/Text/BaseText/RCTBaseTextViewManager.m +++ b/Libraries/Text/BaseText/RCTBaseTextViewManager.m @@ -42,6 +42,7 @@ - (RCTShadowView *)shadowView RCT_REMAP_SHADOW_PROPERTY(lineHeight, textAttributes.lineHeight, CGFloat) RCT_REMAP_SHADOW_PROPERTY(textAlign, textAttributes.alignment, NSTextAlignment) RCT_REMAP_SHADOW_PROPERTY(writingDirection, textAttributes.baseWritingDirection, NSWritingDirection) +RCT_REMAP_SHADOW_PROPERTY(lineBreakStrategyIOS, textAttributes.lineBreakStrategy, NSLineBreakStrategy) // Decoration RCT_REMAP_SHADOW_PROPERTY(textDecorationColor, textAttributes.textDecorationColor, UIColor) RCT_REMAP_SHADOW_PROPERTY(textDecorationStyle, textAttributes.textDecorationStyle, NSUnderlineStyle) diff --git a/Libraries/Text/RCTTextAttributes.h b/Libraries/Text/RCTTextAttributes.h index a95536d1b671d7..3491d598971b8e 100644 --- a/Libraries/Text/RCTTextAttributes.h +++ b/Libraries/Text/RCTTextAttributes.h @@ -41,6 +41,7 @@ extern NSString *const RCTTextAttributesTagAttributeName; @property (nonatomic, assign) CGFloat lineHeight; @property (nonatomic, assign) NSTextAlignment alignment; @property (nonatomic, assign) NSWritingDirection baseWritingDirection; +@property (nonatomic, assign) NSLineBreakStrategy lineBreakStrategy; // Decoration @property (nonatomic, strong, nullable) UIColor *textDecorationColor; @property (nonatomic, assign) NSUnderlineStyle textDecorationStyle; diff --git a/Libraries/Text/RCTTextAttributes.m b/Libraries/Text/RCTTextAttributes.m index 1fa15be2a5f801..1fb13011430736 100644 --- a/Libraries/Text/RCTTextAttributes.m +++ b/Libraries/Text/RCTTextAttributes.m @@ -27,6 +27,7 @@ - (instancetype)init _maxFontSizeMultiplier = NAN; _alignment = NSTextAlignmentNatural; _baseWritingDirection = NSWritingDirectionNatural; + _lineBreakStrategy = NSLineBreakStrategyNone; _textShadowRadius = NAN; _opacity = NAN; _textTransform = RCTTextTransformUndefined; @@ -66,6 +67,7 @@ - (void)applyTextAttributes:(RCTTextAttributes *)textAttributes _baseWritingDirection = textAttributes->_baseWritingDirection != NSWritingDirectionNatural ? textAttributes->_baseWritingDirection : _baseWritingDirection; // * + _lineBreakStrategy = textAttributes->_lineBreakStrategy ?: _lineBreakStrategy; // Decoration _textDecorationColor = textAttributes->_textDecorationColor ?: _textDecorationColor; @@ -117,6 +119,13 @@ - (NSParagraphStyle *)effectiveParagraphStyle isParagraphStyleUsed = YES; } + if (_lineBreakStrategy != NSLineBreakStrategyNone) { + if (@available(iOS 14.0, *)) { + paragraphStyle.lineBreakStrategy = _lineBreakStrategy; + isParagraphStyleUsed = YES; + } + } + if (!isnan(_lineHeight)) { CGFloat lineHeight = _lineHeight * self.effectiveFontSizeMultiplier; paragraphStyle.minimumLineHeight = lineHeight; @@ -318,7 +327,7 @@ - (BOOL)isEqual:(RCTTextAttributes *)textAttributes RCTTextAttributesCompareFloats(_letterSpacing) && // Paragraph Styles RCTTextAttributesCompareFloats(_lineHeight) && RCTTextAttributesCompareFloats(_alignment) && - RCTTextAttributesCompareOthers(_baseWritingDirection) && + RCTTextAttributesCompareOthers(_baseWritingDirection) && RCTTextAttributesCompareOthers(_lineBreakStrategy) && // Decoration RCTTextAttributesCompareObjects(_textDecorationColor) && RCTTextAttributesCompareOthers(_textDecorationStyle) && RCTTextAttributesCompareOthers(_textDecorationLine) && diff --git a/Libraries/Text/TextNativeComponent.js b/Libraries/Text/TextNativeComponent.js index 22b29bfbab1527..dd687c1697bf9a 100644 --- a/Libraries/Text/TextNativeComponent.js +++ b/Libraries/Text/TextNativeComponent.js @@ -45,6 +45,7 @@ const textViewConfig = { onInlineViewLayout: true, dataDetectorType: true, android_hyphenationFrequency: true, + lineBreakStrategyIOS: true, }, directEventTypes: { topTextLayout: { diff --git a/Libraries/Text/TextProps.js b/Libraries/Text/TextProps.js index 16e4f87b25cc03..3a927ae48681e4 100644 --- a/Libraries/Text/TextProps.js +++ b/Libraries/Text/TextProps.js @@ -236,4 +236,11 @@ export type TextProps = $ReadOnly<{| * See https://reactnative.dev/docs/text#supperhighlighting */ suppressHighlighting?: ?boolean, + + /** + * Set line break strategy on iOS. + * + * See https://reactnative.dev/docs/text.html#linebreakstrategyios + */ + lineBreakStrategyIOS?: ?('none' | 'standard' | 'hangul-word' | 'push-out'), |}>; diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index ad92515fd584d6..7a7cc75ebdce8f 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -65,6 +65,7 @@ typedef NSURL RCTFileURL; + (NSTextAlignment)NSTextAlignment:(id)json; + (NSUnderlineStyle)NSUnderlineStyle:(id)json; + (NSWritingDirection)NSWritingDirection:(id)json; ++ (NSLineBreakStrategy)NSLineBreakStrategy:(id)json; + (UITextAutocapitalizationType)UITextAutocapitalizationType:(id)json; + (UITextFieldViewMode)UITextFieldViewMode:(id)json; + (UIKeyboardType)UIKeyboardType:(id)json; diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index b44a5430db1ab2..33e2a094a0484a 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -375,6 +375,25 @@ + (NSLocale *)NSLocale:(id)json NSWritingDirectionNatural, integerValue) ++ (NSLineBreakStrategy)NSLineBreakStrategy:(id)json RCT_DYNAMIC +{ + if (@available(iOS 14.0, *)) { + static NSDictionary *mapping; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + mapping = @{ + @"none" : @(NSLineBreakStrategyNone), + @"standard" : @(NSLineBreakStrategyStandard), + @"hangul-word" : @(NSLineBreakStrategyHangulWordPriority), + @"push-out" : @(NSLineBreakStrategyPushOut) + }; + }); + return RCTConvertEnumValue("NSLineBreakStrategy", mapping, @(NSLineBreakStrategyNone), json).integerValue; + } else { + return NSLineBreakStrategyNone; + } +} + RCT_ENUM_CONVERTER( UITextAutocapitalizationType, (@{ diff --git a/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp b/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp index c1c657c4fbe1b3..24f5bbd07b8072 100644 --- a/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp +++ b/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp @@ -62,6 +62,9 @@ void TextAttributes::apply(TextAttributes textAttributes) { baseWritingDirection = textAttributes.baseWritingDirection.has_value() ? textAttributes.baseWritingDirection : baseWritingDirection; + lineBreakStrategy = textAttributes.lineBreakStrategy.has_value() + ? textAttributes.lineBreakStrategy + : lineBreakStrategy; // Decoration textDecorationColor = textAttributes.textDecorationColor @@ -110,6 +113,7 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const { allowFontScaling, alignment, baseWritingDirection, + lineBreakStrategy, textDecorationColor, textDecorationLineType, textDecorationStyle, @@ -129,6 +133,7 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const { rhs.allowFontScaling, rhs.alignment, rhs.baseWritingDirection, + rhs.lineBreakStrategy, rhs.textDecorationColor, rhs.textDecorationLineType, rhs.textDecorationStyle, @@ -187,6 +192,7 @@ SharedDebugStringConvertibleList TextAttributes::getDebugProps() const { debugStringConvertibleItem("lineHeight", lineHeight), debugStringConvertibleItem("alignment", alignment), debugStringConvertibleItem("baseWritingDirection", baseWritingDirection), + debugStringConvertibleItem("lineBreakStrategyIOS", lineBreakStrategy), // Decoration debugStringConvertibleItem("textDecorationColor", textDecorationColor), diff --git a/ReactCommon/react/renderer/attributedstring/TextAttributes.h b/ReactCommon/react/renderer/attributedstring/TextAttributes.h index 82b8547507e574..69400f2ed172f7 100644 --- a/ReactCommon/react/renderer/attributedstring/TextAttributes.h +++ b/ReactCommon/react/renderer/attributedstring/TextAttributes.h @@ -57,6 +57,7 @@ class TextAttributes : public DebugStringConvertible { Float lineHeight{std::numeric_limits::quiet_NaN()}; std::optional alignment{}; std::optional baseWritingDirection{}; + std::optional lineBreakStrategy{}; // Decoration SharedColor textDecorationColor{}; @@ -121,6 +122,7 @@ struct hash { textAttributes.lineHeight, textAttributes.alignment, textAttributes.baseWritingDirection, + textAttributes.lineBreakStrategy, textAttributes.textDecorationColor, textAttributes.textDecorationLineType, textAttributes.textDecorationStyle, diff --git a/ReactCommon/react/renderer/attributedstring/conversions.h b/ReactCommon/react/renderer/attributedstring/conversions.h index 96cbc0c5d7e1de..f854d6f53036ca 100644 --- a/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/ReactCommon/react/renderer/attributedstring/conversions.h @@ -420,6 +420,52 @@ inline std::string toString(const WritingDirection &writingDirection) { return "auto"; } +inline void fromRawValue( + const PropsParserContext &context, + const RawValue &value, + LineBreakStrategy &result) { + react_native_assert(value.hasType()); + if (value.hasType()) { + auto string = (std::string)value; + if (string == "none") { + result = LineBreakStrategy::None; + } else if (string == "push-out") { + result = LineBreakStrategy::PushOut; + } else if (string == "hangul-word") { + result = LineBreakStrategy::HangulWordPriority; + } else if (string == "standard") { + result = LineBreakStrategy::Standard; + } else { + LOG(ERROR) << "Unsupported LineBreakStrategy value: " << string; + react_native_assert(false); + // sane default for prod + result = LineBreakStrategy::None; + } + return; + } + + LOG(ERROR) << "Unsupported LineBreakStrategy type"; + // sane default for prod + result = LineBreakStrategy::None; +} + +inline std::string toString(const LineBreakStrategy &lineBreakStrategy) { + switch (lineBreakStrategy) { + case LineBreakStrategy::None: + return "none"; + case LineBreakStrategy::PushOut: + return "push-out"; + case LineBreakStrategy::HangulWordPriority: + return "hangul-word"; + case LineBreakStrategy::Standard: + return "standard"; + } + + LOG(ERROR) << "Unsupported LineBreakStrategy value"; + // sane default for prod + return "none"; +} + inline void fromRawValue( const PropsParserContext &context, const RawValue &value, @@ -873,6 +919,10 @@ inline folly::dynamic toDynamic(const TextAttributes &textAttributes) { _textAttributes( "baseWritingDirection", toString(*textAttributes.baseWritingDirection)); } + if (textAttributes.lineBreakStrategy.has_value()) { + _textAttributes( + "lineBreakStrategyIOS", toString(*textAttributes.lineBreakStrategy)); + } // Decoration if (textAttributes.textDecorationColor) { _textAttributes( @@ -982,6 +1032,7 @@ constexpr static MapBuffer::Key TA_KEY_TEXT_SHADOW_COLOR = 19; constexpr static MapBuffer::Key TA_KEY_IS_HIGHLIGHTED = 20; constexpr static MapBuffer::Key TA_KEY_LAYOUT_DIRECTION = 21; constexpr static MapBuffer::Key TA_KEY_ACCESSIBILITY_ROLE = 22; +constexpr static MapBuffer::Key TA_KEY_LINE_BREAK_STRATEGY = 23; // constants for ParagraphAttributes serialization constexpr static MapBuffer::Key PA_KEY_MAX_NUMBER_OF_LINES = 0; @@ -1084,6 +1135,11 @@ inline MapBuffer toMapBuffer(const TextAttributes &textAttributes) { TA_KEY_BEST_WRITING_DIRECTION, toString(*textAttributes.baseWritingDirection)); } + if (textAttributes.lineBreakStrategy.has_value()) { + builder.putString( + TA_KEY_LINE_BREAK_STRATEGY, + toString(*textAttributes.lineBreakStrategy)); + } // Decoration if (textAttributes.textDecorationColor) { builder.putInt( diff --git a/ReactCommon/react/renderer/attributedstring/primitives.h b/ReactCommon/react/renderer/attributedstring/primitives.h index 1535210aca2b64..3c6c7bda2654b6 100644 --- a/ReactCommon/react/renderer/attributedstring/primitives.h +++ b/ReactCommon/react/renderer/attributedstring/primitives.h @@ -74,6 +74,15 @@ enum class WritingDirection { RightToLeft // Right to left writing direction. }; +enum class LineBreakStrategy { + None, // Don't use any line break strategies + PushOut, // Use the push out line break strategy. + HangulWordPriority, // When specified, it prohibits breaking between Hangul + // characters. + Standard // Use the same configuration of line break strategies that the + // system uses for standard UI labels. +}; + enum class TextDecorationLineType { None, Underline, diff --git a/ReactCommon/react/renderer/components/text/BaseTextProps.cpp b/ReactCommon/react/renderer/components/text/BaseTextProps.cpp index 0b4b5f88aa938b..6f4741ba69c9ee 100644 --- a/ReactCommon/react/renderer/components/text/BaseTextProps.cpp +++ b/ReactCommon/react/renderer/components/text/BaseTextProps.cpp @@ -105,6 +105,12 @@ static TextAttributes convertRawProp( "baseWritingDirection", sourceTextAttributes.baseWritingDirection, defaultTextAttributes.baseWritingDirection); + textAttributes.lineBreakStrategy = convertRawProp( + context, + rawProps, + "lineBreakStrategyIOS", + sourceTextAttributes.lineBreakStrategy, + defaultTextAttributes.lineBreakStrategy); // Decoration textAttributes.textDecorationColor = convertRawProp( @@ -243,6 +249,12 @@ void BaseTextProps::setProp( textAttributes, baseWritingDirection, "baseWritingDirection"); + REBUILD_FIELD_SWITCH_CASE( + defaults, + value, + textAttributes, + lineBreakStrategy, + "lineBreakStrategyIOS"); REBUILD_FIELD_SWITCH_CASE( defaults, value, diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTAttributedTextUtils.mm b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTAttributedTextUtils.mm index 1e2343cde80193..1194d476cbc34d 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTAttributedTextUtils.mm +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTAttributedTextUtils.mm @@ -156,6 +156,12 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex isParagraphStyleUsed = YES; } + if (textAttributes.lineBreakStrategy.has_value()) { + paragraphStyle.lineBreakStrategy = + RCTNSLineBreakStrategyFromLineBreakStrategy(textAttributes.lineBreakStrategy.value()); + isParagraphStyleUsed = YES; + } + if (!isnan(textAttributes.lineHeight)) { CGFloat lineHeight = textAttributes.lineHeight * RCTEffectiveFontSizeMultiplierFromTextAttributes(textAttributes); paragraphStyle.minimumLineHeight = lineHeight; diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTTextPrimitivesConversions.h b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTTextPrimitivesConversions.h index 235137bde0c13a..7cba9d5d0f3fb3 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTTextPrimitivesConversions.h +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTTextPrimitivesConversions.h @@ -40,6 +40,28 @@ inline static NSWritingDirection RCTNSWritingDirectionFromWritingDirection(Writi } } +inline static NSLineBreakStrategy RCTNSLineBreakStrategyFromLineBreakStrategy(LineBreakStrategy lineBreakStrategy) +{ + switch (lineBreakStrategy) { + case LineBreakStrategy::None: + return NSLineBreakStrategyNone; + case LineBreakStrategy::PushOut: + return NSLineBreakStrategyPushOut; + case LineBreakStrategy::HangulWordPriority: + if (@available(iOS 14.0, *)) { + return NSLineBreakStrategyHangulWordPriority; + } else { + return NSLineBreakStrategyNone; + } + case LineBreakStrategy::Standard: + if (@available(iOS 14.0, *)) { + return NSLineBreakStrategyStandard; + } else { + return NSLineBreakStrategyNone; + } + } +} + inline static RCTFontStyle RCTFontStyleFromFontStyle(FontStyle fontStyle) { switch (fontStyle) { diff --git a/packages/rn-tester/js/examples/Text/TextExample.ios.js b/packages/rn-tester/js/examples/Text/TextExample.ios.js index 9f92af41c71252..b0e6572a07722b 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.ios.js +++ b/packages/rn-tester/js/examples/Text/TextExample.ios.js @@ -1246,4 +1246,41 @@ exports.examples = [ ); }, }, + { + title: 'Line Break Strategy', + render: function (): React.Node { + const lineBreakStrategy = ['none', 'standard', 'hangul-word', 'push-out']; + const textByCode = { + en: 'lineBreakStrategy lineBreakStrategy lineBreakStrategy lineBreakStrategy', + ko: '한글개행 한글개행 한글개행 한글개행 한글개행 한글개행 한글개행 한글개행', + ja: 'かいぎょう かいぎょう かいぎょう かいぎょう かいぎょう かいぎょう', + cn: '改行 改行 改行 改行 改行 改行 改行 改行 改行 改行 改行 改行', + }; + + return ( + + {lineBreakStrategy.map(strategy => { + return ( + + {`Strategy: ${strategy}`} + {Object.keys(textByCode).map(code => { + return ( + + {`[${code}]`} + + {textByCode[code]} + + + ); + })} + + ); + })} + + ); + }, + }, ]; diff --git a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js index f507161cfa3126..7cff25b37adae0 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js @@ -862,4 +862,43 @@ exports.examples = ([ ); }, }, + { + title: 'Line Break Strategy', + render: function (): React.Node { + const lineBreakStrategy = ['none', 'standard', 'hangul-word', 'push-out']; + const textByCode = { + en: 'lineBreakStrategy lineBreakStrategy lineBreakStrategy lineBreakStrategy', + ko: '한글개행한글개행 한글개행한글개행 한글개행한글개행 한글개행한글개행 한글개행한글개행 한글개행한글개행', + ja: 'かいぎょう かいぎょう かいぎょう かいぎょう かいぎょう かいぎょう', + cn: '改行 改行 改行 改行 改行 改行 改行 改行 改行 改行 改行 改行', + }; + return ( + + {lineBreakStrategy.map(strategy => { + return ( + + {`Strategy: ${strategy}`} + {Object.keys(textByCode).map(code => { + return ( + + {`[${code}]`} + + + ); + })} + + ); + })} + + ); + }, + }, ]: Array);