Skip to content

Commit 80a17b9

Browse files
authored
Fix Left Column and Support Transpose in Overview Table (#96) (#99)
1 parent bb25b00 commit 80a17b9

File tree

5 files changed

+514
-178
lines changed

5 files changed

+514
-178
lines changed

components/DoubleStickyTable.tsx

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { RefreshControl, StyleSheet, View } from 'react-native';
2+
import { ScrollView } from 'react-native-gesture-handler';
3+
import { Cell, Table, TableWrapper } from 'react-native-reanimated-table';
4+
import colors from '../constants/colors';
5+
import { useOrientation } from '../hooks/useOrientation';
6+
import useSyncedScrollViews from '../hooks/useSyncedScrollViews';
7+
8+
export type CellType = Omit<
9+
React.ComponentProps<typeof Cell>,
10+
'width' | 'height' | 'flex'
11+
>;
12+
13+
export type DynamicCellType = CellType | ((opts: CellOpts) => CellType);
14+
15+
export interface CellOpts {
16+
transposed: boolean;
17+
}
18+
19+
export default function DoubleStickyTable({
20+
table,
21+
heightArr,
22+
transposedHeightArr,
23+
widthArr,
24+
transposedWidthArr,
25+
...rest
26+
}: Omit<React.ComponentProps<typeof DoubleStickyTableBase>, 'table'> & {
27+
table: DynamicCellType[][];
28+
transposedHeightArr: number[];
29+
transposedWidthArr: number[];
30+
}) {
31+
const totalWidth = widthArr.reduce((a, b) => a + b, 0);
32+
const totalHeight = heightArr.reduce((a, b) => a + b, 0);
33+
34+
const { isPortrait } = useOrientation();
35+
36+
if (
37+
(totalWidth > totalHeight && isPortrait) ||
38+
(totalWidth < totalHeight && !isPortrait)
39+
) {
40+
return (
41+
<DoubleStickyTableBase
42+
table={mapCells(transpose(table), (cell) => {
43+
if (typeof cell === 'function') return cell({ transposed: true });
44+
return cell;
45+
})}
46+
heightArr={transposedHeightArr}
47+
widthArr={transposedWidthArr}
48+
{...rest}
49+
/>
50+
);
51+
}
52+
53+
return (
54+
<DoubleStickyTableBase
55+
table={mapCells(table, (cell) => {
56+
if (typeof cell === 'function') return cell({ transposed: false });
57+
return cell;
58+
})}
59+
heightArr={heightArr}
60+
widthArr={widthArr}
61+
{...rest}
62+
/>
63+
);
64+
}
65+
66+
function DoubleStickyTableBase({
67+
table,
68+
heightArr,
69+
widthArr,
70+
refreshing,
71+
onRefresh,
72+
}: {
73+
table: CellType[][];
74+
heightArr: number[];
75+
widthArr: number[];
76+
refreshing: React.ComponentProps<typeof RefreshControl>['refreshing'];
77+
onRefresh: React.ComponentProps<typeof RefreshControl>['onRefresh'];
78+
}) {
79+
const { topRowHeader, lowerRowHeaders, columnHeaders, columnContent } =
80+
sliceTable(table);
81+
82+
const [headerHeight, ...contentHeight] = heightArr;
83+
const [headerWidth, ...contentWidth] = widthArr;
84+
85+
const { LeaderScrollView, FollowerScrollView } = useSyncedScrollViews({
86+
horizontal: true,
87+
});
88+
89+
return (
90+
<ScrollView
91+
stickyHeaderIndices={[0]}
92+
style={styles.table}
93+
refreshControl={
94+
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
95+
}
96+
>
97+
<View style={styles.rowSeparator}>
98+
<View style={{ flexDirection: 'row' }}>
99+
<Table style={styles.columnSeparator}>
100+
<Cell width={headerWidth} height={headerHeight} {...topRowHeader} />
101+
</Table>
102+
<FollowerScrollView>
103+
<Table>
104+
<TableWrapper style={{ flexDirection: 'row' }}>
105+
{columnHeaders.map((columnHeader, index) => (
106+
<Cell
107+
key={index}
108+
{...columnHeader}
109+
width={contentWidth[index]}
110+
height={headerHeight}
111+
/>
112+
))}
113+
</TableWrapper>
114+
</Table>
115+
</FollowerScrollView>
116+
</View>
117+
</View>
118+
<View style={{ flexDirection: 'row' }}>
119+
<Table style={styles.columnSeparator}>
120+
{lowerRowHeaders.map((rowHeader, index) => (
121+
<TableWrapper key={index}>
122+
<Cell
123+
width={headerWidth}
124+
height={contentHeight[index]}
125+
{...rowHeader}
126+
/>
127+
</TableWrapper>
128+
))}
129+
</Table>
130+
<LeaderScrollView style={styles.rowSeparator}>
131+
<Table>
132+
{columnContent.map((contentRow, rowIndex) => (
133+
<TableWrapper key={rowIndex} style={{ flexDirection: 'row' }}>
134+
{contentRow.map((contentCell, columnIndex) => (
135+
<Cell
136+
key={columnIndex}
137+
{...contentCell}
138+
width={contentWidth[columnIndex]}
139+
height={contentHeight[rowIndex]}
140+
/>
141+
))}
142+
</TableWrapper>
143+
))}
144+
</Table>
145+
</LeaderScrollView>
146+
</View>
147+
</ScrollView>
148+
);
149+
}
150+
151+
function extractTableRowHeaders<T>(table: T[][]): {
152+
headers: T[];
153+
content: T[][];
154+
} {
155+
return {
156+
headers: table.map(([rowHeader]) => rowHeader),
157+
content: table.map(([, ...content]) => content),
158+
};
159+
}
160+
161+
function sliceTable<T>(table: T[][]) {
162+
const { headers: rowHeaders, content: rowContent } =
163+
extractTableRowHeaders(table);
164+
const [topRowHeader, ...lowerRowHeaders] = rowHeaders;
165+
const [columnHeaders, ...columnContent] = rowContent;
166+
167+
return {
168+
topRowHeader,
169+
lowerRowHeaders,
170+
columnHeaders,
171+
columnContent,
172+
};
173+
}
174+
175+
function mapCells<A, B>(
176+
table: A[][],
177+
mapper: (cell: A, index: number) => B
178+
): B[][] {
179+
return table.map((row) => row.map((cell, index) => mapper(cell, index)));
180+
}
181+
182+
function transpose<T>(data: T[][]): T[][] {
183+
return data[0].map((_, i) => data.map((row) => row[i]));
184+
}
185+
186+
const styles = StyleSheet.create({
187+
table: {
188+
backgroundColor: colors.white,
189+
},
190+
columnSeparator: {
191+
borderColor: colors.primary,
192+
borderRightWidth: StyleSheet.hairlineWidth,
193+
},
194+
rowSeparator: {
195+
backgroundColor: colors.white,
196+
borderColor: colors.primary,
197+
borderTopWidth: StyleSheet.hairlineWidth,
198+
},
199+
});

hooks/useSyncedScrollViews.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useRef } from 'react';
2+
import {
3+
NativeScrollEvent,
4+
NativeSyntheticEvent,
5+
ScrollView,
6+
} from 'react-native';
7+
8+
export type LeaderScrollViewProps = Omit<
9+
React.ComponentProps<typeof ScrollView>,
10+
'onScroll' | 'scrollEventThrottle' | 'bounces' | 'horizontal'
11+
>;
12+
13+
export type FollowerScrollViewProps = Omit<
14+
React.ComponentProps<typeof ScrollView>,
15+
'bounces' | 'scrollEnabled' | 'bounces' | 'horizontal'
16+
>;
17+
18+
export default function useSyncedScrollViews({
19+
horizontal = false,
20+
}: {
21+
horizontal?: boolean;
22+
}) {
23+
const followerScroll = useRef<ScrollView>(null);
24+
const axisProp = horizontal ? 'x' : 'y';
25+
26+
function onLeaderScroll(event: NativeSyntheticEvent<NativeScrollEvent>) {
27+
followerScroll.current?.scrollTo({
28+
[axisProp]: event.nativeEvent.contentOffset[axisProp],
29+
animated: false,
30+
});
31+
}
32+
33+
return {
34+
LeaderScrollView: (props: LeaderScrollViewProps) => (
35+
<ScrollView
36+
onScroll={onLeaderScroll}
37+
scrollEventThrottle={16}
38+
bounces={false}
39+
horizontal={horizontal}
40+
{...props}
41+
/>
42+
),
43+
FollowerScrollView: (props: FollowerScrollViewProps) => (
44+
<ScrollView
45+
ref={followerScroll}
46+
scrollEnabled={false}
47+
bounces={false}
48+
horizontal={horizontal}
49+
{...props}
50+
{...(horizontal
51+
? { showsHorizontalScrollIndicator: false }
52+
: { showsVerticalScrollIndicator: false })}
53+
/>
54+
),
55+
};
56+
}

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"react-native-modal": "^13.0.1",
5454
"react-native-numeric-input": "https://github.com/athena-logistics/react-native-numeric-input.git",
5555
"react-native-reanimated": "~2.14.4",
56+
"react-native-reanimated-table": "^0.0.2",
5657
"react-native-sectioned-multi-select": "^0.9.1",
5758
"react-native-toast-message": "^2.1.6"
5859
},

0 commit comments

Comments
 (0)