diff --git a/.maestro/enrichedInput/flows/font_scaling.yaml b/.maestro/enrichedInput/flows/font_scaling.yaml
new file mode 100644
index 000000000..38ce16624
--- /dev/null
+++ b/.maestro/enrichedInput/flows/font_scaling.yaml
@@ -0,0 +1,57 @@
+appId: swmansion.enriched.example
+tags:
+ - accessibility
+---
+- launchApp
+
+- tapOn:
+ id: "toggle-screen-button"
+
+- runFlow:
+ file: "../subflows/capture_or_assert_screenshot.yaml"
+ env:
+ SCREENSHOT_NAME: "font_scaling__placeholder"
+
+- tapOn:
+ id: "editor-input"
+
+- inputText: "Plain "
+
+- tapOn:
+ id: "toolbar-bold"
+- inputText: "bold"
+- tapOn:
+ id: "toolbar-bold"
+- inputText: " "
+
+- tapOn:
+ id: "toolbar-italic"
+- inputText: "italic"
+- tapOn:
+ id: "toolbar-italic"
+- inputText: " "
+
+- tapOn:
+ id: "toolbar-underline"
+- inputText: "underline"
+- tapOn:
+ id: "toolbar-underline"
+- inputText: " "
+
+- tapOn:
+ id: "toolbar-strikethrough"
+- inputText: "strike-through"
+- tapOn:
+ id: "toolbar-strikethrough"
+- inputText: " "
+
+- tapOn:
+ id: "toolbar-inline-code"
+- inputText: "code"
+- tapOn:
+ id: "toolbar-inline-code"
+
+- runFlow:
+ file: "../subflows/capture_or_assert_screenshot.yaml"
+ env:
+ SCREENSHOT_NAME: "font_scaling"
diff --git a/.maestro/enrichedInput/screenshots/android/font_scaling.png b/.maestro/enrichedInput/screenshots/android/font_scaling.png
new file mode 100644
index 000000000..6b955aa4e
Binary files /dev/null and b/.maestro/enrichedInput/screenshots/android/font_scaling.png differ
diff --git a/.maestro/enrichedInput/screenshots/android/font_scaling__placeholder.png b/.maestro/enrichedInput/screenshots/android/font_scaling__placeholder.png
new file mode 100644
index 000000000..1288cc4b3
Binary files /dev/null and b/.maestro/enrichedInput/screenshots/android/font_scaling__placeholder.png differ
diff --git a/.maestro/enrichedInput/screenshots/ios/font_scaling.png b/.maestro/enrichedInput/screenshots/ios/font_scaling.png
new file mode 100644
index 000000000..ccc86279c
Binary files /dev/null and b/.maestro/enrichedInput/screenshots/ios/font_scaling.png differ
diff --git a/.maestro/enrichedInput/screenshots/ios/font_scaling__placeholder.png b/.maestro/enrichedInput/screenshots/ios/font_scaling__placeholder.png
new file mode 100644
index 000000000..99c329c32
Binary files /dev/null and b/.maestro/enrichedInput/screenshots/ios/font_scaling__placeholder.png differ
diff --git a/.maestro/enrichedText/flows/font_scaling.yaml b/.maestro/enrichedText/flows/font_scaling.yaml
new file mode 100644
index 000000000..faa23a0a7
--- /dev/null
+++ b/.maestro/enrichedText/flows/font_scaling.yaml
@@ -0,0 +1,34 @@
+appId: swmansion.enriched.example
+tags:
+ - accessibility
+---
+# Validates that text alignment is displayed correctly
+- launchApp
+
+- tapOn:
+ id: 'toggle-screen-button'
+
+- tapOn:
+ id: 'toggle-enriched-text-screen-button'
+
+- runFlow:
+ file: '../subflows/set_enriched_text_value.yaml'
+ env:
+ VALUE: >
+
+
Left aligned
+ Centre aligned
+ Heading 6
+ Right aligned
+
+ - Element 1
+ - Element 2
+
+
+
+- scroll
+
+- runFlow:
+ file: '../subflows/capture_or_assert_screenshot.yaml'
+ env:
+ SCREENSHOT_NAME: 'font_scaling'
diff --git a/.maestro/enrichedText/screenshots/android/font_scaling.png b/.maestro/enrichedText/screenshots/android/font_scaling.png
new file mode 100644
index 000000000..1547d1e91
Binary files /dev/null and b/.maestro/enrichedText/screenshots/android/font_scaling.png differ
diff --git a/.maestro/enrichedText/screenshots/ios/font_scaling.png b/.maestro/enrichedText/screenshots/ios/font_scaling.png
new file mode 100644
index 000000000..eb1481f54
Binary files /dev/null and b/.maestro/enrichedText/screenshots/ios/font_scaling.png differ
diff --git a/.maestro/scripts/run-tests.sh b/.maestro/scripts/run-tests.sh
index 4e9230d64..55fcaef94 100755
--- a/.maestro/scripts/run-tests.sh
+++ b/.maestro/scripts/run-tests.sh
@@ -71,6 +71,25 @@ app_installed() {
fi
}
+# Adjusts the system's text-size setting.
+set_font_scale() {
+ case "$1" in
+ default) ios_size="large"; android_scale="1.0" ;;
+ large) ios_size="accessibility-large"; android_scale="1.5" ;;
+ *) echo "set_font_scale: unknown size '$1'" >&2; return 1 ;;
+ esac
+ if [ "$PLATFORM" = ios ]; then
+ xcrun simctl ui "$DEVICE_ID" content_size "$ios_size"
+ else
+ adb -s "$DEVICE_ID" shell settings put system font_scale "$android_scale"
+ fi
+}
+
+# Guarantees the font scale is restored on any exit.
+# Without this, the accessibility tests below would leave the device
+# in a scaled-up state.
+trap 'set_font_scale default' EXIT
+
if [ -n "$REBUILD" ] || ! app_installed; then
[ -n "$REBUILD" ] && echo "=== rebuild requested, building and installing ==="
[ -z "$REBUILD" ] && echo "=== App ($BUNDLE_ID) not found, building and installing ==="
@@ -97,6 +116,18 @@ esac
ASSETS_DIR="$MAESTRO_ROOT/assets"
[ -d "$ASSETS_DIR" ] && FLOWS="$ASSETS_DIR $FLOWS"
+# A previous run could have died before its EXIT trap fired (e.g. SIGKILL),
+# leaving the device scaled. Force a known state before the normal tests.
+set_font_scale default
+
echo "=== Running maestro tests ==="
# shellcheck disable=SC2086
-maestro test --device "$DEVICE_ID" $EXTRA $FLOWS
+maestro test --device "$DEVICE_ID" --exclude-tags accessibility $EXTRA $FLOWS
+
+# These are the tests that require changing the system's settings
+# - something maestro cannot run internally
+echo "=== Running maestro accessibility tests ==="
+set_font_scale large
+
+# shellcheck disable=SC2086
+maestro test --device "$DEVICE_ID" --include-tags accessibility $EXTRA $FLOWS
diff --git a/android/src/main/java/com/swmansion/enriched/common/AllowFontScaling.kt b/android/src/main/java/com/swmansion/enriched/common/AllowFontScaling.kt
new file mode 100644
index 000000000..05f5075d1
--- /dev/null
+++ b/android/src/main/java/com/swmansion/enriched/common/AllowFontScaling.kt
@@ -0,0 +1,36 @@
+package com.swmansion.enriched.common
+
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.uimanager.PixelUtil
+
+internal const val ALLOW_FONT_SCALING_PROP = "allowFontScaling"
+
+// Converts a logical font-unit value to pixels.
+// Respects the system font scaling, depending on the `allowFontScaling` value
+internal fun pixelFromSpOrDp(
+ value: Float,
+ allowFontScaling: Boolean,
+): Float =
+ if (allowFontScaling) {
+ PixelUtil.toPixelFromSP(value)
+ } else {
+ PixelUtil.toPixelFromDIP(value)
+ }
+
+internal fun pixelFromSpOrDp(
+ value: Double,
+ allowFontScaling: Boolean,
+): Float =
+ if (allowFontScaling) {
+ PixelUtil.toPixelFromSP(value)
+ } else {
+ PixelUtil.toPixelFromDIP(value)
+ }
+
+// Reads allowFontScaling from a serialized prop map (used in MeasurementStore
+// where no view instance is available yet).
+internal fun allowFontScalingFromProps(props: ReadableMap?): Boolean {
+ if (props == null) return EnrichedConstants.ALLOW_FONT_SCALING_DEFAULT
+ if (!props.hasKey(ALLOW_FONT_SCALING_PROP) || props.isNull(ALLOW_FONT_SCALING_PROP)) return EnrichedConstants.ALLOW_FONT_SCALING_DEFAULT
+ return props.getBoolean(ALLOW_FONT_SCALING_PROP)
+}
diff --git a/android/src/main/java/com/swmansion/enriched/common/EnrichedConstants.kt b/android/src/main/java/com/swmansion/enriched/common/EnrichedConstants.kt
index fb68f2049..5a79cc57e 100644
--- a/android/src/main/java/com/swmansion/enriched/common/EnrichedConstants.kt
+++ b/android/src/main/java/com/swmansion/enriched/common/EnrichedConstants.kt
@@ -11,4 +11,6 @@ object EnrichedConstants {
const val TEXT_DEFAULT_FONT_SIZE = 16f
const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
+
+ const val ALLOW_FONT_SCALING_DEFAULT = true
}
diff --git a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextStyle.kt b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextStyle.kt
index e6d6c7461..d263b5d61 100644
--- a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextStyle.kt
+++ b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextStyle.kt
@@ -4,10 +4,10 @@ import android.graphics.Color
import com.facebook.react.bridge.ColorPropConverter
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableMap
-import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
import com.swmansion.enriched.common.EnrichedStyle
import com.swmansion.enriched.common.MentionStyle
+import com.swmansion.enriched.common.pixelFromSpOrDp
import kotlin.math.ceil
data class EnrichedTextStyle(
@@ -63,6 +63,7 @@ data class EnrichedTextStyle(
context: ReactContext,
fontSize: Int,
map: ReadableMap,
+ allowFontScaling: Boolean,
): EnrichedTextStyle {
val h1 = map.getMap("h1")
val h2 = map.getMap("h2")
@@ -80,40 +81,40 @@ data class EnrichedTextStyle(
val mentions = map.getMap("mention")
return EnrichedTextStyle(
- h1FontSize = parseFloat(h1, "fontSize").toInt(),
+ h1FontSize = parseFloat(h1, "fontSize", allowFontScaling).toInt(),
h1Bold = h1?.getBoolean("bold") ?: false,
- h2FontSize = parseFloat(h2, "fontSize").toInt(),
+ h2FontSize = parseFloat(h2, "fontSize", allowFontScaling).toInt(),
h2Bold = h2?.getBoolean("bold") ?: false,
- h3FontSize = parseFloat(h3, "fontSize").toInt(),
+ h3FontSize = parseFloat(h3, "fontSize", allowFontScaling).toInt(),
h3Bold = h3?.getBoolean("bold") ?: false,
- h4FontSize = parseFloat(h4, "fontSize").toInt(),
+ h4FontSize = parseFloat(h4, "fontSize", allowFontScaling).toInt(),
h4Bold = h4?.getBoolean("bold") ?: false,
- h5FontSize = parseFloat(h5, "fontSize").toInt(),
+ h5FontSize = parseFloat(h5, "fontSize", allowFontScaling).toInt(),
h5Bold = h5?.getBoolean("bold") ?: false,
- h6FontSize = parseFloat(h6, "fontSize").toInt(),
+ h6FontSize = parseFloat(h6, "fontSize", allowFontScaling).toInt(),
h6Bold = h6?.getBoolean("bold") ?: false,
blockquoteColor = parseOptionalColor(context, blockquote, "color"),
blockquoteBorderColor = parseColor(context, blockquote, "borderColor"),
- blockquoteStripeWidth = parseFloat(blockquote, "borderWidth").toInt(),
- blockquoteGapWidth = parseFloat(blockquote, "gapWidth").toInt(),
- olGapWidth = parseFloat(orderedList, "gapWidth").toInt(),
- olMarginLeft = calculateOlMarginLeft(fontSize, parseFloat(orderedList, "marginLeft").toInt()),
+ blockquoteStripeWidth = parseFloat(blockquote, "borderWidth", allowFontScaling).toInt(),
+ blockquoteGapWidth = parseFloat(blockquote, "gapWidth", allowFontScaling).toInt(),
+ olGapWidth = parseFloat(orderedList, "gapWidth", allowFontScaling).toInt(),
+ olMarginLeft = calculateOlMarginLeft(fontSize, parseFloat(orderedList, "marginLeft", allowFontScaling).toInt()),
olMarkerFontWeight = parseOptionalFontWeight(orderedList, "markerFontWeight"),
olMarkerColor = parseOptionalColor(context, orderedList, "markerColor"),
- ulGapWidth = parseFloat(unorderedList, "gapWidth").toInt(),
- ulMarginLeft = parseFloat(unorderedList, "marginLeft").toInt(),
- ulBulletSize = parseFloat(unorderedList, "bulletSize").toInt(),
+ ulGapWidth = parseFloat(unorderedList, "gapWidth", allowFontScaling).toInt(),
+ ulMarginLeft = parseFloat(unorderedList, "marginLeft", allowFontScaling).toInt(),
+ ulBulletSize = parseFloat(unorderedList, "bulletSize", allowFontScaling).toInt(),
ulBulletColor = parseColor(context, unorderedList, "bulletColor"),
ulCheckboxBoxColor = parseColor(context, checkboxList, "boxColor"),
- ulCheckboxBoxSize = parseFloat(checkboxList, "boxSize").toInt(),
- ulCheckboxGapWidth = parseFloat(checkboxList, "gapWidth").toInt(),
- ulCheckboxMarginLeft = parseFloat(checkboxList, "marginLeft").toInt(),
+ ulCheckboxBoxSize = parseFloat(checkboxList, "boxSize", allowFontScaling).toInt(),
+ ulCheckboxGapWidth = parseFloat(checkboxList, "gapWidth", allowFontScaling).toInt(),
+ ulCheckboxMarginLeft = parseFloat(checkboxList, "marginLeft", allowFontScaling).toInt(),
aColor = parseColor(context, link, "color"),
aUnderline = parseIsUnderline(link),
aPressColor = parseColor(context, link, "pressColor"),
codeBlockColor = parseColor(context, codeblock, "color"),
codeBlockBackgroundColor = parseColorWithOpacity(context, codeblock, "backgroundColor", 80),
- codeBlockRadius = parseFloat(codeblock, "borderRadius"),
+ codeBlockRadius = parseFloat(codeblock, "borderRadius", allowFontScaling),
inlineCodeColor = parseColor(context, inlineCode, "color"),
inlineCodeBackgroundColor = parseColorWithOpacity(context, inlineCode, "backgroundColor", 80),
mentionsStyle = parseMentionsStyle(context, mentions),
@@ -123,9 +124,10 @@ data class EnrichedTextStyle(
private fun parseFloat(
map: ReadableMap?,
key: String,
+ allowFontScaling: Boolean,
): Float {
if (map == null || !map.hasKey(key) || map.isNull(key)) return 0f
- return ceil(PixelUtil.toPixelFromSP(map.getDouble(key)))
+ return ceil(pixelFromSpOrDp(map.getDouble(key), allowFontScaling))
}
private fun parseColor(
diff --git a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt
index 7ff8ccd5e..d3133ee8c 100644
--- a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt
+++ b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt
@@ -18,7 +18,6 @@ import androidx.appcompat.widget.AppCompatTextView
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.ReactConstants
-import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.uimanager.ViewDefaults
import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
@@ -26,6 +25,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
import com.swmansion.enriched.common.EnrichedConstants
import com.swmansion.enriched.common.GumboNormalizer
import com.swmansion.enriched.common.parser.EnrichedParser
+import com.swmansion.enriched.common.pixelFromSpOrDp
import com.swmansion.enriched.text.spans.EnrichedTextImageSpan
import com.swmansion.enriched.text.spans.interfaces.EnrichedTextClickableSpan
import com.swmansion.enriched.text.spans.interfaces.EnrichedTextSpan
@@ -39,6 +39,15 @@ class EnrichedTextView : AppCompatTextView {
private var fontStyle: Int = ReactConstants.UNSET
private var fontWeight: Int = ReactConstants.UNSET
private var fontSize: Float = EnrichedConstants.TEXT_DEFAULT_FONT_SIZE
+ private var fontSizeRaw: Float? = null
+ private var htmlStyleMap: ReadableMap? = null
+ var allowFontScaling: Boolean = EnrichedConstants.ALLOW_FONT_SCALING_DEFAULT
+ set(value) {
+ if (field == value) return
+ field = value
+ fontSizeRaw?.let { setFontSize(it) }
+ htmlStyleMap?.let { setHtmlStyle(it) }
+ }
private var enrichedStyle: EnrichedTextStyle? = null
private val spannableFactory = EnrichedTextSpanFactory()
@@ -247,7 +256,9 @@ class EnrichedTextView : AppCompatTextView {
fun setHtmlStyle(style: ReadableMap?) {
if (style == null) return
- val enrichedStyle = EnrichedTextStyle.fromReadableMap(context as ReactContext, fontSize.toInt(), style)
+ htmlStyleMap = style
+ val enrichedStyle =
+ EnrichedTextStyle.fromReadableMap(context as ReactContext, fontSize.toInt(), style, allowFontScaling)
this.enrichedStyle = enrichedStyle
val currentText = text ?: return
@@ -287,7 +298,8 @@ class EnrichedTextView : AppCompatTextView {
fun setFontSize(size: Float) {
if (size == 0f) return
- val sizeInt = ceil(PixelUtil.toPixelFromSP(size))
+ fontSizeRaw = size
+ val sizeInt = ceil(pixelFromSpOrDp(size, allowFontScaling))
fontSize = sizeInt
setTextSize(TypedValue.COMPLEX_UNIT_PX, sizeInt)
}
diff --git a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextViewManager.kt b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextViewManager.kt
index e3fb3b083..62f91256c 100644
--- a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextViewManager.kt
+++ b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextViewManager.kt
@@ -121,6 +121,13 @@ class EnrichedTextViewManager :
view?.setHtmlStyle(value)
}
+ override fun setAllowFontScaling(
+ view: EnrichedTextView?,
+ value: Boolean,
+ ) {
+ view?.allowFontScaling = value
+ }
+
override fun setUseHtmlNormalizer(
view: EnrichedTextView?,
value: Boolean,
diff --git a/android/src/main/java/com/swmansion/enriched/text/MeasurementStore.kt b/android/src/main/java/com/swmansion/enriched/text/MeasurementStore.kt
index e07e8080a..690f2a831 100644
--- a/android/src/main/java/com/swmansion/enriched/text/MeasurementStore.kt
+++ b/android/src/main/java/com/swmansion/enriched/text/MeasurementStore.kt
@@ -17,7 +17,9 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
import com.facebook.yoga.YogaMeasureMode
import com.facebook.yoga.YogaMeasureOutput
import com.swmansion.enriched.common.EnrichedConstants
+import com.swmansion.enriched.common.allowFontScalingFromProps
import com.swmansion.enriched.common.parser.EnrichedParser
+import com.swmansion.enriched.common.pixelFromSpOrDp
import kotlin.math.ceil
object MeasurementStore {
@@ -108,7 +110,9 @@ object MeasurementStore {
try {
val style = props?.getMap("htmlStyle") ?: return text
- val enrichedStyle = EnrichedTextStyle.fromReadableMap(context as ReactContext, fontSize, style)
+ val allowFontScaling = allowFontScalingFromProps(props)
+ val enrichedStyle =
+ EnrichedTextStyle.fromReadableMap(context as ReactContext, fontSize, style, allowFontScaling)
val factory = EnrichedTextSpanFactory()
val parsed = EnrichedParser.fromHtml(text, enrichedStyle, factory)
return parsed.trimEnd('\n')
@@ -126,7 +130,7 @@ object MeasurementStore {
else -> EnrichedConstants.TEXT_DEFAULT_FONT_SIZE
}
- return ceil(PixelUtil.toPixelFromSP(fontSize))
+ return ceil(pixelFromSpOrDp(fontSize, allowFontScalingFromProps(props)))
}
private fun getMeasureById(
diff --git a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt
index 885a12ea4..87518d515 100644
--- a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt
+++ b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt
@@ -32,7 +32,6 @@ import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.ReactConstants
-import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.uimanager.StateWrapper
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
@@ -41,6 +40,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
import com.swmansion.enriched.common.EnrichedConstants
import com.swmansion.enriched.common.GumboNormalizer
import com.swmansion.enriched.common.parser.EnrichedParser
+import com.swmansion.enriched.common.pixelFromSpOrDp
import com.swmansion.enriched.textinput.events.MentionHandler
import com.swmansion.enriched.textinput.events.OnContextMenuItemPressEvent
import com.swmansion.enriched.textinput.events.OnInputBlurEvent
@@ -88,6 +88,20 @@ class EnrichedTextInputView :
var isDuringTransaction: Boolean = false
var isRemovingMany: Boolean = false
var scrollEnabled: Boolean = true
+ var allowFontScaling: Boolean = EnrichedConstants.ALLOW_FONT_SCALING_DEFAULT
+ set(value) {
+ if (field != value) {
+ field = value
+ val raw = fontSizeRaw
+ if (raw != null) {
+ setFontSize(raw) // re-invokes invalidateStyles internally
+ } else {
+ htmlStyle.invalidateStyles()
+ }
+ applyLineSpacing()
+ reApplyHtmlStyleForSpans(htmlStyle, htmlStyle) // force re-apply
+ }
+ }
val mentionHandler: MentionHandler? = MentionHandler(this)
var htmlStyle: HtmlStyle = HtmlStyle(this, null)
@@ -108,6 +122,7 @@ class EnrichedTextInputView :
var experimentalSynchronousEvents: Boolean = false
var useHtmlNormalizer: Boolean = false
+ private var fontSizeRaw: Float? = null
var fontSize: Float? = null
private var lineHeight: Float? = null
var submitBehavior: String? = null
@@ -520,8 +535,9 @@ class EnrichedTextInputView :
fun setFontSize(size: Float) {
if (size == 0f) return
+ fontSizeRaw = size
- val sizeInt = ceil(PixelUtil.toPixelFromSP(size))
+ val sizeInt = ceil(pixelFromSpOrDp(size, allowFontScaling))
fontSize = sizeInt
setTextSize(TypedValue.COMPLEX_UNIT_PX, sizeInt)
@@ -546,7 +562,7 @@ class EnrichedTextInputView :
val lh = lineHeight ?: return
spannable.setSpan(
- EnrichedLineHeightSpan(lh),
+ EnrichedLineHeightSpan(lh, allowFontScaling),
0,
spannable.length,
Spannable.SPAN_INCLUSIVE_INCLUSIVE,
diff --git a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt
index 7b5313462..eb16451ca 100644
--- a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt
+++ b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt
@@ -241,6 +241,14 @@ class EnrichedTextInputViewManager :
view.scrollEnabled = scrollEnabled
}
+ @ReactProp(name = "allowFontScaling")
+ override fun setAllowFontScaling(
+ view: EnrichedTextInputView,
+ allowFontScaling: Boolean,
+ ) {
+ view.allowFontScaling = allowFontScaling
+ }
+
override fun onAfterUpdateTransaction(view: EnrichedTextInputView) {
super.onAfterUpdateTransaction(view)
view.afterUpdateTransaction()
diff --git a/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt b/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt
index 59e83383c..09363e08a 100644
--- a/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt
+++ b/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt
@@ -16,7 +16,9 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
import com.facebook.yoga.YogaMeasureMode
import com.facebook.yoga.YogaMeasureOutput
+import com.swmansion.enriched.common.allowFontScalingFromProps
import com.swmansion.enriched.common.parser.EnrichedParser
+import com.swmansion.enriched.common.pixelFromSpOrDp
import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
import com.swmansion.enriched.textinput.styles.HtmlStyle
import java.util.concurrent.ConcurrentHashMap
@@ -130,7 +132,7 @@ object MeasurementStore {
val propsFontSize = props?.getDouble("fontSize")?.toFloat()
if (propsFontSize == null) return defaultView.textSize
- return ceil(PixelUtil.toPixelFromSP(propsFontSize))
+ return ceil(pixelFromSpOrDp(propsFontSize, allowFontScalingFromProps(props)))
}
// Called when view measurements are not available in the store
@@ -142,6 +144,9 @@ object MeasurementStore {
props: ReadableMap?,
): Long {
val defaultView = EnrichedTextInputView(context)
+ val allowFontScaling = allowFontScalingFromProps(props)
+ // mirrors the real view's state
+ defaultView.allowFontScaling = allowFontScaling
val rawText = getInitialText(defaultView, props)
val fontSize = getInitialFontSize(defaultView, props)
@@ -155,7 +160,7 @@ object MeasurementStore {
if (lineHeight > 0f) {
val spannable = SpannableString(rawText)
spannable.setSpan(
- EnrichedLineHeightSpan(lineHeight),
+ EnrichedLineHeightSpan(lineHeight, allowFontScaling),
0,
spannable.length,
Spannable.SPAN_INCLUSIVE_INCLUSIVE,
diff --git a/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedLineHeightSpan.kt b/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedLineHeightSpan.kt
index 83495bd79..ddaeb7a5d 100644
--- a/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedLineHeightSpan.kt
+++ b/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedLineHeightSpan.kt
@@ -5,11 +5,12 @@ import android.text.Spannable
import android.text.TextPaint
import android.text.style.LineHeightSpan
import android.text.style.MetricAffectingSpan
-import com.facebook.react.uimanager.PixelUtil
+import com.swmansion.enriched.common.pixelFromSpOrDp
import com.swmansion.enriched.common.spans.interfaces.EnrichedHeadingSpan
class EnrichedLineHeightSpan(
val lineHeight: Float,
+ val allowFontScaling: Boolean,
) : MetricAffectingSpan(),
LineHeightSpan {
override fun updateDrawState(p0: TextPaint?) {
@@ -33,7 +34,7 @@ class EnrichedLineHeightSpan(
// In the future we may consider adding custom lineHeight support for each paragraph style
if (spannable.getSpans(start, end, EnrichedHeadingSpan::class.java).isNotEmpty()) return
- val lineHeightPx = PixelUtil.toPixelFromSP(lineHeight)
+ val lineHeightPx = pixelFromSpOrDp(lineHeight, allowFontScaling)
val currentHeight = (fm.descent - fm.ascent).toFloat()
if (lineHeightPx <= currentHeight) return
diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/HtmlStyle.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/HtmlStyle.kt
index d72db9eb0..3918e2a22 100644
--- a/android/src/main/java/com/swmansion/enriched/textinput/styles/HtmlStyle.kt
+++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/HtmlStyle.kt
@@ -4,10 +4,11 @@ import android.graphics.Color
import com.facebook.react.bridge.ColorPropConverter
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableMap
-import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
+import com.swmansion.enriched.common.EnrichedConstants
import com.swmansion.enriched.common.EnrichedStyle
import com.swmansion.enriched.common.MentionStyle
+import com.swmansion.enriched.common.pixelFromSpOrDp
import com.swmansion.enriched.textinput.EnrichedTextInputView
import kotlin.Float
import kotlin.Int
@@ -153,9 +154,8 @@ class HtmlStyle : EnrichedStyle {
key: String,
): Float {
val safeMap = ensureValueIsSet(map, key)
-
val value = safeMap.getDouble(key)
- return ceil(PixelUtil.toPixelFromSP(value))
+ return ceil(pixelFromSpOrDp(value, view?.allowFontScaling ?: EnrichedConstants.ALLOW_FONT_SCALING_DEFAULT))
}
private fun parseColorWithOpacity(
diff --git a/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/conversions.h b/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/conversions.h
index f77dd91ec..7c4dcb629 100644
--- a/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/conversions.h
+++ b/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/conversions.h
@@ -18,6 +18,7 @@ inline folly::dynamic toDynamic(const EnrichedTextInputViewProps &props) {
serializedProps["fontStyle"] = props.fontStyle;
serializedProps["fontFamily"] = props.fontFamily;
serializedProps["lineHeight"] = props.lineHeight;
+ serializedProps["allowFontScaling"] = props.allowFontScaling;
serializedProps["htmlStyle"] = toDynamic(props.htmlStyle);
return serializedProps;
@@ -34,6 +35,7 @@ inline folly::dynamic toDynamic(const EnrichedTextViewProps &props) {
serializedProps["fontFamily"] = props.fontFamily;
serializedProps["numberOfLines"] = props.numberOfLines;
serializedProps["ellipsizeMode"] = props.ellipsizeMode;
+ serializedProps["allowFontScaling"] = props.allowFontScaling;
serializedProps["htmlStyle"] = toDynamic(props.htmlStyle);
return serializedProps;
diff --git a/apps/example/src/screens/DevScreen.tsx b/apps/example/src/screens/DevScreen.tsx
index 04c8d03ed..c2cb45c40 100644
--- a/apps/example/src/screens/DevScreen.tsx
+++ b/apps/example/src/screens/DevScreen.tsx
@@ -75,6 +75,7 @@ export function DevScreen({ onSwitch }: DevScreenProps) {
onPasteImages={(e) => editor.handlePasteImagesEvent(e.nativeEvent)}
useHtmlNormalizer
testID="editor-input"
+ allowFontScaling
/>
(_props);
+ if (!viewProps.allowFontScaling) {
+ return;
+ }
+
if (previousTraitCollection.preferredContentSizeCategory !=
self.traitCollection.preferredContentSizeCategory) {
[config invalidateFonts];
diff --git a/ios/EnrichedTextView.mm b/ios/EnrichedTextView.mm
index 5f6384b6a..da00a7f1a 100644
--- a/ios/EnrichedTextView.mm
+++ b/ios/EnrichedTextView.mm
@@ -582,6 +582,14 @@ - (void)updateProps:(Props::Shared const &)props
useHtmlNormalizer = newViewProps.useHtmlNormalizer;
}
+ // allowFontScaling
+ if (newViewProps.allowFontScaling != oldViewProps.allowFontScaling ||
+ isFirstMount) {
+ textView.adjustsFontForContentSizeCategory = newViewProps.allowFontScaling;
+ [newConfig setAllowFontScaling:newViewProps.allowFontScaling];
+ stylePropChanged = YES;
+ }
+
if (stylePropChanged) {
config = newConfig;
}
@@ -698,6 +706,39 @@ - (void)tryUpdatingHeight {
_state->updateState(EnrichedTextViewState(selfRef));
}
+/**
+ * Handles iOS Dynamic Type changes (user changing font size in System
+ * Settings).
+ *
+ * Unlike Android, iOS Views do not automatically rescale existing
+ * NSAttributedStrings when the system font size changes. The text attributes
+ * are static once drawn, so we re-parse the HTML to rebuild every run with
+ * fonts at the new content size category.
+ */
+- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
+ [super traitCollectionDidChange:previousTraitCollection];
+
+ if (_props == nullptr) {
+ return;
+ }
+
+ const auto &viewProps =
+ *std::static_pointer_cast(_props);
+ if (!viewProps.allowFontScaling) {
+ return;
+ }
+
+ if (previousTraitCollection.preferredContentSizeCategory ==
+ self.traitCollection.preferredContentSizeCategory) {
+ return;
+ }
+
+ [config invalidateFonts];
+ [self syncDefaultTypingAttributesFromConfig];
+ [self renderText:[NSString fromCppString:viewProps.text]];
+ [self tryUpdatingHeight];
+}
+
- (std::shared_ptr)getEventEmitter {
if (_eventEmitter != nullptr) {
auto emitter =
diff --git a/ios/config/EnrichedConfig.h b/ios/config/EnrichedConfig.h
index a49fb501f..d39248baf 100644
--- a/ios/config/EnrichedConfig.h
+++ b/ios/config/EnrichedConfig.h
@@ -90,6 +90,8 @@
- (void)setCodeBlockBorderRadius:(CGFloat)newValue;
- (void)invalidateFonts;
- (NSNumber *)scaledPrimaryFontSize;
+- (BOOL)allowFontScaling;
+- (void)setAllowFontScaling:(BOOL)newValue;
- (CGFloat)checkboxListBoxSize;
- (void)setCheckboxListBoxSize:(CGFloat)newValue;
- (CGFloat)checkboxListGapWidth;
diff --git a/ios/config/EnrichedConfig.mm b/ios/config/EnrichedConfig.mm
index 0322b88bc..f7a492d28 100644
--- a/ios/config/EnrichedConfig.mm
+++ b/ios/config/EnrichedConfig.mm
@@ -12,6 +12,7 @@ @implementation EnrichedConfig {
UIFont *_monospacedFont;
BOOL _primaryFontNeedsRecreation;
BOOL _monospacedFontNeedsRecreation;
+ BOOL _allowFontScaling;
NSSet *_mentionIndicators;
CGFloat _h1FontSize;
BOOL _h1Bold;
@@ -67,6 +68,7 @@ - (instancetype)init {
_primaryFontNeedsRecreation = YES;
_monospacedFontNeedsRecreation = YES;
_olMarkerFontNeedsRecreation = YES;
+ _allowFontScaling = YES;
return self;
}
@@ -80,6 +82,7 @@ - (id)copyWithZone:(NSZone *)zone {
copy->_primaryFontFamily = [_primaryFontFamily copy];
copy->_primaryFont = [_primaryFont copy];
copy->_monospacedFont = [_monospacedFont copy];
+ copy->_allowFontScaling = _allowFontScaling;
copy->_mentionIndicators = [_mentionIndicators copy];
copy->_h1FontSize = _h1FontSize;
copy->_h1Bold = _h1Bold;
@@ -160,6 +163,9 @@ - (void)setPrimaryLineHeight:(CGFloat)newValue {
}
- (CGFloat)scaledPrimaryLineHeight {
+ if (!_allowFontScaling) {
+ return [self primaryLineHeight];
+ }
return [[UIFontMetrics defaultMetrics]
scaledValueForValue:[self primaryLineHeight]];
}
@@ -230,6 +236,9 @@ - (void)setMentionIndicators:(NSSet *)newValue {
}
- (CGFloat)h1FontSize {
+ if (!_allowFontScaling) {
+ return _h1FontSize;
+ }
return [[UIFontMetrics defaultMetrics] scaledValueForValue:_h1FontSize];
}
@@ -246,6 +255,9 @@ - (void)setH1Bold:(BOOL)newValue {
}
- (CGFloat)h2FontSize {
+ if (!_allowFontScaling) {
+ return _h2FontSize;
+ }
return [[UIFontMetrics defaultMetrics] scaledValueForValue:_h2FontSize];
}
@@ -262,6 +274,9 @@ - (void)setH2Bold:(BOOL)newValue {
}
- (CGFloat)h3FontSize {
+ if (!_allowFontScaling) {
+ return _h3FontSize;
+ }
return [[UIFontMetrics defaultMetrics] scaledValueForValue:_h3FontSize];
}
@@ -278,6 +293,9 @@ - (void)setH3Bold:(BOOL)newValue {
}
- (CGFloat)h4FontSize {
+ if (!_allowFontScaling) {
+ return _h4FontSize;
+ }
return [[UIFontMetrics defaultMetrics] scaledValueForValue:_h4FontSize];
}
@@ -294,6 +312,9 @@ - (void)setH4Bold:(BOOL)newValue {
}
- (CGFloat)h5FontSize {
+ if (!_allowFontScaling) {
+ return _h5FontSize;
+ }
return [[UIFontMetrics defaultMetrics] scaledValueForValue:_h5FontSize];
}
@@ -310,6 +331,9 @@ - (void)setH5Bold:(BOOL)newValue {
}
- (CGFloat)h6FontSize {
+ if (!_allowFontScaling) {
+ return _h6FontSize;
+ }
return [[UIFontMetrics defaultMetrics] scaledValueForValue:_h6FontSize];
}
@@ -526,7 +550,21 @@ - (void)invalidateFonts {
_olMarkerFontNeedsRecreation = YES;
}
+- (BOOL)allowFontScaling {
+ return _allowFontScaling;
+}
+
+- (void)setAllowFontScaling:(BOOL)newValue {
+ if (_allowFontScaling != newValue) {
+ _allowFontScaling = newValue;
+ [self invalidateFonts];
+ }
+}
+
- (NSNumber *)scaledPrimaryFontSize {
+ if (!_allowFontScaling) {
+ return [self primaryFontSize];
+ }
CGFloat scaledSize = [[UIFontMetrics defaultMetrics]
scaledValueForValue:[[self primaryFontSize] floatValue]];
return @(scaledSize);
diff --git a/src/native/EnrichedText.tsx b/src/native/EnrichedText.tsx
index e472f8c21..ec24debfd 100644
--- a/src/native/EnrichedText.tsx
+++ b/src/native/EnrichedText.tsx
@@ -34,6 +34,7 @@ export const EnrichedText = ({
numberOfLines = 0,
selectable = false,
selectionColor,
+ allowFontScaling = true,
onLinkPress: _onLinkPress,
onMentionPress: _onMentionPress,
...rest
@@ -105,6 +106,7 @@ export const EnrichedText = ({
numberOfLines={numberOfLines}
selectable={selectable}
selectionColor={selectionColor}
+ allowFontScaling={allowFontScaling}
onLinkPress={onLinkPress}
onMentionPress={onMentionPress}
{...rest}
diff --git a/src/native/EnrichedTextInput.tsx b/src/native/EnrichedTextInput.tsx
index 51e68fa14..ba31ba978 100644
--- a/src/native/EnrichedTextInput.tsx
+++ b/src/native/EnrichedTextInput.tsx
@@ -80,6 +80,7 @@ export const EnrichedTextInput = ({
androidExperimentalSynchronousEvents = ENRICHED_TEXT_INPUT_DEFAULT_PROPS.androidExperimentalSynchronousEvents,
useHtmlNormalizer = ENRICHED_TEXT_INPUT_DEFAULT_PROPS.useHtmlNormalizer,
scrollEnabled = ENRICHED_TEXT_INPUT_DEFAULT_PROPS.scrollEnabled,
+ allowFontScaling = ENRICHED_TEXT_INPUT_DEFAULT_PROPS.allowFontScaling,
...rest
}: EnrichedTextInputProps) => {
const nativeRef = useRef(null);
@@ -364,6 +365,7 @@ export const EnrichedTextInput = ({
}
useHtmlNormalizer={useHtmlNormalizer}
scrollEnabled={scrollEnabled}
+ allowFontScaling={allowFontScaling}
{...rest}
/>
);
diff --git a/src/spec/EnrichedTextInputNativeComponent.ts b/src/spec/EnrichedTextInputNativeComponent.ts
index b344aff64..2eb29ded1 100644
--- a/src/spec/EnrichedTextInputNativeComponent.ts
+++ b/src/spec/EnrichedTextInputNativeComponent.ts
@@ -370,6 +370,7 @@ export interface NativeProps extends ViewProps {
returnKeyType?: string;
returnKeyLabel?: string;
submitBehavior?: string;
+ allowFontScaling?: boolean;
// event callbacks
onInputFocus?: DirectEventHandler;
diff --git a/src/spec/EnrichedTextNativeComponent.ts b/src/spec/EnrichedTextNativeComponent.ts
index a47fc4438..fec62bd3d 100644
--- a/src/spec/EnrichedTextNativeComponent.ts
+++ b/src/spec/EnrichedTextNativeComponent.ts
@@ -84,6 +84,7 @@ export interface NativeProps extends ViewProps {
text: string;
htmlStyle?: EnrichedTextHtmlStyleInternal;
useHtmlNormalizer: boolean;
+ allowFontScaling?: boolean;
// ReactNative TextProps
ellipsizeMode: string;
diff --git a/src/types.ts b/src/types.ts
index fffd83c9f..33ed31f64 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -498,6 +498,11 @@ export interface EnrichedTextInputProps extends Omit {
* Disabled by default.
*/
useHtmlNormalizer?: boolean;
+ /**
+ * If true, fonts will scale to respect the system's accessibility text size.
+ * Enabled by default.
+ */
+ allowFontScaling?: boolean;
}
export interface EnrichedTextInstance extends NativeMethods {}
@@ -512,6 +517,11 @@ export interface EnrichedTextProps extends ViewProps {
numberOfLines?: number;
selectable?: boolean;
selectionColor?: ColorValue;
+ /**
+ * If true, fonts will scale to respect the system's accessibility text size.
+ * Enabled by default.
+ */
+ allowFontScaling?: boolean;
onLinkPress?: (event: OnLinkPressEvent) => void;
onMentionPress?: (event: OnMentionPressEvent) => void;
}
diff --git a/src/utils/EnrichedTextInputDefaultProps.ts b/src/utils/EnrichedTextInputDefaultProps.ts
index e34e2f4b1..ac41ade56 100644
--- a/src/utils/EnrichedTextInputDefaultProps.ts
+++ b/src/utils/EnrichedTextInputDefaultProps.ts
@@ -6,4 +6,5 @@ export const ENRICHED_TEXT_INPUT_DEFAULT_PROPS = {
scrollEnabled: true,
androidExperimentalSynchronousEvents: false,
useHtmlNormalizer: false,
+ allowFontScaling: true,
} as const;