fix(text): support start and end text alignment (#57201)

Summary:
Closes https://github.com/react/react-native/issues/45255.

Adds `textAlign: 'start' | 'end'` support for Text and TextInput across the JS types, Android, iOS, and Fabric text conversion paths.

- Android legacy Text and TextInput now accept logical `start`/`end` alignment values.
- Fabric preserves `start`/`end` as distinct `TextAlignment` values and resolves them against layout direction for iOS paragraph layout.
- Existing `left`/`right` behavior is left unchanged to avoid changing current RTL semantics.

This replaces https://github.com/react/react-native/issues/57007 because the original fork became locked and could not be updated after the upstream conflict.

## Changelog:

[GENERAL] [ADDED] - Add support for `textAlign: 'start'` and `textAlign: 'end'`.

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

Test Plan:
- `yarn build-types`
- `yarn test-typescript`
- `yarn flow-check`
- `./node_modules/.bin/prettier --check packages/react-native/Libraries/Components/TextInput/TextInput.d.ts packages/react-native/Libraries/Components/TextInput/TextInput.flow.js packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js packages/react-native/types/__typetests__/index.tsx packages/react-native/ReactNativeApi.d.ts`
- `./gradlew ktfmtCheck -Preact.internal.useHermesStable=true --no-daemon`
- `./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests com.facebook.react.views.textinput.ReactTextInputPropertyTest.testTextAlign --tests com.facebook.react.views.text.TextAttributePropsTest -Preact.internal.useHermesStable=true --no-daemon`
- `./gradlew ':packages:react-native:ReactAndroid:buildCMakeDebug[arm64-v8a][hermestooling,jsi,etc]' -Preact.internal.useHermesStable=true --no-daemon`
- `git diff --check`

The Android unit test and CMake checks were run from an ASCII-only temporary worktree because Kotlin unit test compilation in my main checkout fails before running these tests when the workspace path contains non-ASCII characters.

Reviewed By: christophpurrer

Differential Revision: D108628602

Pulled By: javache

fbshipit-source-id: 28111d0553451d7de0424a07e956f71bda8787ca
This commit is contained in:
SJLIM
2026-07-01 03:43:49 -07:00
committed by meta-codesync[bot]
parent fd10410d51
commit ca29d38537
27 changed files with 196 additions and 27 deletions
@@ -979,9 +979,9 @@ export interface TextInputProps
style?: StyleProp<TextStyle> | undefined;
/**
* Align the input text to the left, center, or right sides of the input field.
* Align the input text to the left, center, right, start, or end side of the input field.
*/
textAlign?: 'left' | 'center' | 'right' | undefined;
textAlign?: 'left' | 'center' | 'right' | 'start' | 'end' | undefined;
/**
* Used to locate this view in end-to-end tests
@@ -1051,9 +1051,9 @@ type TextInputBaseProps = Readonly<{
value?: ?Stringish,
/**
* Align the input text to the left, center, or right sides of the input field.
* Align the input text to the left, center, right, start, or end side of the input field.
*/
textAlign?: ?('left' | 'center' | 'right'),
textAlign?: ?('left' | 'center' | 'right' | 'start' | 'end'),
}>;
/** @build-types emit-as-interface Uniwind compatibility */
@@ -634,7 +634,15 @@ export interface TextStyle extends TextStyleIOS, TextStyleAndroid, ViewStyle {
| undefined;
letterSpacing?: number | undefined;
lineHeight?: number | undefined;
textAlign?: 'auto' | 'left' | 'right' | 'center' | 'justify' | undefined;
textAlign?:
| 'auto'
| 'left'
| 'right'
| 'center'
| 'justify'
| 'start'
| 'end'
| undefined;
textDecorationLine?:
| 'none'
| 'underline'
@@ -1031,7 +1031,14 @@ type ____TextStyle_InternalBase = Readonly<{
textShadowColor?: ____ColorValue_Internal,
letterSpacing?: number,
lineHeight?: number,
textAlign?: 'auto' | 'left' | 'right' | 'center' | 'justify',
textAlign?:
| 'auto'
| 'left'
| 'right'
| 'center'
| 'justify'
| 'start'
| 'end',
textAlignVertical?: 'auto' | 'top' | 'bottom' | 'center',
includeFontPadding?: boolean,
textDecorationLine?:
@@ -316,6 +316,8 @@ RCT_ENUM_CONVERTER(
NSTextAlignment,
(@{
@"auto" : @(NSTextAlignmentNatural),
@"start" : @(NSTextAlignmentLeft),
@"end" : @(NSTextAlignmentRight),
@"left" : @(NSTextAlignmentLeft),
@"center" : @(NSTextAlignmentCenter),
@"right" : @(NSTextAlignmentRight),
@@ -10,6 +10,9 @@
#import <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h>
#include <react/renderer/attributedstring/conversions.h>
#include <react/renderer/core/RawValue.h>
using namespace facebook::react;
@interface RCTAttributedTextUtilsTest : XCTestCase
@@ -18,6 +21,30 @@ using namespace facebook::react;
@implementation RCTAttributedTextUtilsTest
static NSTextAlignment NSTextAlignmentFromTextAlign(NSString *textAlign, LayoutDirection layoutDirection)
{
ContextContainer contextContainer{};
PropsParserContext parserContext{-1, contextContainer};
TextAlignment textAlignment = TextAlignment::Natural;
fromRawValue(parserContext, RawValue{folly::dynamic{textAlign.UTF8String}}, textAlignment);
TextAttributes textAttributes;
textAttributes.alignment = textAlignment;
textAttributes.layoutDirection = layoutDirection;
NSDictionary<NSAttributedStringKey, id> *attributes = RCTNSTextAttributesFromTextAttributes(textAttributes);
NSParagraphStyle *paragraphStyle = attributes[NSParagraphStyleAttributeName];
return paragraphStyle.alignment;
}
- (void)testTextAlignmentStartAndEndResolveWithLayoutDirection
{
XCTAssertEqual(NSTextAlignmentFromTextAlign(@"start", LayoutDirection::LeftToRight), NSTextAlignmentLeft);
XCTAssertEqual(NSTextAlignmentFromTextAlign(@"start", LayoutDirection::RightToLeft), NSTextAlignmentRight);
XCTAssertEqual(NSTextAlignmentFromTextAlign(@"end", LayoutDirection::LeftToRight), NSTextAlignmentRight);
XCTAssertEqual(NSTextAlignmentFromTextAlign(@"end", LayoutDirection::RightToLeft), NSTextAlignmentLeft);
}
- (void)testSamenessOfEmptyAttributedStrings
{
NSAttributedString *attributedString1 = [[NSAttributedString alloc] initWithString:@""];
@@ -527,6 +527,8 @@ public class TextAttributeProps private constructor() {
"justify" -> Gravity.LEFT
null,
"auto" -> Gravity.NO_GRAVITY
"start" -> if (isRTL) Gravity.RIGHT else Gravity.LEFT
"end" -> if (isRTL) Gravity.LEFT else Gravity.RIGHT
"left" -> if (isRTL) Gravity.RIGHT else Gravity.LEFT
"right" -> if (isRTL) Gravity.LEFT else Gravity.RIGHT
"center" -> Gravity.CENTER_HORIZONTAL
@@ -211,7 +211,7 @@ internal object TextLayoutManager {
if (alignmentAttr == "center") {
alignment = Layout.Alignment.ALIGN_CENTER
} else if (alignmentAttr == "right") {
} else if (alignmentAttr == "right" || alignmentAttr == "end") {
alignment =
if (swapNormalAndOpposite) Layout.Alignment.ALIGN_NORMAL
else Layout.Alignment.ALIGN_OPPOSITE
@@ -558,6 +558,8 @@ public open class ReactTextInputManager public constructor() :
when (textAlign) {
null,
"auto" -> view.gravityHorizontal = Gravity.NO_GRAVITY
"start" -> view.gravityHorizontal = Gravity.START
"end" -> view.gravityHorizontal = Gravity.END
"left" -> view.gravityHorizontal = Gravity.LEFT
"right" -> view.gravityHorizontal = Gravity.RIGHT
"center" -> view.gravityHorizontal = Gravity.CENTER_HORIZONTAL
@@ -0,0 +1,37 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.views.text
import android.view.Gravity
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.uimanager.ReactStylesDiffMap
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class TextAttributePropsTest {
@Test
fun textAlignStartUsesStartSide() {
assertThat(textAlignment("start", isRTL = false)).isEqualTo(Gravity.LEFT)
assertThat(textAlignment("start", isRTL = true)).isEqualTo(Gravity.RIGHT)
}
@Test
fun textAlignEndUsesEndSide() {
assertThat(textAlignment("end", isRTL = false)).isEqualTo(Gravity.RIGHT)
assertThat(textAlignment("end", isRTL = true)).isEqualTo(Gravity.LEFT)
}
private fun textAlignment(textAlign: String, isRTL: Boolean): Int {
return TextAttributeProps.getTextAlignment(
ReactStylesDiffMap(JavaOnlyMap.of("textAlign", textAlign)),
isRTL,
Gravity.CENTER_HORIZONTAL,
)
}
}
@@ -438,6 +438,20 @@ class ReactTextInputPropertyTest {
assertThat(view.gravity and Gravity.HORIZONTAL_GRAVITY_MASK)
.isEqualTo(Gravity.CENTER_HORIZONTAL)
manager.updateProperties(view, buildStyles("textAlign", "start"))
assertThat(
view.gravity and
(Gravity.HORIZONTAL_GRAVITY_MASK or Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)
)
.isEqualTo(Gravity.START)
manager.updateProperties(view, buildStyles("textAlign", "end"))
assertThat(
view.gravity and
(Gravity.HORIZONTAL_GRAVITY_MASK or Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)
)
.isEqualTo(Gravity.END)
manager.updateProperties(view, buildStyles("textAlign", null))
assertThat(view.gravity and Gravity.HORIZONTAL_GRAVITY_MASK).isEqualTo(defaultHorizontalGravity)
@@ -629,8 +629,12 @@ inline void fromRawValue(const PropsParserContext &context, const RawValue &valu
react_native_expect(value.hasType<std::string>());
if (value.hasType<std::string>()) {
auto string = (std::string)value;
if (string == "auto" || string == "start") {
if (string == "auto") {
result = TextAlignment::Natural;
} else if (string == "start") {
result = TextAlignment::Start;
} else if (string == "end") {
result = TextAlignment::End;
} else if (string == "left") {
result = TextAlignment::Left;
} else if (string == "center") {
@@ -665,6 +669,10 @@ inline std::string toString(const TextAlignment &textAlignment)
return "right";
case TextAlignment::Justified:
return "justified";
case TextAlignment::Start:
return "start";
case TextAlignment::End:
return "end";
}
LOG(ERROR) << "Unsupported TextAlignment value";
@@ -97,7 +97,9 @@ enum class TextAlignment {
Left, // Visually left aligned.
Center, // Visually centered.
Right, // Visually right aligned.
Justified // Fully-justified. The last line in a paragraph is natural-aligned.
Justified, // Fully-justified. The last line in a paragraph is natural-aligned.
Start, // Aligned to the start side of the paragraph direction.
End // Aligned to the end side of the paragraph direction.
};
enum class TextAlignmentVertical {
@@ -9,6 +9,8 @@
#include <react/renderer/element/ComponentBuilder.h>
#include <gtest/gtest.h>
#include <react/renderer/attributedstring/conversions.h>
#include <react/renderer/core/RawValue.h>
#include <react/renderer/element/Element.h>
#include <react/renderer/element/testUtils.h>
@@ -22,8 +24,21 @@ Element<RawTextShadowNode> rawTextElement(const char* text) {
return Element<RawTextShadowNode>().props(rawTextProps);
}
std::string roundTripTextAlignment(const char* textAlignment) {
ContextContainer contextContainer{};
PropsParserContext parserContext{-1, contextContainer};
TextAlignment result = TextAlignment::Natural;
fromRawValue(parserContext, RawValue{folly::dynamic{textAlignment}}, result);
return toString(result);
}
} // namespace
TEST(BaseTextShadowNodeTest, textAlignmentStartAndEndRoundTrip) {
EXPECT_EQ(roundTripTextAlignment("start"), "start");
EXPECT_EQ(roundTripTextAlignment("end"), "end");
}
TEST(BaseTextShadowNodeTest, fragmentsWithDifferentAttributes) {
ContextContainer contextContainer{};
PropsParserContext parserContext{-1, contextContainer};
@@ -18,6 +18,23 @@
using namespace facebook::react;
inline static TextAlignment RCTResolveTextAlignment(TextAlignment textAlignment, LayoutDirection layoutDirection)
{
const bool isRTL = layoutDirection == LayoutDirection::RightToLeft;
switch (textAlignment) {
case TextAlignment::Start:
return isRTL ? TextAlignment::Right : TextAlignment::Left;
case TextAlignment::End:
return isRTL ? TextAlignment::Left : TextAlignment::Right;
case TextAlignment::Right:
return isRTL ? TextAlignment::Left : TextAlignment::Right;
case TextAlignment::Left:
return isRTL ? TextAlignment::Right : TextAlignment::Left;
default:
return textAlignment;
}
}
inline static UIFontWeight RCTUIFontWeightFromInteger(NSInteger fontWeight)
{
assert(fontWeight > 50);
@@ -196,14 +213,9 @@ NSMutableDictionary<NSAttributedStringKey, id> *RCTNSTextAttributesFromTextAttri
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
BOOL isParagraphStyleUsed = NO;
if (textAttributes.alignment.has_value()) {
TextAlignment textAlignment = textAttributes.alignment.value_or(TextAlignment::Natural);
if (textAttributes.layoutDirection.value_or(LayoutDirection::LeftToRight) == LayoutDirection::RightToLeft) {
if (textAlignment == TextAlignment::Right) {
textAlignment = TextAlignment::Left;
} else if (textAlignment == TextAlignment::Left) {
textAlignment = TextAlignment::Right;
}
}
TextAlignment textAlignment = RCTResolveTextAlignment(
textAttributes.alignment.value_or(TextAlignment::Natural),
textAttributes.layoutDirection.value_or(LayoutDirection::LeftToRight));
paragraphStyle.alignment = RCTNSTextAlignmentFromTextAlignment(textAlignment);
isParagraphStyleUsed = YES;
@@ -24,6 +24,10 @@ inline static NSTextAlignment RCTNSTextAlignmentFromTextAlignment(facebook::reac
return NSTextAlignmentCenter;
case facebook::react::TextAlignment::Justified:
return NSTextAlignmentJustified;
case facebook::react::TextAlignment::Start:
return NSTextAlignmentNatural;
case facebook::react::TextAlignment::End:
return NSTextAlignmentRight;
}
}
+17 -10
View File
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<20bbe8284af4b8863a39651a46b7a53a>>
* @generated SignedSource<<10889ead2857238c3286b036b73f9945>>
*
* This file was generated by scripts/js-api/build-types/index.js.
*/
@@ -805,7 +805,14 @@ declare type ____TextStyle_InternalBase = {
readonly includeFontPadding?: boolean
readonly letterSpacing?: number
readonly lineHeight?: number
readonly textAlign?: "auto" | "center" | "justify" | "left" | "right"
readonly textAlign?:
| "auto"
| "center"
| "end"
| "justify"
| "left"
| "right"
| "start"
readonly textAlignVertical?: "auto" | "bottom" | "center" | "top"
readonly textDecorationColor?: ____ColorValue_Internal
readonly textDecorationLine?:
@@ -5183,7 +5190,7 @@ declare type TextInputBaseProps = {
readonly selectTextOnFocus?: boolean
readonly style?: TextStyleProp
readonly submitBehavior?: SubmitBehavior
readonly textAlign?: "center" | "left" | "right"
readonly textAlign?: "center" | "end" | "left" | "right" | "start"
readonly value?: string
}
declare type TextInputBlurEvent = BlurEvent
@@ -5919,7 +5926,7 @@ export {
AlertOptions, // a0cdac0f
AlertType, // 5ab91217
AndroidKeyboardEvent, // e03becc8
Animated, // 1ac00401
Animated, // 8d5b3c35
AppConfig, // 35c0ca70
AppRegistry, // 7ef8e53a
AppState, // 12012be5
@@ -6139,7 +6146,7 @@ export {
StatusBarProps, // b72a9127
StatusBarStyle, // 78f53eea
StyleProp, // fa0e9b4a
StyleSheet, // ebb07d46
StyleSheet, // cdddb9ae
SubmitBehavior, // c4ddf490
Switch, // b004beeb
SwitchChangeEvent, // 899635b1
@@ -6149,9 +6156,9 @@ export {
TVViewPropsIOS, // 330ce7b5
TargetedEvent, // 16e98910
TaskProvider, // 266dedf2
Text, // 90eee1c6
Text, // f792e51d
TextContentType, // 239b3ecc
TextInput, // d6b1ef70
TextInput, // 1c32d882
TextInputAndroidProps, // 3f09ce49
TextInputBlurEvent, // b77af40e
TextInputChangeEvent, // f55eef98
@@ -6161,13 +6168,13 @@ export {
TextInputIOSProps, // 0d05a855
TextInputInstance, // 5a0c0e0d
TextInputKeyPressEvent, // 546c5d07
TextInputProps, // d989bb55
TextInputProps, // 08c36ff7
TextInputSelectionChangeEvent, // e58f2abc
TextInputSubmitEditingEvent, // 6bcb2aa5
TextInstance, // 05463a96
TextLayoutEvent, // 3f54186f
TextProps, // 490e7cc7
TextStyle, // ab0f32ca
TextProps, // 58466ea1
TextStyle, // b62b8399
ToastAndroid, // 88a8969a
Touchable, // c15da0a2
TouchableHighlight, // 20ba0199
@@ -232,6 +232,10 @@ const viewStyle: StyleProp<ViewStyle> = {
};
const textStyle: StyleProp<TextStyle> = {
fontSize: 20,
textAlign: 'start',
};
const endAlignedTextStyle: StyleProp<TextStyle> = {
textAlign: 'end',
};
const imageStyle: StyleProp<ImageStyle> = {
resizeMode: 'contain',
@@ -6517,10 +6517,12 @@ enum facebook::react::SubmitBehavior {
enum facebook::react::TextAlignment {
Center,
End,
Justified,
Left,
Natural,
Right,
Start,
}
enum facebook::react::TextAlignmentVertical {
@@ -6331,10 +6331,12 @@ enum facebook::react::SubmitBehavior {
enum facebook::react::TextAlignment {
Center,
End,
Justified,
Left,
Natural,
Right,
Start,
}
enum facebook::react::TextAlignmentVertical {
@@ -6508,10 +6508,12 @@ enum facebook::react::SubmitBehavior {
enum facebook::react::TextAlignment {
Center,
End,
Justified,
Left,
Natural,
Right,
Start,
}
enum facebook::react::TextAlignmentVertical {
@@ -8700,10 +8700,12 @@ enum facebook::react::SubmitBehavior {
enum facebook::react::TextAlignment {
Center,
End,
Justified,
Left,
Natural,
Right,
Start,
}
enum facebook::react::TextAlignmentVertical {
@@ -8542,10 +8542,12 @@ enum facebook::react::SubmitBehavior {
enum facebook::react::TextAlignment {
Center,
End,
Justified,
Left,
Natural,
Right,
Start,
}
enum facebook::react::TextAlignmentVertical {
@@ -8691,10 +8691,12 @@ enum facebook::react::SubmitBehavior {
enum facebook::react::TextAlignment {
Center,
End,
Justified,
Left,
Natural,
Right,
Start,
}
enum facebook::react::TextAlignmentVertical {
@@ -4855,10 +4855,12 @@ enum facebook::react::SubmitBehavior {
enum facebook::react::TextAlignment {
Center,
End,
Justified,
Left,
Natural,
Right,
Start,
}
enum facebook::react::TextAlignmentVertical {
@@ -4709,10 +4709,12 @@ enum facebook::react::SubmitBehavior {
enum facebook::react::TextAlignment {
Center,
End,
Justified,
Left,
Natural,
Right,
Start,
}
enum facebook::react::TextAlignmentVertical {
@@ -4846,10 +4846,12 @@ enum facebook::react::SubmitBehavior {
enum facebook::react::TextAlignment {
Center,
End,
Justified,
Left,
Natural,
Right,
Start,
}
enum facebook::react::TextAlignmentVertical {