Apply baseline offset separately for each line of text on iOS (#51344)

Summary:
This PR removes extraneous blank space at the bottom of multiline `TextInput` when using nested `Text` with different font sizes on iOS with the New Architecture enabled.

| Before | After |
|:-:|:-:|
| <img width="283" alt="Screenshot 2025-05-15 at 12 16 05" src="https://github.com/user-attachments/assets/47256267-86ff-45f9-9e60-162d444a4b9d" /> | <img width="286" alt="Screenshot 2025-05-15 at 12 01 32" src="https://github.com/user-attachments/assets/43ce4b0b-6410-4ca5-be37-59c6374ea15c" /> |

The proposed solution is to call `RCTApplyBaselineOffset` separately for each line of text.

Ideally, we would call it separately for each part of text with different font size.

## Changelog:

[IOS] [FIXED] - Fixed blank space at the bottom of multiline TextInput on iOS

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

Test Plan:
```tsx
<TextInput
  multiline
  style={{borderWidth: 1, width: 300, fontSize: 20}}
  ref={ref}
  placeholder="Type here...">
  First line{'\n'}
  <Text style={{fontSize: 30, lineHeight: 60}}>Second line</Text>
  {'\n'}Third line{'\n'}Fourth line
  {'\n'}
  Fifth line
</TextInput>
```

Reviewed By: NickGerleman

Differential Revision: D74802648

Pulled By: j-piasecki

fbshipit-source-id: 30f02ae8af66264c1776d241ed8542899e9cdf99
This commit is contained in:
Tomek Zawadzki
2025-05-22 23:02:34 -07:00
committed by Facebook GitHub Bot
parent 139f3d5ef9
commit 2da4a6059a
21 changed files with 191 additions and 57 deletions
@@ -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<<43590bc324e803ccf299e2a6ae6c5304>>
* @generated SignedSource<<62715a19c87a010716eb7d2e904258fa>>
*/
/**
@@ -138,6 +138,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun enableFontScaleChangesUpdatingLayout(): Boolean = accessor.enableFontScaleChangesUpdatingLayout()
/**
* Applies base offset for each line of text separately on iOS.
*/
@JvmStatic
public fun enableIOSTextBaselineOffsetPerLine(): Boolean = accessor.enableIOSTextBaselineOffsetPerLine()
/**
* iOS Views will clip to their padding box vs border box
*/
@@ -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<<5609be38b50c62ff5097b3571940f400>>
* @generated SignedSource<<53e2f9241b9b3396b1ddf5893c7f3c4e>>
*/
/**
@@ -38,6 +38,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var enableFabricRendererCache: Boolean? = null
private var enableFixForParentTagDuringReparentingCache: Boolean? = null
private var enableFontScaleChangesUpdatingLayoutCache: Boolean? = null
private var enableIOSTextBaselineOffsetPerLineCache: Boolean? = null
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
private var enableIntersectionObserverEventLoopIntegrationCache: Boolean? = null
private var enableLayoutAnimationsOnAndroidCache: Boolean? = null
@@ -232,6 +233,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}
override fun enableIOSTextBaselineOffsetPerLine(): Boolean {
var cached = enableIOSTextBaselineOffsetPerLineCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.enableIOSTextBaselineOffsetPerLine()
enableIOSTextBaselineOffsetPerLineCache = cached
}
return cached
}
override fun enableIOSViewClipToPaddingBox(): Boolean {
var cached = enableIOSViewClipToPaddingBoxCache
if (cached == null) {
@@ -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<<15645e36bbca81dfcec07fc82f8f6264>>
* @generated SignedSource<<918dcdb416bb3e2e3f83cded78e06363>>
*/
/**
@@ -64,6 +64,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
@DoNotStrip @JvmStatic public external fun enableFontScaleChangesUpdatingLayout(): Boolean
@DoNotStrip @JvmStatic public external fun enableIOSTextBaselineOffsetPerLine(): Boolean
@DoNotStrip @JvmStatic public external fun enableIOSViewClipToPaddingBox(): Boolean
@DoNotStrip @JvmStatic public external fun enableIntersectionObserverEventLoopIntegration(): Boolean
@@ -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<<9c7d04c48c4c5feb0d2fb250966550c8>>
* @generated SignedSource<<7151e167c4e45a732a341a2cfcf67b58>>
*/
/**
@@ -59,6 +59,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
override fun enableFontScaleChangesUpdatingLayout(): Boolean = false
override fun enableIOSTextBaselineOffsetPerLine(): Boolean = false
override fun enableIOSViewClipToPaddingBox(): Boolean = false
override fun enableIntersectionObserverEventLoopIntegration(): Boolean = true
@@ -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<<efc16fdb113176b932c0005118e7b48c>>
* @generated SignedSource<<d2f4ae1cd4e33cb59903c3b2c3ef0fc2>>
*/
/**
@@ -42,6 +42,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var enableFabricRendererCache: Boolean? = null
private var enableFixForParentTagDuringReparentingCache: Boolean? = null
private var enableFontScaleChangesUpdatingLayoutCache: Boolean? = null
private var enableIOSTextBaselineOffsetPerLineCache: Boolean? = null
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
private var enableIntersectionObserverEventLoopIntegrationCache: Boolean? = null
private var enableLayoutAnimationsOnAndroidCache: Boolean? = null
@@ -254,6 +255,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
return cached
}
override fun enableIOSTextBaselineOffsetPerLine(): Boolean {
var cached = enableIOSTextBaselineOffsetPerLineCache
if (cached == null) {
cached = currentProvider.enableIOSTextBaselineOffsetPerLine()
accessedFeatureFlags.add("enableIOSTextBaselineOffsetPerLine")
enableIOSTextBaselineOffsetPerLineCache = cached
}
return cached
}
override fun enableIOSViewClipToPaddingBox(): Boolean {
var cached = enableIOSViewClipToPaddingBoxCache
if (cached == null) {
@@ -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<<3a61d9b66a7ed6fc9c76548aa72e3ed3>>
* @generated SignedSource<<722ed5b36a043d4eadb095f092c95707>>
*/
/**
@@ -59,6 +59,8 @@ public interface ReactNativeFeatureFlagsProvider {
@DoNotStrip public fun enableFontScaleChangesUpdatingLayout(): Boolean
@DoNotStrip public fun enableIOSTextBaselineOffsetPerLine(): Boolean
@DoNotStrip public fun enableIOSViewClipToPaddingBox(): Boolean
@DoNotStrip public fun enableIntersectionObserverEventLoopIntegration(): Boolean
@@ -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<<93037428475b39d3f2c208e1326b3e86>>
* @generated SignedSource<<422a5922cfe14a1c5f7c0728410dbe8a>>
*/
/**
@@ -147,6 +147,12 @@ class ReactNativeFeatureFlagsJavaProvider
return method(javaProvider_);
}
bool enableIOSTextBaselineOffsetPerLine() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableIOSTextBaselineOffsetPerLine");
return method(javaProvider_);
}
bool enableIOSViewClipToPaddingBox() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableIOSViewClipToPaddingBox");
@@ -427,6 +433,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableFontScaleChangesUpdatingLayout(
return ReactNativeFeatureFlags::enableFontScaleChangesUpdatingLayout();
}
bool JReactNativeFeatureFlagsCxxInterop::enableIOSTextBaselineOffsetPerLine(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableIOSTextBaselineOffsetPerLine();
}
bool JReactNativeFeatureFlagsCxxInterop::enableIOSViewClipToPaddingBox(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
@@ -667,6 +678,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
makeNativeMethod(
"enableFontScaleChangesUpdatingLayout",
JReactNativeFeatureFlagsCxxInterop::enableFontScaleChangesUpdatingLayout),
makeNativeMethod(
"enableIOSTextBaselineOffsetPerLine",
JReactNativeFeatureFlagsCxxInterop::enableIOSTextBaselineOffsetPerLine),
makeNativeMethod(
"enableIOSViewClipToPaddingBox",
JReactNativeFeatureFlagsCxxInterop::enableIOSViewClipToPaddingBox),
@@ -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<<f5365c0a71e4c7638cec79bebfab55c3>>
* @generated SignedSource<<18d7c9c2577450a0bb7e6da91b8597ed>>
*/
/**
@@ -84,6 +84,9 @@ class JReactNativeFeatureFlagsCxxInterop
static bool enableFontScaleChangesUpdatingLayout(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
static bool enableIOSTextBaselineOffsetPerLine(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
static bool enableIOSViewClipToPaddingBox(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
@@ -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<<e4946f3e524cd7571fc62c721d611d19>>
* @generated SignedSource<<9f34463a7cddcfd1a76631041297c4b7>>
*/
/**
@@ -98,6 +98,10 @@ bool ReactNativeFeatureFlags::enableFontScaleChangesUpdatingLayout() {
return getAccessor().enableFontScaleChangesUpdatingLayout();
}
bool ReactNativeFeatureFlags::enableIOSTextBaselineOffsetPerLine() {
return getAccessor().enableIOSTextBaselineOffsetPerLine();
}
bool ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox() {
return getAccessor().enableIOSViewClipToPaddingBox();
}
@@ -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<<83ef02f26eaf2d847efd7042ae6d1dfc>>
* @generated SignedSource<<cb84333e310cfc1a6a77f1b376723e93>>
*/
/**
@@ -129,6 +129,11 @@ class ReactNativeFeatureFlags {
*/
RN_EXPORT static bool enableFontScaleChangesUpdatingLayout();
/**
* Applies base offset for each line of text separately on iOS.
*/
RN_EXPORT static bool enableIOSTextBaselineOffsetPerLine();
/**
* iOS Views will clip to their padding box vs border box
*/
@@ -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<<0d1e4f0581e3aa90e18dc70ccee8a50a>>
* @generated SignedSource<<ec175b9b320e8bf9b0811ba404cd6e5d>>
*/
/**
@@ -353,6 +353,24 @@ bool ReactNativeFeatureFlagsAccessor::enableFontScaleChangesUpdatingLayout() {
return flagValue.value();
}
bool ReactNativeFeatureFlagsAccessor::enableIOSTextBaselineOffsetPerLine() {
auto flagValue = enableIOSTextBaselineOffsetPerLine_.load();
if (!flagValue.has_value()) {
// This block is not exclusive but it is not necessary.
// If multiple threads try to initialize the feature flag, we would only
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(18, "enableIOSTextBaselineOffsetPerLine");
flagValue = currentProvider_->enableIOSTextBaselineOffsetPerLine();
enableIOSTextBaselineOffsetPerLine_ = flagValue;
}
return flagValue.value();
}
bool ReactNativeFeatureFlagsAccessor::enableIOSViewClipToPaddingBox() {
auto flagValue = enableIOSViewClipToPaddingBox_.load();
@@ -362,7 +380,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSViewClipToPaddingBox() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(18, "enableIOSViewClipToPaddingBox");
markFlagAsAccessed(19, "enableIOSViewClipToPaddingBox");
flagValue = currentProvider_->enableIOSViewClipToPaddingBox();
enableIOSViewClipToPaddingBox_ = flagValue;
@@ -380,7 +398,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIntersectionObserverEventLoopIntegra
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(19, "enableIntersectionObserverEventLoopIntegration");
markFlagAsAccessed(20, "enableIntersectionObserverEventLoopIntegration");
flagValue = currentProvider_->enableIntersectionObserverEventLoopIntegration();
enableIntersectionObserverEventLoopIntegration_ = flagValue;
@@ -398,7 +416,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnAndroid() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(20, "enableLayoutAnimationsOnAndroid");
markFlagAsAccessed(21, "enableLayoutAnimationsOnAndroid");
flagValue = currentProvider_->enableLayoutAnimationsOnAndroid();
enableLayoutAnimationsOnAndroid_ = flagValue;
@@ -416,7 +434,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnIOS() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(21, "enableLayoutAnimationsOnIOS");
markFlagAsAccessed(22, "enableLayoutAnimationsOnIOS");
flagValue = currentProvider_->enableLayoutAnimationsOnIOS();
enableLayoutAnimationsOnIOS_ = flagValue;
@@ -434,7 +452,7 @@ bool ReactNativeFeatureFlagsAccessor::enableMainQueueModulesOnIOS() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(22, "enableMainQueueModulesOnIOS");
markFlagAsAccessed(23, "enableMainQueueModulesOnIOS");
flagValue = currentProvider_->enableMainQueueModulesOnIOS();
enableMainQueueModulesOnIOS_ = flagValue;
@@ -452,7 +470,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNativeCSSParsing() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(23, "enableNativeCSSParsing");
markFlagAsAccessed(24, "enableNativeCSSParsing");
flagValue = currentProvider_->enableNativeCSSParsing();
enableNativeCSSParsing_ = flagValue;
@@ -470,7 +488,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNetworkEventReporting() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(24, "enableNetworkEventReporting");
markFlagAsAccessed(25, "enableNetworkEventReporting");
flagValue = currentProvider_->enableNetworkEventReporting();
enableNetworkEventReporting_ = flagValue;
@@ -488,7 +506,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNewBackgroundAndBorderDrawables() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(25, "enableNewBackgroundAndBorderDrawables");
markFlagAsAccessed(26, "enableNewBackgroundAndBorderDrawables");
flagValue = currentProvider_->enableNewBackgroundAndBorderDrawables();
enableNewBackgroundAndBorderDrawables_ = flagValue;
@@ -506,7 +524,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePreparedTextLayout() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(26, "enablePreparedTextLayout");
markFlagAsAccessed(27, "enablePreparedTextLayout");
flagValue = currentProvider_->enablePreparedTextLayout();
enablePreparedTextLayout_ = flagValue;
@@ -524,7 +542,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePropsUpdateReconciliationAndroid() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(27, "enablePropsUpdateReconciliationAndroid");
markFlagAsAccessed(28, "enablePropsUpdateReconciliationAndroid");
flagValue = currentProvider_->enablePropsUpdateReconciliationAndroid();
enablePropsUpdateReconciliationAndroid_ = flagValue;
@@ -542,7 +560,7 @@ bool ReactNativeFeatureFlagsAccessor::enableResourceTimingAPI() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(28, "enableResourceTimingAPI");
markFlagAsAccessed(29, "enableResourceTimingAPI");
flagValue = currentProvider_->enableResourceTimingAPI();
enableResourceTimingAPI_ = flagValue;
@@ -560,7 +578,7 @@ bool ReactNativeFeatureFlagsAccessor::enableSynchronousStateUpdates() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(29, "enableSynchronousStateUpdates");
markFlagAsAccessed(30, "enableSynchronousStateUpdates");
flagValue = currentProvider_->enableSynchronousStateUpdates();
enableSynchronousStateUpdates_ = flagValue;
@@ -578,7 +596,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewCulling() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(30, "enableViewCulling");
markFlagAsAccessed(31, "enableViewCulling");
flagValue = currentProvider_->enableViewCulling();
enableViewCulling_ = flagValue;
@@ -596,7 +614,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecycling() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(31, "enableViewRecycling");
markFlagAsAccessed(32, "enableViewRecycling");
flagValue = currentProvider_->enableViewRecycling();
enableViewRecycling_ = flagValue;
@@ -614,7 +632,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForText() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(32, "enableViewRecyclingForText");
markFlagAsAccessed(33, "enableViewRecyclingForText");
flagValue = currentProvider_->enableViewRecyclingForText();
enableViewRecyclingForText_ = flagValue;
@@ -632,7 +650,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForView() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(33, "enableViewRecyclingForView");
markFlagAsAccessed(34, "enableViewRecyclingForView");
flagValue = currentProvider_->enableViewRecyclingForView();
enableViewRecyclingForView_ = flagValue;
@@ -650,7 +668,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(34, "fixMappingOfEventPrioritiesBetweenFabricAndReact");
markFlagAsAccessed(35, "fixMappingOfEventPrioritiesBetweenFabricAndReact");
flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact();
fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue;
@@ -668,7 +686,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(35, "fuseboxEnabledRelease");
markFlagAsAccessed(36, "fuseboxEnabledRelease");
flagValue = currentProvider_->fuseboxEnabledRelease();
fuseboxEnabledRelease_ = flagValue;
@@ -686,7 +704,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxNetworkInspectionEnabled() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(36, "fuseboxNetworkInspectionEnabled");
markFlagAsAccessed(37, "fuseboxNetworkInspectionEnabled");
flagValue = currentProvider_->fuseboxNetworkInspectionEnabled();
fuseboxNetworkInspectionEnabled_ = flagValue;
@@ -704,7 +722,7 @@ bool ReactNativeFeatureFlagsAccessor::incorporateMaxLinesDuringAndroidLayout() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(37, "incorporateMaxLinesDuringAndroidLayout");
markFlagAsAccessed(38, "incorporateMaxLinesDuringAndroidLayout");
flagValue = currentProvider_->incorporateMaxLinesDuringAndroidLayout();
incorporateMaxLinesDuringAndroidLayout_ = flagValue;
@@ -722,7 +740,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(38, "traceTurboModulePromiseRejectionsOnAndroid");
markFlagAsAccessed(39, "traceTurboModulePromiseRejectionsOnAndroid");
flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid();
traceTurboModulePromiseRejectionsOnAndroid_ = flagValue;
@@ -740,7 +758,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit(
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(39, "updateRuntimeShadowNodeReferencesOnCommit");
markFlagAsAccessed(40, "updateRuntimeShadowNodeReferencesOnCommit");
flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit();
updateRuntimeShadowNodeReferencesOnCommit_ = flagValue;
@@ -758,7 +776,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(40, "useAlwaysAvailableJSErrorHandling");
markFlagAsAccessed(41, "useAlwaysAvailableJSErrorHandling");
flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling();
useAlwaysAvailableJSErrorHandling_ = flagValue;
@@ -776,7 +794,7 @@ bool ReactNativeFeatureFlagsAccessor::useAndroidTextLayoutWidthDirectly() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(41, "useAndroidTextLayoutWidthDirectly");
markFlagAsAccessed(42, "useAndroidTextLayoutWidthDirectly");
flagValue = currentProvider_->useAndroidTextLayoutWidthDirectly();
useAndroidTextLayoutWidthDirectly_ = flagValue;
@@ -794,7 +812,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(42, "useFabricInterop");
markFlagAsAccessed(43, "useFabricInterop");
flagValue = currentProvider_->useFabricInterop();
useFabricInterop_ = flagValue;
@@ -812,7 +830,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(43, "useNativeViewConfigsInBridgelessMode");
markFlagAsAccessed(44, "useNativeViewConfigsInBridgelessMode");
flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode();
useNativeViewConfigsInBridgelessMode_ = flagValue;
@@ -830,7 +848,7 @@ bool ReactNativeFeatureFlagsAccessor::useOptimizedEventBatchingOnAndroid() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(44, "useOptimizedEventBatchingOnAndroid");
markFlagAsAccessed(45, "useOptimizedEventBatchingOnAndroid");
flagValue = currentProvider_->useOptimizedEventBatchingOnAndroid();
useOptimizedEventBatchingOnAndroid_ = flagValue;
@@ -848,7 +866,7 @@ bool ReactNativeFeatureFlagsAccessor::useRawPropsJsiValue() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(45, "useRawPropsJsiValue");
markFlagAsAccessed(46, "useRawPropsJsiValue");
flagValue = currentProvider_->useRawPropsJsiValue();
useRawPropsJsiValue_ = flagValue;
@@ -866,7 +884,7 @@ bool ReactNativeFeatureFlagsAccessor::useShadowNodeStateOnClone() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(46, "useShadowNodeStateOnClone");
markFlagAsAccessed(47, "useShadowNodeStateOnClone");
flagValue = currentProvider_->useShadowNodeStateOnClone();
useShadowNodeStateOnClone_ = flagValue;
@@ -884,7 +902,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(47, "useTurboModuleInterop");
markFlagAsAccessed(48, "useTurboModuleInterop");
flagValue = currentProvider_->useTurboModuleInterop();
useTurboModuleInterop_ = flagValue;
@@ -902,7 +920,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(48, "useTurboModules");
markFlagAsAccessed(49, "useTurboModules");
flagValue = currentProvider_->useTurboModules();
useTurboModules_ = flagValue;
@@ -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<<867dd37ee489203791b933b87562fc67>>
* @generated SignedSource<<e1c1ba4c68d532a7296fa4f91a48de3a>>
*/
/**
@@ -50,6 +50,7 @@ class ReactNativeFeatureFlagsAccessor {
bool enableFabricRenderer();
bool enableFixForParentTagDuringReparenting();
bool enableFontScaleChangesUpdatingLayout();
bool enableIOSTextBaselineOffsetPerLine();
bool enableIOSViewClipToPaddingBox();
bool enableIntersectionObserverEventLoopIntegration();
bool enableLayoutAnimationsOnAndroid();
@@ -92,7 +93,7 @@ class ReactNativeFeatureFlagsAccessor {
std::unique_ptr<ReactNativeFeatureFlagsProvider> currentProvider_;
bool wasOverridden_;
std::array<std::atomic<const char*>, 49> accessedFeatureFlags_;
std::array<std::atomic<const char*>, 50> accessedFeatureFlags_;
std::atomic<std::optional<bool>> commonTestFlag_;
std::atomic<std::optional<bool>> animatedShouldSignalBatch_;
@@ -112,6 +113,7 @@ class ReactNativeFeatureFlagsAccessor {
std::atomic<std::optional<bool>> enableFabricRenderer_;
std::atomic<std::optional<bool>> enableFixForParentTagDuringReparenting_;
std::atomic<std::optional<bool>> enableFontScaleChangesUpdatingLayout_;
std::atomic<std::optional<bool>> enableIOSTextBaselineOffsetPerLine_;
std::atomic<std::optional<bool>> enableIOSViewClipToPaddingBox_;
std::atomic<std::optional<bool>> enableIntersectionObserverEventLoopIntegration_;
std::atomic<std::optional<bool>> enableLayoutAnimationsOnAndroid_;
@@ -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<<1a4bc240cbc6dbdac756555f98f69147>>
* @generated SignedSource<<4c463bb7d1aa97478abe0e74955aa854>>
*/
/**
@@ -99,6 +99,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider {
return false;
}
bool enableIOSTextBaselineOffsetPerLine() override {
return false;
}
bool enableIOSViewClipToPaddingBox() override {
return false;
}
@@ -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<<425dc272cba3224430f9948e6773b30f>>
* @generated SignedSource<<f0c9d2d7027f9408bb7f784adcccbd51>>
*/
/**
@@ -207,6 +207,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef
return ReactNativeFeatureFlagsDefaults::enableFontScaleChangesUpdatingLayout();
}
bool enableIOSTextBaselineOffsetPerLine() override {
auto value = values_["enableIOSTextBaselineOffsetPerLine"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableIOSTextBaselineOffsetPerLine();
}
bool enableIOSViewClipToPaddingBox() override {
auto value = values_["enableIOSViewClipToPaddingBox"];
if (!value.isNull()) {
@@ -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<<aae696b94ad35a3cdee45e80ad457a72>>
* @generated SignedSource<<53bd41086143a19f52e1e96025b24c76>>
*/
/**
@@ -43,6 +43,7 @@ class ReactNativeFeatureFlagsProvider {
virtual bool enableFabricRenderer() = 0;
virtual bool enableFixForParentTagDuringReparenting() = 0;
virtual bool enableFontScaleChangesUpdatingLayout() = 0;
virtual bool enableIOSTextBaselineOffsetPerLine() = 0;
virtual bool enableIOSViewClipToPaddingBox() = 0;
virtual bool enableIntersectionObserverEventLoopIntegration() = 0;
virtual bool enableLayoutAnimationsOnAndroid() = 0;
@@ -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<<fadce2932514be743d6a1175daae39e2>>
* @generated SignedSource<<74f011324e9d4647fa757f29b602c6c3>>
*/
/**
@@ -134,6 +134,11 @@ bool NativeReactNativeFeatureFlags::enableFontScaleChangesUpdatingLayout(
return ReactNativeFeatureFlags::enableFontScaleChangesUpdatingLayout();
}
bool NativeReactNativeFeatureFlags::enableIOSTextBaselineOffsetPerLine(
jsi::Runtime& /*runtime*/) {
return ReactNativeFeatureFlags::enableIOSTextBaselineOffsetPerLine();
}
bool NativeReactNativeFeatureFlags::enableIOSViewClipToPaddingBox(
jsi::Runtime& /*runtime*/) {
return ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
@@ -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<<33c4be38d881a6e09e4a58cac6eeca53>>
* @generated SignedSource<<2b33a7fe9086343c427f06cc62c67a62>>
*/
/**
@@ -73,6 +73,8 @@ class NativeReactNativeFeatureFlags
bool enableFontScaleChangesUpdatingLayout(jsi::Runtime& runtime);
bool enableIOSTextBaselineOffsetPerLine(jsi::Runtime& runtime);
bool enableIOSViewClipToPaddingBox(jsi::Runtime& runtime);
bool enableIntersectionObserverEventLoopIntegration(jsi::Runtime& runtime);
@@ -7,6 +7,7 @@
#import "RCTAttributedTextUtils.h"
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/components/view/accessibilityPropsConversions.h>
#include <react/renderer/core/LayoutableShadowNode.h>
#include <react/renderer/textlayoutmanager/RCTFontProperties.h>
@@ -291,12 +292,12 @@ NSMutableDictionary<NSAttributedStringKey, id> *RCTNSTextAttributesFromTextAttri
return attributes;
}
void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
static void RCTApplyBaselineOffsetForRange(NSMutableAttributedString *attributedText, NSRange attributedTextRange)
{
__block CGFloat maximumLineHeight = 0;
[attributedText enumerateAttribute:NSParagraphStyleAttributeName
inRange:NSMakeRange(0, attributedText.length)
inRange:attributedTextRange
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:^(NSParagraphStyle *paragraphStyle, __unused NSRange range, __unused BOOL *stop) {
if (!paragraphStyle) {
@@ -314,7 +315,7 @@ void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
__block CGFloat maximumFontLineHeight = 0;
[attributedText enumerateAttribute:NSFontAttributeName
inRange:NSMakeRange(0, attributedText.length)
inRange:attributedTextRange
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) {
if (!font) {
@@ -330,9 +331,25 @@ void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
CGFloat baseLineOffset = (maximumLineHeight - maximumFontLineHeight) / 2.0;
[attributedText addAttribute:NSBaselineOffsetAttributeName
value:@(baseLineOffset)
range:NSMakeRange(0, attributedText.length)];
[attributedText addAttribute:NSBaselineOffsetAttributeName value:@(baseLineOffset) range:attributedTextRange];
}
void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
{
if (ReactNativeFeatureFlags::enableIOSTextBaselineOffsetPerLine()) {
[attributedText.string
enumerateSubstringsInRange:NSMakeRange(0, attributedText.length)
options:NSStringEnumerationByLines | NSStringEnumerationSubstringNotRequired
usingBlock:^(
NSString *_Nullable substring,
NSRange substringRange,
NSRange enclosingRange,
BOOL *_Nonnull stop) {
RCTApplyBaselineOffsetForRange(attributedText, enclosingRange);
}];
} else {
RCTApplyBaselineOffsetForRange(attributedText, NSMakeRange(0, attributedText.length));
}
}
static NSMutableAttributedString *RCTNSAttributedStringFragmentFromFragment(
@@ -237,6 +237,17 @@ const definitions: FeatureFlagDefinitions = {
},
ossReleaseStage: 'none',
},
enableIOSTextBaselineOffsetPerLine: {
defaultValue: false,
metadata: {
dateAdded: '2025-05-21',
description:
'Applies base offset for each line of text separately on iOS.',
expectedReleaseValue: true,
purpose: 'experimentation',
},
ossReleaseStage: 'none',
},
enableIOSViewClipToPaddingBox: {
defaultValue: false,
metadata: {
@@ -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<<2b128035e6890d34737a2cfc5d8fa5ad>>
* @generated SignedSource<<e640d7299f0b0624f9dce91e7a39604c>>
* @flow strict
* @noformat
*/
@@ -68,6 +68,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{
enableFabricRenderer: Getter<boolean>,
enableFixForParentTagDuringReparenting: Getter<boolean>,
enableFontScaleChangesUpdatingLayout: Getter<boolean>,
enableIOSTextBaselineOffsetPerLine: Getter<boolean>,
enableIOSViewClipToPaddingBox: Getter<boolean>,
enableIntersectionObserverEventLoopIntegration: Getter<boolean>,
enableLayoutAnimationsOnAndroid: Getter<boolean>,
@@ -252,6 +253,10 @@ export const enableFixForParentTagDuringReparenting: Getter<boolean> = createNat
* Enables font scale changes updating layout for measurable nodes.
*/
export const enableFontScaleChangesUpdatingLayout: Getter<boolean> = createNativeFlagGetter('enableFontScaleChangesUpdatingLayout', false);
/**
* Applies base offset for each line of text separately on iOS.
*/
export const enableIOSTextBaselineOffsetPerLine: Getter<boolean> = createNativeFlagGetter('enableIOSTextBaselineOffsetPerLine', false);
/**
* iOS Views will clip to their padding box vs border box
*/
@@ -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<<ddb43daeb855838f8d396dfa952e9933>>
* @generated SignedSource<<620ba7ace3df80067aa6ba1ed44aec60>>
* @flow strict
* @noformat
*/
@@ -43,6 +43,7 @@ export interface Spec extends TurboModule {
+enableFabricRenderer?: () => boolean;
+enableFixForParentTagDuringReparenting?: () => boolean;
+enableFontScaleChangesUpdatingLayout?: () => boolean;
+enableIOSTextBaselineOffsetPerLine?: () => boolean;
+enableIOSViewClipToPaddingBox?: () => boolean;
+enableIntersectionObserverEventLoopIntegration?: () => boolean;
+enableLayoutAnimationsOnAndroid?: () => boolean;