@idealyst/datagrid 1.0.40 → 1.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/DataGrid/DataGrid.native.tsx +267 -0
- package/src/DataGrid/DataGrid.tsx +216 -108
- package/src/DataGrid/SimpleDataGrid.tsx +74 -0
- package/src/DataGrid/index.native.ts +2 -0
- package/src/DataGrid/index.web.ts +2 -0
- package/src/examples/BasicExample.tsx +1 -1
- package/src/examples/DataGridShowcase.tsx +296 -0
- package/src/examples/index.ts +2 -1
- package/src/index.native.ts +1 -1
- package/src/primitives/Cell/Cell.native.tsx +15 -0
- package/src/primitives/Cell/Cell.web.tsx +36 -0
- package/src/primitives/Cell/index.native.ts +1 -0
- package/src/primitives/Cell/index.ts +1 -0
- package/src/primitives/Row/Row.native.tsx +21 -0
- package/src/primitives/Row/Row.web.tsx +48 -0
- package/src/primitives/Row/index.native.ts +1 -0
- package/src/primitives/Row/index.ts +1 -0
- package/src/primitives/ScrollView/ScrollView.web.tsx +3 -0
- package/src/primitives/Table/Table.native.tsx +99 -0
- package/src/primitives/Table/Table.web.tsx +102 -0
- package/src/primitives/Table/TableBody.native.tsx +27 -0
- package/src/primitives/Table/TableBody.web.tsx +26 -0
- package/src/primitives/Table/TableHeader.native.tsx +27 -0
- package/src/primitives/Table/TableHeader.web.tsx +26 -0
- package/src/primitives/Table/index.native.ts +3 -0
- package/src/primitives/Table/index.ts +3 -0
- package/src/primitives/VirtualizedList/VirtualizedList.web.tsx +9 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/datagrid",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.44",
|
|
4
4
|
"description": "High-performance datagrid component for React and React Native",
|
|
5
5
|
"documentation": "https://github.com/your-username/idealyst-framework/tree/main/packages/datagrid#readme",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"publish:npm": "npm publish"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@idealyst/components": "^1.0.
|
|
39
|
-
"@idealyst/theme": "^1.0.
|
|
38
|
+
"@idealyst/components": "^1.0.44",
|
|
39
|
+
"@idealyst/theme": "^1.0.44",
|
|
40
40
|
"react": ">=16.8.0",
|
|
41
41
|
"react-native": ">=0.60.0",
|
|
42
42
|
"react-native-unistyles": "^3.0.4",
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import React, { useState, useCallback, useMemo, memo } from 'react';
|
|
2
|
+
import { FlatList, FlatListProps, TouchableOpacity, ScrollView } from 'react-native';
|
|
3
|
+
import { View, Text } from '@idealyst/components';
|
|
4
|
+
import type { DataGridProps, Column } from './types';
|
|
5
|
+
|
|
6
|
+
export function DataGrid<T extends Record<string, any>>({
|
|
7
|
+
data,
|
|
8
|
+
columns,
|
|
9
|
+
rowHeight = 48,
|
|
10
|
+
headerHeight = 56,
|
|
11
|
+
onRowClick,
|
|
12
|
+
onSort,
|
|
13
|
+
virtualized = true,
|
|
14
|
+
height = 400,
|
|
15
|
+
width = '100%',
|
|
16
|
+
style,
|
|
17
|
+
headerStyle,
|
|
18
|
+
rowStyle,
|
|
19
|
+
selectedRows = [],
|
|
20
|
+
onSelectionChange,
|
|
21
|
+
multiSelect = false,
|
|
22
|
+
stickyHeader = true,
|
|
23
|
+
}: DataGridProps<T>) {
|
|
24
|
+
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
|
25
|
+
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
|
26
|
+
|
|
27
|
+
// Calculate column widths and total width
|
|
28
|
+
const { columnWidths, totalWidth } = useMemo(() => {
|
|
29
|
+
const widths = columns.map(col => col.width || 120);
|
|
30
|
+
const total = widths.reduce((sum, width) => sum + (typeof width === 'number' ? width : 120), 0);
|
|
31
|
+
return { columnWidths: widths, totalWidth: total };
|
|
32
|
+
}, [columns]);
|
|
33
|
+
|
|
34
|
+
const handleSort = useCallback((column: Column<T>) => {
|
|
35
|
+
if (!column.sortable) return;
|
|
36
|
+
|
|
37
|
+
const newDirection = sortColumn === column.key && sortDirection === 'asc' ? 'desc' : 'asc';
|
|
38
|
+
setSortColumn(column.key);
|
|
39
|
+
setSortDirection(newDirection);
|
|
40
|
+
onSort?.(column, newDirection);
|
|
41
|
+
}, [sortColumn, sortDirection, onSort]);
|
|
42
|
+
|
|
43
|
+
const handleRowPress = useCallback((item: T, index: number) => {
|
|
44
|
+
if (onSelectionChange) {
|
|
45
|
+
let newSelection: number[];
|
|
46
|
+
if (multiSelect) {
|
|
47
|
+
if (selectedRows.includes(index)) {
|
|
48
|
+
newSelection = selectedRows.filter(i => i !== index);
|
|
49
|
+
} else {
|
|
50
|
+
newSelection = [...selectedRows, index];
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
newSelection = selectedRows.includes(index) ? [] : [index];
|
|
54
|
+
}
|
|
55
|
+
onSelectionChange(newSelection);
|
|
56
|
+
}
|
|
57
|
+
onRowClick?.(item, index);
|
|
58
|
+
}, [selectedRows, onSelectionChange, multiSelect, onRowClick]);
|
|
59
|
+
|
|
60
|
+
const renderHeader = useMemo(() => (
|
|
61
|
+
<View style={[
|
|
62
|
+
{
|
|
63
|
+
flexDirection: 'row',
|
|
64
|
+
backgroundColor: '#ffffff',
|
|
65
|
+
borderBottomWidth: 2,
|
|
66
|
+
borderBottomColor: '#ddd',
|
|
67
|
+
height: headerHeight,
|
|
68
|
+
width: totalWidth,
|
|
69
|
+
elevation: stickyHeader ? 4 : 0,
|
|
70
|
+
shadowColor: '#000',
|
|
71
|
+
shadowOffset: { width: 0, height: 2 },
|
|
72
|
+
shadowOpacity: 0.1,
|
|
73
|
+
shadowRadius: 4,
|
|
74
|
+
},
|
|
75
|
+
headerStyle
|
|
76
|
+
]}>
|
|
77
|
+
{columns.map((column, colIndex) => {
|
|
78
|
+
const width = columnWidths[colIndex];
|
|
79
|
+
const isLastColumn = colIndex === columns.length - 1;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<View
|
|
83
|
+
key={column.key}
|
|
84
|
+
style={[
|
|
85
|
+
{
|
|
86
|
+
width: typeof width === 'number' ? width : 120,
|
|
87
|
+
height: '100%',
|
|
88
|
+
paddingHorizontal: 12,
|
|
89
|
+
paddingVertical: 8,
|
|
90
|
+
borderRightWidth: isLastColumn ? 0 : 1,
|
|
91
|
+
borderRightColor: '#ddd',
|
|
92
|
+
justifyContent: 'center',
|
|
93
|
+
backgroundColor: '#ffffff',
|
|
94
|
+
},
|
|
95
|
+
column.headerStyle
|
|
96
|
+
]}
|
|
97
|
+
>
|
|
98
|
+
<Text
|
|
99
|
+
weight="bold"
|
|
100
|
+
size="small"
|
|
101
|
+
onPress={column.sortable ? () => handleSort(column) : undefined}
|
|
102
|
+
style={{
|
|
103
|
+
color: '#374151',
|
|
104
|
+
...(column.sortable ? { textDecorationLine: 'underline' } : {})
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{column.header}
|
|
108
|
+
{column.sortable && sortColumn === column.key && (
|
|
109
|
+
<Text style={{ fontSize: 10, color: '#6366f1' }}>
|
|
110
|
+
{sortDirection === 'asc' ? ' ▲' : ' ▼'}
|
|
111
|
+
</Text>
|
|
112
|
+
)}
|
|
113
|
+
</Text>
|
|
114
|
+
</View>
|
|
115
|
+
);
|
|
116
|
+
})}
|
|
117
|
+
</View>
|
|
118
|
+
), [columns, columnWidths, headerHeight, stickyHeader, headerStyle, sortColumn, sortDirection, handleSort]);
|
|
119
|
+
|
|
120
|
+
// Memoized row component to prevent unnecessary re-renders
|
|
121
|
+
const MemoizedRow = memo<{ item: T; index: number; isSelected: boolean; onPress: () => void }>(
|
|
122
|
+
({ item, index, isSelected, onPress }) => {
|
|
123
|
+
const computedRowStyle = typeof rowStyle === 'function' ? rowStyle(item, index) : rowStyle;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<TouchableOpacity
|
|
127
|
+
onPress={onPress}
|
|
128
|
+
style={[
|
|
129
|
+
{
|
|
130
|
+
flexDirection: 'row',
|
|
131
|
+
backgroundColor: isSelected ? '#e0e7ff' : (index % 2 === 0 ? '#fafafa' : '#ffffff'),
|
|
132
|
+
borderBottomWidth: 1,
|
|
133
|
+
borderBottomColor: '#e5e7eb',
|
|
134
|
+
height: rowHeight,
|
|
135
|
+
width: totalWidth,
|
|
136
|
+
},
|
|
137
|
+
computedRowStyle
|
|
138
|
+
]}
|
|
139
|
+
>
|
|
140
|
+
{columns.map((column, colIndex) => {
|
|
141
|
+
const value = column.accessor ? column.accessor(item) : item[column.key];
|
|
142
|
+
const cellContent = column.render ? column.render(value, item, index) : value;
|
|
143
|
+
const computedCellStyle = typeof column.cellStyle === 'function'
|
|
144
|
+
? column.cellStyle(value, item)
|
|
145
|
+
: column.cellStyle;
|
|
146
|
+
const width = columnWidths[colIndex];
|
|
147
|
+
const isLastColumn = colIndex === columns.length - 1;
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<View
|
|
151
|
+
key={column.key}
|
|
152
|
+
style={[
|
|
153
|
+
{
|
|
154
|
+
width: typeof width === 'number' ? width : 120,
|
|
155
|
+
height: '100%',
|
|
156
|
+
paddingHorizontal: 12,
|
|
157
|
+
paddingVertical: 8,
|
|
158
|
+
borderRightWidth: isLastColumn ? 0 : 1,
|
|
159
|
+
borderRightColor: '#e5e7eb',
|
|
160
|
+
justifyContent: 'center',
|
|
161
|
+
},
|
|
162
|
+
computedCellStyle
|
|
163
|
+
]}
|
|
164
|
+
>
|
|
165
|
+
{typeof cellContent === 'string' || typeof cellContent === 'number' ? (
|
|
166
|
+
<Text size="small" numberOfLines={1}>
|
|
167
|
+
{cellContent}
|
|
168
|
+
</Text>
|
|
169
|
+
) : (
|
|
170
|
+
cellContent
|
|
171
|
+
)}
|
|
172
|
+
</View>
|
|
173
|
+
);
|
|
174
|
+
})}
|
|
175
|
+
</TouchableOpacity>
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
// Custom comparison function to prevent unnecessary re-renders
|
|
179
|
+
(prevProps, nextProps) => {
|
|
180
|
+
// Only re-render if the item, selection state, or index changes
|
|
181
|
+
return (
|
|
182
|
+
prevProps.item === nextProps.item &&
|
|
183
|
+
prevProps.isSelected === nextProps.isSelected &&
|
|
184
|
+
prevProps.index === nextProps.index
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const renderRow = useCallback(({ item, index }: { item: T; index: number }) => {
|
|
190
|
+
const isSelected = selectedRows.includes(index);
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<MemoizedRow
|
|
194
|
+
item={item}
|
|
195
|
+
index={index}
|
|
196
|
+
isSelected={isSelected}
|
|
197
|
+
onPress={() => handleRowPress(item, index)}
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
}, [selectedRows, handleRowPress]);
|
|
201
|
+
|
|
202
|
+
// Memoize the key extractor
|
|
203
|
+
const keyExtractor = useCallback((item: T, index: number) => {
|
|
204
|
+
// Use a unique key based on item data if available, otherwise use index
|
|
205
|
+
return item.id ? `row-${item.id}` : `row-${index}`;
|
|
206
|
+
}, []);
|
|
207
|
+
|
|
208
|
+
const flatListProps: Partial<FlatListProps<T>> = {
|
|
209
|
+
data,
|
|
210
|
+
renderItem: renderRow,
|
|
211
|
+
keyExtractor,
|
|
212
|
+
getItemLayout: virtualized ? (data, index) => ({
|
|
213
|
+
length: rowHeight,
|
|
214
|
+
offset: rowHeight * index,
|
|
215
|
+
index,
|
|
216
|
+
}) : undefined,
|
|
217
|
+
removeClippedSubviews: virtualized,
|
|
218
|
+
maxToRenderPerBatch: virtualized ? 15 : data.length,
|
|
219
|
+
windowSize: virtualized ? 15 : 21,
|
|
220
|
+
initialNumToRender: virtualized ? 10 : data.length,
|
|
221
|
+
updateCellsBatchingPeriod: 100,
|
|
222
|
+
showsVerticalScrollIndicator: true,
|
|
223
|
+
showsHorizontalScrollIndicator: true,
|
|
224
|
+
horizontal: false,
|
|
225
|
+
style: {
|
|
226
|
+
flex: 1,
|
|
227
|
+
backgroundColor: '#ffffff',
|
|
228
|
+
},
|
|
229
|
+
ListHeaderComponent: stickyHeader ? renderHeader : undefined,
|
|
230
|
+
stickyHeaderIndices: stickyHeader ? [0] : undefined,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<View style={[
|
|
235
|
+
{
|
|
236
|
+
backgroundColor: '#ffffff',
|
|
237
|
+
borderWidth: 1,
|
|
238
|
+
borderColor: '#d1d5db',
|
|
239
|
+
borderRadius: 8,
|
|
240
|
+
overflow: 'hidden',
|
|
241
|
+
width,
|
|
242
|
+
height,
|
|
243
|
+
},
|
|
244
|
+
style
|
|
245
|
+
]}>
|
|
246
|
+
<ScrollView
|
|
247
|
+
horizontal
|
|
248
|
+
showsHorizontalScrollIndicator={true}
|
|
249
|
+
style={{ flex: 1 }}
|
|
250
|
+
contentContainerStyle={{ minWidth: totalWidth }}
|
|
251
|
+
>
|
|
252
|
+
<View style={{ width: totalWidth, flex: 1 }}>
|
|
253
|
+
{!stickyHeader && renderHeader()}
|
|
254
|
+
<FlatList
|
|
255
|
+
{...flatListProps}
|
|
256
|
+
style={{ flex: 1 }}
|
|
257
|
+
showsVerticalScrollIndicator={true}
|
|
258
|
+
ListHeaderComponent={stickyHeader ? renderHeader : undefined}
|
|
259
|
+
stickyHeaderIndices={stickyHeader ? [0] : undefined}
|
|
260
|
+
scrollEnabled={true}
|
|
261
|
+
nestedScrollEnabled={true}
|
|
262
|
+
/>
|
|
263
|
+
</View>
|
|
264
|
+
</ScrollView>
|
|
265
|
+
</View>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import React, { useState, useMemo,
|
|
2
|
-
import { View, Text
|
|
3
|
-
import { createStyleSheet, useStyles } from 'react-native-unistyles';
|
|
1
|
+
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
|
2
|
+
import { View, Text } from '@idealyst/components';
|
|
4
3
|
import { ScrollView } from '../primitives/ScrollView';
|
|
5
|
-
import {
|
|
4
|
+
import { Table, TableRow, TableCell, TableHeader, TableBody } from '../primitives/Table';
|
|
6
5
|
import type { DataGridProps, Column } from './types';
|
|
7
6
|
|
|
8
7
|
export function DataGrid<T extends Record<string, any>>({
|
|
@@ -23,9 +22,107 @@ export function DataGrid<T extends Record<string, any>>({
|
|
|
23
22
|
multiSelect = false,
|
|
24
23
|
stickyHeader = true,
|
|
25
24
|
}: DataGridProps<T>) {
|
|
26
|
-
const { styles } = useStyles(stylesheet);
|
|
27
25
|
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
|
28
26
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
|
27
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
28
|
+
const scrollRef = useRef<any>(null);
|
|
29
|
+
|
|
30
|
+
// Virtualization calculations
|
|
31
|
+
const visibleRange = useMemo(() => {
|
|
32
|
+
if (!virtualized || typeof height !== 'number') {
|
|
33
|
+
return { start: 0, end: data.length - 1, offsetY: 0 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const containerHeight = height - headerHeight;
|
|
37
|
+
const startIndex = Math.floor(scrollTop / rowHeight);
|
|
38
|
+
const visibleCount = Math.ceil(containerHeight / rowHeight) + 2; // Add buffer
|
|
39
|
+
const endIndex = Math.min(startIndex + visibleCount, data.length - 1);
|
|
40
|
+
const offsetY = startIndex * rowHeight;
|
|
41
|
+
|
|
42
|
+
return { start: startIndex, end: endIndex, offsetY };
|
|
43
|
+
}, [scrollTop, height, headerHeight, rowHeight, data.length, virtualized]);
|
|
44
|
+
|
|
45
|
+
const totalHeight = useMemo(() => {
|
|
46
|
+
return virtualized ? data.length * rowHeight : 'auto';
|
|
47
|
+
}, [data.length, rowHeight, virtualized]);
|
|
48
|
+
|
|
49
|
+
const visibleData = useMemo(() => {
|
|
50
|
+
if (!virtualized) return data;
|
|
51
|
+
return data.slice(visibleRange.start, visibleRange.end + 1);
|
|
52
|
+
}, [data, visibleRange.start, visibleRange.end, virtualized]);
|
|
53
|
+
|
|
54
|
+
// Calculate minimum table width for horizontal scrolling
|
|
55
|
+
const minTableWidth = useMemo(() => {
|
|
56
|
+
return columns.reduce((total, column) => {
|
|
57
|
+
return total + (column.width ? (typeof column.width === 'number' ? column.width : 120) : 120);
|
|
58
|
+
}, 0);
|
|
59
|
+
}, [columns]);
|
|
60
|
+
|
|
61
|
+
const handleScroll = useCallback((e: any) => {
|
|
62
|
+
if (virtualized) {
|
|
63
|
+
// Handle both web and React Native scroll events
|
|
64
|
+
const scrollY = e.currentTarget?.scrollTop ?? e.nativeEvent?.contentOffset?.y ?? 0;
|
|
65
|
+
setScrollTop(scrollY);
|
|
66
|
+
}
|
|
67
|
+
}, [virtualized]);
|
|
68
|
+
|
|
69
|
+
// Helper function to get consistent column styles
|
|
70
|
+
const getColumnStyle = (column: Column<T>) => {
|
|
71
|
+
const baseStyle = {
|
|
72
|
+
boxSizing: 'border-box' as const,
|
|
73
|
+
flexShrink: 0,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (column.width) {
|
|
77
|
+
return {
|
|
78
|
+
...baseStyle,
|
|
79
|
+
width: column.width,
|
|
80
|
+
flexGrow: 0,
|
|
81
|
+
flexBasis: column.width,
|
|
82
|
+
};
|
|
83
|
+
} else {
|
|
84
|
+
return {
|
|
85
|
+
...baseStyle,
|
|
86
|
+
flexGrow: 1,
|
|
87
|
+
flexBasis: 0,
|
|
88
|
+
minWidth: column.minWidth || 120,
|
|
89
|
+
...(column.maxWidth ? { maxWidth: column.maxWidth } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Helper function for consistent cell base styles
|
|
95
|
+
const getCellBaseStyle = (theme: any) => ({
|
|
96
|
+
padding: theme.spacing.sm,
|
|
97
|
+
borderRightWidth: 1,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Helper function for platform-specific header styles
|
|
101
|
+
const getStickyHeaderStyle = (theme: any) => {
|
|
102
|
+
if (!stickyHeader) return {};
|
|
103
|
+
|
|
104
|
+
// Platform detection - check if we're on web or native
|
|
105
|
+
const isWeb = typeof document !== 'undefined';
|
|
106
|
+
|
|
107
|
+
if (isWeb) {
|
|
108
|
+
return {
|
|
109
|
+
position: 'sticky' as const,
|
|
110
|
+
top: 0,
|
|
111
|
+
zIndex: 100,
|
|
112
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
|
113
|
+
};
|
|
114
|
+
} else {
|
|
115
|
+
// React Native - use elevation instead of boxShadow
|
|
116
|
+
return {
|
|
117
|
+
elevation: 4,
|
|
118
|
+
zIndex: 100,
|
|
119
|
+
shadowColor: '#000',
|
|
120
|
+
shadowOffset: { width: 0, height: 2 },
|
|
121
|
+
shadowOpacity: 0.1,
|
|
122
|
+
shadowRadius: 4,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
};
|
|
29
126
|
|
|
30
127
|
const handleSort = useCallback((column: Column<T>) => {
|
|
31
128
|
if (!column.sortable) return;
|
|
@@ -54,143 +151,154 @@ export function DataGrid<T extends Record<string, any>>({
|
|
|
54
151
|
}, [selectedRows, onSelectionChange, multiSelect, onRowClick]);
|
|
55
152
|
|
|
56
153
|
const renderHeader = () => (
|
|
57
|
-
<
|
|
154
|
+
<TableRow style={(theme) => ({
|
|
155
|
+
backgroundColor: stickyHeader ? '#ffffff' : theme.colors.neutral[50],
|
|
156
|
+
borderBottomWidth: 2,
|
|
157
|
+
borderBottomColor: theme.colors.neutral[200],
|
|
158
|
+
minHeight: headerHeight,
|
|
159
|
+
flexDirection: 'row',
|
|
160
|
+
...getStickyHeaderStyle(theme),
|
|
161
|
+
...headerStyle,
|
|
162
|
+
})}>
|
|
58
163
|
{columns.map((column) => (
|
|
59
|
-
<
|
|
164
|
+
<TableCell
|
|
60
165
|
key={column.key}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
166
|
+
width={column.width}
|
|
167
|
+
style={(theme) => ({
|
|
168
|
+
...getCellBaseStyle(theme),
|
|
169
|
+
borderRightColor: theme.colors.neutral[200],
|
|
170
|
+
backgroundColor: stickyHeader ? '#ffffff' : theme.colors.neutral[50],
|
|
171
|
+
...column.headerStyle,
|
|
172
|
+
})}
|
|
66
173
|
>
|
|
67
|
-
<Text weight="bold" style={
|
|
174
|
+
<Text weight="bold" style={(theme) => ({
|
|
175
|
+
fontSize: 14,
|
|
176
|
+
color: theme.colors.neutral[700],
|
|
177
|
+
})}>
|
|
68
178
|
{column.header}
|
|
179
|
+
{column.sortable && (
|
|
180
|
+
<Text as="span" style={(theme) => ({
|
|
181
|
+
fontSize: 10,
|
|
182
|
+
marginLeft: theme.spacing.xs,
|
|
183
|
+
color: theme.colors.primary[500],
|
|
184
|
+
})}>
|
|
185
|
+
{sortColumn === column.key ? ` ${sortDirection === 'asc' ? '▲' : '▼'}` : ''}
|
|
186
|
+
</Text>
|
|
187
|
+
)}
|
|
69
188
|
</Text>
|
|
70
|
-
|
|
71
|
-
<Text style={styles.sortIndicator}>
|
|
72
|
-
{sortColumn === column.key ? (sortDirection === 'asc' ? '▲' : '▼') : ''}
|
|
73
|
-
</Text>
|
|
74
|
-
)}
|
|
75
|
-
</View>
|
|
189
|
+
</TableCell>
|
|
76
190
|
))}
|
|
77
|
-
</
|
|
191
|
+
</TableRow>
|
|
78
192
|
);
|
|
79
193
|
|
|
80
|
-
const renderRow = (
|
|
81
|
-
const
|
|
82
|
-
const
|
|
194
|
+
const renderRow = (item: T, virtualIndex: number) => {
|
|
195
|
+
const actualIndex = virtualized ? visibleRange.start + virtualIndex : virtualIndex;
|
|
196
|
+
const isSelected = selectedRows.includes(actualIndex);
|
|
197
|
+
const computedRowStyle = typeof rowStyle === 'function' ? rowStyle(item, actualIndex) : rowStyle;
|
|
83
198
|
|
|
84
199
|
return (
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
200
|
+
<TableRow
|
|
201
|
+
key={actualIndex}
|
|
202
|
+
style={(theme) => ({
|
|
203
|
+
borderBottomWidth: 1,
|
|
204
|
+
borderBottomColor: theme.colors.neutral[100],
|
|
205
|
+
backgroundColor: isSelected ? theme.colors.primary[50] : theme.colors.background,
|
|
206
|
+
minHeight: rowHeight,
|
|
207
|
+
flexDirection: 'row',
|
|
208
|
+
...computedRowStyle,
|
|
209
|
+
})}
|
|
210
|
+
onPress={() => handleRowClick(item, actualIndex)}
|
|
92
211
|
>
|
|
93
212
|
{columns.map((column) => {
|
|
94
213
|
const value = column.accessor ? column.accessor(item) : item[column.key];
|
|
95
|
-
const cellContent = column.render ? column.render(value, item,
|
|
214
|
+
const cellContent = column.render ? column.render(value, item, actualIndex) : value;
|
|
96
215
|
const computedCellStyle = typeof column.cellStyle === 'function'
|
|
97
216
|
? column.cellStyle(value, item)
|
|
98
217
|
: column.cellStyle;
|
|
99
218
|
|
|
100
219
|
return (
|
|
101
|
-
<
|
|
220
|
+
<TableCell
|
|
102
221
|
key={column.key}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
222
|
+
width={column.width}
|
|
223
|
+
style={(theme) => ({
|
|
224
|
+
...getCellBaseStyle(theme),
|
|
225
|
+
borderRightColor: theme.colors.neutral[100],
|
|
226
|
+
...computedCellStyle,
|
|
227
|
+
})}
|
|
108
228
|
>
|
|
109
229
|
{typeof cellContent === 'string' || typeof cellContent === 'number' ? (
|
|
110
230
|
<Text>{cellContent}</Text>
|
|
111
231
|
) : (
|
|
112
232
|
cellContent
|
|
113
233
|
)}
|
|
114
|
-
</
|
|
234
|
+
</TableCell>
|
|
115
235
|
);
|
|
116
236
|
})}
|
|
117
|
-
</
|
|
237
|
+
</TableRow>
|
|
118
238
|
);
|
|
119
239
|
};
|
|
120
240
|
|
|
121
241
|
const containerHeight = typeof height === 'number' ? height : undefined;
|
|
122
242
|
|
|
123
243
|
return (
|
|
124
|
-
<View style={
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
{
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
244
|
+
<View style={(theme) => ({
|
|
245
|
+
backgroundColor: theme.colors.background,
|
|
246
|
+
borderWidth: 1,
|
|
247
|
+
borderColor: theme.colors.neutral[200],
|
|
248
|
+
borderRadius: theme.radius.md,
|
|
249
|
+
overflow: 'hidden',
|
|
250
|
+
width,
|
|
251
|
+
height,
|
|
252
|
+
display: 'flex',
|
|
253
|
+
flexDirection: 'column',
|
|
254
|
+
...style,
|
|
255
|
+
})}>
|
|
256
|
+
<ScrollView
|
|
257
|
+
style={{
|
|
258
|
+
flex: 1,
|
|
259
|
+
...(containerHeight ? { maxHeight: containerHeight } : {})
|
|
260
|
+
}}
|
|
261
|
+
contentContainerStyle={{
|
|
262
|
+
width: minTableWidth,
|
|
263
|
+
}}
|
|
264
|
+
showsVerticalScrollIndicator={true}
|
|
265
|
+
showsHorizontalScrollIndicator={true}
|
|
266
|
+
bounces={false}
|
|
267
|
+
directionalLockEnabled={false}
|
|
268
|
+
onScroll={handleScroll}
|
|
269
|
+
scrollEventThrottle={16}
|
|
270
|
+
>
|
|
271
|
+
<Table style={{
|
|
272
|
+
width: minTableWidth,
|
|
273
|
+
...(virtualized ? { height: totalHeight } : {})
|
|
274
|
+
}}>
|
|
275
|
+
<TableHeader>
|
|
276
|
+
{renderHeader()}
|
|
277
|
+
</TableHeader>
|
|
278
|
+
<TableBody>
|
|
279
|
+
{virtualized && visibleRange.offsetY > 0 && (
|
|
280
|
+
<TableRow style={{ height: visibleRange.offsetY }}>
|
|
281
|
+
<TableCell
|
|
282
|
+
style={{ padding: 0, borderWidth: 0, height: visibleRange.offsetY }}
|
|
283
|
+
colSpan={columns.length}
|
|
284
|
+
>
|
|
285
|
+
</TableCell>
|
|
286
|
+
</TableRow>
|
|
287
|
+
)}
|
|
288
|
+
{visibleData.map((item, index) => renderRow(item, index))}
|
|
289
|
+
{virtualized && (data.length - visibleRange.end - 1) > 0 && (
|
|
290
|
+
<TableRow style={{ height: (data.length - visibleRange.end - 1) * rowHeight }}>
|
|
291
|
+
<TableCell
|
|
292
|
+
style={{ padding: 0, borderWidth: 0, height: (data.length - visibleRange.end - 1) * rowHeight }}
|
|
293
|
+
colSpan={columns.length}
|
|
294
|
+
>
|
|
295
|
+
</TableCell>
|
|
296
|
+
</TableRow>
|
|
297
|
+
)}
|
|
298
|
+
</TableBody>
|
|
299
|
+
</Table>
|
|
300
|
+
</ScrollView>
|
|
143
301
|
</View>
|
|
144
302
|
);
|
|
145
303
|
}
|
|
146
304
|
|
|
147
|
-
const stylesheet = createStyleSheet((theme) => ({
|
|
148
|
-
container: {
|
|
149
|
-
backgroundColor: theme.colors.background,
|
|
150
|
-
borderWidth: 1,
|
|
151
|
-
borderColor: theme.colors.neutral[200],
|
|
152
|
-
borderRadius: theme.radius.md,
|
|
153
|
-
overflow: 'hidden',
|
|
154
|
-
},
|
|
155
|
-
header: {
|
|
156
|
-
flexDirection: 'row',
|
|
157
|
-
backgroundColor: theme.colors.neutral[50],
|
|
158
|
-
borderBottomWidth: 2,
|
|
159
|
-
borderBottomColor: theme.colors.neutral[200],
|
|
160
|
-
},
|
|
161
|
-
headerCell: {
|
|
162
|
-
flexDirection: 'row',
|
|
163
|
-
alignItems: 'center',
|
|
164
|
-
justifyContent: 'space-between',
|
|
165
|
-
padding: theme.spacing.sm,
|
|
166
|
-
borderRightWidth: 1,
|
|
167
|
-
borderRightColor: theme.colors.neutral[200],
|
|
168
|
-
},
|
|
169
|
-
headerText: {
|
|
170
|
-
fontSize: 14,
|
|
171
|
-
color: theme.colors.neutral[700],
|
|
172
|
-
},
|
|
173
|
-
sortIndicator: {
|
|
174
|
-
fontSize: 10,
|
|
175
|
-
marginLeft: theme.spacing.xs,
|
|
176
|
-
color: theme.colors.primary[500],
|
|
177
|
-
},
|
|
178
|
-
row: {
|
|
179
|
-
flexDirection: 'row',
|
|
180
|
-
borderBottomWidth: 1,
|
|
181
|
-
borderBottomColor: theme.colors.neutral[100],
|
|
182
|
-
backgroundColor: theme.colors.background,
|
|
183
|
-
},
|
|
184
|
-
selectedRow: {
|
|
185
|
-
backgroundColor: theme.colors.primary[50],
|
|
186
|
-
},
|
|
187
|
-
cell: {
|
|
188
|
-
padding: theme.spacing.sm,
|
|
189
|
-
borderRightWidth: 1,
|
|
190
|
-
borderRightColor: theme.colors.neutral[100],
|
|
191
|
-
justifyContent: 'center',
|
|
192
|
-
},
|
|
193
|
-
scrollView: {
|
|
194
|
-
flex: 1,
|
|
195
|
-
},
|
|
196
|
-
}));
|