diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java index 999e4279a7ac7c..f0f2f6512feb24 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java @@ -61,6 +61,7 @@ public class TextAttributeProps { public static final short TA_KEY_LINE_BREAK_STRATEGY = 25; public static final short TA_KEY_ROLE = 26; public static final short TA_KEY_TEXT_TRANSFORM = 27; + public static final short TA_KEY_MAX_FONT_SIZE_MULTIPLIER = 29; public static final int UNSET = -1; @@ -81,6 +82,7 @@ public class TextAttributeProps { protected float mLineHeight = Float.NaN; protected boolean mIsColorSet = false; protected boolean mAllowFontScaling = true; + protected float mMaxFontSizeMultiplier = Float.NaN; protected int mColor; protected boolean mIsBackgroundColorSet = false; protected int mBackgroundColor; @@ -227,6 +229,9 @@ public static TextAttributeProps fromMapBuffer(MapBuffer props) { case TA_KEY_TEXT_TRANSFORM: result.setTextTransform(entry.getStringValue()); break; + case TA_KEY_MAX_FONT_SIZE_MULTIPLIER: + result.setMaxFontSizeMultiplier((float) entry.getDoubleValue()); + break; } } @@ -243,6 +248,7 @@ public static TextAttributeProps fromReadableMap(ReactStylesDiffMap props) { result.setLineHeight(getFloatProp(props, ViewProps.LINE_HEIGHT, ReactConstants.UNSET)); result.setLetterSpacing(getFloatProp(props, ViewProps.LETTER_SPACING, Float.NaN)); result.setAllowFontScaling(getBooleanProp(props, ViewProps.ALLOW_FONT_SCALING, true)); + result.setMaxFontSizeMultiplier(getFloatProp(props, ViewProps.MAX_FONT_SIZE_MULTIPLIER, Float.NaN)); result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, ReactConstants.UNSET)); result.setColor(props.hasKey(ViewProps.COLOR) ? props.getInt(ViewProps.COLOR, 0) : null); result.setColor( @@ -411,7 +417,14 @@ private void setAllowFontScaling(boolean allowFontScaling) { mAllowFontScaling = allowFontScaling; setFontSize(mFontSizeInput); setLineHeight(mLineHeightInput); - setLetterSpacing(mLetterSpacingInput); + } + } + + private void setMaxFontSizeMultiplier(float maxFontSizeMultiplier) { + if (maxFontSizeMultiplier != mMaxFontSizeMultiplier) { + mMaxFontSizeMultiplier = maxFontSizeMultiplier; + setFontSize(mFontSizeInput); + setLineHeight(mLineHeightInput); } } @@ -420,7 +433,7 @@ private void setFontSize(float fontSize) { if (fontSize != ReactConstants.UNSET) { fontSize = mAllowFontScaling - ? (float) Math.ceil(PixelUtil.toPixelFromSP(fontSize)) + ? (float) Math.ceil(PixelUtil.toPixelFromSP(fontSize, mMaxFontSizeMultiplier)) : (float) Math.ceil(PixelUtil.toPixelFromDIP(fontSize)); } mFontSize = (int) fontSize; diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp index 93b1728d2c0698..193b75b71cc9fe 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp @@ -46,6 +46,9 @@ void TextAttributes::apply(TextAttributes textAttributes) { allowFontScaling = textAttributes.allowFontScaling.has_value() ? textAttributes.allowFontScaling : allowFontScaling; + maxFontSizeMultiplier = !std::isnan(textAttributes.maxFontSizeMultiplier) + ? textAttributes.maxFontSizeMultiplier + : maxFontSizeMultiplier; dynamicTypeRamp = textAttributes.dynamicTypeRamp.has_value() ? textAttributes.dynamicTypeRamp : dynamicTypeRamp; @@ -168,6 +171,7 @@ bool TextAttributes::operator==(const TextAttributes& rhs) const { rhs.accessibilityRole, rhs.role, rhs.textTransform) && + floatEquality(maxFontSizeMultiplier, rhs.maxFontSizeMultiplier) && floatEquality(opacity, rhs.opacity) && floatEquality(fontSize, rhs.fontSize) && floatEquality(fontSizeMultiplier, rhs.fontSizeMultiplier) && @@ -224,6 +228,10 @@ SharedDebugStringConvertibleList TextAttributes::getDebugProps() const { "allowFontScaling", allowFontScaling, textAttributes.allowFontScaling), + debugStringConvertibleItem( + "maxFontSizeMultiplier", + maxFontSizeMultiplier, + textAttributes.maxFontSizeMultiplier), debugStringConvertibleItem( "dynamicTypeRamp", dynamicTypeRamp, textAttributes.dynamicTypeRamp), debugStringConvertibleItem( diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h index 37db36656f8d67..55b4de33223fc2 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h @@ -51,6 +51,7 @@ class TextAttributes : public DebugStringConvertible { std::optional fontStyle{}; std::optional fontVariant{}; std::optional allowFontScaling{}; + Float maxFontSizeMultiplier{std::numeric_limits::quiet_NaN()}; std::optional dynamicTypeRamp{}; Float letterSpacing{std::numeric_limits::quiet_NaN()}; std::optional textTransform{}; @@ -117,6 +118,7 @@ struct hash { textAttributes.opacity, textAttributes.fontFamily, textAttributes.fontSize, + textAttributes.maxFontSizeMultiplier, textAttributes.fontSizeMultiplier, textAttributes.fontWeight, textAttributes.fontStyle, diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index de8676939e4630..4bb23f08d2609f 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -910,6 +910,7 @@ constexpr static MapBuffer::Key TA_KEY_LINE_BREAK_STRATEGY = 25; constexpr static MapBuffer::Key TA_KEY_ROLE = 26; constexpr static MapBuffer::Key TA_KEY_TEXT_TRANSFORM = 27; constexpr static MapBuffer::Key TA_KEY_ALIGNMENT_VERTICAL = 28; +constexpr static MapBuffer::Key TA_KEY_MAX_FONT_SIZE_MULTIPLIER = 29; // constants for ParagraphAttributes serialization constexpr static MapBuffer::Key PA_KEY_MAX_NUMBER_OF_LINES = 0; @@ -1004,6 +1005,9 @@ inline MapBuffer toMapBuffer(const TextAttributes& textAttributes) { builder.putBool( TA_KEY_ALLOW_FONT_SCALING, *textAttributes.allowFontScaling); } + if (!std::isnan(textAttributes.maxFontSizeMultiplier)) { + builder.putDouble(TA_KEY_MAX_FONT_SIZE_MULTIPLIER, textAttributes.maxFontSizeMultiplier); + } if (!std::isnan(textAttributes.letterSpacing)) { builder.putDouble(TA_KEY_LETTER_SPACING, textAttributes.letterSpacing); } diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp index 132281e8ab8333..64ebaba00617ea 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp @@ -73,6 +73,12 @@ static TextAttributes convertRawProp( "allowFontScaling", sourceTextAttributes.allowFontScaling, defaultTextAttributes.allowFontScaling); + textAttributes.maxFontSizeMultiplier = convertRawProp( + context, + rawProps, + "maxFontSizeMultiplier", + sourceTextAttributes.maxFontSizeMultiplier, + defaultTextAttributes.maxFontSizeMultiplier); textAttributes.dynamicTypeRamp = convertRawProp( context, rawProps, @@ -266,6 +272,8 @@ void BaseTextProps::setProp( defaults, value, textAttributes, fontVariant, "fontVariant"); REBUILD_FIELD_SWITCH_CASE( defaults, value, textAttributes, allowFontScaling, "allowFontScaling"); + REBUILD_FIELD_SWITCH_CASE( + defaults, value, textAttributes, maxFontSizeMultiplier, "maxFontSizeMultiplier"); REBUILD_FIELD_SWITCH_CASE( defaults, value, textAttributes, letterSpacing, "letterSpacing"); REBUILD_FIELD_SWITCH_CASE( diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm index 17eec1f275bd6a..2fb0f46e589189 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm @@ -135,6 +135,7 @@ inline static CGFloat RCTBaseSizeForDynamicTypeRamp(const DynamicTypeRamp &dynam inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const TextAttributes &textAttributes) { if (textAttributes.allowFontScaling.value_or(true)) { + CGFloat fontSizeMultiplier = !isnan(textAttributes.fontSizeMultiplier) ? textAttributes.fontSizeMultiplier : 1.0; if (textAttributes.dynamicTypeRamp.has_value()) { DynamicTypeRamp dynamicTypeRamp = textAttributes.dynamicTypeRamp.value(); UIFontMetrics *fontMetrics = @@ -142,10 +143,11 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex // Using a specific font size reduces rounding errors from -scaledValueForValue: CGFloat requestedSize = isnan(textAttributes.fontSize) ? RCTBaseSizeForDynamicTypeRamp(dynamicTypeRamp) : textAttributes.fontSize; - return [fontMetrics scaledValueForValue:requestedSize] / requestedSize; - } else { - return textAttributes.fontSizeMultiplier; + fontSizeMultiplier = [fontMetrics scaledValueForValue:requestedSize] / requestedSize; } + CGFloat maxFontSizeMultiplier = + !isnan(textAttributes.maxFontSizeMultiplier) ? textAttributes.maxFontSizeMultiplier : 0.0; + return maxFontSizeMultiplier >= 1.0 ? fminf(maxFontSizeMultiplier, fontSizeMultiplier) : fontSizeMultiplier; } else { return 1.0; } diff --git a/packages/rn-tester/js/examples/Text/TextExample.android.js b/packages/rn-tester/js/examples/Text/TextExample.android.js index 453b9c2cfe30b9..7d4bf658083b5b 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.android.js +++ b/packages/rn-tester/js/examples/Text/TextExample.android.js @@ -443,6 +443,37 @@ function AllowFontScalingExample(props: {}): React.Node { ); } +function MaxFontSizeMultiplierExample(props: {}): React.Node { + return ( + <> + + When allowFontScaling is enabled, you can use the maxFontSizeMultiplier + prop to set an upper limit on how much the font size will be scaled. + + + This text will not scale up (max 1x) + + + This text will scale up (max 1.5x) + + + Inherit max (max 1x) + + + + Override inherited max (max 1.5x) + + + + Ignore inherited max (no max) + + + ); +} + function NumberOfLinesExample(props: {}): React.Node { return ( <> @@ -1370,6 +1401,13 @@ const examples = [ return ; }, }, + { + title: 'maxFontSizeMultiplier attribute', + name: 'maxFontSizeMultiplier', + render(): React.Node { + return ; + }, + }, { title: 'selectable attribute', name: 'selectable', diff --git a/packages/rn-tester/js/examples/Text/TextExample.ios.js b/packages/rn-tester/js/examples/Text/TextExample.ios.js index 6b65d167894f26..effe1ce139af34 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.ios.js +++ b/packages/rn-tester/js/examples/Text/TextExample.ios.js @@ -1127,6 +1127,62 @@ const examples = [ ); }, }, + { + title: 'maxFontSizeMultiplier attribute', + name: 'maxFontSizeMultiplier', + render(): React.Node { + return ( + <> + + When allowFontScaling is enabled, you can use the + maxFontSizeMultiplier prop to set an upper limit on how much the + font size will be scaled. + + + This text will not scale up (max 1x) + + + This text will scale up (max 1.5x) + + + Inherit max (max 1x) + + + + Override inherited max (max 1.5x) + + + + Ignore inherited max (no max) + + + This text will scale with 'title2' dynamic type ramp (no max) + + + This text will scale with 'title2' dynamic type ramp (max 1.2x) + + + This text uses 'title2' dynamic type ramp but will not scale up (max + 1x) + + + ); + }, + }, { title: 'Inline views', render: (): React.Node => , @@ -1392,9 +1448,27 @@ const examples = [ Title 3 + + Headline + Body + + Callout + + + Subheadline + + + Footnote + + + Caption + + + Caption 2 + Without `dynamicTypeRamp`: @@ -1402,7 +1476,13 @@ const examples = [ Title Title 2 Title 3 + Headline Body + Callout + Subheadline + Footnote + Caption + Caption 2 ); diff --git a/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js b/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js index c2c8eb7084a490..6b69832453ee6e 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js @@ -408,6 +408,57 @@ const examples: Array = [ ); }, }, + { + title: 'allowFontScaling attribute', + render: function (): React.Node { + return ( + + + By default, text will respect Text Size accessibility setting on + Android. It means that all font sizes will be increased or decreased + depending on the value of the Text Size setting in the OS's Settings + app. + + + + + ); + }, + }, + { + title: 'maxFontSizeMultiplier attribute', + name: 'maxFontSizeMultiplier', + render(): React.Node { + return ( + + + When allowFontScaling is enabled, you can use the + maxFontSizeMultiplier prop to set an upper limit on how much the + font size will be scaled. + + + + + ); + }, + }, { title: 'Auto-expanding', render: function (): React.Node { diff --git a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js index 4894dd41b48aef..f264f36f6384d0 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js @@ -717,6 +717,59 @@ const textInputExamples: Array = [ ); }, }, + { + title: 'allowFontScaling attribute', + render: function (): React.Node { + return ( + + + By default, text will respect Text Size accessibility setting on + iOS. It means that all font sizes will be increased or decreased + depending on the value of Text Size setting in{' '} + + Settings.app - Display & Brightness - Text Size + + + + + + ); + }, + }, + { + title: 'maxFontSizeMultiplier attribute', + name: 'maxFontSizeMultiplier', + render(): React.Node { + return ( + + + When allowFontScaling is enabled, you can use the + maxFontSizeMultiplier prop to set an upper limit on how much the + font size will be scaled. + + + + + ); + }, + }, { title: 'Auto-expanding', render: function (): React.Node {