Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 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
7e781a5
Revert "docs: update docs"
hejsztynx Aug 19, 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'
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/empty_lists_parsing.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.
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
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_lists_parsing.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/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,30 @@
package com.swmansion.enriched.common

import android.graphics.Paint
import android.text.Spanned
import com.swmansion.enriched.common.spans.EnrichedOrderedListSpan

// Recomputes the shared marker column width for every ordered-list item.
// Returns true when any item's column width changed, so callers can force a relayout.
fun updateOrderedListColumnMargins(
text: Spanned,
paint: Paint,
): Boolean {
val spans = text.getSpans(0, text.length, EnrichedOrderedListSpan::class.java)
val sortedSpans = spans.sortedBy { text.getSpanStart(it) }

var changed = false
var previousIndex = 0
var highestIndex = 0
for (span in sortedSpans.reversed()) {
val currentIndex = span.index
if (currentIndex > previousIndex) {
highestIndex = currentIndex
}
if (span.updateColumnMargin(paint, highestIndex)) changed = true

previousIndex = currentIndex
}

return changed
}
Comment thread
hejsztynx marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,31 @@ 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 highestIndexWidth = ceil(paint.measureText(highestIndexText)).toInt()
val newColumnMargin = max(enrichedStyle.olMarginLeft, highestIndexWidth)
if (newColumnMargin == columnMargin) return false
columnMargin = newColumnMargin
return true
}
Comment thread
hejsztynx marked this conversation as resolved.

override fun updateMeasureState(p0: TextPaint) {
// Do nothing, but inform layout that this span affects text metrics
}
Expand All @@ -25,7 +43,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 @@ -46,7 +64,7 @@ open class EnrichedOrderedListSpan(
val width = paint.measureText(text)

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

val originalColor = paint.color
val originalTypeface = paint.typeface
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? Spanned)?.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 @@ -13,6 +13,7 @@ import android.text.Editable
import android.text.InputType
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned
import android.util.AttributeSet
import android.util.Log
import android.util.Patterns
Expand Down Expand Up @@ -43,6 +44,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 +450,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? Spanned)?.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 @@ -4,8 +4,10 @@ import android.text.Editable
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.ParagraphStyle
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 @@ -104,17 +106,36 @@ class ListStyles(
return true
}

fun updateOrderedListIndexes(
text: Spannable,
position: Int,
) {
val spans = text.getSpans(position + 1, text.length, EnrichedInputOrderedListSpan::class.java)
fun updateOrderedListIndexes(text: Spannable) {
val spans = text.getSpans(0, text.length, EnrichedInputOrderedListSpan::class.java)
val sortedSpans = spans.sortedBy { text.getSpanStart(it) }
for (span in sortedSpans) {
val spanStart = text.getSpanStart(span)
val index = getOrderedListIndex(text, spanStart)
span.setListIndex(index)
}

val marginsChanged = updateOrderedListColumnMargins(text, view.paint)

// Ordered list margins got updated, so we need to force a re-layout of that list.
// Uses the same empty ParagraphStyle trick as EnrichedSpanWatcher.updateNextLineLayout.
if (marginsChanged) {
forceOrderedListRelayout(text, sortedSpans)
}
}

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

class EmptySpan : ParagraphStyle

val start = text.getSpanStart(sortedSpans.first())
val end = text.getSpanEnd(sortedSpans.last())
val (safeStart, safeEnd) = text.getSafeSpanBoundaries(start, end)
text.setSpan(EmptySpan(), safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
Comment thread
Copilot marked this conversation as resolved.
Outdated
}

private fun toggleStyle(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class EnrichedSpanWatcher(
end: Int,
) {
updateNextLineLayout(what, text, end)
updateUnorderedListSpans(what, text, end)
updateUnorderedListSpans(what, text)
emitEvent(text, what)
}

Expand All @@ -38,7 +38,7 @@ class EnrichedSpanWatcher(
end: Int,
) {
updateNextLineLayout(what, text, end)
updateUnorderedListSpans(what, text, end)
updateUnorderedListSpans(what, text)
emitEvent(text, what)
}

Expand All @@ -56,10 +56,9 @@ class EnrichedSpanWatcher(
private fun updateUnorderedListSpans(
what: Any,
text: Spannable,
end: Int,
) {
if (what is EnrichedInputOrderedListSpan) {
view.listStyles?.updateOrderedListIndexes(text, end)
view.listStyles?.updateOrderedListIndexes(text)
}
}

Expand Down
4 changes: 2 additions & 2 deletions apps/example/src/constants/editorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ export const htmlStyle = {
},
},
ol: {
gapWidth: 16,
marginLeft: 24,
gapWidth: 32,
marginLeft: 16,
markerColor: 'navy',
markerFontWeight: 'bold',
},
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