feat(iOS): added lineBreakStrategy attribute to Text/TextInput (#31272)

Summary:
iOS did not support the implementation of Korean word-wrap(line-break) before iOS14.
If the attribute applied, the word-wrap of Korean will works.

## Changelog

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->

[iOS] [Added] -  Line break strategy for Text and TextInput components

Pull Request resolved: https://github.com/facebook/react-native/pull/31272

Test Plan:
1. Test build and run on above iOS 14.
2. Test it does not affect existing text components when set default(none) strategy.
3. Test whether word-wrap works with Korean when set hangul-word strategy.

<img src="https://user-images.githubusercontent.com/26326015/112963967-d7f70c00-9182-11eb-9a34-8c758b80c219.png" width="300" height="" style="max-width:100%;">

Reviewed By: javache

Differential Revision: D39824809

Pulled By: lunaleaps

fbshipit-source-id: 42cb0385221a38c84e80d3494d1bfc1934ecf32b
This commit is contained in:
bang9
2022-10-17 13:14:17 -07:00
committed by Facebook GitHub Bot
parent a0ee6fae5e
commit 048194849b
19 changed files with 242 additions and 1 deletions
@@ -149,6 +149,7 @@ const RCTTextInputViewConfig = {
clearTextOnFocus: true,
showSoftInputOnFocus: true,
autoFocus: true,
lineBreakStrategyIOS: true,
...ConditionallyIgnoredEventHandlers({
onChange: true,
onSelectionChange: true,
@@ -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<{|
@@ -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<{|
@@ -42,6 +42,7 @@ RCT_REMAP_SHADOW_PROPERTY(letterSpacing, textAttributes.letterSpacing, CGFloat)
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)
+1
View File
@@ -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;
+10 -1
View File
@@ -27,6 +27,7 @@ NSString *const RCTTextAttributesTagAttributeName = @"RCTTextAttributesTagAttrib
_maxFontSizeMultiplier = NAN;
_alignment = NSTextAlignmentNatural;
_baseWritingDirection = NSWritingDirectionNatural;
_lineBreakStrategy = NSLineBreakStrategyNone;
_textShadowRadius = NAN;
_opacity = NAN;
_textTransform = RCTTextTransformUndefined;
@@ -66,6 +67,7 @@ NSString *const RCTTextAttributesTagAttributeName = @"RCTTextAttributesTagAttrib
_baseWritingDirection = textAttributes->_baseWritingDirection != NSWritingDirectionNatural
? textAttributes->_baseWritingDirection
: _baseWritingDirection; // *
_lineBreakStrategy = textAttributes->_lineBreakStrategy ?: _lineBreakStrategy;
// Decoration
_textDecorationColor = textAttributes->_textDecorationColor ?: _textDecorationColor;
@@ -117,6 +119,13 @@ NSString *const RCTTextAttributesTagAttributeName = @"RCTTextAttributesTagAttrib
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 @@ static NSString *capitalizeText(NSString *text)
RCTTextAttributesCompareFloats(_letterSpacing) &&
// Paragraph Styles
RCTTextAttributesCompareFloats(_lineHeight) && RCTTextAttributesCompareFloats(_alignment) &&
RCTTextAttributesCompareOthers(_baseWritingDirection) &&
RCTTextAttributesCompareOthers(_baseWritingDirection) && RCTTextAttributesCompareOthers(_lineBreakStrategy) &&
// Decoration
RCTTextAttributesCompareObjects(_textDecorationColor) && RCTTextAttributesCompareOthers(_textDecorationStyle) &&
RCTTextAttributesCompareOthers(_textDecorationLine) &&
+1
View File
@@ -45,6 +45,7 @@ const textViewConfig = {
onInlineViewLayout: true,
dataDetectorType: true,
android_hyphenationFrequency: true,
lineBreakStrategyIOS: true,
},
directEventTypes: {
topTextLayout: {
+7
View File
@@ -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'),
|}>;
+1
View File
@@ -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;
+19
View File
@@ -375,6 +375,25 @@ RCT_ENUM_CONVERTER(
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,
(@{
@@ -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),
@@ -57,6 +57,7 @@ class TextAttributes : public DebugStringConvertible {
Float lineHeight{std::numeric_limits<Float>::quiet_NaN()};
std::optional<TextAlignment> alignment{};
std::optional<WritingDirection> baseWritingDirection{};
std::optional<LineBreakStrategy> lineBreakStrategy{};
// Decoration
SharedColor textDecorationColor{};
@@ -121,6 +122,7 @@ struct hash<facebook::react::TextAttributes> {
textAttributes.lineHeight,
textAttributes.alignment,
textAttributes.baseWritingDirection,
textAttributes.lineBreakStrategy,
textAttributes.textDecorationColor,
textAttributes.textDecorationLineType,
textAttributes.textDecorationStyle,
@@ -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<std::string>());
if (value.hasType<std::string>()) {
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(
@@ -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,
@@ -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,
@@ -156,6 +156,12 @@ NSDictionary<NSAttributedStringKey, id> *RCTNSTextAttributesFromTextAttributes(T
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;
@@ -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) {
@@ -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 (
<View>
{lineBreakStrategy.map(strategy => {
return (
<View key={strategy} style={{marginBottom: 12}}>
<Text
style={{
backgroundColor: 'lightgrey',
}}>{`Strategy: ${strategy}`}</Text>
{Object.keys(textByCode).map(code => {
return (
<View key={code}>
<Text style={{fontWeight: 'bold'}}>{`[${code}]`}</Text>
<Text lineBreakStrategyIOS={strategy}>
{textByCode[code]}
</Text>
</View>
);
})}
</View>
);
})}
</View>
);
},
},
];
@@ -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 (
<View>
{lineBreakStrategy.map(strategy => {
return (
<View key={strategy} style={{marginBottom: 12}}>
<Text
style={{
backgroundColor: 'lightgrey',
}}>{`Strategy: ${strategy}`}</Text>
{Object.keys(textByCode).map(code => {
return (
<View key={code}>
<Text style={{fontWeight: 'bold'}}>{`[${code}]`}</Text>
<TextInput
multiline
lineBreakStrategyIOS={strategy}
style={styles.default}
defaultValue={textByCode[code]}
/>
</View>
);
})}
</View>
);
})}
</View>
);
},
},
]: Array<RNTesterModuleExample>);