@idealyst/datagrid 1.0.41 → 1.0.45

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/datagrid",
3
- "version": "1.0.41",
3
+ "version": "1.0.45",
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.40",
39
- "@idealyst/theme": "^1.0.40",
38
+ "@idealyst/components": "^1.0.45",
39
+ "@idealyst/theme": "^1.0.45",
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 { VirtualizedList } from '../primitives/VirtualizedList';
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
- <View style={[
57
- (theme) => ({
58
- flexDirection: 'row',
59
- backgroundColor: theme.colors.neutral[50],
60
- borderBottomWidth: 2,
61
- borderBottomColor: theme.colors.neutral[200],
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
- <View
164
+ <TableCell
67
165
  key={column.key}
68
- style={[
69
- (theme) => ({
70
- flexDirection: 'row',
71
- alignItems: 'center',
72
- justifyContent: 'space-between',
73
- padding: theme.spacing.sm,
74
- borderRightWidth: 1,
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
- {column.sortable && (
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
- </View>
191
+ </TableRow>
99
192
  );
100
193
 
101
- const renderRow = ({ item, index }: { item: T; index: number }) => {
102
- const isSelected = selectedRows.includes(index);
103
- const computedRowStyle = typeof rowStyle === 'function' ? rowStyle(item, index) : rowStyle;
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
- <View
107
- style={[
108
- (theme) => ({
109
- flexDirection: 'row',
110
- borderBottomWidth: 1,
111
- borderBottomColor: theme.colors.neutral[100],
112
- backgroundColor: theme.colors.background,
113
- }),
114
- isSelected && ((theme) => ({
115
- backgroundColor: theme.colors.primary[50],
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, index) : value;
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
- <View
220
+ <TableCell
130
221
  key={column.key}
131
- style={[
132
- (theme) => ({
133
- padding: theme.spacing.sm,
134
- borderRightWidth: 1,
135
- borderRightColor: theme.colors.neutral[100],
136
- justifyContent: 'center',
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
- </View>
234
+ </TableCell>
148
235
  );
149
236
  })}
150
- </View>
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
- (theme) => ({
159
- backgroundColor: theme.colors.background,
160
- borderWidth: 1,
161
- borderColor: theme.colors.neutral[200],
162
- borderRadius: theme.radius.md,
163
- overflow: 'hidden',
164
- }),
165
- { width, height },
166
- style
167
- ]}>
168
- {stickyHeader && renderHeader()}
169
- {virtualized && containerHeight ? (
170
- <VirtualizedList
171
- data={data}
172
- renderItem={renderRow}
173
- itemHeight={rowHeight}
174
- height={containerHeight - (stickyHeader ? headerHeight : 0)}
175
- width={width}
176
- />
177
- ) : (
178
- <ScrollView
179
- style={{ flex: 1 }}
180
- showsVerticalScrollIndicator={true}
181
- >
182
- {!stickyHeader && renderHeader()}
183
- {data.map((item, index) => renderRow({ item, index }))}
184
- </ScrollView>
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
  }