From 96792411cbf564614de838da767a8b7174a377c2 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Sun, 19 Jul 2026 00:51:58 +0200 Subject: [PATCH 1/7] docs: basic styles, mentions, links sections --- .../docs/rich-text-formatting/basic-styles.md | 7 - .../rich-text-formatting/basic-styles.mdx | 116 +++++++++++++++++ docs/docs/rich-text-formatting/links.md | 7 - docs/docs/rich-text-formatting/links.mdx | 115 +++++++++++++++++ docs/docs/rich-text-formatting/mentions.md | 7 - docs/docs/rich-text-formatting/mentions.mdx | 97 ++++++++++++++ docs/src/examples/BasicStylesEditor.tsx | 120 ++++++++++++++++++ docs/src/examples/LinksEditor.tsx | 75 +++++++++++ docs/src/examples/MentionEditor.tsx | 52 ++++++++ 9 files changed, 575 insertions(+), 21 deletions(-) delete mode 100644 docs/docs/rich-text-formatting/basic-styles.md create mode 100644 docs/docs/rich-text-formatting/basic-styles.mdx delete mode 100644 docs/docs/rich-text-formatting/links.md create mode 100644 docs/docs/rich-text-formatting/links.mdx delete mode 100644 docs/docs/rich-text-formatting/mentions.md create mode 100644 docs/docs/rich-text-formatting/mentions.mdx create mode 100644 docs/src/examples/BasicStylesEditor.tsx create mode 100644 docs/src/examples/LinksEditor.tsx create mode 100644 docs/src/examples/MentionEditor.tsx diff --git a/docs/docs/rich-text-formatting/basic-styles.md b/docs/docs/rich-text-formatting/basic-styles.md deleted file mode 100644 index f5945cb02..000000000 --- a/docs/docs/rich-text-formatting/basic-styles.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Basic styles - - diff --git a/docs/docs/rich-text-formatting/basic-styles.mdx b/docs/docs/rich-text-formatting/basic-styles.mdx new file mode 100644 index 000000000..619a7a939 --- /dev/null +++ b/docs/docs/rich-text-formatting/basic-styles.mdx @@ -0,0 +1,116 @@ +--- +sidebar_position: 1 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import BasicStylesEditor from '@site/src/examples/BasicStylesEditor'; +import BasicStylesEditorSrc from '!!raw-loader!@site/src/examples/BasicStylesEditor'; + +# Basic styles + +In [Your first editor](/fundamentals/your-first-editor) you wired up a single +**Bold** button. Every other formatting style works exactly the same way: a +`toggle` method on the `ref` changes the text, and `onChangeState` reports back +whether the style is active. The only exceptions to that rule are **mentions**, +**links** and **inline images**, which work a bit differently. They cannot be toggled +on/off on any text - they require additional data to be valid,but you'll learn more +about them in further sections. This page walks through the rest of the basic rich +text styles and the one core distinction that shapes how they behave - **inline** +versus **paragraph** formatting. + +## Inline styles + +Inline styles wrap a range of characters. Toggling one applies it to exactly the +characters you have selected, or - if nothing is selected - arms it so the next +characters you type come out styled. This is the family the bold belongs to: + +- **Bold** - `toggleBold()` +- **Italic** - `toggleItalic()` +- **Underline** - `toggleUnderline()` +- **Strikethrough** - `toggleStrikeThrough()` +- **Inline code** - `toggleInlineCode()` + +They combine freely, so a word can be bold and italic and underlined at +once. + +:::note + +There are two more inline styles, which you cannot toggle quite like the others - **links** and **mentions**. You'll learn about them in their respective sections, once you finish this chapter. + +::: + +## Paragraph styles + +Paragraph styles apply to a whole paragraph at once. Toggling one affects the entire paragraph the +cursor sits in (or every paragraph the selection touches), no matter how much of +it is actually selected. The common ones are: + +- **Headings** - `toggleH1()` through `toggleH6()` +- **Blockquote** - `toggleBlockQuote()` +- **Code block** - `toggleCodeBlock()` +- **Unordered list** - `toggleUnorderedList()` +- **Ordered list** - `toggleOrderedList()` +- **Checkbox list** - `toggleCheckboxList(checked: boolean)` + +:::note + +We will not touch list styles for now - we'll cover them in-depth in their [own section](/rich-text-formatting/lists). + +::: + +The key difference from inline styles is that **a paragraph can only have one +paragraph style at a time**. A line can't be both a heading and a blockquote, so +toggling a second paragraph style replaces the first rather than stacking on top. +The editor surfaces this through the `isConflicting` flag in `onChangeState`. + +Code blocks go one step further: they **block** inline styles entirely. You +can't make text bold inside a code block, and `onChangeState` reports bold as +`isBlocking` there so you can grey the button out. + +:::tip + +The full table of what every style blocks or conflicts with is available in +[Supported tags](/fundamentals/html-format-and-supported-tags) and the `onChangeState` behavior is described in [Style state model](/fundamentals/core-concepts#the-style-state-model). + +::: + +## Building the toolbar + +A toolbar with many styles is just the [first editor](/fundamentals/your-first-editor) +loop repeated. Each button reads its slice of the `onChangeState` payload for +`isActive` (to highlight it) and `isBlocking` (to disable it), and calls the +matching `toggle` method on press: + +```tsx +const inlineButtons = [ + { label: 'Bold', state: state?.bold, onPress: () => ref.current?.toggleBold() }, + { label: 'Italic', state: state?.italic, onPress: () => ref.current?.toggleItalic() }, + { label: 'Underline', state: state?.underline, onPress: () => ref.current?.toggleUnderline() }, + { label: 'Strike', state: state?.strikeThrough, onPress: () => ref.current?.toggleStrikeThrough() }, +]; + +const paragraphButtons = [ + { label: 'H1', state: state?.h1, onPress: () => ref.current?.toggleH1() }, + { label: 'H2', state: state?.h2, onPress: () => ref.current?.toggleH2() }, + { label: 'Quote', state: state?.blockQuote, onPress: () => ref.current?.toggleBlockQuote() }, + { label: 'Code', state: state?.codeBlock, onPress: () => ref.current?.toggleCodeBlock() }, +]; +``` + +## Try it out + +And just like that we've expanded our editor with several new styles to choose +from a small toolbar. Select some text and toggle inline styles, or place the cursor +on a line and try the paragraph ones - notice how switching from **H1** to **Quote** +swaps the style instead of adding to it, and how the inline buttons grey out once +you're inside a **Code** block. + + + +:::info + +This page is about _what_ each style does, using the built-in defaults. To change +how they look - heading sizes, blockquote borders, code block colors - see +[Styling the input](/core-functionalities/styling-the-input). + +::: diff --git a/docs/docs/rich-text-formatting/links.md b/docs/docs/rich-text-formatting/links.md deleted file mode 100644 index 549f3ac11..000000000 --- a/docs/docs/rich-text-formatting/links.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Links - - diff --git a/docs/docs/rich-text-formatting/links.mdx b/docs/docs/rich-text-formatting/links.mdx new file mode 100644 index 000000000..1f9e85fbc --- /dev/null +++ b/docs/docs/rich-text-formatting/links.mdx @@ -0,0 +1,115 @@ +--- +sidebar_position: 2 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import LinksEditor from '@site/src/examples/LinksEditor'; +import LinksEditorSrc from '!!raw-loader!@site/src/examples/LinksEditor'; + +# Links + +A link is a piece of text with a URL attached to it. The editor creates links in +two ways - **automatically**, as the user types something that looks like a URL, +and **manually**, when you call `setLink` on a range of text. The two produce +links that behave differently, so it's worth understanding both. + +## Automatic links + +By default the editor watches what the user types and turns anything that looks +like a URL into a link on its own - no code required. These are **autolinks**. + +Because an autolink is tied to the text that produced it, it stays a link only as +long as that text still matches. The moment the user edits the words, the pattern +no longer holds and the link naturally breaks. + +### Customizing detection + +You control what counts as a link with the +`linkRegex` prop. Pass your own `RegExp` +to recognize custom patterns - for example, only `https://` URLs or a custom +scheme that detects references like `issue-123`: + +```tsx +// Detect "issue-123" style tokens as links. +const linkRegex = /issue-\d+/g; + +; +``` + +:::note + +On iOS and Android the pattern is matched by the platform's native regex engine, +so not every JavaScript regex feature is available there - variable-width +lookbehinds, for instance, won't work. Pass `null` to turn automatic detection +off entirely. + +::: + +## Manual links + +Manual links are applied explicitly with the +`setLink` ref method: + +```ts +setLink(start: number, end: number, text: string, url: string) +``` + +It sets a link over the range from `start` to `end`, showing `text` and pointing +at `url`. Unlike an autolink, a manual link is a real, standalone style - the +user can keep editing its text and it stays a link. + +:::tip + +The three positional arguments line up exactly with the +`onChangeSelection` event payload, which reports +the current `start`, `end`, and the `text` it spans. So "link the current +selection" can be implemented quite simply: + +::: + +```tsx +const [selection, setSelection] = useState(null); + +// ... + setSelection(e.nativeEvent)} + /* ... */ +/>; + +const addLink = () => { + if (!selection) return; + ref.current?.setLink( + selection.start, + selection.end, + selection.text, + 'https://swmansion.com', + ); +}; +``` + +:::tip + +Passing a different `text` than what's selected replaces the selected text before +applying the link. To strip a link while keeping its text, call +`removeLink(start, end)`. + +::: + +## Try it out + +The editor below uses a custom `linkRegex` that autolinks `issue-123` tokens on +top of the usual URL detection - type either and watch it become a link. Then +select some words and hit **Link the selection** to attach a URL manually. +Notice the difference afterward: editing the manual link's text keeps it a link, +while editing an autolinked token breaks it. + + + +:::info + +This page covers creating and detecting links. To react to links the user +touches, listen for the +`onLinkDetected` event, and to change how +links look, see [Styling the input](/core-functionalities/styling-the-input). + +::: diff --git a/docs/docs/rich-text-formatting/mentions.md b/docs/docs/rich-text-formatting/mentions.md deleted file mode 100644 index ee0a2d492..000000000 --- a/docs/docs/rich-text-formatting/mentions.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Mentions - - diff --git a/docs/docs/rich-text-formatting/mentions.mdx b/docs/docs/rich-text-formatting/mentions.mdx new file mode 100644 index 000000000..ebfff6e63 --- /dev/null +++ b/docs/docs/rich-text-formatting/mentions.mdx @@ -0,0 +1,97 @@ +--- +sidebar_position: 3 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import MentionEditor from '@site/src/examples/MentionEditor'; +import MentionEditorSrc from '!!raw-loader!@site/src/examples/MentionEditor'; + +# Mentions + +This is a powerful feature allowing for a customizable inline style for a "mentioning" phrase - `@someone`, +`#some-channel`, or whatever pattern fits your app. Each mention is able to carry any custom data you attach to it, so it's more than +styled text: it's a reference you can act on later. + +## Mention indicators + +A mention begins with an **indicator** - a single character that tells the editor +"a mention starts here". The set of recognized indicators is controlled by the +`mentionIndicators` prop, which defaults to +`['@']`: + +```tsx +// Recognize both @user and #channel mentions. +; +``` + +Typing one of these characters starts a mention. You can also start one +programmatically with `startMention(indicator)`. + +## Inserting a mention with `setMention` + +Once a mention is being edited - the user has typed an indicator, or you've +called `startMention` - you insert the finished mention with `setMention`: + +```ts +setMention( + indicator: string, + text: string, + attributes?: Record, +) +``` + +`setMention` replaces whatever the user has typed including the indicator with +`text`. It also stores any `attributes` you pass - this is useful if you want to associate a mention with some data. Those attributes are preserved through the HTML, so they +survive a round-trip through `getHTML` and `setValue`. + +```tsx +const insertMention = (user: { id: string; name: string }) => { + ref.current?.setMention('@', `@${user.name}`, { id: user.id }); +}; +``` + +:::note + +`setMention` is only operational when an actual mention preceded by an indicator is being edited. Otherwise it does nothing. + +::: + +## Try it out + +Type `@`, then tap the button to turn it +into a completed `@mention`. Notice how you can type a part of a mention, e.g. `@Jo`, move the cursor inside its range and the mention insertion will properly replace it. The result is a single styled unit - try deleting one of its characters and see how it's removed. + + + +:::note + +A mention query spans up to two words - the editor keeps tracking the mention after one +space, and a second space ends it. That makes `@John Doe` a valid mention. + +::: + +## Driving mentions with events + +This example is deliberately bare-bones - one hard-coded user behind a button. +A real app watches what the user types after the +indicator and shows a picker. Three events give you everything you need for +that: + +- **`onStartMention`** fires when a mention starts - a good moment to open your suggestion list. +- **`onChangeMention`** fires on every edit to the query, handing you the text + typed after the indicator - use it to filter the list. +- **`onEndMention`** fires when the mention stops being edited - the cursor + moved away, or the query was closed - so you can dismiss the list. + +You'd open the list on `onStartMention`, filter it on `onChangeMention`, and +call `setMention` when the user picks someone. This example doesn't wire up any +of that; the full picker flow is covered end-to-end in the +[Mention-only input](/guides/mention-only-input) guide. + +:::tip + +Prefix your custom attribute names with `data-` (e.g. `{ 'data-profile-url': user.profileUrl }`). +Non-standard attributes can be dropped when the HTML passes through a sanitizer - +this library's, or one on your own backend - whereas `data-*` attributes are the HTML standard for persisting custom data and are kept. + +::: diff --git a/docs/src/examples/BasicStylesEditor.tsx b/docs/src/examples/BasicStylesEditor.tsx new file mode 100644 index 000000000..04f7edd41 --- /dev/null +++ b/docs/src/examples/BasicStylesEditor.tsx @@ -0,0 +1,120 @@ +import { EnrichedTextInput } from 'react-native-enriched-html'; +import type { + EnrichedTextInputInstance, + OnChangeStateEvent, +} from 'react-native-enriched-html'; +import { useRef, useState } from 'react'; +import { View, StyleSheet, Pressable, Text } from 'react-native'; + +export default function App() { + const ref = useRef(null); + const [state, setState] = useState(null); + + // Inline styles apply to the selected characters. + const inlineButtons = [ + { + label: 'Bold', + state: state?.bold, + onPress: () => ref.current?.toggleBold(), + }, + { + label: 'Italic', + state: state?.italic, + onPress: () => ref.current?.toggleItalic(), + }, + { + label: 'Underline', + state: state?.underline, + onPress: () => ref.current?.toggleUnderline(), + }, + { + label: 'Strike', + state: state?.strikeThrough, + onPress: () => ref.current?.toggleStrikeThrough(), + }, + ]; + + // Paragraph styles apply to the whole line the cursor sits in. + const paragraphButtons = [ + { label: 'H1', state: state?.h1, onPress: () => ref.current?.toggleH1() }, + { label: 'H2', state: state?.h2, onPress: () => ref.current?.toggleH2() }, + { + label: 'Quote', + state: state?.blockQuote, + onPress: () => ref.current?.toggleBlockQuote(), + }, + { + label: 'Code', + state: state?.codeBlock, + onPress: () => ref.current?.toggleCodeBlock(), + }, + ]; + + const renderButton = (button: (typeof inlineButtons)[number]) => ( + + + {button.label} + + + ); + + return ( + + setState(e.nativeEvent)} + /> + {inlineButtons.map(renderButton)} + {paragraphButtons.map(renderButton)} + + ); +} + +const styles = StyleSheet.create({ + container: { gap: 12 }, + input: { + fontSize: 18, + color: '#232736', + padding: 12, + borderRadius: 12, + minHeight: 96, + backgroundColor: '#eef0ff', + }, + row: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + gap: 8, + }, + button: { + width: 90, + padding: 8, + borderRadius: 24, + borderWidth: 1, + borderColor: '#919fcf', + }, + buttonActive: { + borderColor: '#57b495', + backgroundColor: '#57b495', + }, + buttonDisabled: { + opacity: 0.4, + }, + text: { + textAlign: 'center', + color: '#919fcf', + }, + textActive: { + color: '#eef0ff', + }, +}); diff --git a/docs/src/examples/LinksEditor.tsx b/docs/src/examples/LinksEditor.tsx new file mode 100644 index 000000000..6bd61e279 --- /dev/null +++ b/docs/src/examples/LinksEditor.tsx @@ -0,0 +1,75 @@ +import { EnrichedTextInput } from 'react-native-enriched-html'; +import type { + EnrichedTextInputInstance, + OnChangeSelectionEvent, +} from 'react-native-enriched-html'; +import { useRef, useState } from 'react'; +import { View, StyleSheet, Pressable, Text } from 'react-native'; + +// Autolink any "issue-123" style token on top of the default URL detection. +const linkRegex = /issue-\d+/g; + +export default function App() { + const ref = useRef(null); + const [selection, setSelection] = useState( + null + ); + + const hasSelection = !!selection && selection.start !== selection.end; + + const addLink = () => { + if (!selection) return; + // Turn the current selection into a link pointing at our docs. + ref.current?.setLink( + selection.start, + selection.end, + selection.text, + 'https://swmansion.com' + ); + }; + + return ( + + setSelection(e.nativeEvent)} + /> + + Link the selection + + + ); +} + +const styles = StyleSheet.create({ + container: { gap: 12 }, + input: { + fontSize: 18, + color: '#232736', + padding: 12, + borderRadius: 12, + minHeight: 96, + backgroundColor: '#eef0ff', + }, + button: { + alignSelf: 'center', + padding: 8, + paddingHorizontal: 16, + borderRadius: 24, + borderWidth: 1, + borderColor: '#919fcf', + }, + buttonDisabled: { + opacity: 0.4, + }, + text: { + textAlign: 'center', + color: '#919fcf', + }, +}); diff --git a/docs/src/examples/MentionEditor.tsx b/docs/src/examples/MentionEditor.tsx new file mode 100644 index 000000000..efd3e9216 --- /dev/null +++ b/docs/src/examples/MentionEditor.tsx @@ -0,0 +1,52 @@ +import { EnrichedTextInput } from 'react-native-enriched-html'; +import type { EnrichedTextInputInstance } from 'react-native-enriched-html'; +import { useRef } from 'react'; +import { View, StyleSheet, Pressable, Text } from 'react-native'; + +const user = { id: '1', name: 'John' }; + +export default function App() { + const ref = useRef(null); + + const insertMention = () => { + // Replaces the active '@' mention the user has started by typing '@'. + ref.current?.setMention('@', `@${user.name}`, { id: user.id }); + }; + + return ( + + + + @{user.name} + + + ); +} + +const styles = StyleSheet.create({ + container: { gap: 12 }, + input: { + fontSize: 18, + color: '#232736', + padding: 12, + borderRadius: 12, + minHeight: 96, + backgroundColor: '#eef0ff', + }, + button: { + alignSelf: 'center', + padding: 8, + paddingHorizontal: 16, + borderRadius: 24, + borderWidth: 1, + borderColor: '#919fcf', + }, + text: { + textAlign: 'center', + color: '#919fcf', + }, +}); From 55e8fd0299fbdd0a86d9d8a1452a177f0e4bc942 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Mon, 20 Jul 2026 14:05:56 +0200 Subject: [PATCH 2/7] docs: lists, imgs, alignment, shortcuts sections --- .../rich-text-formatting/inline-images.md | 7 -- .../rich-text-formatting/inline-images.mdx | 61 +++++++++++ docs/docs/rich-text-formatting/lists.md | 7 -- docs/docs/rich-text-formatting/lists.mdx | 69 ++++++++++++ .../rich-text-formatting/text-alignment.md | 7 -- .../rich-text-formatting/text-alignment.mdx | 40 +++++++ .../rich-text-formatting/text-shortcuts.md | 7 -- .../rich-text-formatting/text-shortcuts.mdx | 82 ++++++++++++++ docs/src/examples/ImagesEditor.tsx | 67 ++++++++++++ docs/src/examples/ListsEditor.tsx | 100 ++++++++++++++++++ docs/src/examples/TextAlignmentEditor.tsx | 77 ++++++++++++++ docs/src/examples/TextShortcutsEditor.tsx | 37 +++++++ 12 files changed, 533 insertions(+), 28 deletions(-) delete mode 100644 docs/docs/rich-text-formatting/inline-images.md create mode 100644 docs/docs/rich-text-formatting/inline-images.mdx delete mode 100644 docs/docs/rich-text-formatting/lists.md create mode 100644 docs/docs/rich-text-formatting/lists.mdx delete mode 100644 docs/docs/rich-text-formatting/text-alignment.md create mode 100644 docs/docs/rich-text-formatting/text-alignment.mdx delete mode 100644 docs/docs/rich-text-formatting/text-shortcuts.md create mode 100644 docs/docs/rich-text-formatting/text-shortcuts.mdx create mode 100644 docs/src/examples/ImagesEditor.tsx create mode 100644 docs/src/examples/ListsEditor.tsx create mode 100644 docs/src/examples/TextAlignmentEditor.tsx create mode 100644 docs/src/examples/TextShortcutsEditor.tsx diff --git a/docs/docs/rich-text-formatting/inline-images.md b/docs/docs/rich-text-formatting/inline-images.md deleted file mode 100644 index 464ec5746..000000000 --- a/docs/docs/rich-text-formatting/inline-images.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Inline images - - diff --git a/docs/docs/rich-text-formatting/inline-images.mdx b/docs/docs/rich-text-formatting/inline-images.mdx new file mode 100644 index 000000000..ce30e32b3 --- /dev/null +++ b/docs/docs/rich-text-formatting/inline-images.mdx @@ -0,0 +1,61 @@ +--- +sidebar_position: 5 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import ImagesEditor from '@site/src/examples/ImagesEditor'; +import ImagesEditorSrc from '!!raw-loader!@site/src/examples/ImagesEditor'; + +# Inline images + +Aside from just styled text, the library offers much more. This includes inline images - pictures embedded straight into the text flow. + +## Inserting an image + +You can insert it explicitly with the `setImage` ref method: + +```ts +setImage(src: string, width: number, height: number) +``` + +- `src` - an absolute file path or a remote image URL. +- `width` / `height` - the size the image should render at. + +The image lands on its own line at the current selection, replacing any +selected text. + +```tsx +const insertImage = () => { + ref.current?.setImage('https://picsum.photos/320/160', 160, 80); +}; +``` + +:::note + +You are responsible for the `width` and `height` you pass - the editor does not +measure the image for you, so if you want to preserve a picture's aspect ratio you +need to compute it yourself before calling `setImage`. + +::: + +## When the source can't load + +If the `src` can't be resolved - a broken URL, a missing file - the editor doesn't +error out and renders a static placeholder in the image's place instead. + +## Try it out + +Place the cursor in the input and insert an image. The first button inserts a real +photo from [Lorem Picsum](https://picsum.photos) at a matching size; the second +points at a source that doesn't exist, so you get the fallback placeholder rather +than a crash. + + + +:::info + +Images can also arrive by pasting rather than through `setImage` - the editor +surfaces those through the `onPasteImages` event, which hands you each image's URI, +MIME type, and dimensions so you can upload or resize before inserting. + +::: diff --git a/docs/docs/rich-text-formatting/lists.md b/docs/docs/rich-text-formatting/lists.md deleted file mode 100644 index 8f296a6e0..000000000 --- a/docs/docs/rich-text-formatting/lists.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Lists - - diff --git a/docs/docs/rich-text-formatting/lists.mdx b/docs/docs/rich-text-formatting/lists.mdx new file mode 100644 index 000000000..e358f3e0b --- /dev/null +++ b/docs/docs/rich-text-formatting/lists.mdx @@ -0,0 +1,69 @@ +--- +sidebar_position: 4 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import ListsEditor from '@site/src/examples/ListsEditor'; +import ListsEditorSrc from '!!raw-loader!@site/src/examples/ListsEditor'; + +# Lists + +Lists are [paragraph styles](/rich-text-formatting/basic-styles#paragraph-styles), +just like headings and blockquotes - toggling one affects the whole paragraph the +cursor sits in, or every paragraph the selection touches. The editor supports three +kinds: + +- **Unordered list** - `toggleUnorderedList()` +- **Ordered list** - `toggleOrderedList()` +- **Checkbox list** - `toggleCheckboxList(checked: boolean)` + +Because they're paragraph styles, **only one can be active on a paragraph at a +time**. Toggling a second list type (or any other paragraph style) replaces the +first rather than stacking - the editor reports this through the `isConflicting` +flag in `onChangeState`, exactly as described in +[Basic styles](/rich-text-formatting/basic-styles#paragraph-styles). + +:::note + +The library does not support nested lists. You can only create a single level list. If you try to insert a nested list in any way, e.g. by calling `setValue` on the editor's `ref`, the list will get flattened by the [normalizer](/fundamentals/core-concepts#normalization). + +::: + +## Bulleted and numbered lists + +These two take no arguments - each call converts the affected paragraphs into a +list, and calling it again turns them back into plain paragraphs: + +```tsx +ref.current?.toggleUnorderedList(); +ref.current?.toggleOrderedList(); +``` + +## Checkbox lists + +This list's items need a starting state. `toggleCheckboxList` takes a `checked` boolean that +decides whether newly created boxes begin checked or unchecked: + +```tsx +// New items start unchecked. +ref.current?.toggleCheckboxList(false); +``` + +Once the list exists, the user can tick individual items by tapping their +checkbox - you don't need to wire anything up for that. + +## Try it out + +Type a few lines, place the cursor on one (or select several), and toggle each +list type. Notice how switching from **Bulleted** to **Numbered** swaps the style +instead of stacking, inserting a new line keeps the list format by creating an another element and how tapping a checkbox in the checkbox list toggles just that item. + + + +:::info + +This page covers creating lists. To change how they look - bullet color and size, +marker weight, checkbox size, indentation - see +[Styling the input](/core-functionalities/styling-the-input). + +::: diff --git a/docs/docs/rich-text-formatting/text-alignment.md b/docs/docs/rich-text-formatting/text-alignment.md deleted file mode 100644 index 1619e7a0a..000000000 --- a/docs/docs/rich-text-formatting/text-alignment.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Text alignment - - diff --git a/docs/docs/rich-text-formatting/text-alignment.mdx b/docs/docs/rich-text-formatting/text-alignment.mdx new file mode 100644 index 000000000..5054ec87b --- /dev/null +++ b/docs/docs/rich-text-formatting/text-alignment.mdx @@ -0,0 +1,40 @@ +--- +sidebar_position: 6 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import TextAlignmentEditor from '@site/src/examples/TextAlignmentEditor'; +import TextAlignmentEditorSrc from '!!raw-loader!@site/src/examples/TextAlignmentEditor'; + +# Text alignment + +Alignment controls how a paragraph's text sits within the input's width. It's a +per-paragraph property - every paragraph the selection touches gets aligned - and +you set it with the `setTextAlignment` ref method: + +```ts +setTextAlignment(alignment: 'left' | 'center' | 'right' | 'justify' | 'auto') +``` + +The five values are: + +- **`left`**, **`center`**, **`right`** - align the text to that direction. +- **`justify`** - stretch each line to fill the full width. +- **`auto`** - reset to the system's natural alignment. + +Unlike the paragraph styles from [Basic styles](/rich-text-formatting/basic-styles), +text alignment does not report its state with the `isActive` / `isConflicting` booleans in `onChangeState`. Instead it exposes `alignment` with its relevant string value. + +```tsx +const [state, setState] = useState(null); + +// Highlight the button that matches the paragraph at the cursor. +const isActive = state?.alignment === 'center'; +``` + +## Try it out + +Type a paragraph or two, place the cursor on one, and try each alignment. The +active button reflects `state.alignment` for the paragraph the cursor is in. + + diff --git a/docs/docs/rich-text-formatting/text-shortcuts.md b/docs/docs/rich-text-formatting/text-shortcuts.md deleted file mode 100644 index 9b8de802b..000000000 --- a/docs/docs/rich-text-formatting/text-shortcuts.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 7 ---- - -# Text shortcuts - - diff --git a/docs/docs/rich-text-formatting/text-shortcuts.mdx b/docs/docs/rich-text-formatting/text-shortcuts.mdx new file mode 100644 index 000000000..122f9e282 --- /dev/null +++ b/docs/docs/rich-text-formatting/text-shortcuts.mdx @@ -0,0 +1,82 @@ +--- +sidebar_position: 7 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import TextShortcutsEditor from '@site/src/examples/TextShortcutsEditor'; +import TextShortcutsEditorSrc from '!!raw-loader!@site/src/examples/TextShortcutsEditor'; + +# Text shortcuts + +Text shortcuts let users format as they type, the way Markdown editors do - +typing `#` turns a line into a heading, wrapping a word in `**` makes it bold. +You configure them with the `textShortcuts` prop, an array that maps a `trigger` +string to a `style`: + +```tsx +; +``` + +The `style` values are the same rich text styles you met in +[Basic styles](/rich-text-formatting/basic-styles) - and the inline vs. paragraph +distinction is exactly what decides how a shortcut fires. + +## Paragraph vs. inline shortcuts + +The two families of styles trigger differently, because they format different +things: + +- **Paragraph shortcuts** (`h1`–`h6`, `blockquote`, `codeblock`, + `unordered_list`, `ordered_list`, `checkbox_list`) fire at the **start of a + paragraph**. The trigger is the prefix you type before the line's content - + e.g. `#` for a heading, `-` for a bulleted list. Since a paragraph can only hold + one paragraph style, these only fire on a **plain** paragraph: if the line is + already a heading or a list item, typing the prefix does nothing. + +- **Inline shortcuts** (`bold`, `italic`, `underline`, `strikethrough`, + `inline_code`) fire when you type a **closing delimiter** around some text, so + typing `**word**` bolds `word`. + +:::note + +The usual [style rules](/rich-text-formatting/basic-styles#paragraph-styles) still +apply to shortcut-triggered styles. If the target style is **blocked** by an +active one (e.g. bold inside a code block). + +::: + +## Defaults + +Even without the prop, two shortcuts are active out of the box: + +```ts +[ + { trigger: '- ', style: 'unordered_list' }, + { trigger: '1. ', style: 'ordered_list' }, +]; +``` + +Passing your own array replaces these defaults entirely, and passing an empty +array (`textShortcuts={[]}`) disables shortcuts altogether. + +## Try it out + +The editor below wires up one shortcut of each kind - a paragraph one (`#` → H1) +and an inline one (`**` → bold). Start a line with `#` and watch it become a +heading, then wrap a word in `**stars**` to bold it. + + + +:::info + +On Web, the same styles are also reachable through +[keyboard shortcuts](/api-reference/enriched-text-input) like ⌘B / Ctrl+B, +independent of the `textShortcuts` prop. + +::: diff --git a/docs/src/examples/ImagesEditor.tsx b/docs/src/examples/ImagesEditor.tsx new file mode 100644 index 000000000..2e459522e --- /dev/null +++ b/docs/src/examples/ImagesEditor.tsx @@ -0,0 +1,67 @@ +import { EnrichedTextInput } from 'react-native-enriched-html'; +import type { EnrichedTextInputInstance } from 'react-native-enriched-html'; +import { useRef } from 'react'; +import { View, StyleSheet, Pressable, Text } from 'react-native'; + +export default function App() { + const ref = useRef(null); + + const insertImage = () => { + // A real, loadable image. You supply the dimensions yourself - here we + // ask Lorem Picsum for a 320x160 photo and pass the proportionally down-scaled size. + ref.current?.setImage('https://picsum.photos/320/160', 160, 80); + }; + + const insertBrokenImage = () => { + // An unreachable source. The editor renders its static placeholder + // instead of failing, so you can see what a broken image looks like. + ref.current?.setImage('https://picsum.photos/does-not-exist', 160, 80); + }; + + return ( + + + + + Insert image + + + Insert broken image + + + + ); +} + +const styles = StyleSheet.create({ + container: { gap: 12 }, + input: { + fontSize: 18, + color: '#232736', + padding: 12, + borderRadius: 12, + minHeight: 96, + backgroundColor: '#eef0ff', + }, + row: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + gap: 8, + }, + button: { + padding: 8, + paddingHorizontal: 16, + borderRadius: 24, + borderWidth: 1, + borderColor: '#919fcf', + }, + text: { + textAlign: 'center', + color: '#919fcf', + }, +}); diff --git a/docs/src/examples/ListsEditor.tsx b/docs/src/examples/ListsEditor.tsx new file mode 100644 index 000000000..4336969f4 --- /dev/null +++ b/docs/src/examples/ListsEditor.tsx @@ -0,0 +1,100 @@ +import { EnrichedTextInput } from 'react-native-enriched-html'; +import type { + EnrichedTextInputInstance, + OnChangeStateEvent, +} from 'react-native-enriched-html'; +import { useRef, useState } from 'react'; +import { View, StyleSheet, Pressable, Text } from 'react-native'; + +export default function App() { + const ref = useRef(null); + const [state, setState] = useState(null); + + // Lists are paragraph styles - each toggle affects whole lines, and only + // one list type can be active on a paragraph at a time. + const listButtons = [ + { + label: 'Bulleted', + state: state?.unorderedList, + onPress: () => ref.current?.toggleUnorderedList(), + }, + { + label: 'Numbered', + state: state?.orderedList, + onPress: () => ref.current?.toggleOrderedList(), + }, + { + label: 'Checkbox', + state: state?.checkboxList, + // Pass whether new checkboxes start checked or unchecked. + onPress: () => ref.current?.toggleCheckboxList(false), + }, + ]; + + const renderButton = (button: (typeof listButtons)[number]) => ( + + + {button.label} + + + ); + + return ( + + setState(e.nativeEvent)} + /> + {listButtons.map(renderButton)} + + ); +} + +const styles = StyleSheet.create({ + container: { gap: 12 }, + input: { + fontSize: 18, + color: '#232736', + padding: 12, + borderRadius: 12, + minHeight: 96, + backgroundColor: '#eef0ff', + }, + row: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + gap: 8, + }, + button: { + width: 90, + padding: 8, + borderRadius: 24, + borderWidth: 1, + borderColor: '#919fcf', + }, + buttonActive: { + borderColor: '#57b495', + backgroundColor: '#57b495', + }, + buttonDisabled: { + opacity: 0.4, + }, + text: { + textAlign: 'center', + color: '#919fcf', + }, + textActive: { + color: '#eef0ff', + }, +}); diff --git a/docs/src/examples/TextAlignmentEditor.tsx b/docs/src/examples/TextAlignmentEditor.tsx new file mode 100644 index 000000000..addc7d991 --- /dev/null +++ b/docs/src/examples/TextAlignmentEditor.tsx @@ -0,0 +1,77 @@ +import { EnrichedTextInput } from 'react-native-enriched-html'; +import type { + EnrichedTextInputInstance, + OnChangeStateEvent, +} from 'react-native-enriched-html'; +import { useRef, useState } from 'react'; +import { View, StyleSheet, Pressable, Text } from 'react-native'; + +const alignments = ['left', 'center', 'right', 'justify'] as const; + +export default function App() { + const ref = useRef(null); + const [state, setState] = useState(null); + + const renderButton = (alignment: (typeof alignments)[number]) => { + // A single string reports the alignment of the paragraph at the cursor. + const isActive = state?.alignment === alignment; + return ( + ref.current?.setTextAlignment(alignment)}> + + {alignment} + + + ); + }; + + return ( + + setState(e.nativeEvent)} + /> + {alignments.map(renderButton)} + + ); +} + +const styles = StyleSheet.create({ + container: { gap: 12 }, + input: { + fontSize: 18, + color: '#232736', + padding: 12, + borderRadius: 12, + minHeight: 96, + backgroundColor: '#eef0ff', + }, + row: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + gap: 8, + }, + button: { + width: 90, + padding: 8, + borderRadius: 24, + borderWidth: 1, + borderColor: '#919fcf', + }, + buttonActive: { + borderColor: '#57b495', + backgroundColor: '#57b495', + }, + text: { + textAlign: 'center', + color: '#919fcf', + }, + textActive: { + color: '#eef0ff', + }, +}); diff --git a/docs/src/examples/TextShortcutsEditor.tsx b/docs/src/examples/TextShortcutsEditor.tsx new file mode 100644 index 000000000..d3d26422b --- /dev/null +++ b/docs/src/examples/TextShortcutsEditor.tsx @@ -0,0 +1,37 @@ +import { EnrichedTextInput } from 'react-native-enriched-html'; +import type { EnrichedTextInputInstance } from 'react-native-enriched-html'; +import { useRef } from 'react'; +import { View, StyleSheet } from 'react-native'; + +export default function App() { + const ref = useRef(null); + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + container: { gap: 12 }, + input: { + fontSize: 18, + color: '#232736', + padding: 12, + borderRadius: 12, + minHeight: 96, + backgroundColor: '#eef0ff', + }, +}); From 2aaaf9bdc6dd5dfbaa5420ffb41c6c6e709ef348 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Mon, 20 Jul 2026 14:16:26 +0200 Subject: [PATCH 3/7] docs: update links section --- docs/docs/rich-text-formatting/links.mdx | 3 +-- docs/src/examples/LinksEditor.tsx | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/docs/rich-text-formatting/links.mdx b/docs/docs/rich-text-formatting/links.mdx index 1f9e85fbc..3868d787d 100644 --- a/docs/docs/rich-text-formatting/links.mdx +++ b/docs/docs/rich-text-formatting/links.mdx @@ -97,8 +97,7 @@ applying the link. To strip a link while keeping its text, call ## Try it out -The editor below uses a custom `linkRegex` that autolinks `issue-123` tokens on -top of the usual URL detection - type either and watch it become a link. Then +The editor below uses a custom `linkRegex` that autolinks `issue-123` tokens - type them and watch it become a link. Then select some words and hit **Link the selection** to attach a URL manually. Notice the difference afterward: editing the manual link's text keeps it a link, while editing an autolinked token breaks it. diff --git a/docs/src/examples/LinksEditor.tsx b/docs/src/examples/LinksEditor.tsx index 6bd61e279..e4d64fd94 100644 --- a/docs/src/examples/LinksEditor.tsx +++ b/docs/src/examples/LinksEditor.tsx @@ -6,7 +6,7 @@ import type { import { useRef, useState } from 'react'; import { View, StyleSheet, Pressable, Text } from 'react-native'; -// Autolink any "issue-123" style token on top of the default URL detection. +// Autolink any "issue-123" style token. const linkRegex = /issue-\d+/g; export default function App() { @@ -33,7 +33,7 @@ export default function App() { setSelection(e.nativeEvent)} /> From 5fd037c13e1f703656b36573635449a432f6ddc5 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz <146986839+hejsztynx@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:38:21 +0200 Subject: [PATCH 4/7] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/docs/rich-text-formatting/basic-styles.mdx | 2 +- docs/docs/rich-text-formatting/lists.mdx | 4 ++-- docs/docs/rich-text-formatting/text-alignment.mdx | 5 +++++ docs/docs/rich-text-formatting/text-shortcuts.mdx | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/docs/rich-text-formatting/basic-styles.mdx b/docs/docs/rich-text-formatting/basic-styles.mdx index 619a7a939..575ceff0e 100644 --- a/docs/docs/rich-text-formatting/basic-styles.mdx +++ b/docs/docs/rich-text-formatting/basic-styles.mdx @@ -13,7 +13,7 @@ In [Your first editor](/fundamentals/your-first-editor) you wired up a single `toggle` method on the `ref` changes the text, and `onChangeState` reports back whether the style is active. The only exceptions to that rule are **mentions**, **links** and **inline images**, which work a bit differently. They cannot be toggled -on/off on any text - they require additional data to be valid,but you'll learn more +on/off on any text - they require additional data to be valid, but you'll learn more about them in further sections. This page walks through the rest of the basic rich text styles and the one core distinction that shapes how they behave - **inline** versus **paragraph** formatting. diff --git a/docs/docs/rich-text-formatting/lists.mdx b/docs/docs/rich-text-formatting/lists.mdx index e358f3e0b..b662d604a 100644 --- a/docs/docs/rich-text-formatting/lists.mdx +++ b/docs/docs/rich-text-formatting/lists.mdx @@ -25,7 +25,7 @@ flag in `onChangeState`, exactly as described in :::note -The library does not support nested lists. You can only create a single level list. If you try to insert a nested list in any way, e.g. by calling `setValue` on the editor's `ref`, the list will get flattened by the [normalizer](/fundamentals/core-concepts#normalization). +The library does not support nested lists. You can only create a single-level list. If you try to insert a nested list in any way, e.g. by calling `setValue` on the editor's `ref`, the list will get flattened by the [normalizer](/fundamentals/core-concepts#normalization). ::: @@ -56,7 +56,7 @@ checkbox - you don't need to wire anything up for that. Type a few lines, place the cursor on one (or select several), and toggle each list type. Notice how switching from **Bulleted** to **Numbered** swaps the style -instead of stacking, inserting a new line keeps the list format by creating an another element and how tapping a checkbox in the checkbox list toggles just that item. +instead of stacking. Inserting a new line preserves the list format by creating another item, and tapping a checkbox toggles just that item. diff --git a/docs/docs/rich-text-formatting/text-alignment.mdx b/docs/docs/rich-text-formatting/text-alignment.mdx index 5054ec87b..c4735ea44 100644 --- a/docs/docs/rich-text-formatting/text-alignment.mdx +++ b/docs/docs/rich-text-formatting/text-alignment.mdx @@ -22,6 +22,11 @@ The five values are: - **`justify`** - stretch each line to fill the full width. - **`auto`** - reset to the system's natural alignment. +:::note + +On Android, `justify` is not supported. Calling `setTextAlignment('justify')` results in natural alignment (the same as `auto`). + +::: Unlike the paragraph styles from [Basic styles](/rich-text-formatting/basic-styles), text alignment does not report its state with the `isActive` / `isConflicting` booleans in `onChangeState`. Instead it exposes `alignment` with its relevant string value. diff --git a/docs/docs/rich-text-formatting/text-shortcuts.mdx b/docs/docs/rich-text-formatting/text-shortcuts.mdx index 122f9e282..aa8bf82dd 100644 --- a/docs/docs/rich-text-formatting/text-shortcuts.mdx +++ b/docs/docs/rich-text-formatting/text-shortcuts.mdx @@ -47,7 +47,7 @@ things: The usual [style rules](/rich-text-formatting/basic-styles#paragraph-styles) still apply to shortcut-triggered styles. If the target style is **blocked** by an -active one (e.g. bold inside a code block). +active one (e.g. bold inside a code block), the shortcut has no effect. ::: From 942497f2917a04d29ad2a3f530db289e0aa37a97 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz <146986839+hejsztynx@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:08:58 +0200 Subject: [PATCH 5/7] refactor: basic styles editor types clarity Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/src/examples/BasicStylesEditor.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/examples/BasicStylesEditor.tsx b/docs/src/examples/BasicStylesEditor.tsx index 04f7edd41..8adff21ee 100644 --- a/docs/src/examples/BasicStylesEditor.tsx +++ b/docs/src/examples/BasicStylesEditor.tsx @@ -50,7 +50,9 @@ export default function App() { }, ]; - const renderButton = (button: (typeof inlineButtons)[number]) => ( + const renderButton = ( + button: (typeof inlineButtons)[number] | (typeof paragraphButtons)[number] + ) => ( Date: Tue, 21 Jul 2026 09:52:34 +0200 Subject: [PATCH 6/7] Update docs/docs/rich-text-formatting/lists.mdx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kacper Żółkiewski <74975508+kacperzolkiewski@users.noreply.github.com> --- docs/docs/rich-text-formatting/lists.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/docs/rich-text-formatting/lists.mdx b/docs/docs/rich-text-formatting/lists.mdx index b662d604a..7eed30edd 100644 --- a/docs/docs/rich-text-formatting/lists.mdx +++ b/docs/docs/rich-text-formatting/lists.mdx @@ -18,9 +18,7 @@ kinds: - **Checkbox list** - `toggleCheckboxList(checked: boolean)` Because they're paragraph styles, **only one can be active on a paragraph at a -time**. Toggling a second list type (or any other paragraph style) replaces the -first rather than stacking - the editor reports this through the `isConflicting` -flag in `onChangeState`, exactly as described in +time** exactly as described in [Basic styles](/rich-text-formatting/basic-styles#paragraph-styles). :::note From da510a4855a7aca032a9b8f3afd61392883da2ee Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Tue, 21 Jul 2026 10:24:11 +0200 Subject: [PATCH 7/7] docs: links and mention onDeteced events --- docs/docs/rich-text-formatting/links.mdx | 12 ++++++++---- docs/docs/rich-text-formatting/mentions.mdx | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/docs/rich-text-formatting/links.mdx b/docs/docs/rich-text-formatting/links.mdx index 3868d787d..3ae976ac5 100644 --- a/docs/docs/rich-text-formatting/links.mdx +++ b/docs/docs/rich-text-formatting/links.mdx @@ -104,11 +104,15 @@ while editing an autolinked token breaks it. +## Handling links with events + +There is an event that allows you to handle user's interaction with links inside `EnrichedTextInput`. + +- **`onLinkDetected`** fires when the cursor enters or leaves a created link. + :::info -This page covers creating and detecting links. To react to links the user -touches, listen for the -`onLinkDetected` event, and to change how -links look, see [Styling the input](/core-functionalities/styling-the-input). +This page covers creating and detecting links. To change how +they look, see [Styling the input](/core-functionalities/styling-the-input). ::: diff --git a/docs/docs/rich-text-formatting/mentions.mdx b/docs/docs/rich-text-formatting/mentions.mdx index ebfff6e63..2df1fd0b4 100644 --- a/docs/docs/rich-text-formatting/mentions.mdx +++ b/docs/docs/rich-text-formatting/mentions.mdx @@ -74,7 +74,7 @@ space, and a second space ends it. That makes `@John Doe` a valid mention. This example is deliberately bare-bones - one hard-coded user behind a button. A real app watches what the user types after the -indicator and shows a picker. Three events give you everything you need for +indicator and shows a picker. Four events give you everything you need for that: - **`onStartMention`** fires when a mention starts - a good moment to open your suggestion list. @@ -82,6 +82,7 @@ that: typed after the indicator - use it to filter the list. - **`onEndMention`** fires when the mention stops being edited - the cursor moved away, or the query was closed - so you can dismiss the list. +- **`onMentionDetected`** fires when the cursor enters or leaves a created mention. You'd open the list on `onStartMention`, filter it on `onChangeMention`, and call `setMention` when the user picks someone. This example doesn't wire up any