Repository navigation
Conversation
Adds optional vertical margins to h1–h6, ul, ol and ulCheckbox in htmlStyle, for EnrichedTextInput and EnrichedText on iOS, Android and web. Adjacent margins collapse into the larger one and no space is added above the first block or below the last. The spacing is visual only: paragraph spacing on iOS, a LineHeightSpan on Android and CSS margins on web, so the HTML, the text and selection offsets don't change. Closes software-mansion#832 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved checkbox hit-testing issues and stateful native line-height handling require corrections and final device-level validation.
2 open issues
What changed in this PR
Adds optional heading and list margins to htmlStyle for EnrichedTextInput and EnrichedText, addressing #832’s request for vertical spacing without changing document content.
Changes:
- Extends public types, native specs, and defaults with zero-default margins.
- Implements collapsed block spacing across iOS, Android, and web, including native measurement and checkbox hit-testing adjustments.
- Documents the API and updates examples and CSS-variable tests.
| File | Description |
|---|---|
| src/web/styleConversion/htmlStyleToCSSVariables.ts | Converts margins to CSS variables. |
| src/web/styleConversion/__tests__/htmlStyleToCSSVariables.test.ts | Tests margin-variable conversion. |
| src/web/EnrichedText.css | Applies margins and removes outer spacing. |
| src/utils/defaultHtmlStyle.ts | Adds zero-default margins. |
| src/types.ts | Exposes optional margin properties. |
| src/spec/EnrichedTextNativeComponent.ts | Extends renderer native props. |
| src/spec/EnrichedTextInputNativeComponent.ts | Extends editor native props. |
| ios/utils/CheckboxHitTestUtils.mm | Uses used-line bounds for checkbox positioning. |
| ios/utils/BlockSpacingUtils.mm | Computes collapsed paragraph spacing. |
| ios/utils/BlockSpacingUtils.h | Declares the spacing utility. |
| ios/EnrichedTextView.mm | Applies renderer margin configuration. |
| ios/EnrichedTextInputView.mm | Refreshes spacing after editor styling. |
| ios/config/EnrichedConfig.mm | Stores and scales block margins. |
| ios/config/EnrichedConfig.h | Declares margin configuration methods. |
| docs/docs/core-functionalities/styling-the-input.mdx | Adds a spacing guide and example. |
| docs/docs/api-reference/enriched-text-input.md | Documents margin properties and behavior. |
| apps/example/src/screens/DevScreen.tsx | Enables margins in the native demo editor. |
| apps/example/src/constants/editorConfig.ts | Defines margin-enabled demo styles. |
| apps/example/src/components/TextRenderer.tsx | Enables margins in the native demo renderer. |
| apps/example-web/src/defaultHtmlStyle.ts | Defines margin-enabled web demo styles. |
| apps/example-web/src/components/TextRenderer.tsx | Enables margins in the web demo renderer. |
| apps/example-web/src/App.tsx | Enables margins in the web demo editor. |
| android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt | Excludes spacing from checkbox tap bounds. |
| android/src/main/java/com/swmansion/enriched/textinput/styles/HtmlStyle.kt | Parses editor margins. |
| android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt | Includes spacing in editor measurement. |
| android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewLayoutManager.kt | Refreshes spacing during layout invalidation. |
| android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt | Integrates spacing and line-height priorities. |
| android/src/main/java/com/swmansion/enriched/text/MeasurementStore.kt | Includes spacing in renderer measurement. |
| android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt | Applies spacing after parsing and style updates. |
| android/src/main/java/com/swmansion/enriched/text/EnrichedTextStyle.kt | Parses renderer margins. |
| android/src/main/java/com/swmansion/enriched/common/spans/EnrichedBlockSpacingSpan.kt | Adds spacing to paragraph boundary lines. |
| android/src/main/java/com/swmansion/enriched/common/EnrichedStyle.kt | Extends the shared style contract. |
| android/src/main/java/com/swmansion/enriched/common/EnrichedSpanFlags.kt | Orders line-height and spacing spans. |
| android/src/main/java/com/swmansion/enriched/common/EnrichedBlockSpacing.kt | Computes and updates collapsed spacing spans. |
Review effort: Balanced
Comment on lines
+67
to
+69
| CGRect lineRect = | ||
| [layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphIndex | ||
| effectiveRange:nil]; |
Comment on lines
+67
to
71
| CGRect lineRect = | ||
| [layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphIndex | ||
| effectiveRange:nil]; | ||
|
|
||
| CGFloat originX = lineRect.origin.x + config.checkboxListMarginLeft; |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Closes #832. Follows up on #259, where vertical spacing between elements came up and you asked for a separate feature request.
Adds optional
marginTopandmarginBottomtohtmlStyleforh1–h6,ul,olandulCheckbox, in bothEnrichedTextInputandEnrichedText:Both default to
0, so nothing changes unless you set them. The names and behaviour matchmarkdownStylein react-native-enriched-markdown.Behaviour (the same on all three platforms):
max(previous.marginBottom, next.marginTop).allowTrailingMargin: false).How it works
BlockSpacingUtilssetsparagraphSpacingBefore/paragraphSpacingon the paragraphs next to each gap. It runs over the whole text after every styling pass in the input (anyTextMayHaveBeenModified) and after rendering inEnrichedText, because a block's spacing depends on its neighbours. It returns early when no margins are set, so existing users don't pay for it. The margins live inEnrichedConfig, keyed byStyleType, and scale with Dynamic Type like the heading font sizes. The gap goes on the block whose margin it is, so blockquote borders and code block backgrounds (drawn over whole line fragments) don't stretch into it. Bullets and checkboxes are drawn fromusedRect, which leaves out paragraph spacing. I checked that with TextKit directly. The checkbox hit test used the line fragment rect, so I switched it to the used rect.EnrichedBlockSpacingcomputes the same gaps and keeps oneEnrichedBlockSpacingSpan(LineHeightSpan+UpdateLayout) on each spaced paragraph. The span adds to the first line's ascent and the last line's descent. The pass only adds or removes spans whose values changed, soDynamicLayoutreflows just the affected paragraphs. The input runs it frominvalidateLayout().EnrichedTextruns it when it parses the value or the style changes. BothMeasurementStores run it too, so the measured height includes the spacing.EnrichedSpanFlagsnow orders theLineHeightSpans explicitly: paragraph spans (checkbox size) run first, thenEnrichedLineHeightSpan, then the spacing span. So the margins add to the final line box. Before, the line-height span's order depended on when it was inserted. A checkbox tap in the margin above an item doesn't toggle it, matching the iOS hit test.--et-h1-margin-topetc.) feed themarginof the block elements. The browser collapses margins on its own.:first-child/:last-childrules drop the outer margins.HtmlStyle, both native component specs andDEFAULT_HTML_STYLE. They need to be in the defaults because Android'sHtmlStylerequires every key.Docs: the
HtmlStylereference has the new fields and a "Vertical spacing" section, and the styling guide shows an example. The example apps' main screens (nativeDevScreen, webApp) use the margins. The test screens behind the E2E screenshots don't, so those screenshots stay the same.Known limitation: on both platforms the caret on a block's first line is as tall as the line box, so it also covers the space above the line. That comes from the platform text views (
caretRectForPosition/ the Android cursor drawable both use the line height). I left it alone to keep this PR small.Android detail:
StaticLayoutpasses a line's adjusted ascent on to the next line of a wrapped paragraph, so the space above a heading would repeat between its own wrapped lines.EnrichedBlockSpacingSpanremembers what it added on the first line and undoes it on the next ones.Not in this PR (happy to follow up if you want them): margins for
p,blockquoteandcodeblock. A blockquote or code block draws its decoration over the whole line fragment, so it needs its own handling.Test Plan
Use the example app's main screen with HTML that mixes the blocks, e.g.
EnrichedText. Expected: the heading/list gaps are the collapsed values (e.g. list →h4ismax(8, 12) = 12). There is no extra space above "Intro paragraph" or below "Last heading". Items inside a list keep their normal spacing.H1on a plain paragraph and off again. Expected: the spacing shows up and goes away with the style.main.Verified on:
EnrichedTextandEnrichedTextInput: steps 1–3, plus Enter in the middle of a heading. I checked the emitted HTML and plain text after each edit.Medium_Phone_API_34emulator.EnrichedTextandEnrichedTextInput: steps 1–3 with the emitted HTML checked, plus a heading that wraps onto two lines (the margin is only added above the first line and below the last). A tap in the margin above a checkbox item doesn't toggle it, and a tap on the box does.EnrichedText, the zero outer margins, and Enter after a heading. They match iOS.yarn typecheck,yarn lint,yarn lint-clang:ios, ktlint, the web unit tests (new cases inhtmlStyleToCSSVariables.test.ts) and the docs build pass.Screenshots / Videos
Left: no margins (same as
main). Right: with the example's margins. Same HTML.Compatibility
Checklist
🤖 Generated with Claude Code