-
Notifications
You must be signed in to change notification settings - Fork 23
Description
Description
On Android, rendered markdown tables that overflow horizontally are nearly impossible to scroll. Occasionally you can touch them in just the right spot and scroll, but it's extremely difficult and unreliable.
Root Cause
The table's horizontal ScrollView is nested inside three layers of gesture-consuming ancestors that compete for touch events on Android:
Layer 1: GestureDetector with LongPress (MarkdownView.tsx:84-99)
The entire markdown content (including the table) is wrapped in a GestureDetector for long-press-to-copy:
const longPressGesture = Gesture.LongPress()
.minDuration(500)
.onStart(() => { handleLongPress(); })
.runOnJS(true);
<GestureDetector gesture={longPressGesture}>
<View style={{ width: '100%' }}>
{renderContent()} // ← table's ScrollView is inside here
</View>
</GestureDetector>The comment says "it doesn't block pan gestures so horizontal scrolling in code blocks and tables still works" — but on Android, react-native-gesture-handler's GestureDetector can still interfere with the touch event pipeline by consuming the initial touch-down, making it harder for the inner ScrollView to recognize a horizontal pan.
Layer 2: Pressable wrapper (MessageView.tsx:274)
Agent messages are wrapped in <Pressable> for hover detection:
<Pressable
style={styles.agentMessageContainer}
onHoverIn={isWeb ? () => setIsMessageHovered(true) : undefined}
onHoverOut={isWeb ? () => setIsMessageHovered(false) : undefined}
>
...
<MarkdownView markdown={markdown} ... />
...
</Pressable>On Android, Pressable participates in the responder system and can delay or intercept touches intended for child scroll views.
Layer 3: Inverted FlatList (ChatList.tsx:429-477)
The parent chat list is an inverted FlatList:
<FlatList
inverted={true}
keyboardShouldPersistTaps="handled"
...
/>The vertical FlatList handles vertical scroll gestures, and on Android it doesn't always correctly negotiate with nested horizontal ScrollViews — even when the inner ScrollView has nestedScrollEnabled={true}.
The table's ScrollView (MarkdownView.tsx:267-272)
<ScrollView
horizontal
showsHorizontalScrollIndicator={Platform.OS !== 'web'}
nestedScrollEnabled={true}
style={style.tableScrollView}
>The ScrollView has nestedScrollEnabled={true}, which is the correct Android property for nested scrolling. But it's not sufficient when three ancestor gesture consumers compete for the same touch event.
Why it works "sometimes in just the right spot"
The gesture system uses hit-testing to determine which handler gets the touch:
- Touching text inside the table →
PressableandGestureDetectorboth compete for the touch; the horizontalScrollViewloses - Touching empty space (padding/borders) inside the table → the
ScrollViewmay win the gesture negotiation, allowing horizontal scroll
Suggested Fixes
-
Exclude tables from the
GestureDetectorwrapper — Render table blocks outside theGestureDetector, or useGesture.Exclusive/Gesture.Simultaneousto explicitly allow pan gestures to pass through to table ScrollViews -
Replace
Pressablewith a non-touch-consuming wrapper on mobile — ThePressableis only used for hover detection (onHoverIn/onHoverOut) which is web-only. On native, it could be a plainViewthat doesn't participate in the responder system -
Use
react-native-gesture-handler'sScrollViewinstead of RN's built-inScrollViewfor the table — RNGH's ScrollView integrates better with the gesture handler system and may resolve the negotiation issue -
Add
simultaneousHandlerscoordination — Configure theLongPressgesture to explicitly allow simultaneous pan gestures from the table's ScrollView
Affected Files
apps/ui/sources/components/markdown/MarkdownView.tsx(lines 84-99 gesture wrapper, lines 255-305 table rendering)apps/ui/sources/components/sessions/transcript/MessageView.tsx(lines 274-319 Pressable wrapper)apps/ui/sources/components/sessions/transcript/ChatList.tsx(lines 429-477 FlatList config)
🤖 Generated with Claude Code