@idealyst/datagrid 1.0.41 → 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 +211 -96
- 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, useCallback } from 'react';
|
|
1
|
+
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
|
2
2
|
import { View, Text } from '@idealyst/components';
|
|
3
|
-
import { UnistylesRuntime } from 'react-native-unistyles';
|
|
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>>({
|
|
@@ -25,6 +24,105 @@ export function DataGrid<T extends Record<string, any>>({
|
|
|
25
24
|
}: DataGridProps<T>) {
|
|
26
25
|
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
|
27
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
|
+
};
|
|
28
126
|
|
|
29
127
|
const handleSort = useCallback((column: Column<T>) => {
|
|
30
128
|
if (!column.sortable) return;
|
|
@@ -53,136 +151,153 @@ export function DataGrid<T extends Record<string, any>>({
|
|
|
53
151
|
}, [selectedRows, onSelectionChange, multiSelect, onRowClick]);
|
|
54
152
|
|
|
55
153
|
const renderHeader = () => (
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
headerStyle
|
|
64
|
-
|
|
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
|
+
})}>
|
|
65
163
|
{columns.map((column) => (
|
|
66
|
-
<
|
|
164
|
+
<TableCell
|
|
67
165
|
key={column.key}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
borderRightColor: theme.colors.neutral[200],
|
|
76
|
-
}),
|
|
77
|
-
{ width: column.width || 'auto', minWidth: column.minWidth, maxWidth: column.maxWidth },
|
|
78
|
-
column.headerStyle,
|
|
79
|
-
]}
|
|
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
|
+
})}
|
|
80
173
|
>
|
|
81
174
|
<Text weight="bold" style={(theme) => ({
|
|
82
175
|
fontSize: 14,
|
|
83
176
|
color: theme.colors.neutral[700],
|
|
84
177
|
})}>
|
|
85
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
|
+
)}
|
|
86
188
|
</Text>
|
|
87
|
-
|
|
88
|
-
<Text style={(theme) => ({
|
|
89
|
-
fontSize: 10,
|
|
90
|
-
marginLeft: theme.spacing.xs,
|
|
91
|
-
color: theme.colors.primary[500],
|
|
92
|
-
})}>
|
|
93
|
-
{sortColumn === column.key ? (sortDirection === 'asc' ? '▲' : '▼') : ''}
|
|
94
|
-
</Text>
|
|
95
|
-
)}
|
|
96
|
-
</View>
|
|
189
|
+
</TableCell>
|
|
97
190
|
))}
|
|
98
|
-
</
|
|
191
|
+
</TableRow>
|
|
99
192
|
);
|
|
100
193
|
|
|
101
|
-
const renderRow = (
|
|
102
|
-
const
|
|
103
|
-
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;
|
|
104
198
|
|
|
105
199
|
return (
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
computedRowStyle,
|
|
118
|
-
]}
|
|
119
|
-
onPress={() => handleRowClick(item, index)}
|
|
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)}
|
|
120
211
|
>
|
|
121
212
|
{columns.map((column) => {
|
|
122
213
|
const value = column.accessor ? column.accessor(item) : item[column.key];
|
|
123
|
-
const cellContent = column.render ? column.render(value, item,
|
|
214
|
+
const cellContent = column.render ? column.render(value, item, actualIndex) : value;
|
|
124
215
|
const computedCellStyle = typeof column.cellStyle === 'function'
|
|
125
216
|
? column.cellStyle(value, item)
|
|
126
217
|
: column.cellStyle;
|
|
127
218
|
|
|
128
219
|
return (
|
|
129
|
-
<
|
|
220
|
+
<TableCell
|
|
130
221
|
key={column.key}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}),
|
|
138
|
-
{ width: column.width || 'auto', minWidth: column.minWidth, maxWidth: column.maxWidth },
|
|
139
|
-
computedCellStyle,
|
|
140
|
-
]}
|
|
222
|
+
width={column.width}
|
|
223
|
+
style={(theme) => ({
|
|
224
|
+
...getCellBaseStyle(theme),
|
|
225
|
+
borderRightColor: theme.colors.neutral[100],
|
|
226
|
+
...computedCellStyle,
|
|
227
|
+
})}
|
|
141
228
|
>
|
|
142
229
|
{typeof cellContent === 'string' || typeof cellContent === 'number' ? (
|
|
143
230
|
<Text>{cellContent}</Text>
|
|
144
231
|
) : (
|
|
145
232
|
cellContent
|
|
146
233
|
)}
|
|
147
|
-
</
|
|
234
|
+
</TableCell>
|
|
148
235
|
);
|
|
149
236
|
})}
|
|
150
|
-
</
|
|
237
|
+
</TableRow>
|
|
151
238
|
);
|
|
152
239
|
};
|
|
153
240
|
|
|
154
241
|
const containerHeight = typeof height === 'number' ? height : undefined;
|
|
155
242
|
|
|
156
243
|
return (
|
|
157
|
-
<View style={
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
width
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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>
|
|
186
301
|
</View>
|
|
187
302
|
);
|
|
188
303
|
}
|