Skip to content

Commit 9ca4639

Browse files
authored
fix: prevent parent Pressable from firing onPress on link/task tap (#143)
* fix: prevent parent Pressable from firing onPress on link/task tap * fix: enhance touch event handling to prevent unintended parent press actions * fix: rename cancelJSTouch to cancelJSTouchForCheckboxTap for clarity in touch event handling
1 parent 4dfb9c2 commit 9ca4639

8 files changed

Lines changed: 107 additions & 11 deletions

File tree

android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import com.swmansion.enriched.markdown.accessibility.MarkdownAccessibilityHelper
99
import com.swmansion.enriched.markdown.utils.text.interaction.CheckboxTouchHelper
1010
import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod
1111
import com.swmansion.enriched.markdown.utils.text.view.applySelectableState
12+
import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForCheckboxTap
13+
import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap
1214
import com.swmansion.enriched.markdown.utils.text.view.setupAsMarkdownTextView
1315
import com.swmansion.enriched.markdown.views.BlockSegmentView
1416

@@ -53,8 +55,17 @@ class EnrichedMarkdownInternalText
5355
}
5456

5557
override fun onTouchEvent(event: MotionEvent): Boolean {
56-
if (checkboxTouchHelper.onTouchEvent(event)) return true
57-
return super.onTouchEvent(event)
58+
if (checkboxTouchHelper.onTouchEvent(event)) {
59+
if (event.action == MotionEvent.ACTION_DOWN) {
60+
cancelJSTouchForCheckboxTap(event)
61+
}
62+
return true
63+
}
64+
val result = super.onTouchEvent(event)
65+
if (event.action == MotionEvent.ACTION_DOWN) {
66+
cancelJSTouchForLinkTap(event)
67+
}
68+
return result
5869
}
5970

6071
fun setJustificationMode(needsJustify: Boolean) {

android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import com.swmansion.enriched.markdown.utils.text.TailFadeInAnimator
2020
import com.swmansion.enriched.markdown.utils.text.interaction.CheckboxTouchHelper
2121
import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod
2222
import com.swmansion.enriched.markdown.utils.text.view.applySelectableState
23+
import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForCheckboxTap
24+
import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap
2325
import com.swmansion.enriched.markdown.utils.text.view.emitLinkLongPressEvent
2426
import com.swmansion.enriched.markdown.utils.text.view.emitLinkPressEvent
2527
import com.swmansion.enriched.markdown.utils.text.view.setupAsMarkdownTextView
@@ -259,8 +261,17 @@ class EnrichedMarkdownText
259261
}
260262

261263
override fun onTouchEvent(event: MotionEvent): Boolean {
262-
if (checkboxTouchHelper.onTouchEvent(event)) return true
263-
return super.onTouchEvent(event)
264+
if (checkboxTouchHelper.onTouchEvent(event)) {
265+
if (event.action == MotionEvent.ACTION_DOWN) {
266+
cancelJSTouchForCheckboxTap(event)
267+
}
268+
return true
269+
}
270+
val result = super.onTouchEvent(event)
271+
if (event.action == MotionEvent.ACTION_DOWN) {
272+
cancelJSTouchForLinkTap(event)
273+
}
274+
return result
264275
}
265276

266277
companion object {

android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkEvents.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package com.swmansion.enriched.markdown.utils.text.view
22

3+
import android.view.MotionEvent
34
import android.view.View
5+
import android.widget.TextView
46
import com.facebook.react.bridge.ReactContext
57
import com.facebook.react.uimanager.UIManagerHelper
8+
import com.facebook.react.uimanager.events.NativeGestureUtil
69
import com.swmansion.enriched.markdown.events.LinkLongPressEvent
710
import com.swmansion.enriched.markdown.events.LinkPressEvent
811

@@ -19,3 +22,22 @@ fun View.emitLinkLongPressEvent(url: String) {
1922
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
2023
dispatcher?.dispatchEvent(LinkLongPressEvent(surfaceId, id, url))
2124
}
25+
26+
/**
27+
* Cancels the JS touch for an active link tap, preventing parent
28+
* Pressable/TouchableOpacity from firing onPress for the same tap.
29+
*/
30+
fun TextView.cancelJSTouchForLinkTap(event: MotionEvent) {
31+
val currentMovementMethod = movementMethod
32+
if (currentMovementMethod is LinkLongPressMovementMethod && currentMovementMethod.isLinkTouchActive) {
33+
NativeGestureUtil.notifyNativeGestureStarted(this, event)
34+
}
35+
}
36+
37+
/**
38+
* Cancels the JS touch unconditionally, preventing parent
39+
* Pressable/TouchableOpacity from firing onPress for the same gesture.
40+
*/
41+
fun View.cancelJSTouchForCheckboxTap(event: MotionEvent) {
42+
NativeGestureUtil.notifyNativeGestureStarted(this, event)
43+
}

android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLongPressMovementMethod.kt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ class LinkLongPressMovementMethod : LinkMovementMethod() {
2222
private var startX = 0f
2323
private var startY = 0f
2424

25+
var isLinkTouchActive: Boolean = false
26+
private set
27+
2528
override fun onTouchEvent(
2629
widget: TextView,
2730
buffer: Spannable,
@@ -32,32 +35,30 @@ class LinkLongPressMovementMethod : LinkMovementMethod() {
3235
startX = event.x
3336
startY = event.y
3437

35-
// Identify if a LinkSpan exists at the touch coordinates
36-
findLinkSpan(widget, buffer, event)?.let { span ->
37-
scheduleLongPress(widget, span)
38-
}
38+
val span = findLinkSpan(widget, buffer, event)
39+
isLinkTouchActive = span != null
40+
span?.let { scheduleLongPress(widget, it) }
3941
}
4042

4143
MotionEvent.ACTION_MOVE -> {
4244
val config = ViewConfiguration.get(widget.context)
43-
// Cancel if the finger moves beyond the standard system touch slop
4445
if (abs(event.x - startX) > config.scaledTouchSlop ||
4546
abs(event.y - startY) > config.scaledTouchSlop
4647
) {
4748
cancelLongPress()
49+
isLinkTouchActive = false
4850
}
4951
}
5052

5153
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
5254
cancelLongPress()
53-
// Clear text selection to prevent the "stuck" highlight look
55+
isLinkTouchActive = false
5456
if (widget.hasSelection()) {
5557
Selection.removeSelection(buffer)
5658
}
5759
}
5860
}
5961

60-
// Let the parent LinkMovementMethod handle the standard click logic
6162
val result = super.onTouchEvent(widget, buffer, event)
6263

6364
// LinkMovementMethod sets a Selection highlight around the link on ACTION_DOWN,

ios/EnrichedMarkdown.mm

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,25 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
610610
return EnrichedMarkdown.class;
611611
}
612612

613+
- (facebook::react::SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
614+
{
615+
for (UIView *segment in _segmentViews) {
616+
if (![segment isKindOfClass:[EnrichedMarkdownInternalText class]]) {
617+
continue;
618+
}
619+
EnrichedMarkdownInternalText *textSegment = (EnrichedMarkdownInternalText *)segment;
620+
CGPoint segmentPoint = [self convertPoint:point toView:textSegment.textView];
621+
if ([textSegment.textView pointInside:segmentPoint withEvent:nil]) {
622+
if (isPointOnInteractiveElement(textSegment.textView, segmentPoint)) {
623+
return nil;
624+
}
625+
break;
626+
}
627+
}
628+
629+
return [super touchEventEmitterAtPoint:point];
630+
}
631+
613632
- (void)textTapped:(UITapGestureRecognizer *)recognizer
614633
{
615634
UITextView *textView = (UITextView *)recognizer.view;

ios/EnrichedMarkdownText.mm

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,18 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
468468
return EnrichedMarkdownText.class;
469469
}
470470

471+
- (facebook::react::SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
472+
{
473+
if (_textView) {
474+
CGPoint textViewPoint = [self convertPoint:point toView:_textView];
475+
if (isPointOnInteractiveElement(_textView, textViewPoint)) {
476+
return nil;
477+
}
478+
}
479+
480+
return [super touchEventEmitterAtPoint:point];
481+
}
482+
471483
- (void)textTapped:(UITapGestureRecognizer *)recognizer
472484
{
473485
UITextView *textView = (UITextView *)recognizer.view;

ios/utils/LinkTapUtils.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ NSString *_Nullable linkURLAtTapLocation(UITextView *textView, UITapGestureRecog
1414
/// Returns the link URL at the given character range, or nil if none found.
1515
NSString *_Nullable linkURLAtRange(UITextView *textView, NSRange characterRange);
1616

17+
/// Returns YES if the point (in textView coordinates) is on a link or task list checkbox.
18+
BOOL isPointOnInteractiveElement(UITextView *textView, CGPoint point);
19+
1720
#ifdef __cplusplus
1821
}
1922
#endif

ios/utils/LinkTapUtils.m

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,20 @@
2626
}
2727
return [textView.attributedText attribute:@"linkURL" atIndex:characterRange.location effectiveRange:NULL];
2828
}
29+
30+
BOOL isPointOnInteractiveElement(UITextView *textView, CGPoint point)
31+
{
32+
NSLayoutManager *layoutManager = textView.layoutManager;
33+
CGPoint adjusted = CGPointMake(point.x - textView.textContainerInset.left, point.y - textView.textContainerInset.top);
34+
35+
NSUInteger charIndex = [layoutManager characterIndexForPoint:adjusted
36+
inTextContainer:textView.textContainer
37+
fractionOfDistanceBetweenInsertionPoints:NULL];
38+
39+
if (charIndex >= textView.textStorage.length) {
40+
return NO;
41+
}
42+
43+
NSDictionary *attrs = [textView.attributedText attributesAtIndex:charIndex effectiveRange:NULL];
44+
return attrs[@"linkURL"] != nil || [attrs[@"TaskItem"] boolValue];
45+
}

0 commit comments

Comments
 (0)