Skip to content

Commit e2e4ebe

Browse files
committed
fix scroll issue on native
1 parent 57ce9e6 commit e2e4ebe

5 files changed

Lines changed: 101 additions & 20 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import useThemeStyles from '@hooks/useThemeStyles';
2+
3+
import {useMemo} from 'react';
4+
import {View} from 'react-native';
5+
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
6+
import Animated, {cancelAnimation, useAnimatedStyle, useSharedValue, withDecay} from 'react-native-reanimated';
7+
8+
import type HTMLTableScrollProps from './types';
9+
10+
/**
11+
* Native horizontal scroller for HTML tables. A plain ScrollView rendered inside react-native-render-html never wins the
12+
* pan on native, and a gesture-handler ScrollView can only ever claim one axis (blocking the chat's vertical scroll).
13+
* This custom pan scrolls the table horizontally while activeOffsetX/failOffsetY let vertical drags fall through to the
14+
* chat's vertical scroll.
15+
*/
16+
function HTMLTableScroll({viewportWidth, contentWidth, children}: HTMLTableScrollProps) {
17+
const styles = useThemeStyles();
18+
const maxScroll = Math.max(0, contentWidth - viewportWidth);
19+
20+
const translateX = useSharedValue(0);
21+
const panGesture = useMemo(
22+
() =>
23+
Gesture.Pan()
24+
.activeOffsetX([-10, 10])
25+
.failOffsetY([-10, 10])
26+
.onBegin(() => {
27+
cancelAnimation(translateX);
28+
})
29+
.onChange((event) => {
30+
translateX.set(Math.min(0, Math.max(-maxScroll, translateX.get() + event.changeX)));
31+
})
32+
.onEnd((event) => {
33+
translateX.set(withDecay({velocity: event.velocityX, clamp: [-maxScroll, 0]}));
34+
}),
35+
[maxScroll, translateX],
36+
);
37+
38+
const animatedStyle = useAnimatedStyle(() => ({transform: [{translateX: translateX.get()}]}));
39+
40+
return (
41+
<View style={[styles.htmlTable, {width: viewportWidth}]}>
42+
<GestureDetector gesture={panGesture}>
43+
<Animated.View style={[{width: contentWidth}, animatedStyle]}>{children}</Animated.View>
44+
</GestureDetector>
45+
</View>
46+
);
47+
}
48+
49+
export default HTMLTableScroll;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import ScrollView from '@components/ScrollView';
2+
3+
import useThemeStyles from '@hooks/useThemeStyles';
4+
5+
import {View} from 'react-native';
6+
7+
import type HTMLTableScrollProps from './types';
8+
9+
/**
10+
* Web/default horizontal scroller for HTML tables. A plain ScrollView scrolls fine here because web is not affected by
11+
* the native responder issue that requires the custom pan gesture in the native variant. The rows are wrapped in a
12+
* column View because a horizontal ScrollView's content container lays its children out in a row by default.
13+
*/
14+
function HTMLTableScroll({viewportWidth, contentWidth, children}: HTMLTableScrollProps) {
15+
const styles = useThemeStyles();
16+
17+
return (
18+
<ScrollView
19+
horizontal
20+
showsHorizontalScrollIndicator={false}
21+
style={{width: viewportWidth}}
22+
>
23+
<View style={[styles.htmlTable, {width: contentWidth}]}>{children}</View>
24+
</ScrollView>
25+
);
26+
}
27+
28+
export default HTMLTableScroll;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type {ReactNode} from 'react';
2+
3+
type HTMLTableScrollProps = {
4+
/** Width of the visible scroll viewport, i.e. the available message width. */
5+
viewportWidth: number;
6+
7+
/** Full width of the table content; at least the viewport width. */
8+
contentWidth: number;
9+
10+
/** The rendered table rows. */
11+
children: ReactNode;
12+
};
13+
14+
export default HTMLTableScrollProps;

src/components/HTMLEngineProvider/HTMLRenderers/TableRenderer.tsx

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
import useThemeStyles from '@hooks/useThemeStyles';
2-
31
import variables from '@styles/variables';
42

53
import type {CustomRendererProps, TBlock, TNode} from 'react-native-render-html';
64

75
import {useContext, useMemo} from 'react';
8-
import {View} from 'react-native';
9-
import {ScrollView} from 'react-native-gesture-handler';
106
import {useContentWidth} from 'react-native-render-html';
117

128
import type {CellHorizontalAlignment} from './TableColumnAlignmentContext';
139

10+
import HTMLTableScroll from './HTMLTableScroll';
1411
import TableChildrenRenderer, {getElementChildren} from './TableChildrenRenderer';
1512
import TableColumnAlignmentContext from './TableColumnAlignmentContext';
1613
import TableContentWidthContext from './TableContentWidthContext';
@@ -33,11 +30,10 @@ function getColumnAlignments(tableNode: TNode): CellHorizontalAlignment[] {
3330
}
3431

3532
function TableRenderer({tnode}: CustomRendererProps<TBlock>) {
36-
const styles = useThemeStyles();
3733
const columnAlignments = useMemo(() => getColumnAlignments(tnode), [tnode]);
3834

3935
// The comment-level width fills the message exactly; fall back to the HTML content width when the table is rendered
40-
// outside a comment. A concrete number is required because a percentage does not resolve inside a horizontal ScrollView.
36+
// outside a comment. A concrete number is required because the scroller needs a fixed viewport and content width.
4137
const measuredContentWidth = useContext(TableContentWidthContext);
4238
const fallbackContentWidth = useContentWidth();
4339
const viewportWidth = measuredContentWidth || fallbackContentWidth;
@@ -46,17 +42,12 @@ function TableRenderer({tnode}: CustomRendererProps<TBlock>) {
4642

4743
return (
4844
<TableColumnAlignmentContext.Provider value={columnAlignments}>
49-
<ScrollView
50-
horizontal
51-
shouldActivateOnStart
52-
showsHorizontalScrollIndicator={false}
53-
style={{width: viewportWidth}}
54-
contentContainerStyle={{width: contentWidth}}
45+
<HTMLTableScroll
46+
viewportWidth={viewportWidth}
47+
contentWidth={contentWidth}
5548
>
56-
<View style={styles.htmlTable}>
57-
<TableChildrenRenderer tnode={tnode} />
58-
</View>
59-
</ScrollView>
49+
<TableChildrenRenderer tnode={tnode} />
50+
</HTMLTableScroll>
6051
</TableColumnAlignmentContext.Provider>
6152
);
6253
}

src/styles/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2726,10 +2726,9 @@ const staticStyles = (theme: ThemeColors) =>
27262726
},
27272727

27282728
htmlTableCell: {
2729-
// A definite flexBasis (not flex: 1 / basis 0) keeps columns aligned across rows inside the horizontal
2730-
// ScrollView, where the indefinite main-axis width otherwise collapses each cell to its content width.
2731-
// flexShrink: 0 stops columns from shrinking to fit the viewport, so a wide table overflows and can be
2732-
// scrolled horizontally; flexGrow: 1 still lets columns expand to fill the message width when it is wider.
2729+
// A definite flexBasis with flexShrink: 0 gives every column a fixed width so a wide table keeps its size
2730+
// and can be scrolled horizontally, and columns stay aligned across rows; flexGrow: 1 still lets columns
2731+
// expand to fill the message width when the table is narrower than it.
27332732
flexGrow: 1,
27342733
flexShrink: 0,
27352734
flexBasis: variables.htmlTableColumnMaxWidth,

0 commit comments

Comments
 (0)