Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
appId: swmansion.enriched.markdown.example
tags:
- input
- smoke
- unordered-list
- block
---
- launchApp

- runFlow:
file: '../../../../subflows/move_to_playground.yaml'

# Start a bullet list, then type two items separated by Return. Return on a
# non-empty item must continue the list (new bullet at the same depth); the
# second item is then indented one level via the toolbar.
- tapOn:
id: focus-button
- waitForAnimationToEnd

# The list buttons sit past H1-H6 in the horizontally scrolling toolbar and are
# offscreen on phone-width viewports; swipe the toolbar left to reveal them.
- swipe:
from:
id: toolbar-bold
direction: LEFT
- swipe:
from:
id: toolbar-h2
direction: LEFT
- waitForAnimationToEnd

- tapOn:
id: toolbar-unordered-list
- waitForAnimationToEnd

- inputText: 'First item'
- waitForAnimationToEnd

- pressKey: Enter
- waitForAnimationToEnd

- inputText: 'Second item'
- waitForAnimationToEnd

- tapOn:
id: toolbar-indent
- waitForAnimationToEnd

- tapOn:
id: blur-button

- runFlow:
file: '../../../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'unordered_list_input'
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions apps/react-native-example/src/components/FormattingToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,30 @@ export function FormattingToolbar({
<Text style={styles.headingButtonText}>{`H${level}`}</Text>
</TouchableOpacity>
))}
<TouchableOpacity
style={[
styles.toolbarButton,
state?.unorderedList.isActive && styles.toolbarButtonActive,
]}
onPress={() => inputRef.current?.toggleUnorderedList()}
testID="toolbar-unordered-list"
>
<Text style={styles.headingButtonText}>•</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.toolbarButton}
onPress={() => inputRef.current?.outdentList()}
testID="toolbar-outdent"
>
<Text style={styles.headingButtonText}>⇤</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.toolbarButton}
onPress={() => inputRef.current?.indentList()}
testID="toolbar-indent"
>
<Text style={styles.headingButtonText}>⇥</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.toolbarButton,
Expand Down
23 changes: 22 additions & 1 deletion docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,14 @@ Style configuration for formatted text in the input.
- `spoiler.backgroundColor` — background color for spoiler text.
- `h1`–`h6` — per-level heading styling, each accepting `fontSize`, `fontWeight`, and `color`. Defaults match the read-only renderer (sizes `30/24/20/18/16/14`, bold).

### `listItemSpacing`

Vertical spacing (points) added above each bullet list item so items read as separate rows. iOS applies it via `paragraphSpacingBefore`; Android via a `LineHeightSpan`.

| Type | Default Value | Platform |
| -------- | ------------- | -------- |
| `number` | `0` | Both |

### `mentionIndicators`

List of trigger strings that start a mention flow (e.g. `['@', '#']`). See [Mentions](MENTIONS.md).
Expand Down Expand Up @@ -611,7 +619,7 @@ Fires when the text selection changes.

### `onChangeState`

Fires when the active style state changes. The payload provides a nested object for each style with an `isActive` property; `heading` additionally carries the cursor paragraph's `level` (`0` when it is not a heading).
Fires when the active style state changes. The payload provides a nested object for each style with an `isActive` property; `heading` additionally carries the cursor paragraph's `level` (`0` when it is not a heading) and `unorderedList` its 0-based nesting `depth`.

| Type | Default Value | Platform |
| --------------------------------- | ------------- | -------- |
Expand All @@ -629,6 +637,7 @@ interface StyleState {
link: { isActive: boolean };
// Heading level of the cursor's paragraph: 0 = none, 1-6 = H1-H6.
heading: { isActive: boolean; level: number };
unorderedList: { isActive: boolean; depth: number };
}
```

Expand Down Expand Up @@ -922,6 +931,18 @@ Toggles spoiler on the current selection or cursor.

Toggles a heading of the given level (`1`–`6`) on the cursor's paragraph. Calling it with the level already applied turns the paragraph back into regular text. Unlike the inline `toggle*` methods, this operates on the whole paragraph, not a character range.

### `toggleUnorderedList()`

Turns the cursor's paragraph(s) into bullet list items, or back into regular paragraphs if they already are. Operates on the whole paragraph.

### `indentList()`

Nests the current list item one level deeper (up to a maximum depth). Called on a non-list paragraph, it starts a bullet list at depth 0. Equivalent to pressing **Tab** with a hardware keyboard.

### `outdentList()`

Lifts the current list item out one nesting level. Outdenting a depth-0 item removes the bullet, turning it back into a regular paragraph. Equivalent to **Shift+Tab**.

### `setLink(url: string)`

Applies a link URL to the currently selected text.
Expand Down
23 changes: 22 additions & 1 deletion docs/INPUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,27 @@ Behavior notes:

Heading font sizes, weights, and colors are configurable per level — see [Customizing Styles](#customizing-enrichedmarkdowntextinput--styles).

## Bullet Lists

`EnrichedMarkdownTextInput` supports nested unordered (bullet) lists, serialized to Markdown as `- ` with two spaces of indentation per nesting level. Like headings, list items are block-level — they apply to whole paragraphs. Manage them through the component ref:

- [`toggleUnorderedList()`](API_REFERENCE.md#toggleunorderedlist) — turns the cursor's paragraph(s) into bullet items, or back into regular paragraphs if they already are.
- [`indentList()`](API_REFERENCE.md#indentlist) — nests the current item one level deeper. On a non-list paragraph it starts a bullet list.
- [`outdentList()`](API_REFERENCE.md#outdentlist) — lifts the current item out one level. Outdenting a top-level (depth 0) item removes the bullet.

On a hardware keyboard, **Tab** and **Shift+Tab** indent and outdent the current item.

Behavior notes:

- **Return continues the list** — pressing Return inside an item starts a new bullet at the same depth on the next line. Pressing Return on an empty item exits the list (the line becomes a regular paragraph).
- **Backspace at the start of an item** outdents it; at depth 0 it removes the bullet.
- An emptied list line stays a list item (its bullet keeps showing) until you toggle it off or Backspace out of it, so text you type next continues in the list.
- The bullet marker exists only in the serialized Markdown and the rendered glyph — it is never part of the editor's text, so it won't collide with `-`-prefixed text such as mentions.

Whether the cursor's paragraph is a bullet item, and its nesting depth, are reported through the [`onChangeState`](API_REFERENCE.md#onchangestate) payload as `unorderedList` (`{ isActive, depth }`), so you can highlight a list button in your toolbar.

Use the [`listItemSpacing`](API_REFERENCE.md#listitemspacing) prop to add vertical spacing between items so they read as separate rows.

## Links

Links are a piece of text with a URL attributed to it. They can be managed by calling methods on the input ref:
Expand Down Expand Up @@ -224,7 +245,7 @@ No setup is required. Both platforms autodetect direction per paragraph out of t

- **Placeholder** follows the host view's layout direction, not the prop. If you need an RTL placeholder, wrap the input in `<View style={{ direction: 'rtl' }}>` or set `I18nManager.forceRTL(true)`.
- **Mixed paragraphs while typing** — newly inserted characters in an empty paragraph briefly inherit the previous paragraph's direction; the first-strong pass corrects this on the next input event.
- **Code blocks, tables, blockquotes, lists** are not supported in the input (it's a flat inline-formatting surface). For those, use [`EnrichedMarkdownText`](TEXT.md).
- **Code blocks, tables, blockquotes** are not supported in the input. Headings and bullet lists are; other block elements require [`EnrichedMarkdownText`](TEXT.md).

See [RTL Support](RTL.md) for the full per-element behavior on the rendered output side.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class EnrichedMarkdownTextInputManager :
view: EnrichedMarkdownTextInputView?,
value: String?,
) {
view?.hint = value
view?.setUserHint(value)
}

@ReactProp(name = "placeholderTextColor", customType = "Color")
Expand Down Expand Up @@ -198,13 +198,20 @@ class EnrichedMarkdownTextInputManager :
if (view == null || value == null) return

val style = MarkdownStyleParser.parse(value)
view.setAutoLinkStyle(style)
val changed = view.formatter.updateStyle(style)
val changed = view.setMarkdownStyleFromProps(style)
if (changed) {
view.applyFormatting()
}
}

@ReactProp(name = "listItemSpacing", defaultInt = 0)
override fun setListItemSpacing(
view: EnrichedMarkdownTextInputView?,
value: Int,
) {
view?.setListItemSpacingFromProps(value.toFloat())
}

@ReactProp(name = "color", customType = "Color")
override fun setColor(
view: EnrichedMarkdownTextInputView?,
Expand Down Expand Up @@ -429,6 +436,18 @@ class EnrichedMarkdownTextInputManager :
view?.toggleHeading(level)
}

override fun toggleUnorderedList(view: EnrichedMarkdownTextInputView?) {
view?.toggleUnorderedList()
}

override fun indentList(view: EnrichedMarkdownTextInputView?) {
view?.indentList()
}

override fun outdentList(view: EnrichedMarkdownTextInputView?) {
view?.outdentList()
}

override fun setLink(
view: EnrichedMarkdownTextInputView?,
url: String?,
Expand Down
Loading
Loading