Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
510d308
fix(ios): bullet render in unordered lists
hejsztynx Jul 27, 2026
b05bd53
feat(ios): dynamic ol margins
hejsztynx Jul 28, 2026
1493c35
feat(android): dynamic ordered list margin
hejsztynx Jul 29, 2026
999f54c
fix(ios): ordered lists recalculation on edit
hejsztynx Jul 29, 2026
edf431c
fix(android): ol margins compute at setValue
hejsztynx Jul 29, 2026
478de71
fix(android): parse original marginLeft values
hejsztynx Jul 30, 2026
4d3ec61
feat: dynamic ol markers mobile e2e tests
hejsztynx Jul 30, 2026
b4b98cf
chore: update e2e tests screenshot baselines
hejsztynx Jul 30, 2026
84963b1
chore: screenshot baseline update
hejsztynx Jul 30, 2026
540b211
fix(ios): ol indent resolution when applying typing attributes
hejsztynx Jul 31, 2026
07f3960
test: update e2e baseline screenshots
hejsztynx Jul 31, 2026
ab42817
test: update e2e baseline screenshots
hejsztynx Jul 31, 2026
c5008f0
fix(android): true typeface on ol margin calculation
hejsztynx Aug 3, 2026
35f2ca3
fix(ios): nil pointer handling
hejsztynx Aug 3, 2026
af7834f
refactor(android): ol index update and margin update logic separation
hejsztynx Aug 3, 2026
3e28bba
feat(android): ordered list relayout on margin change optimization
hejsztynx Aug 3, 2026
c9ed47f
fix(android): distinct list detection edge case
hejsztynx Aug 3, 2026
3ce8b5d
Merge branch 'main' into @ksienkiewicz/fix-list-styling
hejsztynx Aug 5, 2026
05079bb
Merge branch 'main' into @ksienkiewicz/fix-list-styling
hejsztynx Aug 10, 2026
af516e3
Merge branch 'main' into @ksienkiewicz/fix-list-styling
hejsztynx Aug 13, 2026
64d4ada
docs: update docs
hejsztynx Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .maestro/enrichedInput/flows/dynamic_ol_markers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
appId: swmansion.enriched.example
---
# Validates that ordered list margins dynamically adjust when transitioning
# between single-digit (9) and double-digit (10) indexes.
- launchApp

- tapOn:
id: 'toggle-screen-button'

- runFlow:
file: '../subflows/set_editor_value.yaml'
env:
VALUE: >
<html>
<ol>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
<li>E</li>
<li>F</li>
<li>G</li>
<li>H</li>
<li>I</li>
</ol>
</html>

- tapOn:
id: 'size-max-button'

- runFlow:
file: '../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'dynamic_ol_markers_before'

- tapOn:
id: "editor-input"
point: "50%, 95%"
- pressKey: Enter
- inputText: "J"

- runFlow:
file: '../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'dynamic_ol_markers_after'

- tapOn:
id: "editor-input"
point: "50%, 95%"
- pressKey: Backspace
- pressKey: Backspace

- runFlow:
file: '../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'dynamic_ol_markers_revert'
Binary file modified .maestro/enrichedInput/screenshots/android/checkbox_toggle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/android/font_scaling.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/android/inline_styles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/android/link_not_extended.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/android/links_visual_auto.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/android/links_visual_manual.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/ios/empty_element_parsing.png
Binary file modified .maestro/enrichedInput/screenshots/ios/empty_lists_parsing.png
Binary file modified .maestro/enrichedInput/screenshots/ios/paragraph_styles_lists.png
41 changes: 41 additions & 0 deletions .maestro/enrichedText/flows/dynamic_ol_markers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
appId: swmansion.enriched.example
---
# Validates that ordered list margins dynamically adjust when transitioning
# between single-digit (9) and double-digit (10) indexes.
- launchApp

- tapOn:
id: 'toggle-screen-button'

- tapOn:
id: 'toggle-enriched-text-screen-button'

- runFlow:
file: '../subflows/set_enriched_text_value.yaml'
env:
VALUE: >
<html>
<ol>
<li>A</li>
<li>B</li>
<li>C</li>
</ol>
<br>
<ol>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
<li>E</li>
<li>F</li>
<li>G</li>
<li>H</li>
<li>I</li>
<li>J</li>
</ol>
</html>

- runFlow:
file: '../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'dynamic_ol_markers'
Binary file modified .maestro/enrichedText/screenshots/android/alignment_visual.png
Binary file modified .maestro/enrichedText/screenshots/android/font_scaling.png
Binary file modified .maestro/enrichedText/screenshots/ios/alignment_visual.png
Binary file modified .maestro/enrichedText/screenshots/ios/font_scaling.png
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.swmansion.enriched.common

import android.graphics.Paint
import android.text.Spannable
import android.text.style.ParagraphStyle
import com.swmansion.enriched.common.spans.EnrichedOrderedListSpan
import com.swmansion.enriched.textinput.utils.getSafeSpanBoundaries

// Recomputes the shared marker column width for every ordered-list item.
fun updateOrderedListColumnMargins(
text: Spannable,
paint: Paint,
) {
val spans = text.getSpans(0, text.length, EnrichedOrderedListSpan::class.java)
val sortedSpans = spans.sortedBy { text.getSpanStart(it) }

val changedLists = mutableSetOf<MutableList<EnrichedOrderedListSpan>>()
var currentList = mutableListOf<EnrichedOrderedListSpan>()
var currentListChanged = false
var previousIndex = 0
var highestIndex = 0

for (span in sortedSpans.reversed()) {
if (span.index >= previousIndex) {
if (currentListChanged) {
// we'll re-layout that list
changedLists.add(currentList)
}

// we entered a new distinct list
currentList = mutableListOf()
currentListChanged = false
highestIndex = span.index
}

currentList.add(span)
if (span.updateColumnMargin(paint, highestIndex)) {
currentListChanged = true
}

previousIndex = span.index
}

if (currentListChanged) {
changedLists.add(currentList)
}

// A single empty ParagraphStyle over the whole list forces one re-layout of it.
// Uses the same trick as EnrichedSpanWatcher.updateNextLineLayout.
for (list in changedLists) {
forceOrderedListRelayout(text, list)
}
}

private fun forceOrderedListRelayout(
text: Spannable,
listSpans: List<EnrichedOrderedListSpan>,
) {
if (listSpans.isEmpty()) return

// Because the list was populated during a reversed() loop, the elements
// are in descending order. last() is the start of the list, first() is the end.
val start = text.getSpanStart(listSpans.last())
val end = text.getSpanEnd(listSpans.first())

if (start < 0 || end < 0 || start > end) return

class EmptySpan : ParagraphStyle

val (safeStart, safeEnd) = text.getSafeSpanBoundaries(start, end)
text.getSpans(safeStart, safeEnd, EmptySpan::class.java).forEach { text.removeSpan(it) }
text.setSpan(EmptySpan(), safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,36 @@ import android.text.style.LeadingMarginSpan
import android.text.style.MetricAffectingSpan
import com.swmansion.enriched.common.EnrichedStyle
import com.swmansion.enriched.common.spans.interfaces.EnrichedParagraphSpan
import kotlin.math.ceil
import kotlin.math.max

open class EnrichedOrderedListSpan(
var index: Int,
private val enrichedStyle: EnrichedStyle,
) : MetricAffectingSpan(),
LeadingMarginSpan,
EnrichedParagraphSpan {
var columnMargin = enrichedStyle.olMarginLeft

// Computes the marker column width from the widest marker in the list ("<highestIndex>.").
// Returns true when the column width actually changed, so callers can force a relayout.
fun updateColumnMargin(
paint: Paint,
highestIndex: Int,
): Boolean {
val highestIndexText = "$highestIndex."

val originalTypeface = paint.typeface
paint.typeface = getTypeface(enrichedStyle.olMarkerFontWeight, originalTypeface)
val highestIndexWidth = ceil(paint.measureText(highestIndexText)).toInt()
paint.typeface = originalTypeface

val newColumnMargin = max(enrichedStyle.olMarginLeft, highestIndexWidth)
if (newColumnMargin == columnMargin) return false
columnMargin = newColumnMargin
return true
}

override fun updateMeasureState(p0: TextPaint) {
// Do nothing, but inform layout that this span affects text metrics
}
Expand All @@ -25,7 +48,7 @@ open class EnrichedOrderedListSpan(
// Do nothing, but inform layout that this span affects text metrics
}

override fun getLeadingMargin(first: Boolean): Int = enrichedStyle.olMarginLeft + enrichedStyle.olGapWidth
override fun getLeadingMargin(first: Boolean): Int = columnMargin + enrichedStyle.olGapWidth

override fun drawLeadingMargin(
canvas: Canvas,
Expand All @@ -42,17 +65,17 @@ open class EnrichedOrderedListSpan(
layout: Layout?,
) {
if (first) {
val originalColor = paint.color
val originalTypeface = paint.typeface
paint.color = enrichedStyle.olMarkerColor ?: originalColor
paint.typeface = getTypeface(enrichedStyle.olMarkerFontWeight, originalTypeface)

val text = "$index."
val width = paint.measureText(text)

val yPosition = baseline.toFloat()
val xPosition = (enrichedStyle.olMarginLeft + x - width / 2) * dir

val originalColor = paint.color
val originalTypeface = paint.typeface
val xPosition = (columnMargin + x - width) * dir

paint.color = enrichedStyle.olMarkerColor ?: originalColor
paint.typeface = getTypeface(enrichedStyle.olMarkerFontWeight, originalTypeface)
canvas.drawText(text, xPosition, yPosition, paint)

paint.color = originalColor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ data class EnrichedTextStyle(
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()),
olMarginLeft = parseFloat(orderedList, "marginLeft", allowFontScaling).toInt(),
olMarkerFontWeight = parseOptionalFontWeight(orderedList, "markerFontWeight"),
olMarkerColor = parseOptionalColor(context, orderedList, "markerColor"),
ulGapWidth = parseFloat(unorderedList, "gapWidth", allowFontScaling).toInt(),
Expand Down Expand Up @@ -169,14 +169,6 @@ data class EnrichedTextStyle(
return parseFontWeight(weight)
}

private fun calculateOlMarginLeft(
fontSize: Int,
userMargin: Int,
): Int {
val leadMargin = fontSize / 2
return leadMargin + userMargin
}

private fun parseMentionsStyle(
context: ReactContext,
map: ReadableMap?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.swmansion.enriched.common.EnrichedSpanFlags
import com.swmansion.enriched.common.GumboNormalizer
import com.swmansion.enriched.common.parser.EnrichedParser
import com.swmansion.enriched.common.pixelFromSpOrDp
import com.swmansion.enriched.common.updateOrderedListColumnMargins
import com.swmansion.enriched.text.spans.EnrichedTextImageSpan
import com.swmansion.enriched.text.spans.interfaces.EnrichedTextClickableSpan
import com.swmansion.enriched.text.spans.interfaces.EnrichedTextSpan
Expand Down Expand Up @@ -179,6 +180,7 @@ class EnrichedTextView : AppCompatTextView {
val parsed = parseText(text, style)
if (parsed != null) {
parsedText = parsed
(parsed as? Spannable)?.let { updateOrderedListColumnMargins(it, paint) }
setText(parsed, BufferType.NORMAL)
observeAsyncImages()
} else {
Expand Down Expand Up @@ -283,6 +285,7 @@ class EnrichedTextView : AppCompatTextView {
}

if (modified) {
updateOrderedListColumnMargins(spannable, paint)
this.text = spannable
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import com.swmansion.enriched.common.EnrichedSpanFlags
import com.swmansion.enriched.common.GumboNormalizer
import com.swmansion.enriched.common.parser.EnrichedParser
import com.swmansion.enriched.common.pixelFromSpOrDp
import com.swmansion.enriched.common.updateOrderedListColumnMargins
import com.swmansion.enriched.textinput.events.MentionHandler
import com.swmansion.enriched.textinput.events.OnContextMenuItemPressEvent
import com.swmansion.enriched.textinput.events.OnInputBlurEvent
Expand Down Expand Up @@ -448,6 +449,9 @@ class EnrichedTextInputView :

runAsATransaction {
val newText = if (shouldParseHtml) parseText(value) else value
// Compute ordered-list marker column widths before setText so the first layout reserves
// the correct leading margin (getLeadingMargin runs during layout, before drawing).
(newText as? Spannable)?.let { updateOrderedListColumnMargins(it, paint) }
setText(newText)
applyLineSpacing()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,7 @@ class HtmlStyle : EnrichedStyle {
blockquoteStripeWidth = parseFloat(blockquoteStyle, "borderWidth").toInt()

val olStyle = style.getMap("ol")
val userDefinedMarginLeft = parseFloat(olStyle, "marginLeft").toInt()
val calculatedMarginLeft = calculateOlMarginLeft(view, userDefinedMarginLeft)
olMarginLeft = calculatedMarginLeft
olMarginLeft = parseFloat(olStyle, "marginLeft").toInt()
olGapWidth = parseFloat(olStyle, "gapWidth").toInt()
olMarkerColor = parseOptionalColor(olStyle, "markerColor")
olMarkerFontWeight = parseOptionalFontWeight(olStyle, "markerFontWeight")
Expand Down Expand Up @@ -213,16 +211,6 @@ class HtmlStyle : EnrichedStyle {
throw Error("Specified textDecorationLine value is not supported: $underline. Supported values are 'underline' and 'none'.")
}

private fun calculateOlMarginLeft(
view: EnrichedTextInputView?,
userMargin: Int,
): Int {
val fontSize = view?.fontSize?.toInt() ?: 0
val leadMargin = fontSize / 2

return leadMargin + userMargin
}

private fun ensureValueIsSet(
map: ReadableMap?,
key: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.text.SpannableStringBuilder
import android.text.Spanned
import com.swmansion.enriched.common.EnrichedConstants
import com.swmansion.enriched.common.EnrichedSpanFlags
import com.swmansion.enriched.common.updateOrderedListColumnMargins
import com.swmansion.enriched.textinput.EnrichedTextInputView
import com.swmansion.enriched.textinput.spans.EnrichedInputCheckboxListSpan
import com.swmansion.enriched.textinput.spans.EnrichedInputOrderedListSpan
Expand Down Expand Up @@ -115,6 +116,8 @@ class ListStyles(
val index = getOrderedListIndex(text, spanStart)
span.setListIndex(index)
}

updateOrderedListColumnMargins(text, view.paint)
}

private fun toggleStyle(
Expand Down
2 changes: 1 addition & 1 deletion apps/example/src/constants/editorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const htmlStyle = {
},
ol: {
gapWidth: 16,
marginLeft: 24,
marginLeft: 16,
markerColor: 'navy',
markerFontWeight: 'bold',
},
Expand Down
9 changes: 9 additions & 0 deletions docs/docs/api-reference/enriched-text-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,15 @@ By marker, we mean the number that denotes consecutive lines of the list.
[color](https://reactnative.dev/docs/colors) value; if not set, defaults to
the input's [color](#style)

:::note

As you may have noticed, there is no way of defining the marker's width unlike with other list styles.
This is because the value of an ordered list marker changes, which makes its width dynamic.
The marker is rendered _inside_ the defined space with `marginLeft`, so keep this in mind
when styling your UI.

:::

### ul (unordered list)

By bullet, we mean the dot that begins each line of the list.
Expand Down
2 changes: 1 addition & 1 deletion ios/extensions/LayoutManagerExtension.mm
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ - (void)drawDecimal:(id<EnrichedViewHost>)host
indent:(CGFloat)indent {
CGFloat gapWidth = [host.config orderedListGapWidth];
CGSize markerSize = [marker sizeWithAttributes:markerAttributes];
CGFloat markerX = origin.x + indent - gapWidth - markerSize.width / 2;
CGFloat markerX = origin.x + indent - gapWidth - markerSize.width;
UIFont *markerFont = markerAttributes[NSFontAttributeName];

// drawAtPoint draws from the top-left bounding box of the string.
Expand Down
Loading
Loading