diff --git a/.maestro/enrichedText/flows/image_press.yaml b/.maestro/enrichedText/flows/image_press.yaml
new file mode 100644
index 000000000..6cbf1a3d9
--- /dev/null
+++ b/.maestro/enrichedText/flows/image_press.yaml
@@ -0,0 +1,24 @@
+appId: swmansion.enriched.example
+---
+# Validates that image press events are triggered correctly
+- launchApp
+
+- tapOn:
+ id: 'toggle-screen-button'
+
+- tapOn:
+ id: 'toggle-enriched-text-screen-button'
+
+- runFlow:
+ file: '../subflows/set_enriched_text_value.yaml'
+ env:
+ VALUE: '
'
+
+- tapOn:
+ id: 'enriched-text'
+ point: '5%,50%'
+
+- runFlow:
+ file: '../subflows/capture_or_assert_screenshot.yaml'
+ env:
+ SCREENSHOT_NAME: 'image_press'
diff --git a/.maestro/enrichedText/screenshots/android/image_press.png b/.maestro/enrichedText/screenshots/android/image_press.png
new file mode 100644
index 000000000..a574ba4ac
Binary files /dev/null and b/.maestro/enrichedText/screenshots/android/image_press.png differ
diff --git a/.maestro/enrichedText/screenshots/ios/image_press.png b/.maestro/enrichedText/screenshots/ios/image_press.png
new file mode 100644
index 000000000..32a6b570c
Binary files /dev/null and b/.maestro/enrichedText/screenshots/ios/image_press.png differ
diff --git a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextViewManager.kt b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextViewManager.kt
index 62f91256c..042b278f5 100644
--- a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextViewManager.kt
+++ b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextViewManager.kt
@@ -9,6 +9,7 @@ import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.viewmanagers.EnrichedTextViewManagerDelegate
import com.facebook.react.viewmanagers.EnrichedTextViewManagerInterface
import com.facebook.yoga.YogaMeasureMode
+import com.swmansion.enriched.text.events.OnImagePressEvent
import com.swmansion.enriched.text.events.OnLinkPressEvent
import com.swmansion.enriched.text.events.OnMentionPressEvent
@@ -29,6 +30,7 @@ class EnrichedTextViewManager :
val map = mutableMapOf()
map.put(OnLinkPressEvent.EVENT_NAME, mapOf("registrationName" to OnLinkPressEvent.EVENT_NAME))
map.put(OnMentionPressEvent.EVENT_NAME, mapOf("registrationName" to OnMentionPressEvent.EVENT_NAME))
+ map.put(OnImagePressEvent.EVENT_NAME, mapOf("registrationName" to OnImagePressEvent.EVENT_NAME))
return map
}
diff --git a/android/src/main/java/com/swmansion/enriched/text/events/OnImagePressEvent.kt b/android/src/main/java/com/swmansion/enriched/text/events/OnImagePressEvent.kt
new file mode 100644
index 000000000..c694b4acc
--- /dev/null
+++ b/android/src/main/java/com/swmansion/enriched/text/events/OnImagePressEvent.kt
@@ -0,0 +1,29 @@
+package com.swmansion.enriched.text.events
+
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.bridge.WritableMap
+import com.facebook.react.uimanager.events.Event
+
+class OnImagePressEvent(
+ surfaceId: Int,
+ viewId: Int,
+ private val uri: String,
+ private val width: Double,
+ private val height: Double,
+) : Event(surfaceId, viewId) {
+ override fun getEventName(): String = EVENT_NAME
+
+ override fun getEventData(): WritableMap? {
+ val eventData: WritableMap = Arguments.createMap()
+ val imageMap = Arguments.createMap()
+ imageMap.putString("uri", uri)
+ imageMap.putDouble("width", width)
+ imageMap.putDouble("height", height)
+ eventData.putMap("image", imageMap)
+ return eventData
+ }
+
+ companion object {
+ const val EVENT_NAME: String = "onImagePress"
+ }
+}
diff --git a/android/src/main/java/com/swmansion/enriched/text/spans/EnrichedTextImageSpan.kt b/android/src/main/java/com/swmansion/enriched/text/spans/EnrichedTextImageSpan.kt
index 21f2c9fe3..5f26af56f 100644
--- a/android/src/main/java/com/swmansion/enriched/text/spans/EnrichedTextImageSpan.kt
+++ b/android/src/main/java/com/swmansion/enriched/text/spans/EnrichedTextImageSpan.kt
@@ -3,11 +3,16 @@ package com.swmansion.enriched.text.spans
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.Looper
+import android.view.View
+import com.facebook.react.bridge.ReactContext
+import com.facebook.react.uimanager.UIManagerHelper
import com.swmansion.enriched.R
import com.swmansion.enriched.common.AsyncDrawable
import com.swmansion.enriched.common.ResourceManager
import com.swmansion.enriched.common.spans.EnrichedImageSpan
import com.swmansion.enriched.text.EnrichedTextStyle
+import com.swmansion.enriched.text.events.OnImagePressEvent
+import com.swmansion.enriched.text.spans.interfaces.EnrichedTextClickableSpan
import com.swmansion.enriched.text.spans.interfaces.EnrichedTextSpan
class EnrichedTextImageSpan(
@@ -16,11 +21,28 @@ class EnrichedTextImageSpan(
width: Int,
height: Int,
) : EnrichedImageSpan(drawable, source, width, height),
- EnrichedTextSpan {
+ EnrichedTextSpan,
+ EnrichedTextClickableSpan {
override val dependsOnHtmlStyle = false
+ override var isPressed = false
override fun rebuildWithStyle(style: EnrichedTextStyle) = this
+ override fun onClick(view: View) {
+ val context = view.context as? ReactContext ?: return
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
+ dispatcher?.dispatchEvent(
+ OnImagePressEvent(
+ surfaceId,
+ view.id,
+ source ?: "",
+ getWidth().toDouble(),
+ getHeight().toDouble(),
+ ),
+ )
+ }
+
// We use a callback to trigger a layout rebuild because ForceRedrawSpan/SpanWatcher
// (used in EnrichedImageSpan) doesn’t work in EnrichedTextView with SpannedString.
fun observeAsyncDrawableLoaded(onLoaded: () -> Unit) {
diff --git a/apps/example/src/components/TextRenderer.tsx b/apps/example/src/components/TextRenderer.tsx
index a8a3e4b9c..38a67b347 100644
--- a/apps/example/src/components/TextRenderer.tsx
+++ b/apps/example/src/components/TextRenderer.tsx
@@ -3,6 +3,7 @@ import {
EnrichedText,
type OnLinkPressEvent,
type OnMentionPressEvent,
+ type OnImagePressEvent,
} from 'react-native-enriched-html';
import { enrichedTextHtmlStyle } from '../constants/editorConfig';
@@ -22,6 +23,13 @@ export const TextRenderer = ({ nodes }: TextRendererProps) => {
);
};
+ const handleImagePress = (e: OnImagePressEvent) => {
+ Alert.alert(
+ 'Image Pressed',
+ `You pressed the image: ${JSON.stringify(e.image)}`
+ );
+ };
+
if (nodes.length === 0) {
return null;
}
@@ -35,6 +43,7 @@ export const TextRenderer = ({ nodes }: TextRendererProps) => {
htmlStyle={enrichedTextHtmlStyle}
onLinkPress={handleLinkPress}
onMentionPress={handleMentionPress}
+ onImagePress={handleImagePress}
useHtmlNormalizer
>
{node}
diff --git a/apps/example/src/screens/EnrichedTextScreen.tsx b/apps/example/src/screens/EnrichedTextScreen.tsx
index fa8cdf62e..c61b8321a 100644
--- a/apps/example/src/screens/EnrichedTextScreen.tsx
+++ b/apps/example/src/screens/EnrichedTextScreen.tsx
@@ -3,6 +3,7 @@ import { View, StyleSheet, ScrollView, Text } from 'react-native';
import {
EnrichedText,
type EnrichedTextProps,
+ type OnImagePressEvent,
type OnLinkPressEvent,
type OnMentionPressEvent,
} from 'react-native-enriched-html';
@@ -37,6 +38,10 @@ export function EnrichedTextScreen({ onSwitch }: EnrichedTextScreenProps) {
);
};
+ const handleImagePress = (e: OnImagePressEvent) => {
+ setHtml(`You pressed the image: ${JSON.stringify(e.image)}`);
+ };
+
return (
<>
{html}
diff --git a/docs/TEXT_API_REFERENCE.md b/docs/TEXT_API_REFERENCE.md
index 145ddd237..b9d6320e8 100644
--- a/docs/TEXT_API_REFERENCE.md
+++ b/docs/TEXT_API_REFERENCE.md
@@ -114,6 +114,27 @@ interface OnMentionPressEvent {
| -------------------------------------- | ------------- | ------------ |
| `(event: OnMentionPressEvent) => void` | - | iOS, Android |
+### `onImagePress`
+
+Called when the user presses an inline image. Receives an `OnImagePressEvent` with the image uri and dimensions.
+
+```ts
+interface OnImagePressEvent {
+ image: {
+ uri: string;
+ width: number;
+ height: number;
+ };
+}
+```
+
+| Type | Default Value | Platform |
+| ------------------------------------ | ------------- | ------------ |
+| `(event: OnImagePressEvent) => void` | - | iOS, Android |
+
+> [!NOTE]
+> No visual feedback is applied on press.
+
## EnrichedTextHtmlStyle type
Extends [`HtmlStyle`](API_REFERENCE.md#htmlstyle-type) with additional press-state styling for interactive elements. All properties from `HtmlStyle` are supported except `a` and `mention`, which are replaced by the extended versions below.
diff --git a/ios/EnrichedTextView.h b/ios/EnrichedTextView.h
index e0af096f7..cd5d47243 100644
--- a/ios/EnrichedTextView.h
+++ b/ios/EnrichedTextView.h
@@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
- (CGSize)measureSize:(CGFloat)maxWidth;
- (void)emitOnLinkPressEvent:(NSString *)url;
- (void)emitOnMentionPressEvent:(MentionParams *)mention;
+- (void)emitOnImagePressEvent:(MediaAttachment *)attachment;
@end
NS_ASSUME_NONNULL_END
diff --git a/ios/EnrichedTextView.mm b/ios/EnrichedTextView.mm
index 16ef47083..75e7ded9a 100644
--- a/ios/EnrichedTextView.mm
+++ b/ios/EnrichedTextView.mm
@@ -785,6 +785,21 @@ - (void)emitOnMentionPressEvent:(MentionParams *)mention {
}
}
+- (void)emitOnImagePressEvent:(MediaAttachment *)attachment {
+ if (!attachment)
+ return;
+ auto emitter = [self getEventEmitter];
+ if (emitter != nullptr) {
+ emitter->onImagePress(
+ {.image = {
+ .uri =
+ attachment.uri ? [attachment.uri toCppString] : std::string{},
+ .width = attachment.width,
+ .height = attachment.height,
+ }});
+ }
+}
+
// MARK: - Media attachments delegate
- (void)mediaAttachmentDidUpdate:(MediaAttachment *)attachment {
diff --git a/ios/utils/EnrichedTextTouchHandler.mm b/ios/utils/EnrichedTextTouchHandler.mm
index 255daef6d..48665320c 100644
--- a/ios/utils/EnrichedTextTouchHandler.mm
+++ b/ios/utils/EnrichedTextTouchHandler.mm
@@ -1,6 +1,7 @@
#import "EnrichedTextTouchHandler.h"
#import "ColorExtension.h"
#import "EnrichedTextView.h"
+#import "ImageAttachment.h"
#import "LinkData.h"
#import "MentionParams.h"
#import "MentionStyleProps.h"
@@ -78,8 +79,10 @@ - (id)findClickableAt:(NSUInteger)idx
if (idx == NSNotFound || idx >= self.view.textView.textStorage.length)
return nil;
- NSArray *keys =
- @[ @"EnrichedManualLink", @"EnrichedAutomaticLink", @"EnrichedMention" ];
+ NSArray *keys = @[
+ @"EnrichedManualLink", @"EnrichedAutomaticLink", @"EnrichedMention",
+ @"EnrichedImage"
+ ];
for (NSString *k in keys) {
id val = [self.view.textView.textStorage attribute:k
atIndex:idx
@@ -93,6 +96,10 @@ - (id)findClickableAt:(NSUInteger)idx
}
- (void)updateVisualsPressed:(BOOL)pressed {
+ if ([_activeAttrKey isEqualToString:@"EnrichedImage"]) {
+ return;
+ }
+
if (pressed) {
UIColor *color = nil;
UIColor *bgColor = nil;
@@ -141,6 +148,15 @@ - (void)dispatchEvent {
[self.view emitOnLinkPressEvent:((LinkData *)_activeValue).url];
} else if ([_activeAttrKey isEqualToString:@"EnrichedMention"]) {
[self.view emitOnMentionPressEvent:(MentionParams *)_activeValue];
+ } else if ([_activeAttrKey isEqualToString:@"EnrichedImage"]) {
+ NSRange attachmentRange = _activeRange;
+ ImageAttachment *attachment =
+ [self.view.textView.textStorage attribute:NSAttachmentAttributeName
+ atIndex:attachmentRange.location
+ effectiveRange:NULL];
+ if ([attachment isKindOfClass:[ImageAttachment class]]) {
+ [self.view emitOnImagePressEvent:attachment];
+ }
}
}
diff --git a/src/index.native.tsx b/src/index.native.tsx
index 82d450b16..8f7473c7a 100644
--- a/src/index.native.tsx
+++ b/src/index.native.tsx
@@ -31,4 +31,5 @@ export type {
EnrichedTextHtmlStyle,
OnMentionPressEvent,
OnLinkPressEvent,
+ OnImagePressEvent,
} from './types';
diff --git a/src/index.tsx b/src/index.tsx
index 59d9054fa..24108e2a4 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -22,4 +22,5 @@ export type {
EnrichedTextHtmlStyle,
OnMentionPressEvent,
OnLinkPressEvent,
+ OnImagePressEvent,
} from './types';
diff --git a/src/native/EnrichedText.tsx b/src/native/EnrichedText.tsx
index ec24debfd..a236b8a9a 100644
--- a/src/native/EnrichedText.tsx
+++ b/src/native/EnrichedText.tsx
@@ -16,6 +16,7 @@ import EnrichedTextNativeComponent, {
type NativeProps,
type OnLinkPressEvent,
type OnMentionPressEventInternal,
+ type OnImagePressEvent,
} from '../spec/EnrichedTextNativeComponent';
import { nullthrows } from '../utils/nullthrows';
import { normalizeEnrichedTextHtmlStyle } from '../utils/normalizeHtmlStyle';
@@ -37,6 +38,7 @@ export const EnrichedText = ({
allowFontScaling = true,
onLinkPress: _onLinkPress,
onMentionPress: _onMentionPress,
+ onImagePress: _onImagePress,
...rest
}: EnrichedTextProps) => {
const nativeRef = useRef(null);
@@ -66,6 +68,13 @@ export const EnrichedText = ({
[_onMentionPress]
);
+ const onImagePress: DirectEventHandler = useCallback(
+ (e) => {
+ _onImagePress?.(e.nativeEvent);
+ },
+ [_onImagePress]
+ );
+
useImperativeHandle(ref, () => ({
measureInWindow: (callback: MeasureInWindowOnSuccessCallback) => {
nullthrows(nativeRef.current).measureInWindow(callback);
@@ -109,6 +118,7 @@ export const EnrichedText = ({
allowFontScaling={allowFontScaling}
onLinkPress={onLinkPress}
onMentionPress={onMentionPress}
+ onImagePress={onImagePress}
{...rest}
/>
);
diff --git a/src/spec/EnrichedTextNativeComponent.ts b/src/spec/EnrichedTextNativeComponent.ts
index fec62bd3d..790165b97 100644
--- a/src/spec/EnrichedTextNativeComponent.ts
+++ b/src/spec/EnrichedTextNativeComponent.ts
@@ -79,6 +79,14 @@ export interface OnMentionPressEvent {
attributes: Record;
}
+export interface OnImagePressEvent {
+ image: {
+ uri: string;
+ width: Float;
+ height: Float;
+ };
+}
+
export interface NativeProps extends ViewProps {
// Custom props
text: string;
@@ -95,6 +103,7 @@ export interface NativeProps extends ViewProps {
// Events
onLinkPress?: DirectEventHandler;
onMentionPress?: DirectEventHandler;
+ onImagePress?: DirectEventHandler;
// Style related props - used for generating proper setters in component's manager
// These should not be passed as regular props
diff --git a/src/types.ts b/src/types.ts
index 90282deff..6c0155f94 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -836,6 +836,9 @@ export interface EnrichedTextProps extends ViewProps {
/** Called when the user taps a mention node inside the rendered content. */
onMentionPress?: (event: OnMentionPressEvent) => void;
+
+ /** Called when the user taps an inline image inside the rendered content. */
+ onImagePress?: (event: OnImagePressEvent) => void;
}
export interface EnrichedTextMentionStyleProperties extends MentionStyleProperties {
@@ -864,3 +867,11 @@ export interface OnMentionPressEvent {
indicator: string;
attributes: Record;
}
+
+export interface OnImagePressEvent {
+ image: {
+ uri: string;
+ width: number;
+ height: number;
+ };
+}