Skip to content

Android: tables in markdown are nearly impossible to scroll horizontally #92

@begna112

Description

@begna112

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 → Pressable and GestureDetector both compete for the touch; the horizontal ScrollView loses
  • Touching empty space (padding/borders) inside the table → the ScrollView may win the gesture negotiation, allowing horizontal scroll

Suggested Fixes

  1. Exclude tables from the GestureDetector wrapper — Render table blocks outside the GestureDetector, or use Gesture.Exclusive/Gesture.Simultaneous to explicitly allow pan gestures to pass through to table ScrollViews

  2. Replace Pressable with a non-touch-consuming wrapper on mobile — The Pressable is only used for hover detection (onHoverIn/onHoverOut) which is web-only. On native, it could be a plain View that doesn't participate in the responder system

  3. Use react-native-gesture-handler's ScrollView instead of RN's built-in ScrollView for the table — RNGH's ScrollView integrates better with the gesture handler system and may resolve the negotiation issue

  4. Add simultaneousHandlers coordination — Configure the LongPress gesture 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions