diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt
index 4ebaa5630c09..5c76e0838cc6 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt
@@ -40,7 +40,8 @@ public class TextAttributeProps private constructor() {
if (value == ReactConstants.UNSET.toFloat()) {
Float.NaN
} else {
- if (allowFontScaling) toPixelFromSP(value) else toPixelFromDIP(value)
+ if (allowFontScaling) toPixelFromSP(value, maxFontSizeMultiplier)
+ else toPixelFromDIP(value)
}
}
@@ -169,7 +170,7 @@ public class TextAttributeProps private constructor() {
public var letterSpacing: Float
get() {
val letterSpacingPixels =
- if (allowFontScaling) toPixelFromSP(letterSpacingInput)
+ if (allowFontScaling) toPixelFromSP(letterSpacingInput, maxFontSizeMultiplier)
else toPixelFromDIP(letterSpacingInput)
require(fontSize > 0) { "FontSize should be a positive value. Current value: $fontSize" }
diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextAttributePropsTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextAttributePropsTest.kt
index e656463b0f68..c7ea970d19be 100644
--- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextAttributePropsTest.kt
+++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextAttributePropsTest.kt
@@ -5,13 +5,18 @@
* LICENSE file in the root directory of this source tree.
*/
+@file:Suppress("DEPRECATION") // DisplayMetrics.scaledDensity, to simulate a system font scale
+
package com.facebook.react.views.text
+import android.util.DisplayMetrics
import android.view.Gravity
import com.facebook.react.bridge.JavaOnlyMap
+import com.facebook.react.common.mapbuffer.WritableMapBuffer
import com.facebook.react.uimanager.DisplayMetricsHolder
import com.facebook.react.uimanager.ReactStylesDiffMap
import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.within
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -84,6 +89,124 @@ class TextAttributePropsTest {
assertThat(textAlignment("end", isRTL = true)).isEqualTo(Gravity.LEFT)
}
+ @Test
+ fun lineHeightIsCappedByMaxFontSizeMultiplier() {
+ setFontScale(2f)
+
+ val textAttributes =
+ TextAttributeProps.fromReadableMap(
+ ReactStylesDiffMap(
+ JavaOnlyMap.of("fontSize", 14.0, "lineHeight", 20.0, "maxFontSizeMultiplier", 1.3)
+ ),
+ )
+
+ assertThat(textAttributes.lineHeight).isCloseTo(20f * DENSITY * 1.3f, within(0.001f))
+ }
+
+ @Test
+ fun lineHeightIsCappedByMaxFontSizeMultiplierFromMapBuffer() {
+ setFontScale(2f)
+
+ // Keys are iterated in ascending order, so lineHeight is set before maxFontSizeMultiplier.
+ val textAttributes =
+ TextAttributeProps.fromMapBuffer(
+ WritableMapBuffer()
+ .put(TextAttributeProps.TA_KEY_FONT_SIZE, 14.0)
+ .put(TextAttributeProps.TA_KEY_LINE_HEIGHT, 20.0)
+ .put(TextAttributeProps.TA_KEY_MAX_FONT_SIZE_MULTIPLIER, 1.3),
+ )
+
+ assertThat(textAttributes.lineHeight).isCloseTo(20f * DENSITY * 1.3f, within(0.001f))
+ }
+
+ @Test
+ fun letterSpacingIsCappedByMaxFontSizeMultiplier() {
+ setFontScale(2f)
+
+ val textAttributes =
+ TextAttributeProps.fromReadableMap(
+ ReactStylesDiffMap(
+ JavaOnlyMap.of("fontSize", 14.0, "letterSpacing", 2.0, "maxFontSizeMultiplier", 1.3)
+ ),
+ )
+
+ assertThat(textAttributes.fontSize).isEqualTo(37) // ceil(14 * 2 * 1.3)
+ assertThat(textAttributes.letterSpacing)
+ .isCloseTo(2f * DENSITY * 1.3f / textAttributes.fontSize, within(0.001f))
+ }
+
+ @Test
+ fun lineHeightScalesWithFontScaleWithoutMaxFontSizeMultiplier() {
+ setFontScale(2f)
+
+ val textAttributes =
+ TextAttributeProps.fromReadableMap(
+ ReactStylesDiffMap(JavaOnlyMap.of("fontSize", 14.0, "lineHeight", 20.0)),
+ )
+
+ assertThat(textAttributes.lineHeight).isCloseTo(20f * DENSITY * 2f, within(0.001f))
+ }
+
+ @Test
+ fun lineHeightIsNotCappedWhenFontScaleIsBelowMaxFontSizeMultiplier() {
+ setFontScale(1.2f)
+
+ val textAttributes =
+ TextAttributeProps.fromReadableMap(
+ ReactStylesDiffMap(
+ JavaOnlyMap.of("fontSize", 14.0, "lineHeight", 20.0, "maxFontSizeMultiplier", 1.5)
+ ),
+ )
+
+ assertThat(textAttributes.lineHeight).isCloseTo(20f * DENSITY * 1.2f, within(0.001f))
+ }
+
+ @Test
+ fun lineHeightIsNotCappedWhenMaxFontSizeMultiplierIsZero() {
+ setFontScale(2f)
+
+ val textAttributes =
+ TextAttributeProps.fromReadableMap(
+ ReactStylesDiffMap(
+ JavaOnlyMap.of("fontSize", 14.0, "lineHeight", 20.0, "maxFontSizeMultiplier", 0.0)
+ ),
+ )
+
+ assertThat(textAttributes.lineHeight).isCloseTo(20f * DENSITY * 2f, within(0.001f))
+ }
+
+ @Test
+ fun lineHeightIgnoresFontScaleWhenFontScalingIsDisabled() {
+ setFontScale(2f)
+
+ val textAttributes =
+ TextAttributeProps.fromReadableMap(
+ ReactStylesDiffMap(
+ JavaOnlyMap.of(
+ "fontSize",
+ 14.0,
+ "lineHeight",
+ 20.0,
+ "maxFontSizeMultiplier",
+ 1.3,
+ "allowFontScaling",
+ false,
+ )
+ ),
+ )
+
+ assertThat(textAttributes.lineHeight).isCloseTo(20f * DENSITY, within(0.001f))
+ }
+
+ private fun setFontScale(fontScale: Float) {
+ DisplayMetricsHolder.setScreenDisplayMetrics(
+ DisplayMetrics().apply {
+ density = DENSITY
+ scaledDensity = DENSITY * fontScale
+ },
+ )
+ }
+
private fun textAlignment(textAlign: String, isRTL: Boolean): Int {
return TextAttributeProps.getTextAlignment(
ReactStylesDiffMap(JavaOnlyMap.of("textAlign", textAlign)),
@@ -91,4 +214,8 @@ class TextAttributePropsTest {
Gravity.CENTER_HORIZONTAL,
)
}
+
+ private companion object {
+ const val DENSITY = 2f
+ }
}
diff --git a/packages/rn-tester/js/examples/Text/TextExample.android.js b/packages/rn-tester/js/examples/Text/TextExample.android.js
index 39b1c94aa248..896cede7ce64 100644
--- a/packages/rn-tester/js/examples/Text/TextExample.android.js
+++ b/packages/rn-tester/js/examples/Text/TextExample.android.js
@@ -531,6 +531,18 @@ function MaxFontSizeMultiplierExample(props: {}): React.Node {
Ignore inherited max (no max)
+
+ lineHeight and letterSpacing are capped too (max 1x): these lines keep
+ the same spacing at any system font size
+
);
}
diff --git a/packages/rn-tester/js/examples/Text/TextExample.ios.js b/packages/rn-tester/js/examples/Text/TextExample.ios.js
index e367974e3da2..ff5ff29a097f 100644
--- a/packages/rn-tester/js/examples/Text/TextExample.ios.js
+++ b/packages/rn-tester/js/examples/Text/TextExample.ios.js
@@ -1269,6 +1269,18 @@ const examples = [
Ignore inherited max (no max)
+
+ lineHeight and letterSpacing are capped too (max 1x): these lines
+ keep the same spacing at any system font size
+